merge mozilla-inbound to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Sun, 17 Jul 2016 10:08:08 +0200
changeset 347333 711963e8daa312ae06409f8ab5c06612cb0b8f7b
parent 347233 ef5f932101e5b833b2429407cb0873471b4d764e (current diff)
parent 347332 bd1d2c40a07b7b5760ddc81a46d594b9b37d0983 (diff)
child 347334 0fbdcd21fad76a00328e67875c6f40dc219235f4
child 347337 53900593419c215f37e70c40763ca8b45d86ecb5
child 347402 ce145f8f4c4a7d08b107f6796f1f03deb1d41ff6
child 347412 9c04250731112aaf2a6774aa79e7781851e8a5b6
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
711963e8daa3 / 50.0a1 / 20160717030211 / files
nightly linux64
711963e8daa3 / 50.0a1 / 20160717030211 / files
nightly mac
711963e8daa3 / 50.0a1 / 20160717030211 / files
nightly win32
711963e8daa3 / 50.0a1 / 20160717030211 / files
nightly win64
711963e8daa3 / 50.0a1 / 20160717030211 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central a=merge
browser/components/nsBrowserGlue.js
browser/locales/en-US/chrome/browser/accounts.properties
dom/bindings/BindingUtils.cpp
dom/bindings/BindingUtils.h
dom/bindings/Codegen.py
dom/html/test/mochitest.ini
dom/presentation/tests/mochitest/mochitest.ini
dom/tests/mochitest/geolocation/mochitest.ini
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
modules/libpref/init/all.js
old-configure.in
testing/web-platform/meta/XMLHttpRequest/xmlhttprequest-network-error.htm.ini
toolkit/components/extensions/ExtensionUtils.jsm
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1267887 - Build skew after updating rust mp4parse
+Bug 1286754 - Update the version of rust used on Windows
--- a/accessible/xpcom/xpcAccessibleGeneric.h
+++ b/accessible/xpcom/xpcAccessibleGeneric.h
@@ -81,20 +81,20 @@ xpcAccessible::Intl()
 }
 
 inline AccessibleOrProxy
 xpcAccessible::IntlGeneric()
 {
   return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
 }
 
-inline Accessible*
+inline AccessibleOrProxy
 xpcAccessibleHyperLink::Intl()
 {
-  return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible();
+  return static_cast<xpcAccessibleGeneric*>(this)->mIntl;
 }
 
 inline Accessible*
 xpcAccessibleSelectable::Intl()
 {
   return static_cast<xpcAccessibleGeneric*>(this)->mIntl.AsAccessible();
 }
 
--- a/accessible/xpcom/xpcAccessibleHyperLink.cpp
+++ b/accessible/xpcom/xpcAccessibleHyperLink.cpp
@@ -1,94 +1,156 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=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 "Accessible-inl.h"
 #include "xpcAccessibleDocument.h"
+#include "nsNetUtil.h"
 
 using namespace mozilla::a11y;
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetStartIndex(int32_t* aStartIndex)
 {
   NS_ENSURE_ARG_POINTER(aStartIndex);
   *aStartIndex = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aStartIndex = Intl()->StartOffset();
+  if (Intl().IsAccessible()) {
+    *aStartIndex = Intl().AsAccessible()->StartOffset();
+  } else { 
+    bool isIndexValid = false;
+    uint32_t startOffset = Intl().AsProxy()->StartOffset(&isIndexValid);
+    if (!isIndexValid)
+      return NS_ERROR_FAILURE;
+
+    *aStartIndex = startOffset;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetEndIndex(int32_t* aEndIndex)
 {
   NS_ENSURE_ARG_POINTER(aEndIndex);
   *aEndIndex = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aEndIndex = Intl()->EndOffset();
+  if (Intl().IsAccessible()) {
+    *aEndIndex = Intl().AsAccessible()->EndOffset();
+  } else { 
+    bool isIndexValid = false;
+    uint32_t endOffset = Intl().AsProxy()->EndOffset(&isIndexValid);
+    if (!isIndexValid)
+      return NS_ERROR_FAILURE;
+
+    *aEndIndex = endOffset;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetAnchorCount(int32_t* aAnchorCount)
 {
   NS_ENSURE_ARG_POINTER(aAnchorCount);
   *aAnchorCount = 0;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aAnchorCount = Intl()->AnchorCount();
+  if (Intl().IsAccessible()) {
+    *aAnchorCount = Intl().AsAccessible()->AnchorCount();
+  } else { 
+    bool isCountValid = false;
+    uint32_t anchorCount = Intl().AsProxy()->AnchorCount(&isCountValid);
+    if (!isCountValid)
+      return NS_ERROR_FAILURE;
+
+    *aAnchorCount = anchorCount;
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetURI(int32_t aIndex, nsIURI** aURI)
 {
   NS_ENSURE_ARG_POINTER(aURI);
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount()))
+  if (aIndex < 0)
     return NS_ERROR_INVALID_ARG;
 
-  RefPtr<nsIURI>(Intl()->AnchorURIAt(aIndex)).forget(aURI);
+  if (Intl().IsAccessible()) {
+    if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount()))
+      return NS_ERROR_INVALID_ARG;
+
+    RefPtr<nsIURI>(Intl().AsAccessible()->AnchorURIAt(aIndex)).forget(aURI);
+  } else {
+    nsCString spec;
+    bool isURIValid = false;
+    Intl().AsProxy()->AnchorURIAt(aIndex, spec, &isURIValid);
+    if (!isURIValid)
+      return NS_ERROR_FAILURE;
+
+    nsCOMPtr<nsIURI> uri;
+    nsresult rv = NS_NewURI(getter_AddRefs(uri), spec);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    uri.forget(aURI);
+  }
+
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetAnchor(int32_t aIndex, nsIAccessible** aAccessible)
 {
   NS_ENSURE_ARG_POINTER(aAccessible);
   *aAccessible = nullptr;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
-
-  if (aIndex < 0 || aIndex >= static_cast<int32_t>(Intl()->AnchorCount()))
+  
+  if (aIndex < 0)
     return NS_ERROR_INVALID_ARG;
 
-  NS_IF_ADDREF(*aAccessible = ToXPC(Intl()->AnchorAt(aIndex)));
+  if (Intl().IsAccessible()) {
+    if (aIndex >= static_cast<int32_t>(Intl().AsAccessible()->AnchorCount()))
+      return NS_ERROR_INVALID_ARG;
+
+    NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsAccessible()->AnchorAt(aIndex)));
+  } else {
+    NS_IF_ADDREF(*aAccessible = ToXPC(Intl().AsProxy()->AnchorAt(aIndex)));
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 xpcAccessibleHyperLink::GetValid(bool* aValid)
 {
   NS_ENSURE_ARG_POINTER(aValid);
   *aValid = false;
 
-  if (!Intl())
+  if (Intl().IsNull())
     return NS_ERROR_FAILURE;
 
-  *aValid = Intl()->IsLinkValid();
+  if (Intl().IsAccessible()) {
+    *aValid = Intl().AsAccessible()->IsLinkValid();
+  } else {
+    *aValid = Intl().AsProxy()->IsLinkValid();
+  }
+
   return NS_OK;
 }
--- a/accessible/xpcom/xpcAccessibleHyperLink.h
+++ b/accessible/xpcom/xpcAccessibleHyperLink.h
@@ -34,15 +34,15 @@ public:
 protected:
   xpcAccessibleHyperLink() { }
   virtual ~xpcAccessibleHyperLink() {}
 
 private:
   xpcAccessibleHyperLink(const xpcAccessibleHyperLink&) = delete;
   xpcAccessibleHyperLink& operator =(const xpcAccessibleHyperLink&) = delete;
 
-  Accessible* Intl();
+  AccessibleOrProxy Intl();
 };
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/addon-sdk/moz.build
+++ b/addon-sdk/moz.build
@@ -8,17 +8,18 @@
 # 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/.
 
 # Makefile.in uses a misc target through test_addons_TARGET.
 HAS_MISC_RULE = True
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
-JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini']
+JETPACK_PACKAGE_MANIFESTS += ['source/test/jetpack-package.ini',
+                              'source/test/leak/jetpack-package.ini']
 JETPACK_ADDON_MANIFESTS += ['source/test/addons/jetpack-addon.ini']
 
 addons = [
     'addon-manager',
     'author-email',
     'child_process',
     'chrome',
     'content-permissions',
--- a/addon-sdk/source/lib/sdk/event/utils.js
+++ b/addon-sdk/source/lib/sdk/event/utils.js
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
 var { emit, on, once, off, EVENT_TYPE_PATTERN } = require("./core");
+const { Cu } = require("chrome");
 
 // This module provides set of high order function for working with event
 // streams (streams in a NodeJS style that dispatch data, end and error
 // events).
 
 // Function takes a `target` object and returns set of implicit references
 // (non property references) it keeps. This basically allows defining
 // references between objects without storing the explicitly. See transform for
@@ -21,17 +22,17 @@ var refs = (function() {
   let refSets = new WeakMap();
   return function refs(target) {
     if (!refSets.has(target)) refSets.set(target, new Set());
     return refSets.get(target);
   };
 })();
 
 function transform(input, f) {
-  let output = {};
+  let output = new Output();
 
   // Since event listeners don't prevent `input` to be GC-ed we wanna presrve
   // it until `output` can be GC-ed. There for we add implicit reference which
   // is removed once `input` ends.
   refs(output).add(input);
 
   const next = data => receive(output, data);
   once(output, "start", () => start(input));
@@ -59,17 +60,17 @@ exports.filter = filter;
 // mapped via given `f` function.
 const map = (input, f) => transform(input, (data, next) => next(f(data)));
 exports.map = map;
 
 // High order function that takes `input` stream of streams and merges them
 // into single event stream. Like flatten but time based rather than order
 // based.
 function merge(inputs) {
-  let output = {};
+  let output = new Output();
   let open = 1;
   let state = [];
   output.state = state;
   refs(output).add(inputs);
 
   function end(input) {
     open = open - 1;
     refs(output).delete(input);
@@ -102,23 +103,28 @@ exports.merge = merge;
 const expand = (inputs, f) => merge(map(inputs, f));
 exports.expand = expand;
 
 const pipe = (from, to) => on(from, "*", emit.bind(emit, to));
 exports.pipe = pipe;
 
 
 // Shim signal APIs so other modules can be used as is.
-
 const receive = (input, message) => {
   if (input[receive])
     input[receive](input, message);
   else
     emit(input, "data", message);
 
+  // Ideally our input will extend Input and already provide a weak value
+  // getter.  If not, opportunistically shim the weak value getter on
+  // other types passed as the input.
+  if (!("value" in input)) {
+    Object.defineProperty(input, "value", WeakValueGetterSetter);
+  }
   input.value = message;
 };
 receive.toString = () => "@@receive";
 exports.receive = receive;
 exports.send = receive;
 
 const end = input => {
   if (input[end])
@@ -146,17 +152,17 @@ const start = input => {
 };
 start.toString = () => "@@start";
 exports.start = start;
 
 const lift = (step, ...inputs) => {
   let args = null;
   let opened = inputs.length;
   let started = false;
-  const output = {};
+  const output = new Output();
   const init = () => {
     args = [...inputs.map(input => input.value)];
     output.value = step(...args);
   };
 
   inputs.forEach((input, index) => {
     on(input, "data", data => {
       args[index] = data;
@@ -177,17 +183,18 @@ const lift = (step, ...inputs) => {
   init();
 
   return output;
 };
 exports.lift = lift;
 
 const merges = inputs => {
   let opened = inputs.length;
-  let output = { value: inputs[0].value };
+  let output = new Output();
+  output.value = inputs[0].value;
   inputs.forEach((input, index) => {
     on(input, "data", data => receive(output, data));
     on(input, "end", () => {
       opened = opened - 1;
       if (opened <= 0)
         end(output);
     });
   });
@@ -220,22 +227,56 @@ Input.start = input => emit(input, "star
 Input.prototype.start = Input.start;
 
 Input.end = input => {
   emit(input, "end", input);
   stop(input);
 };
 Input.prototype[end] = Input.end;
 
+// The event channel system caches the last event seen as input.value.
+// Unfortunately, if the last event is a DOM object this is a great way
+// leak windows.  Mitigate this by storing input.value using a weak
+// reference.  This allows the system to work for normal event processing
+// while also allowing the objects to be reclaimed.  It means, however,
+// input.value cannot be accessed long after the event was dispatched.
+const WeakValueGetterSetter = {
+  get: function() {
+    return this._weakValue ? this._weakValue.get() : this._simpleValue
+  },
+  set: function(v) {
+    if (v && typeof v === "object") {
+      this._weakValue = Cu.getWeakReference(v)
+      this._simpleValue = undefined;
+      return;
+    }
+    this._simpleValue = v;
+    this._weakValue = undefined;
+  },
+}
+Object.defineProperty(Input.prototype, "value", WeakValueGetterSetter);
+
 exports.Input = Input;
 
+// Define an Output type with a weak value getter for the transformation
+// functions that produce new channels.
+function Output() { }
+Object.defineProperty(Output.prototype, "value", WeakValueGetterSetter);
+exports.Output = Output;
+
 const $source = "@@source";
 const $outputs = "@@outputs";
 exports.outputs = $outputs;
 
+// NOTE: Passing DOM objects through a Reactor can cause them to leak
+// when they get cached in this.value.  We cannot use a weak reference
+// in this case because the Reactor design expects to always have both the
+// past and present value.  If we allow past values to be collected the
+// system breaks.
+
 function Reactor(options={}) {
   const {onStep, onStart, onEnd} = options;
   if (onStep)
     this.onStep = onStep;
   if (onStart)
     this.onStart = onStart;
   if (onEnd)
     this.onEnd = onEnd;
--- a/addon-sdk/source/lib/sdk/window/events.js
+++ b/addon-sdk/source/lib/sdk/window/events.js
@@ -2,32 +2,50 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 module.metadata = {
   "stability": "unstable"
 };
 
-const { Ci } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const { observe } = require("../event/chrome");
 const { open } = require("../event/dom");
 const { windows } = require("../window/utils");
 const { filter, merge, map, expand } = require("../event/utils");
 
+function documentMatches(weakWindow, event) {
+  let window = weakWindow.get();
+  return window && event.target === window.document;
+}
+
+function makeStrictDocumentFilter(window) {
+  // Note: Do not define a closure within this function.  Otherwise
+  //       you may leak the window argument.
+  let weak = Cu.getWeakReference(window);
+  return documentMatches.bind(null, weak);
+}
+
+function toEventWithDefaultViewTarget({type, target}) {
+  return { type: type, target: target.defaultView }
+}
+
 // Function registers single shot event listeners for relevant window events
 // that forward events to exported event stream.
 function eventsFor(window) {
+  // NOTE: Do no use pass a closure from this function into a stream
+  //       transform function.  You will capture the window in the
+  //       closure and leak the window until the event stream is
+  //       completely closed.
   let interactive = open(window, "DOMContentLoaded", { capture: true });
   let complete = open(window, "load", { capture: true });
   let states = merge([interactive, complete]);
-  let changes = filter(states, ({target}) => target === window.document);
-  return map(changes, function({type, target}) {
-    return { type: type, target: target.defaultView }
-  });
+  let changes = filter(states, makeStrictDocumentFilter(window));
+  return map(changes, toEventWithDefaultViewTarget);
 }
 
 // In addition to observing windows that are open we also observe windows
 // that are already already opened in case they're in process of loading.
 var opened = windows(null, { includePrivate: true });
 var currentEvents = merge(opened.map(eventsFor));
 
 // Register system event listeners for top level window open / close.
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/jetpack-package.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+support-files =
+  leak-utils.js
+
+[test-leak-window-events.js]
+[test-leak-event-dom-closed-window.js]
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/leak-utils.js
@@ -0,0 +1,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/. */
+"use strict";
+
+const { Cu, Ci } = require("chrome");
+const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { SelfSupportBackend } = Cu.import("resource:///modules/SelfSupportBackend.jsm", {});
+const Startup = Cu.import("resource://gre/modules/sdk/system/Startup.js", {}).exports;
+
+// Adapted from the SpecialPowers.exactGC() code.  We don't have a
+// window to operate on so we cannot use the exact same logic.  We
+// use 6 GC iterations here as that is what is needed to clean up
+// the windows we have tested with.
+function gc() {
+  return new Promise(resolve => {
+    Cu.forceGC();
+    Cu.forceCC();
+    let count = 0;
+    function genGCCallback() {
+      Cu.forceCC();
+      return function() {
+        if (++count < 5) {
+          Cu.schedulePreciseGC(genGCCallback());
+        } else {
+          resolve();
+        }
+      }
+    }
+
+    Cu.schedulePreciseGC(genGCCallback());
+  });
+}
+
+// Execute the given test function and verify that we did not leak windows
+// in the process.  The test function must return a promise or be a generator.
+// If the promise is resolved, or generator completes, with an sdk loader
+// object then it will be unloaded after the memory measurements.
+exports.asyncWindowLeakTest = function*(assert, asyncTestFunc) {
+
+  // SelfSupportBackend periodically tries to open windows.  This can
+  // mess up our window leak detection below, so turn it off.
+  SelfSupportBackend.uninit();
+
+  // Wait for the browser to finish loading.
+  yield Startup.onceInitialized;
+
+  // Track windows that are opened in an array of weak references.
+  let weakWindows = [];
+  function windowObserver(subject, topic) {
+    let supportsWeak = subject.QueryInterface(Ci.nsISupportsWeakReference);
+    if (supportsWeak) {
+      weakWindows.push(Cu.getWeakReference(supportsWeak));
+    }
+  }
+  Services.obs.addObserver(windowObserver, "domwindowopened", false);
+
+  // Execute the body of the test.
+  let testLoader = yield asyncTestFunc(assert);
+
+  // Stop tracking new windows and attempt to GC any resources allocated
+  // by the test body.
+  Services.obs.removeObserver(windowObserver, "domwindowopened", false);
+  yield gc();
+
+  // Check to see if any of the windows we saw survived the GC.  We consider
+  // these leaks.
+  assert.ok(weakWindows.length > 0, "should see at least one new window");
+  for (let i = 0; i < weakWindows.length; ++i) {
+    assert.equal(weakWindows[i].get(), null, "window " + i + " should be GC'd");
+  }
+
+  // Finally, unload the test body's loader if it provided one.  We do this
+  // after our leak detection to avoid free'ing things on unload.  Users
+  // don't tend to unload their addons very often, so we want to find leaks
+  // that happen while addons are in use.
+  if (testLoader) {
+    testLoader.unload();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/test-leak-event-dom-closed-window.js
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+const { asyncWindowLeakTest } = require("./leak-utils");
+const { Loader } = require('sdk/test/loader');
+const openWindow = require("sdk/window/utils").open;
+
+exports["test sdk/event/dom does not leak when attached to closed window"] = function*(assert) {
+  yield asyncWindowLeakTest(assert, _ => {
+    return new Promise(resolve => {
+      let loader = Loader(module);
+      let { open } = loader.require('sdk/event/dom');
+      let w = openWindow();
+      w.addEventListener("DOMWindowClose", function windowClosed(evt) {
+        w.removeEventListener("DOMWindowClose", windowClosed);
+        // The sdk/event/dom module tries to clean itself up when DOMWindowClose
+        // is fired.  Verify that it doesn't leak if its attached to an
+        // already closed window either. (See bug 1268898.)
+        open(w.document, "TestEvent1");
+        resolve(loader);
+      });
+      w.close();
+    });
+  });
+}
+
+require("sdk/test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/leak/test-leak-window-events.js
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+// Opening new windows in Fennec causes issues
+module.metadata = {
+  engines: {
+    'Firefox': '*'
+  }
+};
+
+const { asyncWindowLeakTest } = require("./leak-utils.js");
+const { Loader } = require("sdk/test/loader");
+const { open } = require("sdk/window/utils");
+
+exports["test window/events for leaks"] = function*(assert) {
+  yield asyncWindowLeakTest(assert, _ => {
+    return new Promise((resolve, reject) => {
+      let loader = Loader(module);
+      let { events } = loader.require("sdk/window/events");
+      let { on, off } = loader.require("sdk/event/core");
+
+      on(events, "data", function handler(e) {
+        try {
+          if (e.type === "load") {
+            e.target.close();
+          }
+          else if (e.type === "close") {
+            off(events, "data", handler);
+
+            // Let asyncWindowLeakTest call loader.unload() after the
+            // leak check.
+            resolve(loader);
+          }
+        } catch (e) {
+          reject(e);
+        }
+      });
+
+      // Open a window.  This will trigger our data events.
+      open();
+    });
+  });
+};
+
+require("sdk/test").run(exports);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1891,18 +1891,20 @@ BrowserGlue.prototype = {
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _showSyncStartedDoorhanger: function () {
     let bundle = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+    let productName = gBrandBundle.GetStringFromName("brandShortName");
     let title = bundle.GetStringFromName("syncStartNotification.title");
-    let body = bundle.GetStringFromName("syncStartNotification.body");
+    let body = bundle.formatStringFromName("syncStartNotification.body2",
+                                            [productName], 1);
 
     let clickCallback = (subject, topic, data) => {
       if (topic != "alertclickcallback")
         return;
       this._openPreferences("sync");
     }
     AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
   },
--- a/browser/config/tooltool-manifests/win32/releng.manifest
+++ b/browser/config/tooltool-manifests/win32/releng.manifest
@@ -1,19 +1,19 @@
 [
 {
 "size": 266240,
 "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869",
 "algorithm": "sha512",
 "filename": "mozmake.exe"
 },
 {
-"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)",
-"size": 82463178,
-"digest": "a3c54c6792e75d53ec79caf958db25b651fcf968a37b00949fb327c54a54cad6305a4af302f267082d86d70fcf837ed0f273f85b97706c20b957ff3690889b40",
+"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)",
+"size": 88820579,
+"digest": "3bc772d951bf90b01cdba9dcd0e1d131a98519dff0710bb219784ea43d4d001dbce191071a4b3824933386bb9613f173760c438939eb396b0e0dfdad9a42e4f0",
 "algorithm": "sha512",
 "filename": "rustc.tar.bz2",
 "unpack": true
 },
 {
 "size": 167175,
 "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
 "algorithm": "sha512",
--- a/browser/config/tooltool-manifests/win64/releng.manifest
+++ b/browser/config/tooltool-manifests/win64/releng.manifest
@@ -1,19 +1,19 @@
 [
 {
 "size": 266240,
 "digest": "bb345b0e700ffab4d09436981f14b5de84da55a3f18a7f09ebc4364a4488acdeab8d46f447b12ac70f2da1444a68b8ce8b8675f0dae2ccf845e966d1df0f0869",
 "algorithm": "sha512",
 "filename": "mozmake.exe"
 },
 {
-"version": "rustc 1.9.0 (e4e8b6668 2016-05-18)",
-"size": 88486080,
-"digest": "a4fb99cd637b236a9c30e111757ca560bc8df1b143324c1d9ab58c32470b9b9a0598e3e0d220278ee157959dcd88421496388e2ed856e6261d9c81f18e6310e9",
+"version": "rustc 1.10.0 (cfcb716cf 2016-07-03)",
+"size": 94067220,
+"digest": "05cabda2a28ce6674f062aab589b4b3758e0cd4a4af364bb9a2e736254baa10d668936b2b7ed0df530c7f5ba8ea1e7f51ff3affc84a6551c46188b2f67f10e05",
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "rustc.tar.bz2",
 "unpack": true
 },
 {
 "size": 167175,
 "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -22,17 +22,18 @@ verificationSentTitle = Verification Sen
 # LOCALIZATION NOTE (verificationSentBody) - %S = Email address of user's Firefox Account
 verificationSentBody = A verification link has been sent to %S.
 verificationNotSentTitle = Unable to Send Verification
 verificationNotSentBody = We are unable to send a verification mail at this time, please try again later.
 
 # LOCALIZATION NOTE (syncStartNotification.title, syncStartNotification.body)
 # These strings are used in a notification shown after Sync is connected.
 syncStartNotification.title = Sync enabled
-syncStartNotification.body = Firefox will begin syncing momentarily.
+# %S is brandShortName
+syncStartNotification.body2 = %S will begin syncing momentarily.
 
 # LOCALIZATION NOTE (deviceDisconnectedNotification.title, deviceDisconnectedNotification.body)
 # These strings are used in a notification shown after Sync was disconnected remotely.
 deviceDisconnectedNotification.title = Sync disconnected
 deviceDisconnectedNotification.body = This computer has been successfully disconnected from Firefox Sync.
 
 # LOCALIZATION NOTE (sendTabToAllDevices.menuitem)
 # Displayed in the Send Tabs context menu when right clicking a tab, a page or a link.
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -132,24 +132,31 @@ def namespace(**kwargs):
 # such as `set_config`. But those functions do not take immediate values.
 # The `delayed_getattr` function allows access to attributes from the result
 # of a @depends function in a non-immediate manner.
 #   @depends('--option')
 #   def option(value)
 #       return namespace(foo=value)
 #   set_config('FOO', delayed_getattr(option, 'foo')
 @template
+@imports('__sandbox__')
 def delayed_getattr(func, key):
-    @depends(func)
-    def result(value):
+    _, deps = __sandbox__._depends.get(func, (None, ()))
+
+    def result(value, _=None):
         # The @depends function we're being passed may have returned
         # None, or an object that simply doesn't have the wanted key.
         # In that case, just return None.
         return getattr(value, key, None)
-    return result
+
+    # Automatically add a dependency on --help when the given @depends
+    # function itself depends on --help.
+    if __sandbox__._help_option in deps:
+        return depends(func, '--help')(result)
+    return depends(func)(result)
 
 
 # Like @depends, but the decorated function is only called if one of the
 # arguments it would be called with has a positive value (bool(value) is True)
 @template
 def depends_if(*args):
     def decorator(func):
         @depends(*args)
--- a/config/check_spidermonkey_style.py
+++ b/config/check_spidermonkey_style.py
@@ -66,16 +66,18 @@ included_inclnames_to_ignore = set([
     'jsautokw.h',               # generated in $OBJDIR
     'jscustomallocator.h',      # provided by embedders;  allowed to be missing
     'js-config.h',              # generated in $OBJDIR
     'fdlibm.h',                 # fdlibm
     'pratom.h',                 # NSPR
     'prcvar.h',                 # NSPR
     'prerror.h',                # NSPR
     'prinit.h',                 # NSPR
+    'prio.h',                   # NSPR
+    'private/pprio.h',          # NSPR
     'prlink.h',                 # NSPR
     'prlock.h',                 # NSPR
     'prprf.h',                  # NSPR
     'prthread.h',               # NSPR
     'prtypes.h',                # NSPR
     'selfhosted.out.h',         # generated in $OBJDIR
     'shellmoduleloader.out.h',  # generated in $OBJDIR
     'unicode/locid.h',          # ICU
--- a/devtools/client/responsivedesign/test/browser.ini
+++ b/devtools/client/responsivedesign/test/browser.ini
@@ -13,8 +13,9 @@ skip-if = e10s && debug # Bug 1252201 - 
 [browser_responsiveruleview.js]
 skip-if = e10s && debug # Bug 1252201 - Docshell leak on debug e10s
 [browser_responsiveui.js]
 [browser_responsiveui_touch.js]
 [browser_responsiveuiaddcustompreset.js]
 [browser_responsive_devicewidth.js]
 [browser_responsiveui_customuseragent.js]
 [browser_responsiveui_window_close.js]
+skip-if = (os == 'linux') && e10s && debug # Bug 1277274
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -14,17 +14,16 @@ const { EnvironmentActor } = require("de
 const { FrameActor } = require("devtools/server/actors/frame");
 const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
 const { SourceActor, getSourceURL } = require("devtools/server/actors/source");
 const { DebuggerServer } = require("devtools/server/main");
 const { ActorClassWithSpec } = require("devtools/shared/protocol");
 const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 const { assert, dumpn, update, fetch } = DevToolsUtils;
 const promise = require("promise");
-const PromiseDebugging = require("PromiseDebugging");
 const xpcInspector = require("xpcInspector");
 const ScriptStore = require("./utils/ScriptStore");
 const { DevToolsWorker } = require("devtools/shared/worker/worker");
 const object = require("sdk/util/object");
 const { threadSpec } = require("devtools/shared/specs/script");
 
 const { defer, resolve, reject, all } = promise;
 
--- a/devtools/shared/worker/loader.js
+++ b/devtools/shared/worker/loader.js
@@ -312,22 +312,16 @@ function WorkerDebuggerLoader(options) {
 }
 
 this.WorkerDebuggerLoader = WorkerDebuggerLoader;
 
 // The following APIs rely on the use of Components, and the worker debugger
 // does not provide alternative definitions for them. Consequently, they are
 // stubbed out both on the main thread and worker threads.
 
-var PromiseDebugging = {
-  getState: function () {
-    throw new Error("PromiseDebugging is not available in workers!");
-  }
-};
-
 var chrome = {
   CC: undefined,
   Cc: undefined,
   ChromeWorker: undefined,
   Cm: undefined,
   Ci: undefined,
   Cu: undefined,
   Cr: undefined,
@@ -491,17 +485,16 @@ this.worker = new WorkerDebuggerLoader({
     "reportError": reportError,
     "rpc": rpc,
     "setImmediate": setImmediate,
     "URL": URL,
   },
   loadSubScript: loadSubScript,
   modules: {
     "Debugger": Debugger,
-    "PromiseDebugging": PromiseDebugging,
     "Services": Object.create(null),
     "chrome": chrome,
     "xpcInspector": xpcInspector
   },
   paths: {
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
     "": "resource://gre/modules/commonjs/",
     // ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠
--- a/docshell/test/browser/browser_timelineMarkers-frame-04.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-04.js
@@ -45,17 +45,21 @@ if (Services.prefs.getBoolPref("javascri
       markers = markers.filter(m => (m.name == "Javascript" &&
                                      m.causeName == "promise callback"));
       ok(markers.length > 0, "Found a Javascript marker");
 
       let frame = markers[0].stack;
       ok(frame.asyncParent !== null, "Parent frame has async parent");
       is(frame.asyncParent.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.asyncParent.functionDisplayName, "do_promise",
+      let asyncFrame = frame.asyncParent;
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted')
+        asyncFrame = asyncFrame.parent;
+      is(asyncFrame.functionDisplayName, "do_promise",
          "Async parent has correct function name");
     }
   }, {
     desc: "Async stack trace on Javascript marker with script",
     searchFor: (markers) => {
       return markers.some(m => (m.name == "Javascript" &&
                                 m.causeName == "promise callback"));
     },
@@ -66,15 +70,19 @@ if (Services.prefs.getBoolPref("javascri
       markers = markers.filter(m => (m.name == "Javascript" &&
                                      m.causeName == "promise callback"));
       ok(markers.length > 0, "Found a Javascript marker");
 
       let frame = markers[0].stack;
       ok(frame.asyncParent !== null, "Parent frame has async parent");
       is(frame.asyncParent.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.asyncParent.functionDisplayName, "do_promise_script",
+      let asyncFrame = frame.asyncParent;
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted')
+        asyncFrame = asyncFrame.parent;
+      is(asyncFrame.functionDisplayName, "do_promise_script",
          "Async parent has correct function name");
     }
   });
 }
 
 timelineContentTest(TESTS);
--- a/docshell/test/browser/browser_timelineMarkers-frame-05.js
+++ b/docshell/test/browser/browser_timelineMarkers-frame-05.js
@@ -86,20 +86,32 @@ if (Services.prefs.getBoolPref("javascri
     searchFor: "ConsoleTime",
     setup: function(docShell) {
       let resolver = makePromise();
       resolvePromise(resolver);
     },
     check: function(markers) {
       markers = markers.filter(m => m.name == "ConsoleTime");
       ok(markers.length > 0, "Promise marker includes stack");
-
+      ok(markers[0].stack.functionDisplayName == "testConsoleTime",
+         "testConsoleTime is on the stack");
       let frame = markers[0].endStack;
-      ok(frame.parent.asyncParent !== null, "Parent frame has async parent");
-      is(frame.parent.asyncParent.asyncCause, "promise callback",
+      ok(frame.functionDisplayName == "testConsoleTimeEnd",
+         "testConsoleTimeEnd is on the stack");
+
+      frame = frame.parent;
+      ok(frame.functionDisplayName == "makePromise/<",
+         "makePromise/< is on the stack");
+      let asyncFrame = frame.asyncParent;
+      ok(asyncFrame !== null, "Frame has async parent");
+      is(asyncFrame.asyncCause, "promise callback",
          "Async parent has correct cause");
-      is(frame.parent.asyncParent.functionDisplayName, "makePromise",
+      // Skip over self-hosted parts of our Promise implementation.
+      while (asyncFrame.source === 'self-hosted') {
+        asyncFrame = asyncFrame.parent;
+      }
+      is(asyncFrame.functionDisplayName, "makePromise",
          "Async parent has correct function name");
     }
   });
 }
 
 timelineContentTest(TESTS);
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1454,19 +1454,16 @@ nsIDocument::nsIDocument()
     mGetUserFontSetCalled(false),
     mPostedFlushUserFontSet(false),
     mPartID(0),
     mDidFireDOMContentLoaded(true),
     mHasScrollLinkedEffect(false),
     mUserHasInteracted(false)
 {
   SetIsDocument();
-  if (IsStyledByServo()) {
-    SetFlags(NODE_IS_DIRTY_FOR_SERVO | NODE_HAS_DIRTY_DESCENDANTS_FOR_SERVO);
-  }
 
   PR_INIT_CLIST(&mDOMMediaQueryLists);
 }
 
 // NOTE! nsDocument::operator new() zeroes out all members, so don't
 // bother initializing members to 0.
 
 nsDocument::nsDocument(const char* aContentType)
--- a/dom/bindings/BindingUtils.cpp
+++ b/dom/bindings/BindingUtils.cpp
@@ -138,53 +138,64 @@ ThrowNoSetterArg(JSContext* aCx, prototy
 {
   nsPrintfCString errorMessage("%s attribute setter",
                                NamesOfInterfacesWithProtos(aProtoId));
   return ThrowErrorMessage(aCx, MSG_MISSING_ARGUMENTS, errorMessage.get());
 }
 
 } // namespace dom
 
-struct ErrorResult::Message {
-  Message() { MOZ_COUNT_CTOR(ErrorResult::Message); }
-  ~Message() { MOZ_COUNT_DTOR(ErrorResult::Message); }
+namespace binding_danger {
+
+template<typename CleanupPolicy>
+struct TErrorResult<CleanupPolicy>::Message {
+  Message() { MOZ_COUNT_CTOR(TErrorResult::Message); }
+  ~Message() { MOZ_COUNT_DTOR(TErrorResult::Message); }
 
   nsTArray<nsString> mArgs;
   dom::ErrNum mErrorNumber;
 
   bool HasCorrectNumberOfArguments()
   {
     return GetErrorArgCount(mErrorNumber) == mArgs.Length();
   }
 };
 
+template<typename CleanupPolicy>
 nsTArray<nsString>&
-ErrorResult::CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType)
+TErrorResult<CleanupPolicy>::CreateErrorMessageHelper(const dom::ErrNum errorNumber,
+                                                      nsresult errorType)
 {
+  AssertInOwningThread();
   mResult = errorType;
 
   mMessage = new Message();
   mMessage->mErrorNumber = errorNumber;
   return mMessage->mArgs;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SerializeMessage(IPC::Message* aMsg) const
+TErrorResult<CleanupPolicy>::SerializeMessage(IPC::Message* aMsg) const
 {
   using namespace IPC;
+  AssertInOwningThread();
   MOZ_ASSERT(mUnionState == HasMessage);
   MOZ_ASSERT(mMessage);
   WriteParam(aMsg, mMessage->mArgs);
   WriteParam(aMsg, mMessage->mErrorNumber);
 }
 
+template<typename CleanupPolicy>
 bool
-ErrorResult::DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter)
+TErrorResult<CleanupPolicy>::DeserializeMessage(const IPC::Message* aMsg,
+                                                PickleIterator* aIter)
 {
   using namespace IPC;
+  AssertInOwningThread();
   nsAutoPtr<Message> readMessage(new Message());
   if (!ReadParam(aMsg, aIter, &readMessage->mArgs) ||
       !ReadParam(aMsg, aIter, &readMessage->mErrorNumber)) {
     return false;
   }
   if (!readMessage->HasCorrectNumberOfArguments()) {
     return false;
   }
@@ -192,19 +203,21 @@ ErrorResult::DeserializeMessage(const IP
   MOZ_ASSERT(mUnionState == HasNothing);
   mMessage = readMessage.forget();
 #ifdef DEBUG
   mUnionState = HasMessage;
 #endif // DEBUG
   return true;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingExceptionWithMessage(JSContext* aCx)
+TErrorResult<CleanupPolicy>::SetPendingExceptionWithMessage(JSContext* aCx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMessage, "SetPendingExceptionWithMessage() can be called only once");
   MOZ_ASSERT(mUnionState == HasMessage);
 
   Message* message = mMessage;
   MOZ_RELEASE_ASSERT(message->HasCorrectNumberOfArguments());
   const uint32_t argCount = message->mArgs.Length();
   const char16_t* args[JS::MaxNumErrorArguments + 1];
   for (uint32_t i = 0; i < argCount; ++i) {
@@ -215,55 +228,61 @@ ErrorResult::SetPendingExceptionWithMess
   JS_ReportErrorNumberUCArray(aCx, dom::GetErrorMessage, nullptr,
                               static_cast<const unsigned>(message->mErrorNumber),
                               argCount > 0 ? args : nullptr);
 
   ClearMessage();
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearMessage()
+TErrorResult<CleanupPolicy>::ClearMessage()
 {
+  AssertInOwningThread();
   MOZ_ASSERT(IsErrorWithMessage());
   delete mMessage;
   mMessage = nullptr;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn)
+TErrorResult<CleanupPolicy>::ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to throw a JS exception?");
 
   ClearUnionData();
 
   // Make sure mJSException is initialized _before_ we try to root it.  But
   // don't set it to exn yet, because we don't want to do that until after we
   // root.
   mJSException.setUndefined();
-  if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) {
+  if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) {
     // Don't use NS_ERROR_DOM_JS_EXCEPTION, because that indicates we have
     // in fact rooted mJSException.
     mResult = NS_ERROR_OUT_OF_MEMORY;
   } else {
     mJSException = exn;
     mResult = NS_ERROR_DOM_JS_EXCEPTION;
 #ifdef DEBUG
     mUnionState = HasJSException;
 #endif // DEBUG
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingJSException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingJSException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(!mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to handle JS exceptions?");
   MOZ_ASSERT(mUnionState == HasJSException);
 
   JS::Rooted<JS::Value> exception(cx, mJSException);
   if (JS_WrapValue(cx, &exception)) {
     JS_SetPendingException(cx, exception);
   }
@@ -273,140 +292,160 @@ ErrorResult::SetPendingJSException(JSCon
   js::RemoveRawValueRoot(cx, &mJSException);
 
   mResult = NS_OK;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
-struct ErrorResult::DOMExceptionInfo {
+template<typename CleanupPolicy>
+struct TErrorResult<CleanupPolicy>::DOMExceptionInfo {
   DOMExceptionInfo(nsresult rv, const nsACString& message)
     : mMessage(message)
     , mRv(rv)
   {}
 
   nsCString mMessage;
   nsresult mRv;
 };
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SerializeDOMExceptionInfo(IPC::Message* aMsg) const
+TErrorResult<CleanupPolicy>::SerializeDOMExceptionInfo(IPC::Message* aMsg) const
 {
   using namespace IPC;
+  AssertInOwningThread();
   MOZ_ASSERT(mDOMExceptionInfo);
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo);
   WriteParam(aMsg, mDOMExceptionInfo->mMessage);
   WriteParam(aMsg, mDOMExceptionInfo->mRv);
 }
 
+template<typename CleanupPolicy>
 bool
-ErrorResult::DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter)
+TErrorResult<CleanupPolicy>::DeserializeDOMExceptionInfo(const IPC::Message* aMsg,
+                                                         PickleIterator* aIter)
 {
   using namespace IPC;
+  AssertInOwningThread();
   nsCString message;
   nsresult rv;
   if (!ReadParam(aMsg, aIter, &message) ||
       !ReadParam(aMsg, aIter, &rv)) {
     return false;
   }
 
   MOZ_ASSERT(mUnionState == HasNothing);
   MOZ_ASSERT(IsDOMException());
   mDOMExceptionInfo = new DOMExceptionInfo(rv, message);
 #ifdef DEBUG
   mUnionState = HasDOMExceptionInfo;
 #endif // DEBUG
   return true;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ThrowDOMException(nsresult rv, const nsACString& message)
+TErrorResult<CleanupPolicy>::ThrowDOMException(nsresult rv,
+                                               const nsACString& message)
 {
+  AssertInOwningThread();
   ClearUnionData();
 
   mResult = NS_ERROR_DOM_DOMEXCEPTION;
   mDOMExceptionInfo = new DOMExceptionInfo(rv, message);
 #ifdef DEBUG
   mUnionState = HasDOMExceptionInfo;
 #endif
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingDOMException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingDOMException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mDOMExceptionInfo,
              "SetPendingDOMException() can be called only once");
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo);
 
   dom::Throw(cx, mDOMExceptionInfo->mRv, mDOMExceptionInfo->mMessage);
 
   ClearDOMExceptionInfo();
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearDOMExceptionInfo()
+TErrorResult<CleanupPolicy>::ClearDOMExceptionInfo()
 {
+  AssertInOwningThread();
   MOZ_ASSERT(IsDOMException());
   MOZ_ASSERT(mUnionState == HasDOMExceptionInfo || !mDOMExceptionInfo);
   delete mDOMExceptionInfo;
   mDOMExceptionInfo = nullptr;
 #ifdef DEBUG
   mUnionState = HasNothing;
 #endif // DEBUG
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::ClearUnionData()
+TErrorResult<CleanupPolicy>::ClearUnionData()
 {
+  AssertInOwningThread();
   if (IsJSException()) {
     JSContext* cx = nsContentUtils::RootingCx();
     MOZ_ASSERT(cx);
     mJSException.setUndefined();
     js::RemoveRawValueRoot(cx, &mJSException);
 #ifdef DEBUG
     mUnionState = HasNothing;
 #endif // DEBUG
   } else if (IsErrorWithMessage()) {
     ClearMessage();
   } else if (IsDOMException()) {
     ClearDOMExceptionInfo();
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingGenericErrorException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingGenericErrorException(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(!IsErrorWithMessage());
   MOZ_ASSERT(!IsJSException());
   MOZ_ASSERT(!IsDOMException());
   dom::Throw(cx, ErrorCode());
   mResult = NS_OK;
 }
 
-ErrorResult&
-ErrorResult::operator=(ErrorResult&& aRHS)
+template<typename CleanupPolicy>
+TErrorResult<CleanupPolicy>&
+TErrorResult<CleanupPolicy>::operator=(TErrorResult<CleanupPolicy>&& aRHS)
 {
+  AssertInOwningThread();
+  aRHS.AssertInOwningThread();
   // Clear out any union members we may have right now, before we
   // start writing to it.
   ClearUnionData();
 
 #ifdef DEBUG
   mMightHaveUnreportedJSException = aRHS.mMightHaveUnreportedJSException;
   aRHS.mMightHaveUnreportedJSException = false;
 #endif
   if (aRHS.IsErrorWithMessage()) {
     mMessage = aRHS.mMessage;
     aRHS.mMessage = nullptr;
   } else if (aRHS.IsJSException()) {
     JSContext* cx = nsContentUtils::RootingCx();
     MOZ_ASSERT(cx);
     mJSException.setUndefined();
-    if (!js::AddRawValueRoot(cx, &mJSException, "ErrorResult::mJSException")) {
+    if (!js::AddRawValueRoot(cx, &mJSException, "TErrorResult::mJSException")) {
       MOZ_CRASH("Could not root mJSException, we're about to OOM");
     }
     mJSException = aRHS.mJSException;
     aRHS.mJSException.setUndefined();
     js::RemoveRawValueRoot(cx, &aRHS.mJSException);
   } else if (aRHS.IsDOMException()) {
     mDOMExceptionInfo = aRHS.mDOMExceptionInfo;
     aRHS.mDOMExceptionInfo = nullptr;
@@ -422,19 +461,23 @@ ErrorResult::operator=(ErrorResult&& aRH
 
   // Note: It's important to do this last, since this affects the condition
   // checks above!
   mResult = aRHS.mResult;
   aRHS.mResult = NS_OK;
   return *this;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::CloneTo(ErrorResult& aRv) const
+TErrorResult<CleanupPolicy>::CloneTo(TErrorResult& aRv) const
 {
+  AssertInOwningThread();
+  aRv.AssertInOwningThread();
+
   aRv.ClearUnionData();
   aRv.mResult = mResult;
 #ifdef DEBUG
   aRv.mMightHaveUnreportedJSException = mMightHaveUnreportedJSException;
 #endif
 
   if (IsErrorWithMessage()) {
 #ifdef DEBUG
@@ -454,29 +497,33 @@ ErrorResult::CloneTo(ErrorResult& aRv) c
     aRv.mUnionState = HasJSException;
 #endif
     JSContext* cx = nsContentUtils::RootingCx();
     JS::Rooted<JS::Value> exception(cx, mJSException);
     aRv.ThrowJSException(cx, exception);
   }
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SuppressException()
+TErrorResult<CleanupPolicy>::SuppressException()
 {
+  AssertInOwningThread();
   WouldReportJSException();
   ClearUnionData();
   // We don't use AssignErrorCode, because we want to override existing error
   // states, which AssignErrorCode is not allowed to do.
   mResult = NS_OK;
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::SetPendingException(JSContext* cx)
+TErrorResult<CleanupPolicy>::SetPendingException(JSContext* cx)
 {
+  AssertInOwningThread();
   if (IsUncatchableException()) {
     // Nuke any existing exception on cx, to make sure we're uncatchable.
     JS_ClearPendingException(cx);
     // Don't do any reporting.  Just return, to create an
     // uncatchable exception.
     mResult = NS_OK;
     return;
   }
@@ -496,42 +543,52 @@ ErrorResult::SetPendingException(JSConte
   }
   if (IsDOMException()) {
     SetPendingDOMException(cx);
     return;
   }
   SetPendingGenericErrorException(cx);
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::StealExceptionFromJSContext(JSContext* cx)
+TErrorResult<CleanupPolicy>::StealExceptionFromJSContext(JSContext* cx)
 {
+  AssertInOwningThread();
   MOZ_ASSERT(mMightHaveUnreportedJSException,
              "Why didn't you tell us you planned to throw a JS exception?");
 
   JS::Rooted<JS::Value> exn(cx);
   if (!JS_GetPendingException(cx, &exn)) {
     ThrowUncatchableException();
     return;
   }
 
   ThrowJSException(cx, exn);
   JS_ClearPendingException(cx);
 }
 
+template<typename CleanupPolicy>
 void
-ErrorResult::NoteJSContextException(JSContext* aCx)
+TErrorResult<CleanupPolicy>::NoteJSContextException(JSContext* aCx)
 {
+  AssertInOwningThread();
   if (JS_IsExceptionPending(aCx)) {
     mResult = NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT;
   } else {
     mResult = NS_ERROR_UNCATCHABLE_EXCEPTION;
   }
 }
 
+template class TErrorResult<JustAssertCleanupPolicy>;
+template class TErrorResult<AssertAndSuppressCleanupPolicy>;
+template class TErrorResult<JustSuppressCleanupPolicy>;
+
+} // namespace binding_danger
+
 namespace dom {
 
 bool
 DefineConstants(JSContext* cx, JS::Handle<JSObject*> obj,
                 const ConstantSpec* cs)
 {
   JS::Rooted<JS::Value> value(cx);
   for (; cs->name; ++cs) {
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -101,17 +101,17 @@ IsNonProxyDOMClass(const js::Class* clas
 }
 
 inline bool
 IsNonProxyDOMClass(const JSClass* clasp)
 {
   return IsNonProxyDOMClass(js::Valueify(clasp));
 }
 
-// Returns true if the JSClass is used for DOM interface and interface 
+// Returns true if the JSClass is used for DOM interface and interface
 // prototype objects.
 inline bool
 IsDOMIfaceAndProtoClass(const JSClass* clasp)
 {
   return clasp->flags & JSCLASS_IS_DOMIFACEANDPROTOJSCLASS;
 }
 
 inline bool
@@ -2006,16 +2006,22 @@ private:
                     "Offset of mLength should match");
       static_assert(offsetof(FakeString, mFlags) ==
                       offsetof(StringAsserter, mFlags),
                     "Offset of mFlags should match");
     }
   };
 };
 
+class FastErrorResult :
+    public mozilla::binding_danger::TErrorResult<
+      mozilla::binding_danger::JustAssertCleanupPolicy>
+{
+};
+
 } // namespace binding_detail
 
 enum StringificationBehavior {
   eStringify,
   eEmpty,
   eNull
 };
 
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -5207,32 +5207,32 @@ def getJSToNativeConversionInfo(type, de
                     globalObj = js::GetGlobalForObjectCrossCompartment(unwrappedVal);
                     """,
                     sourceDescription=sourceDescription)
             else:
                 getPromiseGlobal = ""
 
             templateBody = fill(
                 """
-                { // Scope for our GlobalObject, ErrorResult, JSAutoCompartment,
+                { // Scope for our GlobalObject, FastErrorResult, JSAutoCompartment,
                   // etc.
 
                   JS::Rooted<JSObject*> globalObj(cx, JS::CurrentGlobalOrNull(cx));
                   $*{getPromiseGlobal}
                   JSAutoCompartment ac(cx, globalObj);
                   GlobalObject promiseGlobal(cx, globalObj);
                   if (promiseGlobal.Failed()) {
                     $*{exceptionCode}
                   }
 
                   JS::Rooted<JS::Value> valueToResolve(cx, $${val});
                   if (!JS_WrapValue(cx, &valueToResolve)) {
                     $*{exceptionCode}
                   }
-                  ErrorResult promiseRv;
+                  binding_detail::FastErrorResult promiseRv;
                 #ifdef SPIDERMONKEY_PROMISE
                   nsCOMPtr<nsIGlobalObject> global =
                     do_QueryInterface(promiseGlobal.GetAsSupports());
                   if (!global) {
                     promiseRv.Throw(NS_ERROR_UNEXPECTED);
                     promiseRv.MaybeSetPendingException(cx);
                     $*{exceptionCode}
                   }
@@ -6917,17 +6917,17 @@ class CGCallGenerator(CGThing):
         elif result is not None:
             assert resultOutParam is None
             call = CGWrapper(call, pre=resultVar + " = ")
 
         call = CGWrapper(call, post=";\n")
         self.cgRoot.append(call)
 
         if isFallible:
-            self.cgRoot.prepend(CGGeneric("ErrorResult rv;\n"))
+            self.cgRoot.prepend(CGGeneric("binding_detail::FastErrorResult rv;\n"))
             self.cgRoot.append(CGGeneric(dedent(
                 """
                 if (MOZ_UNLIKELY(rv.MaybeSetPendingException(cx))) {
                   return false;
                 }
                 """)))
 
         self.cgRoot.append(CGGeneric("MOZ_ASSERT(!JS_IsExceptionPending(cx));\n"))
@@ -8432,17 +8432,17 @@ class CGEnumerateHook(CGAbstractBindingM
         # Our "self" is actually the "obj" argument in this case, not the thisval.
         CGAbstractBindingMethod.__init__(
             self, descriptor, ENUMERATE_HOOK_NAME,
             args, getThisObj="", callArgs="")
 
     def generate_code(self):
         return CGGeneric(dedent("""
             AutoTArray<nsString, 8> names;
-            ErrorResult rv;
+            binding_detail::FastErrorResult rv;
             self->GetOwnPropertyNames(cx, names, rv);
             if (rv.MaybeSetPendingException(cx)) {
               return false;
             }
             bool dummy;
             for (uint32_t i = 0; i < names.Length(); ++i) {
               if (!JS_HasUCProperty(cx, obj, names[i].get(), names[i].Length(), &dummy)) {
                 return false;
@@ -10525,17 +10525,17 @@ class CGEnumerateOwnPropertiesViaGetOwnP
         CGAbstractBindingMethod.__init__(self, descriptor,
                                          "EnumerateOwnPropertiesViaGetOwnPropertyNames",
                                          args, getThisObj="",
                                          callArgs="")
 
     def generate_code(self):
         return CGGeneric(dedent("""
             AutoTArray<nsString, 8> names;
-            ErrorResult rv;
+            binding_detail::FastErrorResult rv;
             self->GetOwnPropertyNames(cx, names, rv);
             if (rv.MaybeSetPendingException(cx)) {
               return false;
             }
             // OK to pass null as "proxy" because it's ignored if
             // shadowPrototypeProperties is true
             return AppendNamedPropertyIds(cx, nullptr, names, true, props);
             """))
--- a/dom/bindings/ErrorResult.h
+++ b/dom/bindings/ErrorResult.h
@@ -1,41 +1,45 @@
 /* -*- 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/. */
 
 /**
- * A struct for tracking exceptions that need to be thrown to JS.
+ * A set of structs for tracking exceptions that need to be thrown to JS:
+ * ErrorResult and IgnoredErrorResult.
  *
- * Conceptually, an ErrorResult represents either success or an exception in the
+ * Conceptually, these structs represent either success or an exception in the
  * process of being thrown.  This means that a failing ErrorResult _must_ be
  * handled in one of the following ways before coming off the stack:
  *
  * 1) Suppressed via SuppressException().
  * 2) Converted to a pure nsresult return value via StealNSResult().
  * 3) Converted to an actual pending exception on a JSContext via
  *    MaybeSetPendingException.
  * 4) Converted to an exception JS::Value (probably to then reject a Promise
  *    with) via dom::ToJSValue.
+ *
+ * An IgnoredErrorResult will automatically do the first of those four things.
  */
 
 #ifndef mozilla_ErrorResult_h
 #define mozilla_ErrorResult_h
 
 #include <stdarg.h>
 
 #include "js/GCAnnotations.h"
 #include "js/Value.h"
 #include "nscore.h"
 #include "nsStringGlue.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Move.h"
 #include "nsTArray.h"
+#include "nsISupportsImpl.h"
 
 namespace IPC {
 class Message;
 template <typename> struct ParamTraits;
 } // namespace IPC
 class PickleIterator;
 
 namespace mozilla {
@@ -82,82 +86,107 @@ struct StringArrayAppender
     }
     aArgs.AppendElement(aFirst);
     Append(aArgs, aCount - 1, Forward<Ts>(aOtherArgs)...);
   }
 };
 
 } // namespace dom
 
-class ErrorResult {
+class ErrorResult;
+
+namespace binding_danger {
+
+/**
+ * Templated implementation class for various ErrorResult-like things.  The
+ * instantiations differ only in terms of their cleanup policies (used in the
+ * destructor), which they can specify via the template argument.  Note that
+ * this means it's safe to reinterpret_cast between the instantiations unless
+ * you plan to invoke the destructor through such a cast pointer.
+ *
+ * A cleanup policy consists of two booleans: whether to assert that we've been
+ * reported or suppressed, and whether to then go ahead and suppress the
+ * exception.
+ */
+template<typename CleanupPolicy>
+class TErrorResult {
 public:
-  ErrorResult()
+  TErrorResult()
     : mResult(NS_OK)
 #ifdef DEBUG
     , mMightHaveUnreportedJSException(false)
     , mUnionState(HasNothing)
 #endif
   {
   }
 
-#ifdef DEBUG
-  ~ErrorResult() {
-    // Consumers should have called one of MaybeSetPendingException
-    // (possibly via ToJSValue), StealNSResult, and SuppressException
-    MOZ_ASSERT(!Failed());
-    MOZ_ASSERT(!mMightHaveUnreportedJSException);
-    MOZ_ASSERT(mUnionState == HasNothing);
+  ~TErrorResult() {
+    AssertInOwningThread();
+
+    if (CleanupPolicy::assertHandled) {
+      // Consumers should have called one of MaybeSetPendingException
+      // (possibly via ToJSValue), StealNSResult, and SuppressException
+      AssertReportedOrSuppressed();
+    }
+
+    if (CleanupPolicy::suppress) {
+      SuppressException();
+    }
+
+    // And now assert that we're in a good final state.
+    AssertReportedOrSuppressed();
   }
-#endif // DEBUG
 
-  ErrorResult(ErrorResult&& aRHS)
+  TErrorResult(TErrorResult&& aRHS)
     // Initialize mResult and whatever else we need to default-initialize, so
     // the ClearUnionData call in our operator= will do the right thing
     // (nothing).
-    : ErrorResult()
+    : TErrorResult()
   {
     *this = Move(aRHS);
   }
-  ErrorResult& operator=(ErrorResult&& aRHS);
+  TErrorResult& operator=(TErrorResult&& aRHS);
 
-  explicit ErrorResult(nsresult aRv)
-    : ErrorResult()
+  explicit TErrorResult(nsresult aRv)
+    : TErrorResult()
   {
     AssignErrorCode(aRv);
   }
 
+  operator ErrorResult&();
+
   void Throw(nsresult rv) {
     MOZ_ASSERT(NS_FAILED(rv), "Please don't try throwing success");
     AssignErrorCode(rv);
   }
 
-  // Duplicate our current state on the given ErrorResult object.  Any existing
-  // errors or messages on the target will be suppressed before cloning.  Our
-  // own error state remains unchanged.
-  void CloneTo(ErrorResult& aRv) const;
+  // Duplicate our current state on the given TErrorResult object.  Any
+  // existing errors or messages on the target will be suppressed before
+  // cloning.  Our own error state remains unchanged.
+  void CloneTo(TErrorResult& aRv) const;
 
   // Use SuppressException when you want to suppress any exception that might be
-  // on the ErrorResult.  After this call, the ErrorResult will be back a "no
+  // on the TErrorResult.  After this call, the TErrorResult will be back a "no
   // exception thrown" state.
   void SuppressException();
 
-  // Use StealNSResult() when you want to safely convert the ErrorResult to an
-  // nsresult that you will then return to a caller.  This will
+  // Use StealNSResult() when you want to safely convert the TErrorResult to
+  // an nsresult that you will then return to a caller.  This will
   // SuppressException(), since there will no longer be a way to report it.
   nsresult StealNSResult() {
     nsresult rv = ErrorCode();
     SuppressException();
     return rv;
   }
 
-  // Use MaybeSetPendingException to convert an ErrorResult to a pending
+  // Use MaybeSetPendingException to convert a TErrorResult to a pending
   // exception on the given JSContext.  This is the normal "throw an exception"
   // codepath.
   //
-  // The return value is false if the ErrorResult represents success, true
+  // The return value is false if the TErrorResult represents success, true
   // otherwise.  This does mean that in JSAPI method implementations you can't
   // just use this as |return rv.MaybeSetPendingException(cx)| (though you could
   // |return !rv.MaybeSetPendingException(cx)|), but in practice pretty much any
   // consumer would want to do some more work on the success codepath.  So
   // instead the way you use this is:
   //
   //   if (rv.MaybeSetPendingException(cx)) {
   //     bail out here
@@ -168,31 +197,31 @@ public:
   // want to pay the price of a function call in some of the consumers of this
   // method in the common case.
   //
   // Note that a true return value does NOT mean there is now a pending
   // exception on aCx, due to uncatchable exceptions.  It should still be
   // considered equivalent to a JSAPI failure in terms of what callers should do
   // after true is returned.
   //
-  // After this call, the ErrorResult will no longer return true from Failed(),
+  // After this call, the TErrorResult will no longer return true from Failed(),
   // since the exception will have moved to the JSContext.
   bool MaybeSetPendingException(JSContext* cx)
   {
     WouldReportJSException();
     if (!Failed()) {
       return false;
     }
 
     SetPendingException(cx);
     return true;
   }
 
   // Use StealExceptionFromJSContext to convert a pending exception on a
-  // JSContext to an ErrorResult.  This function must be called only when a
+  // JSContext to a TErrorResult.  This function must be called only when a
   // JSAPI operation failed.  It assumes that lack of pending exception on the
   // JSContext means an uncatchable exception was thrown.
   //
   // Codepaths that might call this method must call MightThrowJSException even
   // if the relevant JSAPI calls do not fail.
   //
   // When this function returns, JS_IsExceptionPending(cx) will definitely be
   // false.
@@ -210,42 +239,42 @@ public:
   {
     ThrowErrorWithMessage<errorNumber>(NS_ERROR_RANGE_ERR,
                                        Forward<Ts>(messageArgs)...);
   }
 
   bool IsErrorWithMessage() const { return ErrorCode() == NS_ERROR_TYPE_ERR || ErrorCode() == NS_ERROR_RANGE_ERR; }
 
   // Facilities for throwing a preexisting JS exception value via this
-  // ErrorResult.  The contract is that any code which might end up calling
+  // TErrorResult.  The contract is that any code which might end up calling
   // ThrowJSException() or StealExceptionFromJSContext() must call
   // MightThrowJSException() even if no exception is being thrown.  Code that
-  // conditionally calls ToJSValue on this ErrorResult only if Failed() must
-  // first call WouldReportJSException even if this ErrorResult has not failed.
+  // conditionally calls ToJSValue on this TErrorResult only if Failed() must
+  // first call WouldReportJSException even if this TErrorResult has not failed.
   //
   // The exn argument to ThrowJSException can be in any compartment.  It does
   // not have to be in the compartment of cx.  If someone later uses it, they
   // will wrap it into whatever compartment they're working in, as needed.
   void ThrowJSException(JSContext* cx, JS::Handle<JS::Value> exn);
   bool IsJSException() const { return ErrorCode() == NS_ERROR_DOM_JS_EXCEPTION; }
 
   // Facilities for throwing a DOMException.  If an empty message string is
   // passed to ThrowDOMException, the default message string for the given
   // nsresult will be used.  The passed-in string must be UTF-8.  The nsresult
   // passed in must be one we create DOMExceptions for; otherwise you may get an
   // XPConnect Exception.
   void ThrowDOMException(nsresult rv, const nsACString& message = EmptyCString());
   bool IsDOMException() const { return ErrorCode() == NS_ERROR_DOM_DOMEXCEPTION; }
 
-  // Flag on the ErrorResult that whatever needs throwing has been
+  // Flag on the TErrorResult that whatever needs throwing has been
   // thrown on the JSContext already and we should not mess with it.
   // If nothing was thrown, this becomes an uncatchable exception.
   void NoteJSContextException(JSContext* aCx);
 
-  // Check whether the ErrorResult says to just throw whatever is on
+  // Check whether the TErrorResult says to just throw whatever is on
   // the JSContext already.
   bool IsJSContextException() {
     return ErrorCode() == NS_ERROR_DOM_EXCEPTION_ON_JSCONTEXT;
   }
 
   // Support for uncatchable exceptions.
   void ThrowUncatchableException() {
     Throw(NS_ERROR_UNCATCHABLE_EXCEPTION);
@@ -301,24 +330,25 @@ private:
   enum UnionState {
     HasMessage,
     HasDOMExceptionInfo,
     HasJSException,
     HasNothing
   };
 #endif // DEBUG
 
+  friend struct IPC::ParamTraits<TErrorResult>;
   friend struct IPC::ParamTraits<ErrorResult>;
   void SerializeMessage(IPC::Message* aMsg) const;
   bool DeserializeMessage(const IPC::Message* aMsg, PickleIterator* aIter);
 
   void SerializeDOMExceptionInfo(IPC::Message* aMsg) const;
   bool DeserializeDOMExceptionInfo(const IPC::Message* aMsg, PickleIterator* aIter);
 
-  // Helper method that creates a new Message for this ErrorResult,
+  // Helper method that creates a new Message for this TErrorResult,
   // and returns the arguments array from that Message.
   nsTArray<nsString>& CreateErrorMessageHelper(const dom::ErrNum errorNumber, nsresult errorType);
 
   template<dom::ErrNum errorNumber, typename... Ts>
   void ThrowErrorWithMessage(nsresult errorType, Ts&&... messageArgs)
   {
 #if defined(DEBUG) && (defined(__clang__) || defined(__GNUC__))
     static_assert(dom::ErrorFormatNumArgs[errorNumber] == sizeof...(messageArgs),
@@ -331,16 +361,22 @@ private:
     uint16_t argCount = dom::GetErrorArgCount(errorNumber);
     dom::StringArrayAppender::Append(messageArgsArray, argCount,
                                      Forward<Ts>(messageArgs)...);
 #ifdef DEBUG
     mUnionState = HasMessage;
 #endif // DEBUG
   }
 
+  MOZ_ALWAYS_INLINE void AssertInOwningThread() const {
+#ifdef DEBUG
+    NS_ASSERT_OWNINGTHREAD(TErrorResult);
+#endif
+  }
+
   void AssignErrorCode(nsresult aRv) {
     MOZ_ASSERT(aRv != NS_ERROR_TYPE_ERR, "Use ThrowTypeError()");
     MOZ_ASSERT(aRv != NS_ERROR_RANGE_ERR, "Use ThrowRangeError()");
     MOZ_ASSERT(!IsErrorWithMessage(), "Don't overwrite errors with message");
     MOZ_ASSERT(aRv != NS_ERROR_DOM_JS_EXCEPTION, "Use ThrowJSException()");
     MOZ_ASSERT(!IsJSException(), "Don't overwrite JS exceptions");
     MOZ_ASSERT(aRv != NS_ERROR_DOM_DOMEXCEPTION, "Use ThrowDOMException()");
     MOZ_ASSERT(!IsDOMException(), "Don't overwrite DOM exceptions");
@@ -366,16 +402,22 @@ private:
   void SetPendingException(JSContext* cx);
 
   // Methods for setting various specific kinds of pending exceptions.
   void SetPendingExceptionWithMessage(JSContext* cx);
   void SetPendingJSException(JSContext* cx);
   void SetPendingDOMException(JSContext* cx);
   void SetPendingGenericErrorException(JSContext* cx);
 
+  MOZ_ALWAYS_INLINE void AssertReportedOrSuppressed()
+  {
+    MOZ_ASSERT(!Failed());
+    MOZ_ASSERT(!mMightHaveUnreportedJSException);
+    MOZ_ASSERT(mUnionState == HasNothing);
+  }
 
   // Special values of mResult:
   // NS_ERROR_TYPE_ERR -- ThrowTypeError() called on us.
   // NS_ERROR_RANGE_ERR -- ThrowRangeError() called on us.
   // NS_ERROR_DOM_JS_EXCEPTION -- ThrowJSException() called on us.
   // NS_ERROR_UNCATCHABLE_EXCEPTION -- ThrowUncatchableException called on us.
   // NS_ERROR_DOM_DOMEXCEPTION -- ThrowDOMException() called on us.
   nsresult mResult;
@@ -399,32 +441,95 @@ private:
   // for assertion purposes.
   bool mMightHaveUnreportedJSException;
 
   // Used to keep track of what's stored in our union right now.  Note
   // that this may be set to HasNothing even if our mResult suggests
   // we should have something, if we have already cleaned up the
   // something.
   UnionState mUnionState;
+
+  // The thread that created this TErrorResult
+  NS_DECL_OWNINGTHREAD;
 #endif
 
   // Not to be implemented, to make sure people always pass this by
   // reference, not by value.
+  TErrorResult(const TErrorResult&) = delete;
+  void operator=(const TErrorResult&) = delete;
+};
+
+struct JustAssertCleanupPolicy {
+  static const bool assertHandled = true;
+  static const bool suppress = false;
+};
+
+struct AssertAndSuppressCleanupPolicy {
+  static const bool assertHandled = true;
+  static const bool suppress = true;
+};
+
+struct JustSuppressCleanupPolicy {
+  static const bool assertHandled = false;
+  static const bool suppress = true;
+};
+
+} // namespace binding_danger
+
+// A class people should normally use on the stack when they plan to actually
+// do something with the exception.
+class ErrorResult :
+    public binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy>
+{
+  typedef binding_danger::TErrorResult<binding_danger::AssertAndSuppressCleanupPolicy> BaseErrorResult;
+
+public:
+  ErrorResult()
+    : BaseErrorResult()
+  {}
+
+  ErrorResult(ErrorResult&& aRHS)
+    : BaseErrorResult(Move(aRHS))
+  {}
+
+  explicit ErrorResult(nsresult aRv)
+    : BaseErrorResult(aRv)
+  {}
+
+  void operator=(nsresult rv)
+  {
+    BaseErrorResult::operator=(rv);
+  }
+
+  ErrorResult& operator=(ErrorResult&& aRHS)
+  {
+    BaseErrorResult::operator=(Move(aRHS));
+    return *this;
+  }
+
+private:
+  // Not to be implemented, to make sure people always pass this by
+  // reference, not by value.
   ErrorResult(const ErrorResult&) = delete;
   void operator=(const ErrorResult&) = delete;
 };
 
-// A class for use when an ErrorResult should just automatically be ignored.
-class IgnoredErrorResult : public ErrorResult
+template<typename CleanupPolicy>
+binding_danger::TErrorResult<CleanupPolicy>::operator ErrorResult&()
 {
-public:
-  ~IgnoredErrorResult()
-  {
-    SuppressException();
-  }
+  return *static_cast<ErrorResult*>(
+     reinterpret_cast<TErrorResult<AssertAndSuppressCleanupPolicy>*>(this));
+}
+
+// A class for use when an ErrorResult should just automatically be ignored.
+// This doesn't inherit from ErrorResult so we don't make two separate calls to
+// SuppressException.
+class IgnoredErrorResult :
+    public binding_danger::TErrorResult<binding_danger::JustSuppressCleanupPolicy>
+{
 };
 
 /******************************************************************************
  ** Macros for checking results
  ******************************************************************************/
 
 #define ENSURE_SUCCESS(res, ret)                                          \
   do {                                                                    \
--- a/dom/bindings/test/test_promise_rejections_from_jsimplemented.html
+++ b/dom/bindings/test/test_promise_rejections_from_jsimplemented.html
@@ -20,105 +20,114 @@ https://bugzilla.mozilla.org/show_bug.cg
     is(exn.name, name,
        "Should have the right exception name in test " + testNumber);
     is("filename" in exn ? exn.filename : exn.fileName, filename,
        "Should have the right file name in test " + testNumber);
     is(exn.message, message,
        "Should have the right message in test " + testNumber);
     is(exn.code, code, "Should have the right .code in test " + testNumber);
     if (message === "") {
-      is(exn.name, "NS_ERROR_UNEXPECTED",
+      is(exn.name, "InternalError",
          "Should have one of our synthetic exceptions in test " + testNumber);
     }
     is(exn.stack, stack, "Should have the right stack in test " + testNumber);
   }
 
   function ensurePromiseFail(testNumber, value) {
     ok(false, "Test " + testNumber + " should not have a fulfilled promise");
   }
 
   function doTest() {
     var t = new TestInterfaceJS();
     /* Async parent frames from pushPrefEnv don't show up in e10s.  */
     var isE10S = !SpecialPowers.isMainProcess();
     var asyncStack = SpecialPowers.getBoolPref("javascript.options.asyncstack");
     var ourFile = location.href;
-    var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:3
+    var unwrapError = "Promise rejection value is a non-unwrappable cross-compartment wrapper.";
+    var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:130:3
 ` : "";
 
     Promise.all([
       t.testPromiseWithThrowingChromePromiseInit().then(
           ensurePromiseFail.bind(null, 1),
-          checkExn.bind(null, 48, "NS_ERROR_UNEXPECTED", "", undefined,
-                        ourFile, 1,
-                        `doTest@${ourFile}:48:7
+          checkExn.bind(null, 49, "InternalError", unwrapError,
+                        undefined, ourFile, 1,
+                        `doTest@${ourFile}:49:7
 ` +
                         parentFrame)),
       t.testPromiseWithThrowingContentPromiseInit(function() {
           thereIsNoSuchContentFunction1();
         }).then(
           ensurePromiseFail.bind(null, 2),
-          checkExn.bind(null, 56, "ReferenceError",
+          checkExn.bind(null, 57, "ReferenceError",
                         "thereIsNoSuchContentFunction1 is not defined",
                         undefined, ourFile, 2,
-                        `doTest/<@${ourFile}:56:11
-doTest@${ourFile}:55:7
+                        `doTest/<@${ourFile}:57:11
+doTest@${ourFile}:56:7
 ` +
                         parentFrame)),
       t.testPromiseWithThrowingChromeThenFunction().then(
           ensurePromiseFail.bind(null, 3),
-          checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 3, "")),
+          checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 3, asyncStack ? (`Async*doTest@${ourFile}:67:7
+` +
+                        parentFrame) : "")),
       t.testPromiseWithThrowingContentThenFunction(function() {
           thereIsNoSuchContentFunction2();
         }).then(
           ensurePromiseFail.bind(null, 4),
-          checkExn.bind(null, 70, "ReferenceError",
+          checkExn.bind(null, 73, "ReferenceError",
                         "thereIsNoSuchContentFunction2 is not defined",
                         undefined, ourFile, 4,
-                        `doTest/<@${ourFile}:70:11
+                        `doTest/<@${ourFile}:73:11
 ` +
-                        (asyncStack ? `Async*doTest@${ourFile}:69:7
+                        (asyncStack ? `Async*doTest@${ourFile}:72:7
 ` : "") +
                         parentFrame)),
       t.testPromiseWithThrowingChromeThenable().then(
           ensurePromiseFail.bind(null, 5),
-          checkExn.bind(null, 0, "NS_ERROR_UNEXPECTED", "", undefined, "", 5, "")),
+          checkExn.bind(null, 0, "InternalError", unwrapError, undefined, "", 5, asyncStack ? (`Async*doTest@${ourFile}:84:7
+` +
+                        parentFrame) : "")),
       t.testPromiseWithThrowingContentThenable({
             then: function() { thereIsNoSuchContentFunction3(); }
         }).then(
           ensurePromiseFail.bind(null, 6),
-          checkExn.bind(null, 85, "ReferenceError",
+          checkExn.bind(null, 90, "ReferenceError",
                         "thereIsNoSuchContentFunction3 is not defined",
                         undefined, ourFile, 6,
-                        `doTest/<.then@${ourFile}:85:32
-`)),
+                        `doTest/<.then@${ourFile}:90:32
+` + (asyncStack ? `Async*doTest@${ourFile}:89:7\n` + parentFrame : ""))),
       t.testPromiseWithDOMExceptionThrowingPromiseInit().then(
           ensurePromiseFail.bind(null, 7),
-          checkExn.bind(null, 93, "NotFoundError",
+          checkExn.bind(null, 98, "NotFoundError",
                         "We are a second DOMException",
                         DOMException.NOT_FOUND_ERR, ourFile, 7,
-                        `doTest@${ourFile}:93:7
+                        `doTest@${ourFile}:98:7
 ` +
                         parentFrame)),
       t.testPromiseWithDOMExceptionThrowingThenFunction().then(
           ensurePromiseFail.bind(null, 8),
-          checkExn.bind(null, asyncStack ? 101 : 0, "NetworkError",
+          checkExn.bind(null, asyncStack ? 106 : 0, "NetworkError",
                          "We are a third DOMException",
                         DOMException.NETWORK_ERR, asyncStack ? ourFile : "", 8,
-                        (asyncStack ? `Async*doTest@${ourFile}:101:7
+                        (asyncStack ? `Async*doTest@${ourFile}:106:7
 ` +
                          parentFrame : ""))),
       t.testPromiseWithDOMExceptionThrowingThenable().then(
           ensurePromiseFail.bind(null, 9),
-          checkExn.bind(null, 0, "TypeMismatchError",
+          checkExn.bind(null, asyncStack ? 114 : 0, "TypeMismatchError",
                         "We are a fourth DOMException",
-                         DOMException.TYPE_MISMATCH_ERR, "", 9, "")),
+                        DOMException.TYPE_MISMATCH_ERR,
+                        asyncStack ? ourFile : "", 9,
+                        (asyncStack ? `Async*doTest@${ourFile}:114:7
+` +
+                         parentFrame : ""))),
     ]).then(SimpleTest.finish,
-            function() {
-              ok(false, "One of our catch statements totally failed");
+            function(err) {
+              ok(false, "One of our catch statements totally failed with err" + err + ', stack: ' + (err ? err.stack : ''));
               SimpleTest.finish();
             });
   }
 
   SpecialPowers.pushPrefEnv({set: [['dom.expose_test_interfaces', true]]},
                             doTest);
   </script>
 </head>
--- a/dom/filesystem/compat/tests/test_no_dnd.html
+++ b/dom/filesystem/compat/tests/test_no_dnd.html
@@ -2,49 +2,50 @@
 <html>
 <head>
   <title>Test for Blink FileSystem API - no DND == no webkitEntries</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 
 <body>
-<input id="entries" type="file"></input>
 <script type="application/javascript;version=1.7">
 
 var fileEntry;
 var directoryEntry;
 var script;
+var entries;
 
 function setup_tests() {
   SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
                                      ["dom.webkitBlink.filesystem.enabled", true]]}, next);
 }
 
 function populate_entries() {
+  entries = document.createElement('input');
+  entries.setAttribute('type', 'file');
+  document.body.appendChild(entries);
+
   var url = SimpleTest.getTestFileURL("script_entries.js");
   script = SpecialPowers.loadChromeScript(url);
 
   function onOpened(message) {
-    var entries = document.getElementById('entries');
-
     for (var i = 0 ; i < message.data.length; ++i) {
       if (message.data[i] instanceof File) {
         SpecialPowers.wrap(entries).mozSetFileArray([message.data[i]]);
         next();
       }
     }
   }
 
   script.addMessageListener("entries.opened", onOpened);
   script.sendAsyncMessage("entries.open");
 }
 
 function test_entries() {
-  var entries = document.getElementById('entries');
   ok("webkitEntries" in entries, "HTMLInputElement.webkitEntries");
   is(entries.webkitEntries.length, 0, "HTMLInputElement.webkitEntries.length == 0");
   is(entries.files.length, 1, "HTMLInputElement.files is still populated");
 
   next();
 }
 
 function cleanUpTestingFiles() {
--- a/dom/geolocation/nsGeolocation.cpp
+++ b/dom/geolocation/nsGeolocation.cpp
@@ -558,28 +558,20 @@ nsGeolocationRequest::GetRequester(nsICo
   return NS_OK;
 }
 
 void
 nsGeolocationRequest::SetTimeoutTimer()
 {
   StopTimeoutTimer();
 
-  int32_t timeout;
-  if (mOptions && (timeout = mOptions->mTimeout) != 0) {
-
-    if (timeout < 0) {
-      timeout = 0;
-    } else if (timeout < 10) {
-      timeout = 10;
-    }
-
+  if (mOptions && mOptions->mTimeout != 0 && mOptions->mTimeout != 0x7fffffff) {
     mTimeoutTimer = do_CreateInstance("@mozilla.org/timer;1");
     RefPtr<TimerCallbackHolder> holder = new TimerCallbackHolder(this);
-    mTimeoutTimer->InitWithCallback(holder, timeout, nsITimer::TYPE_ONE_SHOT);
+    mTimeoutTimer->InitWithCallback(holder, mOptions->mTimeout, nsITimer::TYPE_ONE_SHOT);
   }
 }
 
 void
 nsGeolocationRequest::StopTimeoutTimer()
 {
   if (mTimeoutTimer) {
     mTimeoutTimer->Cancel();
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -3407,17 +3407,18 @@ HTMLInputElement::Focus(ErrorResult& aEr
 }
 
 #if defined(XP_WIN) || defined(XP_LINUX)
 bool
 HTMLInputElement::IsNodeApzAwareInternal() const
 {
   // Tell APZC we may handle mouse wheel event and do preventDefault when input
   // type is number.
-  return (mType == NS_FORM_INPUT_NUMBER) || nsINode::IsNodeApzAwareInternal();
+  return (mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE) ||
+         nsINode::IsNodeApzAwareInternal();
 }
 #endif
 
 bool
 HTMLInputElement::IsInteractiveHTMLContent(bool aIgnoreTabindex) const
 {
   return mType != NS_FORM_INPUT_HIDDEN ||
          nsGenericHTMLFormElementWithState::IsInteractiveHTMLContent(aIgnoreTabindex);
@@ -4502,27 +4503,40 @@ HTMLInputElement::PostHandleEvent(EventC
               StopNumberControlSpinnerSpin();
             }
           }
           break;
         }
 #if defined(XP_WIN) || defined(XP_LINUX)
         case eWheel: {
           // Handle wheel events as increasing / decreasing the input element's
-          // value when it's focused and it's type is number.
+          // value when it's focused and it's type is number or range.
           WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent();
           if (!aVisitor.mEvent->DefaultPrevented() &&
               aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent &&
               wheelEvent->mDeltaY != 0 &&
-              wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL &&
-              mType == NS_FORM_INPUT_NUMBER) {
-            nsNumberControlFrame* numberControlFrame =
-              do_QueryFrame(GetPrimaryFrame());
-            if (numberControlFrame && numberControlFrame->IsFocused()) {
-              StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+              wheelEvent->mDeltaMode != nsIDOMWheelEvent::DOM_DELTA_PIXEL) {
+            if (mType == NS_FORM_INPUT_NUMBER) {
+              nsNumberControlFrame* numberControlFrame =
+                do_QueryFrame(GetPrimaryFrame());
+              if (numberControlFrame && numberControlFrame->IsFocused()) {
+                StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1);
+                aVisitor.mEvent->PreventDefault();
+              }
+            } else if (mType == NS_FORM_INPUT_RANGE &&
+                       nsContentUtils::IsFocusedContent(this) &&
+                       GetMinimum() < GetMaximum()) {
+              Decimal value = GetValueAsDecimal();
+              Decimal step = GetStep();
+              if (step == kStepAny) {
+                step = GetDefaultStep();
+              }
+              MOZ_ASSERT(value.isFinite() && step.isFinite());
+              SetValueOfRangeForUserEvent(wheelEvent->mDeltaY < 0 ?
+                                          value + step : value - step);
               aVisitor.mEvent->PreventDefault();
             }
           }
           break;
         }
 #endif
         default:
           break;
@@ -6009,17 +6023,17 @@ FireEventForAccessibility(nsIDOMHTMLInpu
   return NS_OK;
 }
 #endif
 
 void
 HTMLInputElement::UpdateApzAwareFlag()
 {
 #if defined(XP_WIN) || defined(XP_LINUX)
-  if (mType == NS_FORM_INPUT_NUMBER) {
+  if ((mType == NS_FORM_INPUT_NUMBER) || (mType == NS_FORM_INPUT_RANGE)) {
     SetMayBeApzAware();
   }
 #endif
 }
 
 nsresult
 HTMLInputElement::SetDefaultValueAsValue()
 {
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -615,10 +615,14 @@ skip-if = buildapp == 'b2g' # bug 112901
 [test_bug1230665.html]
 [test_filepicker_default_directory.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android'
 [test_bug1233598.html]
 [test_bug1250401.html]
 [test_bug1260664.html]
 [test_bug1261673.html]
 skip-if = (os != 'win' && os != 'linux')
+[test_bug1261674-1.html]
+skip-if = (os != 'win' && os != 'linux')
+[test_bug1261674-2.html]
+skip-if = (os != 'win' && os != 'linux')
 [test_bug1260704.html]
 [test_allowMedia.html]
new file mode 100644
--- /dev/null
+++ b/dom/html/test/test_bug1261674-1.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1261674</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.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=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" value=5 max=10 min=0>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+  let input = window.document.getElementById("test_input");
+
+  // focus: whether the target input element is focused
+  // deltaY: deltaY of WheelEvent
+  // deltaMode: deltaMode of WheelEvent
+  // valueChanged: expected value changes after input element handled the wheel event
+  let params = [
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1},
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1},
+    {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+    {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0},
+    {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0},
+    {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}
+  ];
+
+  let testIdx = 0;
+  let result = parseInt(input.value);
+
+  function runNext() {
+    let p = params[testIdx];
+    (p["focus"]) ? input.focus() : input.blur();
+    result += parseInt(p["valueChanged"]);
+    sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => {
+      ok(input.value == result,
+         "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+      (++testIdx >= params.length) ? SimpleTest.finish() : runNext();
+    });
+  }
+
+  input.addEventListener("input", () => {
+    ok(input.value == result,
+       "Test-" + testIdx + " receive input event, expect " + result + " get " + input.value);
+  }, false);
+
+  runNext();
+}
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/html/test/test_bug1261674-2.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1261674
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1261674</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/paint_listener.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=1261674">Mozilla Bug 1261674</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<input id="test_input" type="range" max=0 min=10>
+<script type="text/javascript">
+
+/** Test for Bug 1261674 **/
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(runTests);
+
+function runTests() {
+  let input = window.document.getElementById("test_input");
+
+  // deltaY: deltaY of WheelEvent
+  // deltaMode: deltaMode of WheelEvent
+  let params = [
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL},
+    {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE},
+    {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}
+  ];
+
+  let testIdx = 0;
+  let result = parseInt(input.value);
+
+  function runNext() {
+    let p = params[testIdx];
+    (p["focus"]) ? input.focus() : input.blur();
+    sendWheelAndPaint(input, 1, 1, { deltaY: p["deltaY"], deltaMode: p["deltaMode"] }, () => {
+      ok(input.value == result,
+         "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value);
+      testIdx++;
+      (testIdx >= params.length) ? SimpleTest.finish() : runNext();
+    });
+  }
+
+  input.addEventListener("input", () => {
+    ok(false, "Wheel event should be no effect to range input element with max < min");
+  }, false);
+
+  runNext();
+}
+</script>
+</body>
+</html>
--- a/dom/presentation/PresentationConnection.cpp
+++ b/dom/presentation/PresentationConnection.cpp
@@ -183,19 +183,18 @@ PresentationConnection::Close(ErrorResul
     service->CloseSession(mId,
                           mRole,
                           nsIPresentationService::CLOSED_REASON_CLOSED)));
 }
 
 void
 PresentationConnection::Terminate(ErrorResult& aRv)
 {
-  // It only works when the state is CONNECTED or CONNECTING.
-  if (NS_WARN_IF(mState != PresentationConnectionState::Connected &&
-                 mState != PresentationConnectionState::Connecting)) {
+  // It only works when the state is CONNECTED.
+  if (NS_WARN_IF(mState != PresentationConnectionState::Connected)) {
     return;
   }
 
   nsCOMPtr<nsIPresentationService> service =
     do_GetService(PRESENTATION_SERVICE_CONTRACTID);
   if(NS_WARN_IF(!service)) {
     aRv.Throw(NS_ERROR_DOM_OPERATION_ERR);
     return;
@@ -277,31 +276,32 @@ PresentationConnection::ProcessStateChan
         CopyUTF8toUTF16(message, errorMsg);
       }
 
       NS_WARN_IF(NS_FAILED(DispatchConnectionClosedEvent(reason, errorMsg)));
 
       return RemoveFromLoadGroup();
     }
     case PresentationConnectionState::Terminated: {
+      // Ensure onterminate event is fired.
+      RefPtr<AsyncEventDispatcher> asyncDispatcher =
+        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
+      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
+
       nsCOMPtr<nsIPresentationService> service =
         do_GetService(PRESENTATION_SERVICE_CONTRACTID);
       if (NS_WARN_IF(!service)) {
         return NS_ERROR_NOT_AVAILABLE;
       }
 
       nsresult rv = service->UnregisterSessionListener(mId, mRole);
       if(NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
 
-      RefPtr<AsyncEventDispatcher> asyncDispatcher =
-        new AsyncEventDispatcher(this, NS_LITERAL_STRING("terminate"), false);
-      NS_WARN_IF(NS_FAILED(asyncDispatcher->PostDOMEvent()));
-
       return RemoveFromLoadGroup();
     }
     default:
       MOZ_CRASH("Unknown presentation session state.");
       return NS_ERROR_INVALID_ARG;
   }
 }
 
--- a/dom/presentation/PresentationDeviceManager.cpp
+++ b/dom/presentation/PresentationDeviceManager.cpp
@@ -9,16 +9,17 @@
 #include "mozilla/Services.h"
 #include "MainThreadUtils.h"
 #include "nsCategoryCache.h"
 #include "nsCOMPtr.h"
 #include "nsIMutableArray.h"
 #include "nsIObserverService.h"
 #include "nsXULAppAPI.h"
 #include "PresentationSessionRequest.h"
+#include "PresentationTerminateRequest.h"
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_ISUPPORTS(PresentationDeviceManager,
                   nsIPresentationDeviceManager,
                   nsIPresentationDeviceListener,
                   nsIObserver,
@@ -234,16 +235,38 @@ PresentationDeviceManager::OnSessionRequ
     new PresentationSessionRequest(aDevice, aUrl, aPresentationId, aControlChannel);
   obs->NotifyObservers(request,
                        PRESENTATION_SESSION_REQUEST_TOPIC,
                        nullptr);
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+PresentationDeviceManager::OnTerminateRequest(nsIPresentationDevice* aDevice,
+                                              const nsAString& aPresentationId,
+                                              nsIPresentationControlChannel* aControlChannel,
+                                              bool aIsFromReceiver)
+{
+  NS_ENSURE_ARG(aDevice);
+  NS_ENSURE_ARG(aControlChannel);
+
+  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+  NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE);
+
+  RefPtr<PresentationTerminateRequest> request =
+    new PresentationTerminateRequest(aDevice, aPresentationId,
+                                     aControlChannel, aIsFromReceiver);
+  obs->NotifyObservers(request,
+                       PRESENTATION_TERMINATE_REQUEST_TOPIC,
+                       nullptr);
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 PresentationDeviceManager::Observe(nsISupports *aSubject,
                                    const char *aTopic,
                                    const char16_t *aData)
 {
   if (!strcmp(aTopic, "profile-after-change")) {
     Init();
--- a/dom/presentation/PresentationService.cpp
+++ b/dom/presentation/PresentationService.cpp
@@ -1,38 +1,66 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 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 "PresentationService.h"
+
 #include "ipc/PresentationIPCService.h"
 #include "mozilla/Services.h"
 #include "mozIApplication.h"
+#include "nsGlobalWindow.h"
 #include "nsIAppsService.h"
 #include "nsIObserverService.h"
 #include "nsIPresentationControlChannel.h"
 #include "nsIPresentationDeviceManager.h"
 #include "nsIPresentationDevicePrompt.h"
 #include "nsIPresentationListener.h"
 #include "nsIPresentationRequestUIGlue.h"
 #include "nsIPresentationSessionRequest.h"
+#include "nsIPresentationTerminateRequest.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
 #include "PresentationLog.h"
-#include "PresentationService.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 namespace mozilla {
 namespace dom {
 
+static bool
+IsSameDevice(nsIPresentationDevice* aDevice, nsIPresentationDevice* aDeviceAnother) {
+  if (!aDevice || !aDeviceAnother) {
+    return false;
+  }
+
+  nsAutoCString deviceId;
+  aDevice->GetId(deviceId);
+  nsAutoCString anotherId;
+  aDeviceAnother->GetId(anotherId);
+  if (!deviceId.Equals(anotherId)) {
+    return false;
+  }
+
+  nsAutoCString deviceType;
+  aDevice->GetType(deviceType);
+  nsAutoCString anotherType;
+  aDeviceAnother->GetType(anotherType);
+  if (!deviceType.Equals(anotherType)) {
+    return false;
+  }
+
+  return true;
+}
+
 /*
  * Implementation of PresentationDeviceRequest
  */
 
 class PresentationDeviceRequest final : public nsIPresentationDeviceRequest
 {
 public:
   NS_DECL_ISUPPORTS
@@ -186,16 +214,20 @@ PresentationService::Init()
   rv = obs->AddObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC, false);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return false;
   }
   rv = obs->AddObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC, false);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return false;
   }
+  rv = obs->AddObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC, false);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return false;
+  }
 
   nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
     do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
   if (NS_WARN_IF(!deviceManager)) {
     return false;
   }
 
   rv = deviceManager->GetDeviceAvailable(&mIsAvailable);
@@ -214,16 +246,23 @@ PresentationService::Observe(nsISupports
     return HandleDeviceChange();
   } else if (!strcmp(aTopic, PRESENTATION_SESSION_REQUEST_TOPIC)) {
     nsCOMPtr<nsIPresentationSessionRequest> request(do_QueryInterface(aSubject));
     if (NS_WARN_IF(!request)) {
       return NS_ERROR_FAILURE;
     }
 
     return HandleSessionRequest(request);
+  } else if (!strcmp(aTopic, PRESENTATION_TERMINATE_REQUEST_TOPIC)) {
+    nsCOMPtr<nsIPresentationTerminateRequest> request(do_QueryInterface(aSubject));
+    if (NS_WARN_IF(!request)) {
+      return NS_ERROR_FAILURE;
+    }
+
+    return HandleTerminateRequest(request);
   } else if (!strcmp(aTopic, "profile-after-change")) {
     // It's expected since we add and entry to |kLayoutCategories| in
     // |nsLayoutModule.cpp| to launch this service earlier.
     return NS_OK;
   }
 
   MOZ_ASSERT(false, "Unexpected topic for PresentationService");
   return NS_ERROR_UNEXPECTED;
@@ -240,16 +279,17 @@ PresentationService::HandleShutdown()
   mSessionInfoAtController.Clear();
   mSessionInfoAtReceiver.Clear();
 
   nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
   if (obs) {
     obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
     obs->RemoveObserver(this, PRESENTATION_DEVICE_CHANGE_TOPIC);
     obs->RemoveObserver(this, PRESENTATION_SESSION_REQUEST_TOPIC);
+    obs->RemoveObserver(this, PRESENTATION_TERMINATE_REQUEST_TOPIC);
   }
 }
 
 nsresult
 PresentationService::HandleDeviceChange()
 {
   nsCOMPtr<nsIPresentationDeviceManager> deviceManager =
     do_GetService(PRESENTATION_DEVICE_MANAGER_CONTRACTID);
@@ -355,16 +395,68 @@ PresentationService::HandleSessionReques
     return info->ReplyError(NS_ERROR_DOM_OPERATION_ERR);
   }
   nsCOMPtr<Promise> realPromise = do_QueryInterface(promise);
   static_cast<PresentationPresentingInfo*>(info.get())->SetPromise(realPromise);
 
   return NS_OK;
 }
 
+nsresult
+PresentationService::HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest)
+{
+  nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+  nsresult rv = aRequest->GetControlChannel(getter_AddRefs(ctrlChannel));
+  if (NS_WARN_IF(NS_FAILED(rv) || !ctrlChannel)) {
+    return rv;
+  }
+
+  nsAutoString sessionId;
+  rv = aRequest->GetPresentationId(sessionId);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  nsCOMPtr<nsIPresentationDevice> device;
+  rv = aRequest->GetDevice(getter_AddRefs(device));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  bool isFromReceiver;
+  rv = aRequest->GetIsFromReceiver(&isFromReceiver);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    ctrlChannel->Disconnect(rv);
+    return rv;
+  }
+
+  RefPtr<PresentationSessionInfo> info;
+  if (!isFromReceiver) {
+    info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_RECEIVER);
+  } else {
+    info = GetSessionInfo(sessionId, nsIPresentationService::ROLE_CONTROLLER);
+  }
+  if (NS_WARN_IF(!info)) {
+    // Cannot terminate non-existed session.
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+    return NS_ERROR_DOM_ABORT_ERR;
+  }
+
+  // Check if terminate request comes from known device.
+  RefPtr<nsIPresentationDevice> knownDevice = info->GetDevice();
+  if (NS_WARN_IF(!IsSameDevice(device, knownDevice))) {
+    ctrlChannel->Disconnect(NS_ERROR_DOM_OPERATION_ERR);
+    return NS_ERROR_DOM_ABORT_ERR;
+  }
+
+  return info->OnTerminate(ctrlChannel);
+}
+
 void
 PresentationService::NotifyAvailableChange(bool aIsAvailable)
 {
   nsTObserverArray<nsCOMPtr<nsIPresentationAvailabilityListener>>::ForwardIterator iter(mAvailabilityListeners);
   while (iter.HasMore()) {
     nsCOMPtr<nsIPresentationAvailabilityListener> listener = iter.GetNext();
     NS_WARN_IF(NS_FAILED(listener->NotifyAvailableChange(aIsAvailable)));
   }
@@ -520,17 +612,16 @@ PresentationService::CloseSession(const 
              aRole == nsIPresentationService::ROLE_RECEIVER);
 
   RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
   if (NS_WARN_IF(!info)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   if (aClosedReason == nsIPresentationService::CLOSED_REASON_WENTAWAY) {
-    UntrackSessionInfo(aSessionId, aRole);
     // Remove nsIPresentationSessionListener since we don't want to dispatch
     // PresentationConnectionClosedEvent if the page is went away.
     info->SetListener(nullptr);
   }
 
   return info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED);
 }
 
@@ -606,18 +697,19 @@ PresentationService::UnregisterSessionLi
                                                uint8_t aRole)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
              aRole == nsIPresentationService::ROLE_RECEIVER);
 
   RefPtr<PresentationSessionInfo> info = GetSessionInfo(aSessionId, aRole);
   if (info) {
-    NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_TERMINATED)));
-    UntrackSessionInfo(aSessionId, aRole);
+    // When content side decide not handling this session anymore, simply
+    // close the connection. Session info is kept for reconnection.
+    NS_WARN_IF(NS_FAILED(info->Close(NS_OK, nsIPresentationSessionListener::STATE_CLOSED)));
     return info->SetListener(nullptr);
   }
   return NS_OK;
 }
 
 nsresult
 PresentationService::RegisterTransportBuilder(const nsAString& aSessionId,
                                               uint8_t aRole,
@@ -724,16 +816,27 @@ PresentationService::UntrackSessionInfo(
                                         uint8_t aRole)
 {
   MOZ_ASSERT(aRole == nsIPresentationService::ROLE_CONTROLLER ||
              aRole == nsIPresentationService::ROLE_RECEIVER);
   // Remove the session info.
   if (nsIPresentationService::ROLE_CONTROLLER == aRole) {
     mSessionInfoAtController.Remove(aSessionId);
   } else {
+    // Terminate receiver page.
+    uint64_t windowId;
+    nsresult rv = GetWindowIdBySessionIdInternal(aSessionId, &windowId);
+    if (NS_SUCCEEDED(rv)) {
+      NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void {
+        if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) {
+          window->Close();
+        }
+      }));
+    }
+
     mSessionInfoAtReceiver.Remove(aSessionId);
   }
 
   // Remove the in-process responding info if there's still any.
   RemoveRespondingSessionId(aSessionId);
 
   return NS_OK;
 }
--- a/dom/presentation/PresentationService.h
+++ b/dom/presentation/PresentationService.h
@@ -9,16 +9,17 @@
 
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsTObserverArray.h"
 #include "PresentationServiceBase.h"
 #include "PresentationSessionInfo.h"
 
 class nsIPresentationSessionRequest;
+class nsIPresentationTerminateRequest;
 class nsIURI;
 class nsIPresentationSessionTransportBuilder;
 
 namespace mozilla {
 namespace dom {
 
 class PresentationDeviceRequest;
 class PresentationRespondingInfo;
@@ -61,16 +62,17 @@ public:
 
 private:
   friend class PresentationDeviceRequest;
 
   virtual ~PresentationService();
   void HandleShutdown();
   nsresult HandleDeviceChange();
   nsresult HandleSessionRequest(nsIPresentationSessionRequest* aRequest);
+  nsresult HandleTerminateRequest(nsIPresentationTerminateRequest* aRequest);
   void NotifyAvailableChange(bool aIsAvailable);
   bool IsAppInstalled(nsIURI* aUri);
 
   // This is meant to be called by PresentationDeviceRequest.
   already_AddRefed<PresentationSessionInfo>
   CreateControllingSessionInfo(const nsAString& aUrl,
                                const nsAString& aSessionId,
                                uint64_t aWindowId);
--- a/dom/presentation/PresentationSessionInfo.cpp
+++ b/dom/presentation/PresentationSessionInfo.cpp
@@ -231,16 +231,17 @@ PresentationSessionInfo::Shutdown(nsresu
 
   // Close the data transport channel if any.
   if (mTransport) {
     // |mIsTransportReady| will be unset once |NotifyTransportClosed| is called.
     NS_WARN_IF(NS_FAILED(mTransport->Close(aReason)));
   }
 
   mIsResponderReady = false;
+  mIsOnTerminating = false;
 
   SetBuilder(nullptr);
 }
 
 nsresult
 PresentationSessionInfo::SetListener(nsIPresentationSessionListener* aListener)
 {
   mListener = aListener;
@@ -279,19 +280,52 @@ PresentationSessionInfo::Send(const nsAS
 nsresult
 PresentationSessionInfo::Close(nsresult aReason,
                                uint32_t aState)
 {
   if (NS_WARN_IF(!IsSessionReady())) {
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
+  // Do nothing if session is already terminated.
+  if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+    return NS_OK;
+  }
+
   SetStateWithReason(aState, aReason);
 
-  Shutdown(aReason);
+  switch (aState) {
+    case nsIPresentationSessionListener::STATE_CLOSED: {
+      Shutdown(aReason);
+      break;
+    }
+    case nsIPresentationSessionListener::STATE_TERMINATED: {
+      if (!mControlChannel) {
+        nsCOMPtr<nsIPresentationControlChannel> ctrlChannel;
+        nsresult rv = mDevice->EstablishControlChannel(getter_AddRefs(ctrlChannel));
+        if (NS_SUCCEEDED(rv)) {
+          SetControlChannel(ctrlChannel);
+        }
+        return rv;
+      }
+
+      return mControlChannel->Terminate(mSessionId);
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PresentationSessionInfo::OnTerminate(nsIPresentationControlChannel* aControlChannel)
+{
+  mIsOnTerminating = true; // Mark for terminating transport channel
+  SetStateWithReason(nsIPresentationSessionListener::STATE_TERMINATED, NS_OK);
+  SetControlChannel(aControlChannel);
+
   return NS_OK;
 }
 
 nsresult
 PresentationSessionInfo::ReplySuccess()
 {
   SetStateWithReason(nsIPresentationSessionListener::STATE_CONNECTED, NS_OK);
   return NS_OK;
@@ -337,16 +371,28 @@ PresentationSessionInfo::GetWindow()
 
 /* virtual */ bool
 PresentationSessionInfo::IsAccessible(base::ProcessId aProcessId)
 {
   // No restriction by default.
   return true;
 }
 
+void
+PresentationSessionInfo::ContinueTermination()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(mControlChannel);
+
+  if (NS_WARN_IF(NS_FAILED(mControlChannel->Terminate(mSessionId)))
+      || mIsOnTerminating) {
+    Shutdown(NS_OK);
+  }
+}
+
 // nsIPresentationSessionTransportCallback
 NS_IMETHODIMP
 PresentationSessionInfo::NotifyTransportReady()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   mIsTransportReady = true;
 
@@ -687,16 +733,37 @@ PresentationControllingInfo::OnAnswer(ns
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationControllingInfo::NotifyConnected()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
+  switch (mState) {
+    case nsIPresentationSessionListener::STATE_CONNECTING: {
+      Unused << NS_WARN_IF(NS_FAILED(BuildTransport()));
+      break;
+    }
+    case nsIPresentationSessionListener::STATE_TERMINATED: {
+      ContinueTermination();
+      break;
+    }
+    default:
+      break;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PresentationControllingInfo::BuildTransport()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
   nsresult rv = mControlChannel->Launch(GetSessionId(), GetUrl());
   if (NS_FAILED(rv)) {
     return rv;
   }
 
   if (!Preferences::GetBool("dom.presentation.session_transport.data_channel.enable")) {
     // Build TCP session transport
     return GetAddress();
@@ -1134,17 +1201,20 @@ PresentationPresentingInfo::OnIceCandida
     builder = do_QueryInterface(mBuilder);
 
   return builder->OnIceCandidate(aCandidate);
 }
 
 NS_IMETHODIMP
 PresentationPresentingInfo::NotifyConnected()
 {
-  // Do nothing.
+  if (nsIPresentationSessionListener::STATE_TERMINATED == mState) {
+    ContinueTermination();
+  }
+
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationPresentingInfo::NotifyDisconnected(nsresult aReason)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
--- a/dom/presentation/PresentationSessionInfo.h
+++ b/dom/presentation/PresentationSessionInfo.h
@@ -100,16 +100,18 @@ public:
     }
   }
 
   nsresult Send(const nsAString& aData);
 
   nsresult Close(nsresult aReason,
                  uint32_t aState);
 
+  nsresult OnTerminate(nsIPresentationControlChannel* aControlChannel);
+
   nsresult ReplyError(nsresult aReason);
 
   virtual bool IsAccessible(base::ProcessId aProcessId);
 
 protected:
   virtual ~PresentationSessionInfo()
   {
     Shutdown(NS_OK);
@@ -137,28 +139,31 @@ protected:
 
     // Notify session state change.
     if (mListener) {
       nsresult rv = mListener->NotifyStateChange(mSessionId, mState, aReason);
       NS_WARN_IF(NS_FAILED(rv));
     }
   }
 
+  void ContinueTermination();
+
   // Should be nsIPresentationChannelDescription::TYPE_TCP/TYPE_DATACHANNEL
   uint8_t mTransportType = 0;
 
   nsPIDOMWindowInner* GetWindow();
 
   nsString mUrl;
   nsString mSessionId;
   // mRole should be nsIPresentationService::ROLE_CONTROLLER
   //              or nsIPresentationService::ROLE_RECEIVER.
   uint8_t mRole;
   bool mIsResponderReady;
   bool mIsTransportReady;
+  bool mIsOnTerminating = false;
   uint32_t mState; // CONNECTED, CLOSED, TERMINATED
   nsresult mReason;
   nsCOMPtr<nsIPresentationSessionListener> mListener;
   nsCOMPtr<nsIPresentationDevice> mDevice;
   nsCOMPtr<nsIPresentationSessionTransport> mTransport;
   nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
   nsCOMPtr<nsIPresentationSessionTransportBuilder> mBuilder;
 };
@@ -188,16 +193,18 @@ private:
   }
 
   void Shutdown(nsresult aReason) override;
 
   nsresult GetAddress();
 
   nsresult OnGetAddress(const nsACString& aAddress);
 
+  nsresult BuildTransport();
+
   nsCOMPtr<nsIServerSocket> mServerSocket;
 };
 
 // Session info with presenting browsing context (receiver side) behaviors.
 class PresentationPresentingInfo final : public PresentationSessionInfo
                                        , public PromiseNativeHandler
                                        , public nsITimerCallback
 {
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.cpp
@@ -0,0 +1,73 @@
+/* -*- 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 "PresentationTerminateRequest.h"
+#include "nsIPresentationControlChannel.h"
+#include "nsIPresentationDevice.h"
+
+namespace mozilla {
+namespace dom {
+
+NS_IMPL_ISUPPORTS(PresentationTerminateRequest, nsIPresentationTerminateRequest)
+
+PresentationTerminateRequest::PresentationTerminateRequest(
+                                 nsIPresentationDevice* aDevice,
+                                 const nsAString& aPresentationId,
+                                 nsIPresentationControlChannel* aControlChannel,
+                                 bool aIsFromReceiver)
+  : mPresentationId(aPresentationId)
+  , mDevice(aDevice)
+  , mControlChannel(aControlChannel)
+  , mIsFromReceiver(aIsFromReceiver)
+{
+}
+
+PresentationTerminateRequest::~PresentationTerminateRequest()
+{
+}
+
+// nsIPresentationTerminateRequest
+NS_IMETHODIMP
+PresentationTerminateRequest::GetDevice(nsIPresentationDevice** aRetVal)
+{
+  NS_ENSURE_ARG_POINTER(aRetVal);
+
+  nsCOMPtr<nsIPresentationDevice> device = mDevice;
+  device.forget(aRetVal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetPresentationId(nsAString& aRetVal)
+{
+  aRetVal = mPresentationId;
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetControlChannel(
+                                        nsIPresentationControlChannel** aRetVal)
+{
+  NS_ENSURE_ARG_POINTER(aRetVal);
+
+  nsCOMPtr<nsIPresentationControlChannel> controlChannel = mControlChannel;
+  controlChannel.forget(aRetVal);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PresentationTerminateRequest::GetIsFromReceiver(bool* aRetVal)
+{
+  *aRetVal = mIsFromReceiver;
+
+  return NS_OK;
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/presentation/PresentationTerminateRequest.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PresentationTerminateRequest_h__
+#define mozilla_dom_PresentationTerminateRequest_h__
+
+#include "nsIPresentationTerminateRequest.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+namespace dom {
+
+class PresentationTerminateRequest final : public nsIPresentationTerminateRequest
+{
+public:
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIPRESENTATIONTERMINATEREQUEST
+
+  PresentationTerminateRequest(nsIPresentationDevice* aDevice,
+                               const nsAString& aPresentationId,
+                               nsIPresentationControlChannel* aControlChannel,
+                               bool aIsFromReceiver);
+
+private:
+  virtual ~PresentationTerminateRequest();
+
+  nsString mPresentationId;
+  nsCOMPtr<nsIPresentationDevice> mDevice;
+  nsCOMPtr<nsIPresentationControlChannel> mControlChannel;
+  bool mIsFromReceiver;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_PresentationTerminateRequest_h__ */
+
--- a/dom/presentation/interfaces/moz.build
+++ b/dom/presentation/interfaces/moz.build
@@ -13,16 +13,17 @@ XPIDL_SOURCES += [
     'nsIPresentationDeviceProvider.idl',
     'nsIPresentationListener.idl',
     'nsIPresentationLocalDevice.idl',
     'nsIPresentationRequestUIGlue.idl',
     'nsIPresentationService.idl',
     'nsIPresentationSessionRequest.idl',
     'nsIPresentationSessionTransport.idl',
     'nsIPresentationSessionTransportBuilder.idl',
+    'nsIPresentationTerminateRequest.idl',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
     XPIDL_SOURCES += [
         'nsIPresentationNetworkHelper.idl',
     ]
 
 XPIDL_MODULE = 'dom_presentation'
--- a/dom/presentation/interfaces/nsIPresentationControlChannel.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlChannel.idl
@@ -106,13 +106,20 @@ interface nsIPresentationControlChannel:
    * Launch a presentation on remote endpoint.
    * @param presentationId The Id for representing this session.
    * @param url The URL requested to open by remote device.
    * @throws NS_ERROR_FAILURE on failure
    */
   void launch(in DOMString presentationId, in DOMString url);
 
   /*
+   * Terminate a presentation on remote endpoint.
+   * @param presentationId The Id for representing this session.
+   * @throws NS_ERROR_FAILURE on failure
+   */
+  void terminate(in DOMString presentationId);
+
+  /*
    * Disconnect the control channel.
    * @param reason The reason of disconnecting channel; NS_OK represents normal.
    */
   void disconnect(in nsresult reason);
 };
--- a/dom/presentation/interfaces/nsIPresentationControlService.idl
+++ b/dom/presentation/interfaces/nsIPresentationControlService.idl
@@ -40,16 +40,28 @@ interface nsIPresentationControlServerLi
    * @param aUrl The URL requested to open by remote device.
    * @param aPresentationId The Id for representing this session.
    * @param aControlChannel The control channel for this session.
    */
   void onSessionRequest(in nsITCPDeviceInfo aDeviceInfo,
                         in DOMString aUrl,
                         in DOMString aPresentationId,
                         in nsIPresentationControlChannel aControlChannel);
+
+  /**
+   * Callback while the remote host is requesting to terminate a presentation session.
+   * @param aDeviceInfo The device information related to the remote host.
+   * @param aPresentationId The Id for representing this session.
+   * @param aControlChannel The control channel for this session.
+   * @param aIsFromReceiver true if termination is initiated by receiver.
+   */
+  void onTerminateRequest(in nsITCPDeviceInfo aDeviceInfo,
+                          in DOMString aPresentationId,
+                          in nsIPresentationControlChannel aControlChannel,
+                          in boolean aIsFromReceiver);
 };
 
 /**
  * Presentation control service which can be used for both presentation
  * control client and server.
  */
 [scriptable, uuid(55d6b605-2389-4aae-a8fe-60d4440540ea)]
 interface nsIPresentationControlService: nsISupports
--- a/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
+++ b/dom/presentation/interfaces/nsIPresentationDeviceProvider.idl
@@ -27,16 +27,28 @@ interface nsIPresentationDeviceListener:
    * @param url The URL requested to open by remote device.
    * @param presentationId The Id for representing this session.
    * @param controlChannel The control channel for this session.
    */
   void onSessionRequest(in nsIPresentationDevice device,
                         in DOMString url,
                         in DOMString presentationId,
                         in nsIPresentationControlChannel controlChannel);
+
+  /*
+   * Callback while the remote device is requesting to terminate a presentation session.
+   * @param device The remote device that sent session request.
+   * @param presentationId The Id for representing this session.
+   * @param controlChannel The control channel for this session.
+   * @param aIsFromReceiver true if termination is initiated by receiver.
+   */
+  void onTerminateRequest(in nsIPresentationDevice device,
+                          in DOMString presentationId,
+                          in nsIPresentationControlChannel controlChannel,
+                          in boolean aIsFromReceiver);
 };
 
 /*
  * Device provider for any device protocol, can be registered as default
  * providers by adding its contractID to category "presentation-device-provider".
  */
 [scriptable, uuid(3db2578a-0f50-44ad-b01b-28427b71b7bf)]
 interface nsIPresentationDeviceProvider: nsISupports
new file mode 100644
--- /dev/null
+++ b/dom/presentation/interfaces/nsIPresentationTerminateRequest.idl
@@ -0,0 +1,33 @@
+/* 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 "nsISupports.idl"
+
+interface nsIPresentationDevice;
+interface nsIPresentationControlChannel;
+
+%{C++
+#define PRESENTATION_TERMINATE_REQUEST_TOPIC "presentation-terminate-request"
+%}
+
+/*
+ * The event of a device requesting for terminating a presentation session. User can
+ * monitor the terminate request on every device by observing "presentation-terminate-request".
+ */
+[scriptable, uuid(3ddbf3a4-53ee-4b70-9bbc-58ac90dce6b5)]
+interface nsIPresentationTerminateRequest: nsISupports
+{
+  // The device which requesting to terminate presentation session.
+  readonly attribute nsIPresentationDevice device;
+
+  // The Id for representing this session.
+  readonly attribute DOMString presentationId;
+
+  // The control channel for this session.
+  // Should only use this channel to complete session termination.
+  readonly attribute nsIPresentationControlChannel controlChannel;
+
+  // True if termination is initiated by receiver.
+  readonly attribute boolean isFromReceiver;
+};
--- a/dom/presentation/ipc/PresentationIPCService.cpp
+++ b/dom/presentation/ipc/PresentationIPCService.cpp
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/PPresentation.h"
 #include "mozilla/ipc/InputStreamUtils.h"
 #include "mozilla/ipc/URIUtils.h"
+#include "nsGlobalWindow.h"
 #include "nsIPresentationListener.h"
 #include "PresentationCallbacks.h"
 #include "PresentationChild.h"
 #include "PresentationContentSessionInfo.h"
 #include "PresentationIPCService.h"
 
 using namespace mozilla;
 using namespace mozilla::dom;
@@ -336,16 +337,28 @@ PresentationIPCService::NotifyReceiverRe
   mCallback = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PresentationIPCService::UntrackSessionInfo(const nsAString& aSessionId,
                                            uint8_t aRole)
 {
+  if (nsIPresentationService::ROLE_RECEIVER == aRole) {
+    // Terminate receiver page.
+    uint64_t windowId;
+    if (NS_SUCCEEDED(GetWindowIdBySessionIdInternal(aSessionId, &windowId))) {
+      NS_DispatchToMainThread(NS_NewRunnableFunction([windowId]() -> void {
+        if (auto* window = nsGlobalWindow::GetInnerWindowWithId(windowId)) {
+          window->Close();
+        }
+      }));
+    }
+  }
+
   // Remove the OOP responding info (if it has never been used).
   RemoveRespondingSessionId(aSessionId);
   if (mSessionInfos.Contains(aSessionId)) {
     mSessionInfos.Remove(aSessionId);
   }
 
   return NS_OK;
 }
--- a/dom/presentation/moz.build
+++ b/dom/presentation/moz.build
@@ -47,16 +47,17 @@ UNIFIED_SOURCES += [
     'PresentationDeviceManager.cpp',
     'PresentationReceiver.cpp',
     'PresentationRequest.cpp',
     'PresentationService.cpp',
     'PresentationServiceBase.cpp',
     'PresentationSessionInfo.cpp',
     'PresentationSessionRequest.cpp',
     'PresentationTCPSessionTransport.cpp',
+    'PresentationTerminateRequest.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'PresentationDataChannelSessionTransport.js',
     'PresentationDataChannelSessionTransport.manifest',
     'PresentationDeviceInfoManager.js',
     'PresentationDeviceInfoManager.manifest',
 ]
--- a/dom/presentation/provider/ControllerStateMachine.jsm
+++ b/dom/presentation/provider/ControllerStateMachine.jsm
@@ -45,16 +45,22 @@ var handlers = [
     switch (command.type) {
       case CommandType.DISCONNECT:
         stateMachine.state = State.CLOSED;
         stateMachine._notifyDisconnected(command.reason);
         break;
       case CommandType.LAUNCH_ACK:
         stateMachine._notifyLaunch(command.presentationId);
         break;
+      case CommandType.TERMINATE:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
+      case CommandType.TERMINATE_ACK:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
       case CommandType.ANSWER:
       case CommandType.ICE_CANDIDATE:
         stateMachine._notifyChannelDescriptor(command);
         break;
       default:
         debug("unexpected command: " + JSON.stringify(command));
         // ignore unexpected command.
         break;
@@ -82,16 +88,34 @@ ControllerStateMachine.prototype = {
       this._sendCommand({
         type: CommandType.LAUNCH,
         presentationId: presentationId,
         url: url,
       });
     }
   },
 
+  terminate: function _terminate(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE,
+        presentationId: presentationId,
+      });
+    }
+  },
+
+  terminateAck: function _terminateAck(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE_ACK,
+        presentationId: presentationId,
+      });
+    }
+  },
+
   sendOffer: function _sendOffer(offer) {
     if (this.state === State.CONNECTED) {
       this._sendCommand({
         type: CommandType.OFFER,
         offer: offer,
       });
     }
   },
@@ -167,16 +191,20 @@ ControllerStateMachine.prototype = {
   _notifyDisconnected: function _notifyDisconnected(reason) {
     this._channel.notifyDisconnected(reason);
   },
 
   _notifyLaunch: function _notifyLaunch(presentationId) {
     this._channel.notifyLaunch(presentationId);
   },
 
+  _notifyTerminate: function _notifyTerminate(presentationId) {
+    this._channel.notifyTerminate(presentationId);
+  },
+
   _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
     switch (command.type) {
       case CommandType.ANSWER:
         this._channel.notifyAnswer(command.answer);
         break;
       case CommandType.ICE_CANDIDATE:
         this._channel.notifyIceCandidate(command.candidate);
         break;
--- a/dom/presentation/provider/DisplayDeviceProvider.cpp
+++ b/dom/presentation/provider/DisplayDeviceProvider.cpp
@@ -400,16 +400,47 @@ DisplayDeviceProvider::OnSessionRequest(
                                   aControlChannel);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+DisplayDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+                                          const nsAString& aPresentationId,
+                                          nsIPresentationControlChannel* aControlChannel,
+                                          bool aIsFromReceiver)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(aDeviceInfo);
+  MOZ_ASSERT(aControlChannel);
+
+  nsresult rv;
+
+  nsCOMPtr<nsIPresentationDeviceListener> listener;
+  rv = GetListener(getter_AddRefs(listener));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  MOZ_ASSERT(!listener);
+
+  rv = listener->OnTerminateRequest(mDevice,
+                                    aPresentationId,
+                                    aControlChannel,
+                                    aIsFromReceiver);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 DisplayDeviceProvider::Observe(nsISupports* aSubject,
                                const char* aTopic,
                                const char16_t* aData)
 {
   if (!strcmp(aTopic, DISPLAY_CHANGED_NOTIFICATION)) {
     nsCOMPtr<nsIDisplayInfo> displayInfo = do_QueryInterface(aSubject);
--- a/dom/presentation/provider/LegacyPresentationControlService.js
+++ b/dom/presentation/provider/LegacyPresentationControlService.js
@@ -226,16 +226,22 @@ LegacyTCPControlChannel.prototype = {
 
   launch: function(aPresentationId, aUrl) {
     this._presentationId = aPresentationId;
     this._url = aUrl;
 
     this._sendInit();
   },
 
+  terminate: function() {
+    // Legacy protocol doesn't support extra terminate protocol.
+    // Trigger error handling for browser to shutdown all the resource locally.
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
   sendOffer: function(aOffer) {
     let msg = {
       type: "requestSession:Offer",
       presentationId: this._presentationId,
       offer: discriptionAsJson(aOffer),
     };
     this._sendMessage(msg);
   },
--- a/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
+++ b/dom/presentation/provider/MulticastDNSDeviceProvider.cpp
@@ -912,16 +912,60 @@ MulticastDNSDeviceProvider::OnSessionReq
   if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
     Unused << listener->OnSessionRequest(device, aUrl, aPresentationId,
                                          aControlChannel);
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+MulticastDNSDeviceProvider::OnTerminateRequest(nsITCPDeviceInfo* aDeviceInfo,
+                                               const nsAString& aPresentationId,
+                                               nsIPresentationControlChannel* aControlChannel,
+                                               bool aIsFromReceiver)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  nsAutoCString address;
+  Unused << aDeviceInfo->GetAddress(address);
+
+  LOG_I("OnTerminateRequest: %s", address.get());
+
+  RefPtr<Device> device;
+  uint32_t index;
+  if (FindDeviceByAddress(address, index)) {
+    device = mDevices[index];
+  } else {
+    // Create a one-time device object for non-discoverable controller.
+    // This device will not be in the list of available devices and cannot
+    // be used for requesting session.
+    nsAutoCString id;
+    Unused << aDeviceInfo->GetId(id);
+    uint16_t port;
+    Unused << aDeviceInfo->GetPort(&port);
+
+    device = new Device(id,
+                        /* aName = */ id,
+                        /* aType = */ EmptyCString(),
+                        address,
+                        port,
+                        DeviceState::eActive,
+                        /* aProvider = */ nullptr);
+  }
+
+  nsCOMPtr<nsIPresentationDeviceListener> listener;
+  if (NS_SUCCEEDED(GetListener(getter_AddRefs(listener))) && listener) {
+    Unused << listener->OnTerminateRequest(device, aPresentationId,
+                                           aControlChannel, aIsFromReceiver);
+  }
+
+  return NS_OK;
+}
+
 // nsIObserver
 NS_IMETHODIMP
 MulticastDNSDeviceProvider::Observe(nsISupports* aSubject,
                                     const char* aTopic,
                                     const char16_t* aData)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
--- a/dom/presentation/provider/PresentationControlService.js
+++ b/dom/presentation/provider/PresentationControlService.js
@@ -161,23 +161,43 @@ PresentationControlService.prototype = {
                                  aDeviceInfo,
                                  "receiver");
   },
 
   // Triggered by TCPControlChannel
   onSessionRequest: function(aDeviceInfo, aUrl, aPresentationId, aControlChannel) {
     DEBUG && log("PresentationControlService - onSessionRequest: " +
                  aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+    if (!this.listener) {
+      this.releaseControlChannel(aControlChannel);
+      return;
+    }
+
     this.listener.onSessionRequest(aDeviceInfo,
                                    aUrl,
                                    aPresentationId,
                                    aControlChannel);
     this.releaseControlChannel(aControlChannel);
   },
 
+  onSessionTerminate: function(aDeviceInfo, aPresentationId, aControlChannel, aIsFromReceiver) {
+    DEBUG && log("TCPPresentationServer - onSessionTerminate: " +
+                 aDeviceInfo.address + ":" + aDeviceInfo.port); // jshint ignore:line
+    if (!this.listener) {
+      this.releaseControlChannel(aControlChannel);
+      return;
+    }
+
+    this.listener.onTerminateRequest(aDeviceInfo,
+                                     aPresentationId,
+                                     aControlChannel,
+                                     aIsFromReceiver);
+    this.releaseControlChannel(aControlChannel);
+  },
+
   // nsIServerSocketListener (Triggered by nsIServerSocket.init)
   onSocketAccepted: function(aServerSocket, aClientSocket) {
     DEBUG && log("PresentationControlService - onSocketAccepted: " +
                  aClientSocket.host + ":" + aClientSocket.port); // jshint ignore:line
     let deviceInfo = new TCPDeviceInfo(aClientSocket.host, aClientSocket.port);
     this.holdControlChannel(this.responseSession(deviceInfo, aClientSocket));
   },
 
@@ -385,16 +405,26 @@ TCPControlChannel.prototype = {
   sendIceCandidate: function(aCandidate) {
     this._stateMachine.updateIceCandidate(aCandidate);
   },
 
   launch: function(aPresentationId, aUrl) {
     this._stateMachine.launch(aPresentationId, aUrl);
   },
 
+  terminate: function(aPresentationId) {
+    if (!this._terminatingId) {
+      this._terminatingId = aPresentationId;
+      this._stateMachine.terminate(aPresentationId);
+    } else {
+      this._stateMachine.terminateAck(aPresentationId);
+      delete this._terminatingId;
+    }
+  },
+
   // may throw an exception
   _send: function(aMsg) {
     DEBUG && log("TCPControlChannel - Send: " + JSON.stringify(aMsg, null, 2)); // jshint ignore:line
 
     /**
      * XXX In TCP streaming, it is possible that more than one message in one
      * TCP packet. We use line delimited JSON to identify where one JSON encoded
      * object ends and the next begins. Therefore, we do not allow newline
@@ -645,16 +675,36 @@ TCPControlChannel.prototype = {
         this._presentationService.onSessionRequest(this._deviceInfo,
                                                    url,
                                                    presentationId,
                                                    this);
       break;
     }
   },
 
+  notifyTerminate: function(presentationId) {
+    if (!this._terminatingId) {
+      this._terminatingId = presentationId;
+      this._presentationService.onSessionTerminate(this._deviceInfo,
+                                                  presentationId,
+                                                  this,
+                                                  this._direction === "sender");
+      return;
+    }
+
+    if (this._terminatingId !== presentationId) {
+      // Requested presentation Id doesn't matched with the one in ACK.
+      // Disconnect the control channel with error.
+      DEBUG && log("TCPControlChannel - unmatched terminatingId: " + presentationId); // jshint ignore:line
+      this.disconnect(Cr.NS_ERROR_FAILURE);
+    }
+
+    delete this._terminatingId;
+  },
+
   notifyOffer: function(offer) {
     this._onOffer(offer);
   },
 
   notifyAnswer: function(answer) {
     this._onAnswer(answer);
   },
 
--- a/dom/presentation/provider/ReceiverStateMachine.jsm
+++ b/dom/presentation/provider/ReceiverStateMachine.jsm
@@ -53,16 +53,22 @@ var handlers = [
       case CommandType.LAUNCH:
         stateMachine._notifyLaunch(command.presentationId,
                                    command.url);
         stateMachine._sendCommand({
           type: CommandType.LAUNCH_ACK,
           presentationId: command.presentationId
         });
         break;
+      case CommandType.TERMINATE:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
+      case CommandType.TERMINATE_ACK:
+        stateMachine._notifyTerminate(command.presentationId);
+        break;
       case CommandType.OFFER:
       case CommandType.ICE_CANDIDATE:
         stateMachine._notifyChannelDescriptor(command);
         break;
       default:
         debug("unexpected command: " + JSON.stringify(command));
         // ignore unexpected command
         break;
@@ -84,16 +90,34 @@ function ReceiverStateMachine(channel) {
 }
 
 ReceiverStateMachine.prototype = {
   launch: function _launch() {
     // presentation session can only be launched by controlling UA.
     debug("receiver shouldn't trigger launch");
   },
 
+  terminate: function _terminate(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE,
+        presentationId: presentationId,
+      });
+    }
+  },
+
+  terminateAck: function _terminateAck(presentationId) {
+    if (this.state === State.CONNECTED) {
+      this._sendCommand({
+        type: CommandType.TERMINATE_ACK,
+        presentationId: presentationId,
+      });
+    }
+  },
+
   sendOffer: function _sendOffer() {
     // offer can only be sent by controlling UA.
     debug("receiver shouldn't generate offer");
   },
 
   sendAnswer: function _sendAnswer(answer) {
     if (this.state === State.CONNECTED) {
       this._sendCommand({
@@ -166,16 +190,20 @@ ReceiverStateMachine.prototype = {
   _notifyDisconnected: function _notifyDisconnected(reason) {
     this._channel.notifyDisconnected(reason);
   },
 
   _notifyLaunch: function _notifyLaunch(presentationId, url) {
     this._channel.notifyLaunch(presentationId, url);
   },
 
+  _notifyTerminate: function _notifyTerminate(presentationId) {
+    this._channel.notifyTerminate(presentationId);
+  },
+
   _notifyChannelDescriptor: function _notifyChannelDescriptor(command) {
     switch (command.type) {
       case CommandType.OFFER:
         this._channel.notifyOffer(command.offer);
         break;
       case CommandType.ICE_CANDIDATE:
         this._channel.notifyIceCandidate(command.candidate);
         break;
--- a/dom/presentation/provider/StateMachineHelper.jsm
+++ b/dom/presentation/provider/StateMachineHelper.jsm
@@ -20,16 +20,18 @@ const State = Object.freeze({
 const CommandType = Object.freeze({
   // control channel life cycle
   CONNECT: "connect", // { deviceId: <string> }
   CONNECT_ACK: "connect-ack", // { presentationId: <string> }
   DISCONNECT: "disconnect", // { reason: <int> }
   // presentation session life cycle
   LAUNCH: "launch", // { presentationId: <string>, url: <string> }
   LAUNCH_ACK: "launch-ack", // { presentationId: <string> }
+  TERMINATE: "terminate", // { presentationId: <string> }
+  TERMINATE_ACK: "terminate-ack", // { presentationId: <string> }
   // session transport establishment
   OFFER: "offer", // { offer: <json> }
   ANSWER: "answer", // { answer: <json> }
   ICE_CANDIDATE: "ice-candidate", // { candidate: <string> }
 });
 
 this.State = State; // jshint ignore:line
 this.CommandType = CommandType; // jshint ignore:line
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript.js
@@ -40,17 +40,17 @@ function registerMockedFactory(contractI
 function registerOriginalFactory(contractId, mockedClassId, mockedFactory, originalClassId, originalFactory) {
   if (originalFactory) {
     var registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
     registrar.unregisterFactory(mockedClassId, mockedFactory);
     registrar.registerFactory(originalClassId, "", contractId, originalFactory);
   }
 }
 
-const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
+var sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
 
 const address = Cc["@mozilla.org/supports-cstring;1"]
                   .createInstance(Ci.nsISupportsCString);
 address.data = "127.0.0.1";
 const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 addresses.appendElement(address, false);
 
 const mockedChannelDescription = {
@@ -137,16 +137,20 @@ const mockedControlChannel = {
         isValid = false;
       }
     } else if (aSDP.type == Ci.nsIPresentationChannelDescription.TYPE_DATACHANNEL) {
       isValid = (aSDP.dataChannelSDP == "test-sdp");
     }
     return isValid;
   },
   launch: function(presentationId, url) {
+    sessionId = presentationId;
+  },
+  terminate: function(presentationId) {
+    sendAsyncMessage('sender-terminate', presentationId);
   },
   disconnect: function(reason) {
     sendAsyncMessage('control-channel-closed', reason);
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyDisconnected(reason);
   },
   simulateReceiverReady: function() {
     this._listener.QueryInterface(Ci.nsIPresentationControlChannelListener).notifyReceiverReady();
   },
@@ -387,16 +391,23 @@ addMessageListener('trigger-device-promp
 
 addMessageListener('trigger-incoming-session-request', function(url) {
   var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
                       .getService(Ci.nsIPresentationDeviceManager);
   deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
 	       .onSessionRequest(mockedDevice, url, sessionId, mockedControlChannel);
 });
 
+addMessageListener('trigger-incoming-terminate-request', function() {
+  var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
+                      .getService(Ci.nsIPresentationDeviceManager);
+  deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
+	       .onTerminateRequest(mockedDevice, sessionId, mockedControlChannel, true);
+});
+
 addMessageListener('trigger-incoming-offer', function() {
   mockedControlChannel.simulateOnOffer();
 });
 
 addMessageListener('trigger-incoming-answer', function() {
   mockedControlChannel.simulateOnAnswer();
 });
 
--- a/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
+++ b/dom/presentation/tests/mochitest/PresentationSessionChromeScript1UA.js
@@ -12,17 +12,17 @@ Cu.import('resource://gre/modules/XPCOMU
 const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
                       .getService(Ci.nsIUUIDGenerator);
 
 function debug(str) {
   // dump('DEBUG -*- PresentationSessionChromeScript1UA -*-: ' + str + '\n');
 }
 
 const originalFactoryData = [];
-const sessionId = 'test-session-id-' + uuidGenerator.generateUUID().toString();
+var sessionId; // Store the uuid generated by PresentationRequest.
 const address = Cc["@mozilla.org/supports-cstring;1"]
                   .createInstance(Ci.nsISupportsCString);
 address.data = "127.0.0.1";
 const addresses = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
 addresses.appendElement(address, false);
 
 function mockChannelDescription(role) {
   this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIPresentationChannelDescription]);
@@ -179,24 +179,31 @@ const mockControlChannelOfSender = {
     sendAsyncMessage('offer-sent');
   },
   onAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .onAnswer(answer);
   },
   launch: function(presentationId, url) {
+    sessionId = presentationId;
     sendAsyncMessage('sender-launch', url);
   },
   disconnect: function(reason) {
+    if (!this._listener) {
+      return;
+    }
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyDisconnected(reason);
     mockControlChannelOfReceiver.disconnect();
-  }
+  },
+  terminate: function(presentationId) {
+    sendAsyncMessage('sender-terminate');
+  },
 };
 
 // control channel of receiver
 const mockControlChannelOfReceiver = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   set listener(listener) {
     // PresentationPresentingInfo::SetControlChannel
     if (listener) {
@@ -231,20 +238,26 @@ const mockControlChannelOfReceiver = {
   },
   sendAnswer: function(answer) {
     this._listener
         .QueryInterface(Ci.nsIPresentationSessionTransportCallback)
         .notifyTransportReady();
     sendAsyncMessage('answer-sent');
   },
   disconnect: function(reason) {
+    if (!this._listener) {
+      return;
+    }
+
     this._listener
         .QueryInterface(Ci.nsIPresentationControlChannelListener)
         .notifyDisconnected(reason);
     sendAsyncMessage('control-channel-receiver-closed', reason);
+  },
+  terminate: function(presentaionId) {
   }
 };
 
 const mockDevice = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
   id:   'id',
   name: 'name',
   type: 'type',
@@ -362,16 +375,27 @@ function initMockAndListener() {
                           .getService(Ci.nsIPresentationDeviceManager);
     deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
                  .onSessionRequest(mockDevice,
                                    url,
                                    sessionId,
                                    mockControlChannelOfReceiver);
   });
 
+  addMessageListener('trigger-on-terminate-request', function() {
+    debug('Got message: trigger-on-terminate-request');
+    var deviceManager = Cc['@mozilla.org/presentation-device/manager;1']
+                          .getService(Ci.nsIPresentationDeviceManager);
+    deviceManager.QueryInterface(Ci.nsIPresentationDeviceListener)
+                 .onTerminateRequest(mockDevice,
+                                     sessionId,
+                                     mockControlChannelOfReceiver,
+                                     false);
+  });
+
   addMessageListener('trigger-control-channel-open', function(reason) {
     debug('Got message: trigger-control-channel-open');
     mockControlChannelOfSender.notifyConnected();
     mockControlChannelOfReceiver.notifyConnected();
   });
 
   addMessageListener('trigger-on-offer', function() {
     debug('Got message: trigger-on-offer');
--- a/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
+++ b/dom/presentation/tests/mochitest/file_presentation_1ua_receiver.html
@@ -106,28 +106,28 @@ function testSendMessage() {
 }
 
 function testConnectionClosed() {
   return new Promise(function(aResolve, aReject) {
     info('Receiver: --- testConnectionClosed ---');
     connection.onclose = function() {
       connection.onclose = null;
       is(connection.state, "closed", "Receiver: Connection should be closed.");
+      command('forward-command', JSON.stringify({ name: 'receiver-closed' }));
       aResolve();
     };
     command('forward-command', JSON.stringify({ name: 'ready-to-close' }));
   });
 }
 
 function runTests() {
   testConnectionAvailable()
   .then(testConnectionReady)
   .then(testIncomingMessage)
   .then(testSendMessage)
-  .then(testConnectionClosed)
-  .then(finish);
+  .then(testConnectionClosed);
 }
 
 runTests();
 
 </script>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/file_presentation_terminate.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for B2G PresentationReceiver at receiver side</title>
+  </head>
+  <body>
+    <div id='content'></div>
+<script type='application/javascript;version=1.7'>
+
+'use strict';
+
+function is(a, b, msg) {
+  if (a === b) {
+    alert('OK ' + msg);
+  } else {
+    alert('KO ' + msg + ' | reason: ' + a + ' != ' + b);
+  }
+}
+
+function ok(a, msg) {
+  alert((a ? 'OK ' : 'KO ') + msg);
+}
+
+function info(msg) {
+  alert('INFO ' + msg);
+}
+
+function command(name, data) {
+  alert('COMMAND ' + JSON.stringify({name: name, data: data}));
+}
+
+function finish() {
+  alert('DONE');
+}
+
+var connection;
+
+function testConnectionAvailable() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionAvailable ---');
+    ok(navigator.presentation, 'Receiver: navigator.presentation should be available.');
+    ok(navigator.presentation.receiver, 'Receiver: navigator.presentation.receiver should be available.');
+
+    navigator.presentation.receiver.connectionList
+    .then((aList) => {
+      is(aList.connections.length, 1, 'Should get one conncetion.');
+      connection = aList.connections[0];
+      ok(connection.id, 'Connection ID should be set: ' + connection.id);
+      is(connection.state, 'connected', 'Connection state at receiver side should be connected.');
+      aResolve();
+    })
+    .catch((aError) => {
+      ok(false, 'Receiver: Error occurred when getting the connection: ' + aError);
+      finish();
+      aReject();
+    });
+  });
+}
+
+function testConnectionReady() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionReady ---');
+    connection.onconnect = function() {
+      connection.onconnect = null;
+      ok(false, 'Should not get |onconnect| event.')
+      aReject();
+    };
+    if (connection.state === 'connected') {
+      connection.onconnect = null;
+      is(connection.state, 'connected', 'Receiver: Connection state should become connected.');
+      aResolve();
+    }
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Receiver: --- testConnectionTerminate ---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      // Using window.alert at this stage will cause window.close() fail.
+      // Only trigger it if verdict fail.
+      if (connection.state !== 'terminated') {
+        is(connection.state, 'terminated', 'Receiver: Connection should be terminated.');
+      }
+      aResolve();
+     };
+    command('forward-command', JSON.stringify({ name: 'ready-to-terminate' }));
+  });
+}
+
+function runTests() {
+  testConnectionAvailable()
+  .then(testConnectionReady)
+  .then(testConnectionTerminate)
+}
+
+runTests();
+
+</script>
+  </body>
+</html>
--- a/dom/presentation/tests/mochitest/mochitest.ini
+++ b/dom/presentation/tests/mochitest/mochitest.ini
@@ -10,16 +10,18 @@ support-files =
   file_presentation_receiver.html
   file_presentation_receiver_establish_connection_error.html
   file_presentation_receiver_inner_iframe.html
   file_presentation_1ua_wentaway.html
   test_presentation_1ua_connection_wentaway.js
   file_presentation_receiver_auxiliary_navigation.html
   test_presentation_receiver_auxiliary_navigation.js
   file_presentation_sandboxed_presentation.html
+  file_presentation_terminate.html
+  test_presentation_terminate.js
 
 [test_presentation_dc_sender.html]
 [test_presentation_dc_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_dc_receiver_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_1ua_sender_and_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
@@ -41,9 +43,15 @@ skip-if = (e10s || toolkit == 'gonk' || 
 [test_presentation_tcp_receiver.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_tcp_receiver_oop.html]
 skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android') # Bug 1129785
 [test_presentation_receiver_auxiliary_navigation_inproc.html]
 skip-if = (e10s || toolkit == 'gonk')
 [test_presentation_receiver_auxiliary_navigation_oop.html]
 skip-if = (e10s || toolkit == 'gonk')
+[test_presentation_terminate_inproc.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_terminate_oop.html]
+skip-if = (e10s || toolkit == 'gonk' || toolkit == 'android')
+[test_presentation_sender_on_terminate_request.html]
+skip-if = toolkit == 'android'
 [test_presentation_sandboxed_presentation.html]
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_connection_wentaway.js
@@ -1,11 +1,12 @@
 'use strict';
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 
 function debug(str) {
   // info(str);
 }
 
 var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
 var receiverUrl = SimpleTest.getTestFileURL('file_presentation_1ua_wentaway.html');
 var request;
@@ -139,17 +140,21 @@ function testStartConnection() {
 }
 
 function testConnectionWentaway() {
   return new Promise(function(aResolve, aReject) {
     info('Sender: --- testConnectionWentaway ---');
     connection.onclose = function() {
       connection.onclose = null;
       is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
+      receiverIframe.addEventListener('mozbrowserclose', function closeHandler() {
+        ok(false, 'wentaway should not trigger receiver close');
+        aResolve();
+      });
+      setTimeout(aResolve, 3000);
     };
     gScript.addMessageListener('ready-to-remove-receiverFrame', function onReadyToRemove() {
       gScript.removeMessageListener('ready-to-remove-receiverFrame', onReadyToRemove);
       receiverIframe.src = "http://example.com";
     });
   });
 }
 
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver.html
@@ -65,17 +65,16 @@ function setup() {
       } else if (/^INFO /.exec(message)) {
         info(message.replace(/^INFO /, ""));
       } else if (/^COMMAND /.exec(message)) {
         var command = JSON.parse(message.replace(/^COMMAND /, ""));
         gScript.sendAsyncMessage(command.name, command.data);
       } else if (/^DONE$/.exec(message)) {
         receiverIframe.removeEventListener("mozbrowsershowmodalprompt",
                                             receiverListener);
-        teardown();
       }
     }, false);
 
     var promise = new Promise(function(aResolve, aReject) {
       document.body.appendChild(receiverIframe);
       aResolve(receiverIframe);
     });
 
@@ -173,28 +172,65 @@ function testIncomingMessage() {
       postMessageToIframe('message-from-receiver-received');
       aResolve();
     });
     postMessageToIframe('trigger-message-from-receiver');
   });
 }
 
 function testCloseConnection() {
-  return new Promise(function(aResolve, aReject) {
-    info('Sender: --- testCloseConnection ---');
-    connection.onclose = function() {
-      connection.onclose = null;
-      is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
-    };
-    gScript.addMessageListener('ready-to-close', function onReadyToClose() {
-      gScript.removeMessageListener('ready-to-close', onReadyToClose);
-      connection.close();
+  info('Sender: --- testCloseConnection ---');
+  gScript.addMessageListener('ready-to-close', function onReadyToClose() {
+    gScript.removeMessageListener('ready-to-close', onReadyToClose);
+    connection.close();
+
+    // Test terminate immediate after close.
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      ok(false, "terminate after close should do nothing");
     });
+    connection.terminate();
   });
+
+  return Promise.all([
+    new Promise(function(aResolve, aReject) {
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+        aResolve();
+      };
+    }),
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('receiver-closed', function onReceiverClosed() {
+        gScript.removeMessageListener('receiver-closed', onReceiverClosed);
+        aResolve();
+      });
+    }),
+  ]);
+}
+
+function testTerminateAfterClose() {
+  info('Sender: --- testTerminateAfterClose ---');
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onterminate = function() {
+          connection.onterminate = null;
+          ok(false, 'terminate after close should do nothing');
+          aResolve();
+        };
+        connection.terminate();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'closed', 'Sender: Connection should be closed.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
 }
 
 function teardown() {
   gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
     debug('Got message: teardown-complete');
     gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
     gScript.destroy();
     SimpleTest.finish();
@@ -203,20 +239,23 @@ function teardown() {
   gScript.sendAsyncMessage('teardown');
 }
 
 function runTests() {
   setup().then(testCreateRequest)
          .then(testStartConnection)
          .then(testSendMessage)
          .then(testIncomingMessage)
-         .then(testCloseConnection);
+         .then(testCloseConnection)
+         .then(testTerminateAfterClose)
+         .then(teardown);
 }
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 SpecialPowers.pushPermissions([
   {type: 'presentation-device-manage', allow: false, context: document},
   {type: 'presentation', allow: true, context: document},
   {type: "browser", allow: true, context: document},
 ], () => {
   SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
                                       /* Mocked TCP session transport builder in the test */
                                       ["dom.presentation.session_transport.data_channel.enable", false],
--- a/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
+++ b/dom/presentation/tests/mochitest/test_presentation_1ua_sender_and_receiver_oop.html
@@ -69,17 +69,16 @@ function setup() {
       } else if (/^INFO /.exec(message)) {
         info(message.replace(/^INFO /, ""));
       } else if (/^COMMAND /.exec(message)) {
         var command = JSON.parse(message.replace(/^COMMAND /, ""));
         gScript.sendAsyncMessage(command.name, command.data);
       } else if (/^DONE$/.exec(message)) {
         receiverIframe.removeEventListener("mozbrowsershowmodalprompt",
                                             receiverListener);
-        teardown();
       }
     }, false);
 
     var promise = new Promise(function(aResolve, aReject) {
       document.body.appendChild(receiverIframe);
       aResolve(receiverIframe);
     });
 
@@ -180,28 +179,65 @@ function testIncomingMessage() {
       postMessageToIframe('message-from-receiver-received');
       aResolve();
     });
     postMessageToIframe('trigger-message-from-receiver');
   });
 }
 
 function testCloseConnection() {
-  return new Promise(function(aResolve, aReject) {
-    info('Sender: --- testCloseConnection ---');
-    connection.onclose = function() {
-      connection.onclose = null;
-      is(connection.state, "closed", "Sender: Connection should be closed.");
-      aResolve();
-    };
-    gScript.addMessageListener('ready-to-close', function onReadyToClose() {
-      gScript.removeMessageListener('ready-to-close', onReadyToClose);
-      connection.close();
+  info('Sender: --- testCloseConnection ---');
+  gScript.addMessageListener('ready-to-close', function onReadyToClose() {
+    gScript.removeMessageListener('ready-to-close', onReadyToClose);
+    connection.close();
+
+    // Test terminate immediate after close.
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      ok(false, 'terminate after close should do nothing');
     });
+    connection.terminate();
   });
+
+  return Promise.all([
+    new Promise(function(aResolve, aReject) {
+      connection.onclose = function() {
+        connection.onclose = null;
+        is(connection.state, 'closed', 'Sender: Connection should be closed.');
+        aResolve();
+      };
+    }),
+    new Promise(function(aResolve, aReject) {
+      gScript.addMessageListener('receiver-closed', function onReceiverClosed() {
+        gScript.removeMessageListener('receiver-closed', onReceiverClosed);
+        aResolve();
+      });
+    }),
+  ]);
+}
+
+function testTerminateAfterClose() {
+  info('Sender: --- testTerminateAfterClose ---');
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onterminate = function() {
+          connection.onterminate = null;
+          ok(false, 'terminate at closed state should do nothing');
+          aResolve();
+        };
+        connection.terminate();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'closed', 'Sender: Connection should be closed.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
 }
 
 function teardown() {
   gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
     gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
     gScript.destroy();
     SimpleTest.finish();
   });
@@ -210,20 +246,23 @@ function teardown() {
 }
 
 function runTests() {
   setup()
   .then(testCreateRequest)
   .then(testStartConnection)
   .then(testSendMessage)
   .then(testIncomingMessage)
-  .then(testCloseConnection);
+  .then(testCloseConnection)
+  .then(testTerminateAfterClose)
+  .then(teardown);
 }
 
 SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
 SpecialPowers.pushPermissions([
   {type: 'presentation-device-manage', allow: false, context: document},
   {type: 'presentation', allow: true, context: document},
   {type: "browser", allow: true, context: document},
 ], () => {
   SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
                                       /* Mocked TCP session transport builder in the test */
                                       ["dom.presentation.session_transport.data_channel.enable", false],
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_sender_on_terminate_request.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test onTerminateRequest at sender side</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276378">Test onTerminateRequest at sender side</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript.js'));
+var request;
+var connection;
+
+function testSetup() {
+  return new Promise(function(aResolve, aReject) {
+    request = new PresentationRequest("http://example.com");
+
+    request.getAvailability().then(
+      function(aAvailability) {
+        aAvailability.onchange = function() {
+          aAvailability.onchange = null;
+          ok(aAvailability.value, "Device should be available.");
+          aResolve();
+        }
+      },
+      function(aError) {
+        ok(false, "Error occurred when getting availability: " + aError);
+        teardown();
+        aReject();
+      }
+    );
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+      gScript.removeMessageListener('device-prompt', devicePromptHandler);
+      info("Device prompt is triggered.");
+      gScript.sendAsyncMessage('trigger-device-prompt-select');
+    });
+
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established', controlChannelEstablishedHandler);
+      info("A control channel is established.");
+      gScript.sendAsyncMessage('trigger-control-channel-open');
+    });
+
+    gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler);
+      info("The control channel is opened.");
+    });
+
+    gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler);
+      info("The control channel is closed. " + aReason);
+    });
+
+    gScript.addMessageListener('offer-sent', function offerSentHandler(aIsValid) {
+      gScript.removeMessageListener('offer-sent', offerSentHandler);
+      ok(aIsValid, "A valid offer is sent out.");
+      gScript.sendAsyncMessage('trigger-incoming-transport');
+    });
+
+    gScript.addMessageListener('answer-received', function answerReceivedHandler() {
+      gScript.removeMessageListener('answer-received', answerReceivedHandler);
+      info("An answer is received.");
+    });
+
+    gScript.addMessageListener('data-transport-initialized', function dataTransportInitializedHandler() {
+      gScript.removeMessageListener('data-transport-initialized', dataTransportInitializedHandler);
+      info("Data transport channel is initialized.");
+      gScript.sendAsyncMessage('trigger-incoming-answer');
+    });
+
+    gScript.addMessageListener('data-transport-notification-enabled', function dataTransportNotificationEnabledHandler() {
+      gScript.removeMessageListener('data-transport-notification-enabled', dataTransportNotificationEnabledHandler);
+      info("Data notification is enabled for data transport channel.");
+    });
+
+    var connectionFromEvent;
+    request.onconnectionavailable = function(aEvent) {
+      request.onconnectionavailable = null;
+      connectionFromEvent = aEvent.connection;
+      ok(connectionFromEvent, "|connectionavailable| event is fired with a connection.");
+
+      if (connection) {
+        is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+      }
+    };
+
+    request.start().then(
+      function(aConnection) {
+        connection = aConnection;
+        ok(connection, "Connection should be available.");
+        ok(connection.id, "Connection ID should be set.");
+        is(connection.state, "connecting", "The initial state should be connecting.");
+
+        if (connectionFromEvent) {
+          is(connection, connectionFromEvent, "The connection from promise and the one from |connectionavailable| event should be the same.");
+        }
+        connection.onconnect = function() {
+          connection.onconnect = null;
+          is(connection.state, "connected", "Connection should be connected.");
+          aResolve();
+        };
+      },
+      function(aError) {
+        ok(false, "Error occurred when establishing a connection: " + aError);
+        teardown();
+        aReject();
+      }
+    );
+  });
+}
+
+function testOnTerminateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    gScript.addMessageListener('control-channel-opened', function controlChannelOpenedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-opened', controlChannelOpenedHandler);
+      info("The control channel is opened.");
+    });
+
+    gScript.addMessageListener('control-channel-closed', function controlChannelClosedHandler(aReason) {
+      gScript.removeMessageListener('control-channel-closed', controlChannelClosedHandler);
+      info("The control channel is closed. " + aReason);
+    });
+
+    gScript.addMessageListener('data-transport-closed', function dataTransportClosedHandler(aReason) {
+      gScript.removeMessageListener('data-transport-closed', dataTransportClosedHandler);
+      info("The data transport is closed. " + aReason);
+    });
+
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      is(connection.state, "terminated", "Connection should be closed.");
+      aResolve();
+    };
+
+    gScript.sendAsyncMessage('trigger-incoming-terminate-request');
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  ok(window.PresentationRequest, "PresentationRequest should be available.");
+
+  testSetup().
+  then(testStartConnection).
+  then(testOnTerminateRequest).
+  then(teardown);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+], function() {
+  SpecialPowers.pushPrefEnv({ 'set': [["dom.presentation.enabled", true],
+                                      ["dom.presentation.session_transport.data_channel.enable", false]]},
+                            runTests);
+});
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate.js
@@ -0,0 +1,241 @@
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout('Test for guarantee not firing async event');
+
+function debug(str) {
+  // info(str);
+}
+
+var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('PresentationSessionChromeScript1UA.js'));
+var receiverUrl = SimpleTest.getTestFileURL('file_presentation_terminate.html');
+var request;
+var connection;
+var receiverIframe;
+
+function setup() {
+  gScript.addMessageListener('device-prompt', function devicePromptHandler() {
+    debug('Got message: device-prompt');
+    gScript.removeMessageListener('device-prompt', devicePromptHandler);
+    gScript.sendAsyncMessage('trigger-device-prompt-select');
+  });
+
+  gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+    gScript.removeMessageListener('control-channel-established',
+                                  controlChannelEstablishedHandler);
+    gScript.sendAsyncMessage('trigger-control-channel-open');
+  });
+
+  gScript.addMessageListener('sender-launch', function senderLaunchHandler(url) {
+    debug('Got message: sender-launch');
+    gScript.removeMessageListener('sender-launch', senderLaunchHandler);
+    is(url, receiverUrl, 'Receiver: should receive the same url');
+    receiverIframe = document.createElement('iframe');
+    receiverIframe.setAttribute('mozbrowser', 'true');
+    receiverIframe.setAttribute('mozpresentation', receiverUrl);
+    var oop = location.pathname.indexOf('_inproc') == -1;
+    receiverIframe.setAttribute('remote', oop);
+
+    receiverIframe.setAttribute('src', receiverUrl);
+    receiverIframe.addEventListener('mozbrowserloadend', function mozbrowserloadendHander() {
+      receiverIframe.removeEventListener('mozbrowserloadend', mozbrowserloadendHander);
+      info('Receiver loaded.');
+    });
+
+    // This event is triggered when the iframe calls 'alert'.
+    receiverIframe.addEventListener('mozbrowsershowmodalprompt', function receiverListener(evt) {
+      var message = evt.detail.message;
+      if (/^OK /.exec(message)) {
+        ok(true, message.replace(/^OK /, ''));
+      } else if (/^KO /.exec(message)) {
+        ok(false, message.replace(/^KO /, ''));
+      } else if (/^INFO /.exec(message)) {
+        info(message.replace(/^INFO /, ''));
+      } else if (/^COMMAND /.exec(message)) {
+        var command = JSON.parse(message.replace(/^COMMAND /, ''));
+        gScript.sendAsyncMessage(command.name, command.data);
+      } else if (/^DONE$/.exec(message)) {
+        ok(true, 'Messaging from iframe complete.');
+        receiverIframe.removeEventListener('mozbrowsershowmodalprompt',
+                                            receiverListener);
+      }
+    }, false);
+
+    var promise = new Promise(function(aResolve, aReject) {
+      document.body.appendChild(receiverIframe);
+      aResolve(receiverIframe);
+    });
+
+    var obs = SpecialPowers.Cc['@mozilla.org/observer-service;1']
+                           .getService(SpecialPowers.Ci.nsIObserverService);
+    obs.notifyObservers(promise, 'setup-request-promise', null);
+  });
+
+  gScript.addMessageListener('promise-setup-ready', function promiseSetupReadyHandler() {
+    debug('Got message: promise-setup-ready');
+    gScript.removeMessageListener('promise-setup-ready',
+                                  promiseSetupReadyHandler);
+    gScript.sendAsyncMessage('trigger-on-session-request', receiverUrl);
+  });
+
+  gScript.addMessageListener('offer-sent', function offerSentHandler() {
+    debug('Got message: offer-sent');
+    gScript.removeMessageListener('offer-sent', offerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-offer');
+  });
+
+  gScript.addMessageListener('answer-sent', function answerSentHandler() {
+    debug('Got message: answer-sent');
+    gScript.removeMessageListener('answer-sent', answerSentHandler);
+    gScript.sendAsyncMessage('trigger-on-answer');
+  });
+
+  return Promise.resolve();
+}
+
+function testCreateRequest() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testCreateRequest ---');
+    request = new PresentationRequest(receiverUrl);
+    request.getAvailability().then((aAvailability) => {
+      aAvailability.onchange = function() {
+        aAvailability.onchange = null;
+        ok(aAvailability.value, 'Sender: Device should be available.');
+        aResolve();
+      }
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when getting availability: ' + aError);
+      teardown();
+      aReject();
+    });
+
+    gScript.sendAsyncMessage('trigger-device-add');
+  });
+}
+
+function testStartConnection() {
+  return new Promise(function(aResolve, aReject) {
+    request.start().then((aConnection) => {
+      connection = aConnection;
+      ok(connection, 'Sender: Connection should be available.');
+      ok(connection.id, 'Sender: Connection ID should be set.');
+      is(connection.state, 'connecting', 'Sender: The initial state should be connecting.');
+      connection.onconnect = function() {
+        connection.onconnect = null;
+        is(connection.state, 'connected', 'Connection should be connected.');
+        aResolve();
+      };
+
+      info('Sender: test terminate at connecting state');
+      connection.onterminate = function() {
+        connection.onterminate = null;
+        ok(false, 'Should not be able to terminate at connecting state');
+        aReject();
+      }
+      connection.terminate();
+    }).catch((aError) => {
+      ok(false, 'Sender: Error occurred when establishing a connection: ' + aError);
+      teardown();
+      aReject();
+    });
+  });
+}
+
+function testConnectionTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    info('Sender: --- testConnectionTerminate---');
+    connection.onterminate = function() {
+      connection.onterminate = null;
+      is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+    };
+    gScript.addMessageListener('control-channel-established', function controlChannelEstablishedHandler() {
+      gScript.removeMessageListener('control-channel-established',
+                                    controlChannelEstablishedHandler);
+      gScript.sendAsyncMessage('trigger-control-channel-open');
+    });
+    gScript.addMessageListener('sender-terminate', function senderTerminateHandler() {
+      gScript.removeMessageListener('sender-terminate',
+                                    senderTerminateHandler);
+
+      receiverIframe.addEventListener('mozbrowserclose', function() {
+        ok(true, 'observe receiver page closing');
+        aResolve();
+      });
+
+      gScript.sendAsyncMessage('trigger-on-terminate-request');
+    });
+    gScript.addMessageListener('ready-to-terminate', function onReadyToTerminate() {
+      gScript.removeMessageListener('ready-to-terminate', onReadyToTerminate);
+      connection.terminate();
+
+      // test unexpected close right after terminate
+      connection.onclose = function() {
+        ok(false, 'close after terminate should do nothing');
+      };
+      connection.close();
+    });
+  });
+}
+
+function testSendAfterTerminate() {
+  return new Promise(function(aResolve, aReject) {
+    try {
+      connection.send('something');
+      ok(false, 'PresentationConnection.send should be failed');
+    } catch (e) {
+      is(e.name, 'InvalidStateError', 'Must throw InvalidStateError');
+    }
+    aResolve();
+  });
+}
+
+function testCloseAfterTerminate() {
+  return Promise.race([
+      new Promise(function(aResolve, aReject) {
+        connection.onclose = function() {
+          connection.onclose = null;
+          ok(false, 'close at terminated state should do nothing');
+          aResolve();
+        };
+        connection.close();
+      }),
+      new Promise(function(aResolve, aReject) {
+        setTimeout(function() {
+          is(connection.state, 'terminated', 'Sender: Connection should be terminated.');
+          aResolve();
+        }, 3000);
+      }),
+  ]);
+}
+
+function teardown() {
+  gScript.addMessageListener('teardown-complete', function teardownCompleteHandler() {
+    debug('Got message: teardown-complete');
+    gScript.removeMessageListener('teardown-complete', teardownCompleteHandler);
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+  gScript.sendAsyncMessage('teardown');
+}
+
+function runTests() {
+  setup().then(testCreateRequest)
+         .then(testStartConnection)
+         .then(testConnectionTerminate)
+         .then(testSendAfterTerminate)
+         .then(testCloseAfterTerminate)
+         .then(teardown);
+}
+
+SpecialPowers.pushPermissions([
+  {type: 'presentation-device-manage', allow: false, context: document},
+  {type: 'presentation', allow: true, context: document},
+  {type: 'browser', allow: true, context: document},
+], () => {
+  SpecialPowers.pushPrefEnv({ 'set': [['dom.presentation.enabled', true],
+                                      ['dom.presentation.test.enabled', true],
+                                      ['dom.mozBrowserFramesEnabled', true],
+                                      ['dom.ipc.tabs.disabled', false],
+                                      ['dom.presentation.test.stage', 0]]},
+                            runTests);
+});
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_inproc.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+      Test for PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'>
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/presentation/tests/mochitest/test_presentation_terminate_oop.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<!-- vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: -->
+<html>
+  <!-- Any copyright is dedicated to the Public Domain.
+    - http://creativecommons.org/publicdomain/zero/1.0/ -->
+  <head>
+    <meta charset='utf-8'>
+    <title>Test for PresentationConnection.terminate()</title>
+    <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css'/>
+    <script type='application/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  </head>
+  <body>
+    <a target='_blank' href='https://bugzilla.mozilla.org/show_bug.cgi?id=1276378'>
+      Test for PresentationConnection.terminate()</a>
+    <script type='application/javascript;version=1.8' src='test_presentation_terminate.js'>
+    </script>
+  </body>
+</html>
--- a/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
+++ b/dom/presentation/tests/xpcshell/test_presentation_device_manager.js
@@ -18,16 +18,17 @@ function TestPresentationDevice() {}
 function TestPresentationControlChannel() {}
 
 TestPresentationControlChannel.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannel]),
   sendOffer: function(offer) {},
   sendAnswer: function(answer) {},
   disconnect: function() {},
   launch: function() {},
+  terminate: function() {},
   set listener(listener) {},
   get listener() {},
 };
 
 var testProvider = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceProvider]),
 
   forceDiscovery: function() {
@@ -134,16 +135,37 @@ function sessionRequest() {
     Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
 
     run_next_test();
   }, 'presentation-session-request', false);
   manager.QueryInterface(Ci.nsIPresentationDeviceListener)
          .onSessionRequest(testDevice, testUrl, testPresentationId, testControlChannel);
 }
 
+function terminateRequest() {
+  let testUrl = 'http://www.example.org/';
+  let testPresentationId = 'test-presentation-id';
+  let testControlChannel = new TestPresentationControlChannel();
+  let testIsFromReceiver = true;
+  Services.obs.addObserver(function observer(subject, topic, data) {
+    Services.obs.removeObserver(observer, topic);
+
+    let request = subject.QueryInterface(Ci.nsIPresentationTerminateRequest);
+
+    Assert.equal(request.device.id, testDevice.id, 'expected device');
+    Assert.equal(request.presentationId, testPresentationId, 'expected presentation Id');
+    Assert.equal(request.isFromReceiver, testIsFromReceiver, 'expected isFromReceiver');
+
+    run_next_test();
+  }, 'presentation-terminate-request', false);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener)
+         .onTerminateRequest(testDevice, testPresentationId,
+                             testControlChannel, testIsFromReceiver);
+}
+
 function removeDevice() {
   Services.obs.addObserver(function observer(subject, topic, data) {
     Services.obs.removeObserver(observer, topic);
 
     let updatedDevice = subject.QueryInterface(Ci.nsIPresentationDevice);
     Assert.equal(updatedDevice.id, testDevice.id, 'expected device id');
     Assert.equal(updatedDevice.name, testDevice.name, 'expected device name');
     Assert.equal(updatedDevice.type, testDevice.type, 'expected device type');
@@ -171,14 +193,15 @@ function removeProvider() {
   manager.removeDeviceProvider(testProvider);
 }
 
 add_test(addProvider);
 add_test(forceDiscovery);
 add_test(addDevice);
 add_test(updateDevice);
 add_test(sessionRequest);
+add_test(terminateRequest);
 add_test(removeDevice);
 add_test(removeProvider);
 
 function run_test() {
   run_next_test();
 }
--- a/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
+++ b/dom/presentation/tests/xpcshell/test_presentation_state_machine.js
@@ -73,16 +73,55 @@ function launch() {
       Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
       Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
 
       run_next_test();
     };
   };
 }
 
+function terminateByController() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  controllerState.terminate(testPresentationId);
+  mockReceiverChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockControllerChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+
+      run_next_test();
+    };
+
+    receiverState.terminateAck(presentationId);
+  };
+}
+
+function terminateByReceiver() {
+  Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+  Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+
+  receiverState.terminate(testPresentationId);
+  mockControllerChannel.notifyTerminate = function(presentationId) {
+    Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
+    Assert.equal(presentationId, testPresentationId, 'expected presentationId received');
+
+    mockReceiverChannel.notifyTerminate = function(presentationId) {
+      Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
+      Assert.equal(presentationId, testPresentationId, 'expected presentationId received from ack');
+      run_next_test();
+    };
+
+    controllerState.terminateAck(presentationId);
+  };
+}
+
 function exchangeSDP() {
   Assert.equal(controllerState.state, State.CONNECTED, 'controller in connected state');
   Assert.equal(receiverState.state, State.CONNECTED, 'receiver in connected state');
 
   const testOffer = 'test-offer';
   const testAnswer = 'test-answer';
   const testIceCandidate = 'test-ice-candidate';
   controllerState.sendOffer(testOffer);
@@ -180,16 +219,18 @@ function abnormalDisconnect() {
       run_next_test();
     };
     controllerState.onChannelClosed(Cr.NS_OK, true);
   };
 }
 
 add_test(connect);
 add_test(launch);
+add_test(terminateByController);
+add_test(terminateByReceiver);
 add_test(exchangeSDP);
 add_test(disconnect);
 add_test(receiverDisconnect);
 add_test(abnormalDisconnect);
 
 function run_test() { // jshint ignore:line
   run_next_test();
 }
--- a/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
+++ b/dom/presentation/tests/xpcshell/test_tcp_control_channel.js
@@ -176,16 +176,185 @@ function testPresentationServer() {
       this.status = 'closed';
       Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify closed');
       yayFuncs.presenterControlChannelClose();
     },
     QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
   };
 }
 
+function terminateRequest() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected']);
+  let controllerControlChannel;
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiverj) {
+      controllerControlChannel = controlChannel;
+      Assert.equal(deviceInfo.id, pcs.id, 'expected device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      Assert.equal(isFromReceiver, false, 'expected request from controller');
+
+      controllerControlChannel.listener = {
+        notifyConnected: function() {
+          Assert.ok(true, 'control channel notify connected');
+          yayFuncs.controllerControlChannelConnected();
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+          yayFuncs.controllerControlChannelDisconnected();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId', 'http://example.com');
+      presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequest() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected',
+                                   'terminatedByController',
+                                   'terminatedByReceiver']);
+  let controllerControlChannel;
+  let terminatePhase = 'controller';
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      controlChannel.terminate(presentationId); // Reply terminate ack.
+
+      if (terminatePhase === 'controller') {
+        controllerControlChannel = controlChannel;
+        Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+        Assert.equal(isFromReceiver, false, 'expected request from controller');
+        yayFuncs.terminatedByController();
+
+        controllerControlChannel.listener = {
+          notifyConnected: function() {
+            Assert.ok(true, 'control channel notify connected');
+            yayFuncs.controllerControlChannelConnected();
+
+            terminatePhase = 'receiver';
+            controllerControlChannel.terminate('testPresentationId');
+          },
+          notifyDisconnected: function(aReason) {
+            Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, 'controllerControlChannel notify disconncted');
+            yayFuncs.controllerControlChannelDisconnected();
+          },
+          QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+        };
+      } else {
+        Assert.equal(deviceInfo.id, presenterDeviceInfo.id, 'expected presenter device id');
+        Assert.equal(isFromReceiver, true, 'expected request from receiver');
+        yayFuncs.terminatedByReceiver();
+        presenterControlChannel.disconnect(CLOSE_CONTROL_CHANNEL_REASON);
+      }
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, CLOSE_CONTROL_CHANNEL_REASON, '4. presenterControlChannel notify disconnected');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
+function terminateRequestAbnormal() {
+  let yayFuncs = makeJointSuccess(['controllerControlChannelConnected',
+                                   'controllerControlChannelDisconnected',
+                                   'presenterControlChannelDisconnected']);
+  let controllerControlChannel;
+
+  pcs.listener = {
+    onTerminateRequest: function(deviceInfo, presentationId, controlChannel, isFromReceiver) {
+      Assert.equal(deviceInfo.id, pcs.id, 'expected controller device id');
+      Assert.equal(deviceInfo.address, '127.0.0.1', 'expected device address');
+      Assert.equal(presentationId, 'testPresentationId', 'expected presentation id');
+      Assert.equal(isFromReceiver, false, 'expected request from controller');
+      controlChannel.terminate('unmatched-presentationId'); // Reply abnormal terminate ack.
+
+      controllerControlChannel = controlChannel;
+
+      controllerControlChannel.listener = {
+          notifyConnected: function() {
+          Assert.ok(true, 'control channel notify connected');
+          yayFuncs.controllerControlChannelConnected();
+        },
+        notifyDisconnected: function(aReason) {
+          Assert.equal(aReason, Cr.NS_ERROR_FAILURE, 'controllerControlChannel notify disconncted with error');
+          yayFuncs.controllerControlChannelDisconnected();
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+      };
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPPresentationServerListener]),
+  };
+
+  let presenterDeviceInfo = {
+    id: 'presentatorID',
+    address: '127.0.0.1',
+    port: PRESENTER_CONTROL_CHANNEL_PORT,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsITCPDeviceInfo]),
+  };
+
+  let presenterControlChannel = pcs.connect(presenterDeviceInfo);
+
+  presenterControlChannel.listener = {
+    notifyConnected: function() {
+      presenterControlChannel.terminate('testPresentationId');
+    },
+    notifyDisconnected: function(aReason) {
+      Assert.equal(aReason, Cr.NS_ERROR_FAILURE, '4. presenterControlChannel notify disconnected with error');
+      yayFuncs.presenterControlChannelDisconnected();
+    },
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationControlChannelListener]),
+  };
+}
+
 function setOffline() {
   pcs.listener = {
     onPortChange: function(aPort) {
       Assert.notEqual(aPort, 0, 'TCPPresentationServer port changed and the port should be valid');
       pcs.close();
       run_next_test();
     },
   };
@@ -220,16 +389,18 @@ function shutdown()
 
 // Test manually close control channel with NS_ERROR_FAILURE
 function changeCloseReason() {
   CLOSE_CONTROL_CHANNEL_REASON = Cr.NS_ERROR_FAILURE;
   run_next_test();
 }
 
 add_test(loopOfferAnser);
+add_test(terminateRequest);
+add_test(terminateRequestAbnormal);
 add_test(setOffline);
 add_test(changeCloseReason);
 add_test(oneMoreLoop);
 add_test(shutdown);
 
 function run_test() {
   Services.prefs.setBoolPref("dom.presentation.tcp_server.debug", true);
 
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -67,16 +67,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     {name: "NaN", xbl: false},
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -957,18 +958,16 @@ var interfaceNamesInGlobalScope =
     {name: "PresentationSession", disabled: true, permission: ["presentation"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PresentationSessionConnectEvent", disabled: true, permission: ["presentation"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProcessingInstruction",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ProgressEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushManager", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscription", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PushSubscriptionOptions", b2g: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RadioNodeList",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/url/URL.cpp
+++ b/dom/url/URL.cpp
@@ -917,75 +917,79 @@ public:
 // This class creates a URL object on the main thread.
 class ConstructorRunnable : public WorkerMainThreadRunnable
 {
 private:
   const nsString mURL;
 
   nsString mBase; // IsVoid() if we have no base URI string.
   RefPtr<URLProxy> mBaseProxy;
-  ErrorResult& mRv;
 
   RefPtr<URLProxy> mRetval;
 
 public:
   ConstructorRunnable(WorkerPrivate* aWorkerPrivate,
-                      const nsAString& aURL, const Optional<nsAString>& aBase,
-                      ErrorResult& aRv)
+                      const nsAString& aURL, const Optional<nsAString>& aBase)
   : WorkerMainThreadRunnable(aWorkerPrivate,
                              NS_LITERAL_CSTRING("URL :: Constructor"))
   , mURL(aURL)
-  , mRv(aRv)
   {
     if (aBase.WasPassed()) {
       mBase = aBase.Value();
     } else {
       mBase.SetIsVoid(true);
     }
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   ConstructorRunnable(WorkerPrivate* aWorkerPrivate,
-                      const nsAString& aURL, URLProxy* aBaseProxy,
-                      ErrorResult& aRv)
+                      const nsAString& aURL, URLProxy* aBaseProxy)
   : WorkerMainThreadRunnable(aWorkerPrivate,
                              NS_LITERAL_CSTRING("URL :: Constructor with BaseURL"))
   , mURL(aURL)
   , mBaseProxy(aBaseProxy)
-  , mRv(aRv)
   {
     mBase.SetIsVoid(true);
     mWorkerPrivate->AssertIsOnWorkerThread();
   }
 
   bool
   MainThreadRun()
   {
     AssertIsOnMainThread();
 
+    ErrorResult rv;
     RefPtr<URLMainThread> url;
     if (mBaseProxy) {
-      url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, mBaseProxy->URI(), rv);
     } else if (!mBase.IsVoid()) {
-      url = URLMainThread::Constructor(nullptr, mURL, mBase, mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, mBase, rv);
     } else {
-      url = URLMainThread::Constructor(nullptr, mURL, nullptr, mRv);
+      url = URLMainThread::Constructor(nullptr, mURL, nullptr, rv);
     }
 
-    if (mRv.Failed()) {
+    if (rv.Failed()) {
+      rv.SuppressException();
       return true;
     }
 
     mRetval = new URLProxy(url.forget());
     return true;
   }
 
   URLProxy*
-  GetURLProxy()
+  GetURLProxy(ErrorResult& aRv) const
   {
+    MOZ_ASSERT(mWorkerPrivate);
+    mWorkerPrivate->AssertIsOnWorkerThread();
+
+    if (!mRetval) {
+      aRv.ThrowTypeError<MSG_INVALID_URL>(mURL);
+    }
+
     return mRetval;
   }
 };
 
 class TeardownURLRunnable : public Runnable
 {
 public:
   explicit TeardownURLRunnable(URLProxy* aURLProxy)
@@ -1206,19 +1210,18 @@ already_AddRefed<URLWorker>
 FinishConstructor(JSContext* aCx, WorkerPrivate* aPrivate,
                   ConstructorRunnable* aRunnable, ErrorResult& aRv)
 {
   aRunnable->Dispatch(aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  RefPtr<URLProxy> proxy = aRunnable->GetURLProxy();
-  if (NS_WARN_IF(!proxy)) {
-    aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
+  RefPtr<URLProxy> proxy = aRunnable->GetURLProxy(aRv);
+  if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   RefPtr<URLWorker> url = new URLWorker(aPrivate, proxy);
   return url.forget();
 }
 
 /* static */ already_AddRefed<URLWorker>
@@ -1227,46 +1230,46 @@ URLWorker::Constructor(const GlobalObjec
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   URLWorker& base = static_cast<URLWorker&>(aBase);
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy(), aRv);
+    new ConstructorRunnable(workerPrivate, aURL, base.GetURLProxy());
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ already_AddRefed<URLWorker>
 URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL,
                        const Optional<nsAString>& aBase, ErrorResult& aRv)
 {
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, aBase, aRv);
+    new ConstructorRunnable(workerPrivate, aURL, aBase);
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ already_AddRefed<URLWorker>
 URLWorker::Constructor(const GlobalObject& aGlobal, const nsAString& aURL,
                        const nsAString& aBase, ErrorResult& aRv)
 {
   JSContext* cx = aGlobal.Context();
   WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
 
   Optional<nsAString> base;
   base = &aBase;
 
   RefPtr<ConstructorRunnable> runnable =
-    new ConstructorRunnable(workerPrivate, aURL, base, aRv);
+    new ConstructorRunnable(workerPrivate, aURL, base);
 
   return FinishConstructor(cx, workerPrivate, runnable, aRv);
 }
 
 /* static */ void
 URLWorker::CreateObjectURL(const GlobalObject& aGlobal, Blob& aBlob,
                            const mozilla::dom::objectURLOptions& aOptions,
                            nsAString& aResult, mozilla::ErrorResult& aRv)
--- a/dom/webidl/Geolocation.webidl
+++ b/dom/webidl/Geolocation.webidl
@@ -7,18 +7,18 @@
  * http://www.w3.org/TR/geolocation-API
  *
  * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 dictionary PositionOptions {
   boolean enableHighAccuracy = false;
-  long timeout = 0x7fffffff;
-  long maximumAge = 0;
+  [Clamp] unsigned long timeout = 0x7fffffff;
+  [Clamp] unsigned long maximumAge = 0;
 };
 
 [NoInterfaceObject]
 interface Geolocation {
   [Throws]
   void getCurrentPosition(PositionCallback successCallback,
                           optional PositionErrorCallback? errorCallback = null,
                           optional PositionOptions options);
--- a/dom/workers/ServiceWorkerRegistration.cpp
+++ b/dom/workers/ServiceWorkerRegistration.cpp
@@ -1,22 +1,24 @@
 /* -*- 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 "ServiceWorkerRegistration.h"
 
+#include "ipc/ErrorIPCUtils.h"
 #include "mozilla/dom/Notification.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/PromiseWorkerProxy.h"
 #include "mozilla/dom/ServiceWorkerRegistrationBinding.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
+#include "mozilla/unused.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsNetUtil.h"
 #include "nsServiceManagerUtils.h"
 #include "ServiceWorker.h"
 #include "ServiceWorkerManager.h"
 
 #include "nsIDocument.h"
 #include "nsIServiceWorkerManager.h"
@@ -382,49 +384,51 @@ public:
   {
     mPromise->MaybeReject(aStatus);
   }
 };
 
 class UpdateResultRunnable final : public WorkerRunnable
 {
   RefPtr<PromiseWorkerProxy> mPromiseProxy;
-  ErrorResult mStatus;
+  IPC::Message mSerializedErrorResult;
 
   ~UpdateResultRunnable()
   {}
 
 public:
   UpdateResultRunnable(PromiseWorkerProxy* aPromiseProxy, ErrorResult& aStatus)
     : WorkerRunnable(aPromiseProxy->GetWorkerPrivate())
     , mPromiseProxy(aPromiseProxy)
-    , mStatus(Move(aStatus))
-  { }
+  {
+    // ErrorResult is not thread safe.  Serialize it for transfer across
+    // threads.
+    IPC::WriteParam(&mSerializedErrorResult, aStatus);
+    aStatus.SuppressException();
+  }
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
+    // Deserialize the ErrorResult now that we are back in the worker
+    // thread.
+    ErrorResult status;
+    PickleIterator iter = PickleIterator(mSerializedErrorResult);
+    Unused << IPC::ReadParam(&mSerializedErrorResult, &iter, &status);
+
     Promise* promise = mPromiseProxy->WorkerPromise();
-    if (mStatus.Failed()) {
-      promise->MaybeReject(mStatus);
+    if (status.Failed()) {
+      promise->MaybeReject(status);
     } else {
       promise->MaybeResolve(JS::UndefinedHandleValue);
     }
-    mStatus.SuppressException();
+    status.SuppressException();
     mPromiseProxy->CleanUp();
     return true;
   }
-
-  void
-  PostDispatch(WorkerPrivate* aWorkerPrivate, bool aSuccess) override
-  {
-    if (!aSuccess) {
-      mStatus.SuppressException();
-    }
-  }
 };
 
 class WorkerThreadUpdateCallback final : public ServiceWorkerUpdateFinishCallback
 {
   RefPtr<PromiseWorkerProxy> mPromiseProxy;
 
   ~WorkerThreadUpdateCallback()
   {
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -42,16 +42,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -170,18 +171,16 @@ var interfaceNamesInGlobalScope =
     "PerformanceMark",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserver", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserverEntryList", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushEvent", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushManager", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushMessageData", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscription", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -42,16 +42,17 @@ var ecmaGlobals =
     {name: "Intl", android: false},
     "Iterator",
     "JSON",
     "Map",
     "Math",
     "NaN",
     "Number",
     "Object",
+    "Promise",
     "Proxy",
     "RangeError",
     "ReferenceError",
     "Reflect",
     "RegExp",
     "Set",
     {name: "SharedArrayBuffer", nightly: true},
     {name: "SIMD", nightly: true},
@@ -162,18 +163,16 @@ var interfaceNamesInGlobalScope =
     "PerformanceMark",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PerformanceMeasure",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserver", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PerformanceObserverEntryList", nightly: true },
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "Promise",
-// IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushManager", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscription", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     { name: "PushSubscriptionOptions", b2g: false },
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Request",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/xbl/nsXBLBinding.cpp
+++ b/dom/xbl/nsXBLBinding.cpp
@@ -235,17 +235,17 @@ nsXBLBinding::InstallAnonymousContent(ns
     // an element is added to a XUL document), we need to notify the
     // XUL document using its special API.
     nsCOMPtr<nsIXULDocument> xuldoc(do_QueryInterface(doc));
     if (xuldoc)
       xuldoc->AddSubtreeToDocument(child);
 #endif
 
     if (servoStyleSet) {
-      servoStyleSet->RestyleSubtree(child);
+      servoStyleSet->RestyleSubtree(child, /* aForce = */ true);
     }
   }
 }
 
 void
 nsXBLBinding::UninstallAnonymousContent(nsIDocument* aDocument,
                                         nsIContent* aAnonParent)
 {
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -1133,16 +1133,20 @@ XMLHttpRequestMainThread::GetAllResponse
   aResponseHeaders.Truncate();
 
   // If the state is UNSENT or OPENED,
   // return the empty string and terminate these steps.
   if (mState == State::unsent || mState == State::opened) {
     return;
   }
 
+  if (mErrorLoad) {
+    return;
+  }
+
   if (nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel()) {
     RefPtr<nsHeaderVisitor> visitor =
       new nsHeaderVisitor(*this, WrapNotNull(httpChannel));
     if (NS_SUCCEEDED(httpChannel->VisitResponseHeaders(visitor))) {
       aResponseHeaders = visitor->Headers();
     }
     return;
   }
--- a/extensions/auth/nsHttpNegotiateAuth.cpp
+++ b/extensions/auth/nsHttpNegotiateAuth.cpp
@@ -35,27 +35,32 @@
 #include "prprf.h"
 #include "mozilla/Logging.h"
 #include "prmem.h"
 #include "prnetdb.h"
 #include "mozilla/Likely.h"
 #include "mozilla/Snprintf.h"
 #include "nsIChannel.h"
 #include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+#include "nsIHttpAuthenticatorCallback.h"
+#include "mozilla/Mutex.h"
+#include "nsICancelable.h"
 
 //-----------------------------------------------------------------------------
 
 static const char kNegotiate[] = "Negotiate";
 static const char kNegotiateAuthTrustedURIs[] = "network.negotiate-auth.trusted-uris";
 static const char kNegotiateAuthDelegationURIs[] = "network.negotiate-auth.delegation-uris";
 static const char kNegotiateAuthAllowProxies[] = "network.negotiate-auth.allow-proxies";
 static const char kNegotiateAuthAllowNonFqdn[] = "network.negotiate-auth.allow-non-fqdn";
 static const char kNegotiateAuthSSPI[] = "network.auth.use-sspi";
 
 #define kNegotiateLen  (sizeof(kNegotiate)-1)
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
 
 //-----------------------------------------------------------------------------
 
 // Return false when the channel comes from a Private browsing window.
 static bool
 TestNotInPBMode(nsIHttpAuthenticableChannel *authChannel)
 {
     nsCOMPtr<nsIChannel> bareChannel = do_QueryInterface(authChannel);
@@ -179,17 +184,270 @@ nsHttpNegotiateAuth::ChallengeReceived(n
         return rv;
     }
 
     *continuationState = module;
     return NS_OK;
 }
 
 NS_IMPL_ISUPPORTS(nsHttpNegotiateAuth, nsIHttpAuthenticator)
-   
+
+namespace {
+
+//
+// GetNextTokenCompleteEvent
+//
+// This event is fired on main thread when async call of
+// nsHttpNegotiateAuth::GenerateCredentials is finished. During the Run()
+// method the nsIHttpAuthenticatorCallback::OnCredsAvailable is called with
+// obtained credentials, flags and NS_OK when successful, otherwise 
+// NS_ERROR_FAILURE is returned as a result of failed operation.
+//
+class GetNextTokenCompleteEvent final : public nsIRunnable,
+                                        public nsICancelable
+{
+    virtual ~GetNextTokenCompleteEvent()
+    {
+        if (mCreds) {
+            free(mCreds);
+        }
+    };
+
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    explicit GetNextTokenCompleteEvent(nsIHttpAuthenticatorCallback* aCallback)
+        : mCallback(aCallback)
+        , mCreds(nullptr)
+        , mCancelled(false)
+    {
+    }
+
+    NS_IMETHODIMP DispatchSuccess(char *aCreds,
+                                  uint32_t aFlags,
+                                  already_AddRefed<nsISupports> aSessionState,
+                                  already_AddRefed<nsISupports> aContinuationState)
+    {
+        // Called from worker thread
+        MOZ_ASSERT(!NS_IsMainThread());
+
+        mCreds = aCreds;
+        mFlags = aFlags;
+        mResult = NS_OK;
+        mSessionState = aSessionState;
+        mContinuationState = aContinuationState;
+        return NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
+    }
+
+    NS_IMETHODIMP DispatchError(already_AddRefed<nsISupports> aSessionState,
+                                already_AddRefed<nsISupports> aContinuationState)
+    {
+        // Called from worker thread
+        MOZ_ASSERT(!NS_IsMainThread());
+
+        mResult = NS_ERROR_FAILURE;
+        mSessionState = aSessionState;
+        mContinuationState = aContinuationState;
+        return NS_DispatchToMainThread(this, NS_DISPATCH_NORMAL);
+    }
+
+    NS_IMETHODIMP Run() override
+    {
+        // Runs on main thread
+        MOZ_ASSERT(NS_IsMainThread());
+
+        if (!mCancelled) {
+            nsCOMPtr<nsIHttpAuthenticatorCallback> callback;
+            callback.swap(mCallback);
+            callback->OnCredsGenerated(mCreds, mFlags, mResult, mSessionState, mContinuationState);
+        }
+        return NS_OK;
+    }
+
+    NS_IMETHODIMP Cancel(nsresult aReason) override
+    {
+        // Supposed to be called from main thread
+        MOZ_ASSERT(NS_IsMainThread());
+
+        mCancelled = true;
+        return NS_OK;
+    }
+
+private:
+    nsCOMPtr<nsIHttpAuthenticatorCallback> mCallback;
+    char *mCreds; // This class owns it, freed in destructor
+    uint32_t mFlags;
+    nsresult mResult;
+    bool mCancelled;
+    nsCOMPtr<nsISupports> mSessionState;
+    nsCOMPtr<nsISupports> mContinuationState;
+};
+
+NS_IMPL_ISUPPORTS(GetNextTokenCompleteEvent, nsIRunnable, nsICancelable)
+
+//
+// GetNextTokenRunnable
+//
+// This runnable is created by GenerateCredentialsAsync and it runs
+// in nsHttpNegotiateAuth::mNegotiateThread and calling GenerateCredentials.
+//
+class GetNextTokenRunnable final : public mozilla::Runnable
+{
+    virtual ~GetNextTokenRunnable() {}
+    public:
+        GetNextTokenRunnable(nsIHttpAuthenticableChannel *authChannel,
+                             const char *challenge,
+                             bool isProxyAuth,
+                             const char16_t *domain,
+                             const char16_t *username,
+                             const char16_t *password,
+                             nsISupports *sessionState,
+                             nsISupports *continuationState,
+                             GetNextTokenCompleteEvent *aCompleteEvent
+                             )
+            : mAuthChannel(authChannel)
+            , mChallenge(challenge)
+            , mIsProxyAuth(isProxyAuth)
+            , mDomain(domain)
+            , mUsername(username)
+            , mPassword(password)
+            , mSessionState(sessionState)
+            , mContinuationState(continuationState)
+            , mCompleteEvent(aCompleteEvent)
+        {
+        }
+
+        NS_IMETHODIMP Run() override
+        {
+            // Runs on worker thread
+            MOZ_ASSERT(!NS_IsMainThread());
+
+            char *creds;
+            uint32_t flags;
+            nsresult rv = ObtainCredentialsAndFlags(&creds, &flags);
+
+            // Passing session and continuation state this way to not touch
+            // referencing of the object that may not be thread safe.
+            // Not having a thread safe referencing doesn't mean the object
+            // cannot be used on multiple threads (one example is nsAuthSSPI.)
+            // This ensures state objects will be destroyed on the main thread
+            // when not changed by GenerateCredentials.
+            if (NS_FAILED(rv)) {
+                return mCompleteEvent->DispatchError(mSessionState.forget(),
+                                                     mContinuationState.forget());
+            }
+
+            return mCompleteEvent->DispatchSuccess(creds, flags,
+                                                   mSessionState.forget(),
+                                                   mContinuationState.forget());
+        }
+
+        NS_IMETHODIMP ObtainCredentialsAndFlags(char **aCreds, uint32_t *aFlags)
+        {
+            nsresult rv;
+
+            // Use negotiate service to call GenerateCredentials outside of main thread
+            nsAutoCString contractId;
+            contractId.Assign(NS_HTTP_AUTHENTICATOR_CONTRACTID_PREFIX);
+            contractId.Append("negotiate");
+            nsCOMPtr<nsIHttpAuthenticator> authenticator =
+              do_GetService(contractId.get(), &rv);
+            NS_ENSURE_SUCCESS(rv, rv);
+
+            nsISupports *sessionState = mSessionState;
+            nsISupports *continuationState = mContinuationState;
+            // The continuationState is for the sake of completeness propagated
+            // to the caller (despite it is not changed in any GenerateCredentials
+            // implementation).
+            //
+            // The only implementation that use sessionState is the
+            // nsHttpDigestAuth::GenerateCredentials. Since there's no reason
+            // to implement nsHttpDigestAuth::GenerateCredentialsAsync
+            // because digest auth does not block the main thread, we won't
+            // propagate changes to sessionState to the caller because of
+            // the change is too complicated on the caller side.
+            //
+            // Should any of the session or continuation states change inside
+            // this method, they must be threadsafe.
+            rv = authenticator->GenerateCredentials(mAuthChannel,
+                                                    mChallenge.get(),
+                                                    mIsProxyAuth,
+                                                    mDomain.get(),
+                                                    mUsername.get(),
+                                                    mPassword.get(),
+                                                    &sessionState,
+                                                    &continuationState,
+                                                    aFlags,
+                                                    aCreds);
+            if (mSessionState != sessionState) {
+                mSessionState = sessionState;
+            }
+            if (mContinuationState != continuationState) {
+                mContinuationState = continuationState;
+            }
+            return rv;
+        }
+    private:
+        nsCOMPtr<nsIHttpAuthenticableChannel> mAuthChannel;
+        nsCString mChallenge;
+        bool mIsProxyAuth;
+        nsString mDomain;
+        nsString mUsername;
+        nsString mPassword;
+        nsCOMPtr<nsISupports> mSessionState;
+        nsCOMPtr<nsISupports> mContinuationState;
+        RefPtr<GetNextTokenCompleteEvent> mCompleteEvent;
+};
+
+} // anonymous namespace
+
+NS_IMETHODIMP
+nsHttpNegotiateAuth::GenerateCredentialsAsync(nsIHttpAuthenticableChannel *authChannel,
+                                              nsIHttpAuthenticatorCallback* aCallback,
+                                              const char *challenge,
+                                              bool isProxyAuth,
+                                              const char16_t *domain,
+                                              const char16_t *username,
+                                              const char16_t *password,
+                                              nsISupports *sessionState,
+                                              nsISupports *continuationState,
+                                              nsICancelable **aCancelable)
+{
+   NS_ENSURE_ARG(aCallback);
+   NS_ENSURE_ARG_POINTER(aCancelable);
+
+   RefPtr<GetNextTokenCompleteEvent> cancelEvent =
+       new GetNextTokenCompleteEvent(aCallback);
+
+
+   nsCOMPtr<nsIRunnable> getNextTokenRunnable =
+       new GetNextTokenRunnable(authChannel,
+                                challenge,
+                                isProxyAuth,
+                                domain,
+                                username,
+                                password,
+                                sessionState,
+                                continuationState,
+                                cancelEvent);
+   cancelEvent.forget(aCancelable);
+
+   nsresult rv;
+   if (!mNegotiateThread) {
+       mNegotiateThread =
+           new mozilla::LazyIdleThread(DEFAULT_THREAD_TIMEOUT_MS,
+                                       NS_LITERAL_CSTRING("NegotiateAuth"));
+       NS_ENSURE_TRUE(mNegotiateThread, NS_ERROR_OUT_OF_MEMORY);
+   }
+   rv = mNegotiateThread->Dispatch(getNextTokenRunnable, NS_DISPATCH_NORMAL);
+   NS_ENSURE_SUCCESS(rv, rv);
+
+   return NS_OK;
+}
+
 //
 // GenerateCredentials
 //
 // This routine is responsible for creating the correct authentication
 // blob to pass to the server that requested "Negotiate" authentication.
 //
 NS_IMETHODIMP
 nsHttpNegotiateAuth::GenerateCredentials(nsIHttpAuthenticableChannel *authChannel,
--- a/extensions/auth/nsHttpNegotiateAuth.h
+++ b/extensions/auth/nsHttpNegotiateAuth.h
@@ -5,24 +5,25 @@
 
 #ifndef nsHttpNegotiateAuth_h__
 #define nsHttpNegotiateAuth_h__
 
 #include "nsIHttpAuthenticator.h"
 #include "nsIURI.h"
 #include "nsSubstring.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/LazyIdleThread.h"
 
 // The nsHttpNegotiateAuth class provides responses for the GSS-API Negotiate method
 // as specified by Microsoft in draft-brezak-spnego-http-04.txt
 
 class nsHttpNegotiateAuth final : public nsIHttpAuthenticator
 {
 public:
-    NS_DECL_ISUPPORTS
+    NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSIHTTPAUTHENTICATOR
 
 private:
     ~nsHttpNegotiateAuth() {}
 
     // returns the value of the given boolean pref
     bool TestBoolPref(const char *pref);
 
@@ -32,10 +33,12 @@ private:
     // returns true if URI is accepted by the list of hosts in the pref
     bool TestPref(nsIURI *, const char *pref);
 
     bool MatchesBaseURI(const nsCSubstring &scheme,
                           const nsCSubstring &host,
                           int32_t             port,
                           const char         *baseStart,
                           const char         *baseEnd);
+    // Thread for GenerateCredentialsAsync
+    RefPtr<mozilla::LazyIdleThread> mNegotiateThread;
 };
 #endif /* nsHttpNegotiateAuth_h__ */
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -20,28 +20,23 @@ using mozilla::gfx::IntSize;
 using mozilla::gfx::SurfaceFormat;
 
 namespace mozilla {
 namespace image {
 
 class MOZ_STACK_CLASS AutoRecordDecoderTelemetry final
 {
 public:
-  AutoRecordDecoderTelemetry(Decoder* aDecoder, uint32_t aByteCount)
+  explicit AutoRecordDecoderTelemetry(Decoder* aDecoder)
     : mDecoder(aDecoder)
   {
     MOZ_ASSERT(mDecoder);
 
     // Begin recording telemetry data.
     mStartTime = TimeStamp::Now();
-    mDecoder->mChunkCount++;
-
-    // Keep track of the total number of bytes written.
-    mDecoder->mBytesDecoded += aByteCount;
-
   }
 
   ~AutoRecordDecoderTelemetry()
   {
     // Finish telemetry.
     mDecoder->mDecodeTime += (TimeStamp::Now() - mStartTime);
   }
 
@@ -53,27 +48,24 @@ private:
 Decoder::Decoder(RasterImage* aImage)
   : mImageData(nullptr)
   , mImageDataLength(0)
   , mColormap(nullptr)
   , mColormapSize(0)
   , mImage(aImage)
   , mProgress(NoProgress)
   , mFrameCount(0)
-  , mFailCode(NS_OK)
-  , mChunkCount(0)
   , mDecoderFlags(DefaultDecoderFlags())
   , mSurfaceFlags(DefaultSurfaceFlags())
-  , mBytesDecoded(0)
   , mInitialized(false)
   , mMetadataDecode(false)
   , mInFrame(false)
-  , mDataDone(false)
+  , mReachedTerminalState(false)
   , mDecodeDone(false)
-  , mDataError(false)
+  , mError(false)
   , mDecodeAborted(false)
   , mShouldReportError(false)
 { }
 
 Decoder::~Decoder()
 {
   MOZ_ASSERT(mProgress == NoProgress || !mImage,
              "Destroying Decoder without taking all its progress changes");
@@ -87,123 +79,122 @@ Decoder::~Decoder()
     NS_ReleaseOnMainThread(mImage.forget());
   }
 }
 
 /*
  * Common implementation of the decoder interface.
  */
 
-void
+nsresult
 Decoder::Init()
 {
   // No re-initializing
   MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!");
 
   // All decoders must have a SourceBufferIterator.
   MOZ_ASSERT(mIterator);
 
   // It doesn't make sense to decode anything but the first frame if we can't
   // store anything in the SurfaceCache, since only the last frame we decode
   // will be retrievable.
   MOZ_ASSERT(ShouldUseSurfaceCache() || IsFirstFrameDecode());
 
-  // Implementation-specific initialization
-  InitInternal();
+  // Implementation-specific initialization.
+  nsresult rv = InitInternal();
 
   mInitialized = true;
+
+  return rv;
 }
 
 nsresult
-Decoder::Decode(NotNull<IResumable*> aOnResume)
+Decoder::Decode(IResumable* aOnResume /* = nullptr */)
 {
   MOZ_ASSERT(mInitialized, "Should be initialized here");
   MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator");
 
-  // We keep decoding chunks until the decode completes or there are no more
-  // chunks available.
-  while (!GetDecodeDone() && !HasError()) {
-    auto newState = mIterator->AdvanceOrScheduleResume(aOnResume.get());
-
-    if (newState == SourceBufferIterator::WAITING) {
-      // We can't continue because the rest of the data hasn't arrived from the
-      // network yet. We don't have to do anything special; the
-      // SourceBufferIterator will ensure that Decode() gets called again on a
-      // DecodePool thread when more data is available.
-      return NS_OK;
-    }
-
-    if (newState == SourceBufferIterator::COMPLETE) {
-      mDataDone = true;
+  // If we're already done, don't attempt to keep decoding.
+  if (GetDecodeDone()) {
+    return HasError() ? NS_ERROR_FAILURE : NS_OK;
+  }
 
-      nsresult finalStatus = mIterator->CompletionStatus();
-      if (NS_FAILED(finalStatus)) {
-        PostDataError();
-      }
-
-      CompleteDecode();
-      return finalStatus;
-    }
-
-    MOZ_ASSERT(newState == SourceBufferIterator::READY);
+  Maybe<TerminalState> terminalState;
+  {
+    PROFILER_LABEL("ImageDecoder", "Decode", js::ProfileEntry::Category::GRAPHICS);
+    AutoRecordDecoderTelemetry telemetry(this);
 
-    {
-      PROFILER_LABEL("ImageDecoder", "Write",
-        js::ProfileEntry::Category::GRAPHICS);
-
-      AutoRecordDecoderTelemetry telemetry(this, mIterator->Length());
-
-      // Pass the data along to the implementation.
-      Maybe<TerminalState> terminalState = DoDecode(*mIterator);
-
-      if (terminalState == Some(TerminalState::FAILURE)) {
-        PostDataError();
-      }
-    }
+    terminalState = DoDecode(*mIterator, aOnResume);
   }
 
+  if (!terminalState) {
+    // We need more data to continue. If @aOnResume was non-null, the
+    // SourceBufferIterator will automatically reschedule us. Otherwise, it's up
+    // to the caller.
+    return NS_OK;
+  }
+
+  // We reached a terminal state; we're now done decoding.
+  mReachedTerminalState = true;
+
+  // If decoding failed, record that fact.
+  if (terminalState == Some(TerminalState::FAILURE)) {
+    PostError();
+  }
+
+  // Perform final cleanup.
   CompleteDecode();
+
   return HasError() ? NS_ERROR_FAILURE : NS_OK;
 }
 
 bool
 Decoder::ShouldSyncDecode(size_t aByteLimit)
 {
   MOZ_ASSERT(aByteLimit > 0);
   MOZ_ASSERT(mIterator, "Should have a SourceBufferIterator");
 
   return mIterator->RemainingBytesIsNoMoreThan(aByteLimit);
 }
 
 void
 Decoder::CompleteDecode()
 {
-  // Implementation-specific finalization
-  BeforeFinishInternal();
-  if (!HasError()) {
-    FinishInternal();
-  } else {
-    FinishWithErrorInternal();
+  // Implementation-specific finalization.
+  nsresult rv = BeforeFinishInternal();
+  if (NS_FAILED(rv)) {
+    PostError();
+  }
+
+  rv = HasError() ? FinishWithErrorInternal()
+                  : FinishInternal();
+  if (NS_FAILED(rv)) {
+    PostError();
+  }
+
+  // If this was a metadata decode and we never got a size, the decode failed.
+  if (IsMetadataDecode() && !HasSize()) {
+    PostError();
   }
 
   // If the implementation left us mid-frame, finish that up.
   if (mInFrame && !HasError()) {
     PostFrameStop();
   }
 
   // If PostDecodeDone() has not been called, and this decoder wasn't aborted
   // early because of low-memory conditions or losing a race with another
   // decoder, we need to send teardown notifications (and report an error to the
   // console later).
   if (!IsMetadataDecode() && !mDecodeDone && !WasAborted()) {
     mShouldReportError = true;
 
-    // If we only have a data error, we're usable if we have at least one
-    // complete frame.
-    if (!HasDecoderError() && GetCompleteFrameCount() > 0) {
+    // Even if we encountered an error, we're still usable if we have at least
+    // one complete frame.
+    if (GetCompleteFrameCount() > 0) {
       // We're usable, so do exactly what we should have when the decoder
       // completed.
 
       // Not writing to the entire frame may have left us transparent.
       PostHasTransparency();
 
       if (mInFrame) {
         PostFrameStop();
@@ -271,32 +262,30 @@ Decoder::AllocateFrame(uint32_t aFrameNu
     if (aFrameNum + 1 == mFrameCount) {
       // If we're past the first frame, PostIsAnimated() should've been called.
       MOZ_ASSERT_IF(mFrameCount > 1, HasAnimation());
 
       // Update our state to reflect the new frame
       MOZ_ASSERT(!mInFrame, "Starting new frame but not done with old one!");
       mInFrame = true;
     }
-  } else {
-    PostDataError();
   }
 
   return mCurrentFrame ? NS_OK : NS_ERROR_FAILURE;
 }
 
 RawAccessFrameRef
 Decoder::AllocateFrameInternal(uint32_t aFrameNum,
                                const nsIntSize& aTargetSize,
                                const nsIntRect& aFrameRect,
                                SurfaceFormat aFormat,
                                uint8_t aPaletteDepth,
                                imgFrame* aPreviousFrame)
 {
-  if (mDataError || NS_FAILED(mFailCode)) {
+  if (HasError()) {
     return RawAccessFrameRef();
   }
 
   if (aFrameNum != mFrameCount) {
     MOZ_ASSERT_UNREACHABLE("Allocating frames out of order");
     return RawAccessFrameRef();
   }
 
@@ -383,20 +372,20 @@ Decoder::AllocateFrameInternal(uint32_t 
 
   return ref;
 }
 
 /*
  * Hook stubs. Override these as necessary in decoder implementations.
  */
 
-void Decoder::InitInternal() { }
-void Decoder::BeforeFinishInternal() { }
-void Decoder::FinishInternal() { }
-void Decoder::FinishWithErrorInternal() { }
+nsresult Decoder::InitInternal() { return NS_OK; }
+nsresult Decoder::BeforeFinishInternal() { return NS_OK; }
+nsresult Decoder::FinishInternal() { return NS_OK; }
+nsresult Decoder::FinishWithErrorInternal() { return NS_OK; }
 
 /*
  * Progress Notifications
  */
 
 void
 Decoder::PostSize(int32_t aWidth,
                   int32_t aHeight,
@@ -489,35 +478,19 @@ Decoder::PostDecodeDone(int32_t aLoopCou
   mDecodeDone = true;
 
   mImageMetadata.SetLoopCount(aLoopCount);
 
   mProgress |= FLAG_DECODE_COMPLETE;
 }
 
 void
-Decoder::PostDataError()
+Decoder::PostError()
 {
-  mDataError = true;
-
-  if (mInFrame && mCurrentFrame) {
-    mCurrentFrame->Abort();
-  }
-}
-
-void
-Decoder::PostDecoderError(nsresult aFailureCode)
-{
-  MOZ_ASSERT(NS_FAILED(aFailureCode), "Not a failure code!");
-
-  mFailCode = aFailureCode;
-
-  // XXXbholley - we should report the image URI here, but imgContainer
-  // needs to know its URI first
-  NS_WARNING("Image decoding error - This is probably a bug!");
+  mError = true;
 
   if (mInFrame && mCurrentFrame) {
     mCurrentFrame->Abort();
   }
 }
 
 Telemetry::ID
 Decoder::SpeedHistogram()
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -32,28 +32,30 @@ class Decoder
 {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Decoder)
 
   explicit Decoder(RasterImage* aImage);
 
   /**
    * Initialize an image decoder. Decoders may not be re-initialized.
+   *
+   * @return NS_OK if the decoder could be initialized successfully.
    */
-  void Init();
+  nsresult Init();
 
   /**
    * Decodes, reading all data currently available in the SourceBuffer.
    *
-   * If more data is needed, Decode() will schedule @aOnResume to be called when
-   * more data is available.
+   * If more data is needed and @aOnResume is non-null, Decode() will schedule
+   * @aOnResume to be called when more data is available.
    *
    * Any errors are reported by setting the appropriate state on the decoder.
    */
-  nsresult Decode(NotNull<IResumable*> aOnResume);
+  nsresult Decode(IResumable* aOnResume = nullptr);
 
   /**
    * Given a maximum number of bytes we're willing to decode, @aByteLimit,
    * returns true if we should attempt to run this decoder synchronously.
    */
   bool ShouldSyncDecode(size_t aByteLimit);
 
   /**
@@ -157,49 +159,54 @@ public:
   /**
    * Should we stop decoding after the first frame?
    */
   bool IsFirstFrameDecode() const
   {
     return bool(mDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY);
   }
 
-  size_t BytesDecoded() const { return mBytesDecoded; }
+  size_t BytesDecoded() const
+  {
+    MOZ_ASSERT(mIterator);
+    return mIterator->ByteCount();
+  }
 
   // The amount of time we've spent inside DoDecode() so far for this decoder.
   TimeDuration DecodeTime() const { return mDecodeTime; }
 
   // The number of chunks this decoder's input was divided into.
-  uint32_t ChunkCount() const { return mChunkCount; }
+  uint32_t ChunkCount() const
+  {
+    MOZ_ASSERT(mIterator);
+    return mIterator->ChunkCount();
+  }
 
   // The number of frames we have, including anything in-progress. Thus, this
   // is only 0 if we haven't begun any frames.
   uint32_t GetFrameCount() { return mFrameCount; }
 
   // The number of complete frames we have (ie, not including anything
   // in-progress).
   uint32_t GetCompleteFrameCount() {
     return mInFrame ? mFrameCount - 1 : mFrameCount;
   }
 
   // Did we discover that the image we're decoding is animated?
   bool HasAnimation() const { return mImageMetadata.HasAnimation(); }
 
   // Error tracking
-  bool HasError() const { return HasDataError() || HasDecoderError(); }
-  bool HasDataError() const { return mDataError; }
-  bool HasDecoderError() const { return NS_FAILED(mFailCode); }
+  bool HasError() const { return mError; }
   bool ShouldReportError() const { return mShouldReportError; }
-  nsresult GetDecoderError() const { return mFailCode; }
 
   /// Did we finish decoding enough that calling Decode() again would be useless?
   bool GetDecodeDone() const
   {
-    return mDecodeDone || (mMetadataDecode && HasSize()) ||
-           HasError() || mDataDone;
+    return mReachedTerminalState || mDecodeDone ||
+           (mMetadataDecode && HasSize()) || HasError();
   }
 
   /// Are we in the middle of a frame right now? Used for assertions only.
   bool InFrame() const { return mInFrame; }
 
   /// Should we store surfaces created by this decoder in the SurfaceCache?
   bool ShouldUseSurfaceCache() const { return bool(mImage); }
 
@@ -282,23 +289,24 @@ protected:
   virtual ~Decoder();
 
   /*
    * Internal hooks. Decoder implementations may override these and
    * only these methods.
    *
    * BeforeFinishInternal() can be used to detect if decoding is in an
    * incomplete state, e.g. due to file truncation, in which case it should
-   * call PostDataError().
+   * return a failing nsresult.
    */
-  virtual void InitInternal();
-  virtual Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) = 0;
-  virtual void BeforeFinishInternal();
-  virtual void FinishInternal();
-  virtual void FinishWithErrorInternal();
+  virtual nsresult InitInternal();
+  virtual Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                        IResumable* aOnResume) = 0;
+  virtual nsresult BeforeFinishInternal();
+  virtual nsresult FinishInternal();
+  virtual nsresult FinishWithErrorInternal();
 
   /*
    * Progress notifications.
    */
 
   // Called by decoders when they determine the size of the image. Informs
   // the image of its size and sends notifications.
   void PostSize(int32_t aWidth,
@@ -353,27 +361,16 @@ protected:
   // the stream, or by us calling FinishInternal().
   //
   // May not be called mid-frame.
   //
   // For animated images, specify the loop count. -1 means loop forever, 0
   // means a single iteration, stopping on the last frame.
   void PostDecodeDone(int32_t aLoopCount = 0);
 
-  // Data errors are the fault of the source data, decoder errors are our fault
-  void PostDataError();
-  void PostDecoderError(nsresult aFailCode);
-
-  /**
-   * CompleteDecode() finishes up the decoding process after Decode() determines
-   * that we're finished. It records final progress and does all the cleanup
-   * that's possible off-main-thread.
-   */
-  void CompleteDecode();
-
   /**
    * Allocates a new frame, making it our current frame if successful.
    *
    * The @aFrameNum parameter only exists as a sanity check; it's illegal to
    * create a new frame anywhere but immediately after the existing frames.
    *
    * If a non-paletted frame is desired, pass 0 for aPaletteDepth.
    */
@@ -385,16 +382,27 @@ protected:
 
   /// Helper method for decoders which only have 'basic' frame allocation needs.
   nsresult AllocateBasicFrame() {
     nsIntSize size = GetSize();
     return AllocateFrame(0, size, nsIntRect(nsIntPoint(), size),
                          gfx::SurfaceFormat::B8G8R8A8);
   }
 
+private:
+  /// Report that an error was encountered while decoding.
+  void PostError();
+
+  /**
+   * CompleteDecode() finishes up the decoding process after Decode() determines
+   * that we're finished. It records final progress and does all the cleanup
+   * that's possible off-main-thread.
+   */
+  void CompleteDecode();
+
   RawAccessFrameRef AllocateFrameInternal(uint32_t aFrameNum,
                                           const nsIntSize& aTargetSize,
                                           const nsIntRect& aFrameRect,
                                           gfx::SurfaceFormat aFormat,
                                           uint8_t aPaletteDepth,
                                           imgFrame* aPreviousFrame);
 
 protected:
@@ -410,32 +418,28 @@ private:
   Maybe<SourceBufferIterator> mIterator;
   RawAccessFrameRef mCurrentFrame;
   ImageMetadata mImageMetadata;
   nsIntRect mInvalidRect; // Tracks an invalidation region in the current frame.
   Progress mProgress;
 
   uint32_t mFrameCount; // Number of frames, including anything in-progress
 
-  nsresult mFailCode;
-
   // Telemetry data for this decoder.
   TimeDuration mDecodeTime;
-  uint32_t mChunkCount;
 
   DecoderFlags mDecoderFlags;
   SurfaceFlags mSurfaceFlags;
-  size_t mBytesDecoded;
 
   bool mInitialized : 1;
   bool mMetadataDecode : 1;
   bool mInFrame : 1;
-  bool mDataDone : 1;
+  bool mReachedTerminalState : 1;
   bool mDecodeDone : 1;
-  bool mDataError : 1;
+  bool mError : 1;
   bool mDecodeAborted : 1;
   bool mShouldReportError : 1;
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_Decoder_h
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -130,18 +130,17 @@ DecoderFactory::CreateDecoder(DecoderTyp
   decoder->SetSampleSize(aSampleSize);
 
   // Set a target size for downscale-during-decode if applicable.
   if (aTargetSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize);
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
   }
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   // Add a placeholder to the SurfaceCache so we won't trigger any more decoders
   // with the same parameters.
   IntSize surfaceSize = aTargetSize.valueOr(aIntrinsicSize);
   SurfaceKey surfaceKey =
     RasterSurfaceKey(surfaceSize, aSurfaceFlags, /* aFrameNum = */ 0);
@@ -175,18 +174,17 @@ DecoderFactory::CreateAnimationDecoder(D
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
   decoder->SetMetadataDecode(false);
   decoder->SetIterator(aSourceBuffer->Iterator());
   decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::IS_REDECODE);
   decoder->SetSurfaceFlags(aSurfaceFlags);
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   // Add a placeholder for the first frame to the SurfaceCache so we won't
   // trigger any more decoders with the same parameters.
   SurfaceKey surfaceKey =
     RasterSurfaceKey(aIntrinsicSize, aSurfaceFlags, /* aFrameNum = */ 0);
   InsertOutcome outcome =
@@ -213,18 +211,17 @@ DecoderFactory::CreateMetadataDecoder(De
     GetDecoder(aType, aImage, /* aIsRedecode = */ false);
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
   decoder->SetMetadataDecode(true);
   decoder->SetIterator(aSourceBuffer->Iterator());
   decoder->SetSampleSize(aSampleSize);
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   RefPtr<IDecodingTask> task = new MetadataDecodingTask(WrapNotNull(decoder));
   return task.forget();
 }
 
 /* static */ already_AddRefed<Decoder>
@@ -262,18 +259,17 @@ DecoderFactory::CreateDecoderForICOResou
 
   // Set a target size for downscale-during-decode if applicable.
   const Maybe<IntSize> targetSize = aICODecoder->GetTargetSize();
   if (targetSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*targetSize);
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
   }
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   return decoder.forget();
 }
 
 /* static */ already_AddRefed<Decoder>
 DecoderFactory::CreateAnonymousDecoder(DecoderType aType,
@@ -308,18 +304,17 @@ DecoderFactory::CreateAnonymousDecoder(D
   decoder->SetSurfaceFlags(aSurfaceFlags);
 
   // Set a target size for downscale-during-decode if applicable.
   if (aTargetSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*aTargetSize);
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
   }
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   return decoder.forget();
 }
 
 /* static */ already_AddRefed<Decoder>
 DecoderFactory::CreateAnonymousMetadataDecoder(DecoderType aType,
@@ -333,18 +328,17 @@ DecoderFactory::CreateAnonymousMetadataD
     GetDecoder(aType, /* aImage = */ nullptr, /* aIsRedecode = */ false);
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
   decoder->SetMetadataDecode(true);
   decoder->SetIterator(aSourceBuffer->Iterator());
   decoder->SetDecoderFlags(DecoderFlags::FIRST_FRAME_ONLY);
 
-  decoder->Init();
-  if (NS_FAILED(decoder->GetDecoderError())) {
+  if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   return decoder.forget();
 }
 
 } // namespace image
 } // namespace mozilla
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -1777,17 +1777,17 @@ RasterImage::FinalizeDecoder(Decoder* aD
 void
 RasterImage::ReportDecoderError(Decoder* aDecoder)
 {
   nsCOMPtr<nsIConsoleService> consoleService =
     do_GetService(NS_CONSOLESERVICE_CONTRACTID);
   nsCOMPtr<nsIScriptError> errorObject =
     do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
 
-  if (consoleService && errorObject && !aDecoder->HasDecoderError()) {
+  if (consoleService && errorObject) {
     nsAutoString msg(NS_LITERAL_STRING("Image corrupt or truncated."));
     nsAutoString src;
     if (GetURI()) {
       nsCString uri;
       if (GetURI()->GetSpecTruncatedTo1k(uri) == ImageURL::TruncatedTo1k) {
         msg += NS_LITERAL_STRING(" URI in this note truncated due to length.");
       }
       src = NS_ConvertUTF8toUTF16(uri);
--- a/image/SourceBuffer.cpp
+++ b/image/SourceBuffer.cpp
@@ -25,35 +25,93 @@ namespace image {
 
 SourceBufferIterator::~SourceBufferIterator()
 {
   if (mOwner) {
     mOwner->OnIteratorRelease();
   }
 }
 
+SourceBufferIterator&
+SourceBufferIterator::operator=(SourceBufferIterator&& aOther)
+{
+  if (mOwner) {
+    mOwner->OnIteratorRelease();
+  }
+
+  mOwner = Move(aOther.mOwner);
+  mState = aOther.mState;
+  mData = aOther.mData;
+  mChunkCount = aOther.mChunkCount;
+  mByteCount = aOther.mByteCount;
+
+  return *this;
+}
+
 SourceBufferIterator::State
-SourceBufferIterator::AdvanceOrScheduleResume(IResumable* aConsumer)
+SourceBufferIterator::AdvanceOrScheduleResume(size_t aRequestedBytes,
+                                              IResumable* aConsumer)
 {
   MOZ_ASSERT(mOwner);
-  return mOwner->AdvanceIteratorOrScheduleResume(*this, aConsumer);
+
+  if (MOZ_UNLIKELY(!HasMore())) {
+    MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator");
+    return COMPLETE;
+  }
+
+  // The range of data [mOffset, mOffset + mNextReadLength) has just been read
+  // by the caller (or at least they don't have any interest in it), so consume
+  // that data.
+  MOZ_ASSERT(mData.mIterating.mNextReadLength <= mData.mIterating.mAvailableLength);
+  mData.mIterating.mOffset += mData.mIterating.mNextReadLength;
+  mData.mIterating.mAvailableLength -= mData.mIterating.mNextReadLength;
+  mData.mIterating.mNextReadLength = 0;
+
+  if (MOZ_LIKELY(mState == READY)) {
+    // If the caller wants zero bytes of data, that's easy enough; we just
+    // configured ourselves for a zero-byte read above!  In theory we could do
+    // this even in the START state, but it's not important for performance and
+    // breaking the ability of callers to assert that the pointer returned by
+    // Data() is non-null doesn't seem worth it.
+    if (aRequestedBytes == 0) {
+      MOZ_ASSERT(mData.mIterating.mNextReadLength == 0);
+      return READY;
+    }
+
+    // Try to satisfy the request out of our local buffer. This is potentially
+    // much faster than requesting data from our owning SourceBuffer because we
+    // don't have to take the lock. Note that if we have anything at all in our
+    // local buffer, we use it to satisfy the request; @aRequestedBytes is just
+    // the *maximum* number of bytes we can return.
+    if (mData.mIterating.mAvailableLength > 0) {
+      return AdvanceFromLocalBuffer(aRequestedBytes);
+    }
+  }
+
+  // Our local buffer is empty, so we'll have to request data from our owning
+  // SourceBuffer.
+  return mOwner->AdvanceIteratorOrScheduleResume(*this,
+                                                 aRequestedBytes,
+                                                 aConsumer);
 }
 
 bool
 SourceBufferIterator::RemainingBytesIsNoMoreThan(size_t aBytes) const
 {
   MOZ_ASSERT(mOwner);
   return mOwner->RemainingBytesIsNoMoreThan(*this, aBytes);
 }
 
 
 //////////////////////////////////////////////////////////////////////////////
 // SourceBuffer implementation.
 //////////////////////////////////////////////////////////////////////////////
 
+const size_t SourceBuffer::MIN_CHUNK_CAPACITY;
+
 SourceBuffer::SourceBuffer()
   : mMutex("image::SourceBuffer")
   , mConsumerCount(0)
 { }
 
 SourceBuffer::~SourceBuffer()
 {
   MOZ_ASSERT(mConsumerCount == 0,
@@ -216,17 +274,19 @@ SourceBuffer::FibonacciCapacityWithMinim
 
 void
 SourceBuffer::AddWaitingConsumer(IResumable* aConsumer)
 {
   mMutex.AssertCurrentThreadOwns();
 
   MOZ_ASSERT(!mStatus, "Waiting when we're complete?");
 
-  mWaitingConsumers.AppendElement(aConsumer);
+  if (aConsumer) {
+    mWaitingConsumers.AppendElement(aConsumer);
+  }
 }
 
 void
 SourceBuffer::ResumeWaitingConsumers()
 {
   mMutex.AssertCurrentThreadOwns();
 
   if (mWaitingConsumers.Length() == 0) {
@@ -499,17 +559,17 @@ SourceBuffer::RemainingBytesIsNoMoreThan
 
   // If the iterator's at the end, the answer is trivial.
   if (!aIterator.HasMore()) {
     return true;
   }
 
   uint32_t iteratorChunk = aIterator.mData.mIterating.mChunk;
   size_t iteratorOffset = aIterator.mData.mIterating.mOffset;
-  size_t iteratorLength = aIterator.mData.mIterating.mLength;
+  size_t iteratorLength = aIterator.mData.mIterating.mAvailableLength;
 
   // Include the bytes the iterator is currently pointing to in the limit, so
   // that the current chunk doesn't have to be a special case.
   size_t bytes = aBytes + iteratorOffset + iteratorLength;
 
   // Count the length over all of our chunks, starting with the one that the
   // iterator is currently pointing to. (This is O(N), but N is expected to be
   // ~1, so it doesn't seem worth caching the length separately.)
@@ -521,24 +581,23 @@ SourceBuffer::RemainingBytesIsNoMoreThan
     }
   }
 
   return true;
 }
 
 SourceBufferIterator::State
 SourceBuffer::AdvanceIteratorOrScheduleResume(SourceBufferIterator& aIterator,
+                                              size_t aRequestedBytes,
                                               IResumable* aConsumer)
 {
   MutexAutoLock lock(mMutex);
 
-  if (MOZ_UNLIKELY(!aIterator.HasMore())) {
-    MOZ_ASSERT_UNREACHABLE("Should not advance a completed iterator");
-    return SourceBufferIterator::COMPLETE;
-  }
+  MOZ_ASSERT(aIterator.HasMore(), "Advancing a completed iterator and "
+                                  "AdvanceOrScheduleResume didn't catch it");
 
   if (MOZ_UNLIKELY(mStatus && NS_FAILED(*mStatus))) {
     // This SourceBuffer is complete due to an error; all reads fail.
     return aIterator.SetComplete(*mStatus);
   }
 
   if (MOZ_UNLIKELY(mChunks.Length() == 0)) {
     // We haven't gotten an initial chunk yet.
@@ -546,32 +605,33 @@ SourceBuffer::AdvanceIteratorOrScheduleR
     return aIterator.SetWaiting();
   }
 
   uint32_t iteratorChunkIdx = aIterator.mData.mIterating.mChunk;
   MOZ_ASSERT(iteratorChunkIdx < mChunks.Length());
 
   const Chunk& currentChunk = mChunks[iteratorChunkIdx];
   size_t iteratorEnd = aIterator.mData.mIterating.mOffset +
-                       aIterator.mData.mIterating.mLength;
+                       aIterator.mData.mIterating.mAvailableLength;
   MOZ_ASSERT(iteratorEnd <= currentChunk.Length());
   MOZ_ASSERT(iteratorEnd <= currentChunk.Capacity());
 
   if (iteratorEnd < currentChunk.Length()) {
     // There's more data in the current chunk.
     return aIterator.SetReady(iteratorChunkIdx, currentChunk.Data(),
-                              iteratorEnd, currentChunk.Length() - iteratorEnd);
+                              iteratorEnd, currentChunk.Length() - iteratorEnd,
+                              aRequestedBytes);
   }
 
   if (iteratorEnd == currentChunk.Capacity() &&
       !IsLastChunk(iteratorChunkIdx)) {
     // Advance to the next chunk.
     const Chunk& nextChunk = mChunks[iteratorChunkIdx + 1];
     return aIterator.SetReady(iteratorChunkIdx + 1, nextChunk.Data(), 0,
-                              nextChunk.Length());
+                              nextChunk.Length(), aRequestedBytes);
   }
 
   MOZ_ASSERT(IsLastChunk(iteratorChunkIdx), "Should've advanced");
 
   if (mStatus) {
     // There's no more data and this SourceBuffer completed successfully.
     MOZ_ASSERT(NS_SUCCEEDED(*mStatus), "Handled failures earlier");
     return aIterator.SetComplete(*mStatus);
--- a/image/SourceBuffer.h
+++ b/image/SourceBuffer.h
@@ -75,52 +75,85 @@ public:
     READY,    // The iterator is pointing to new data.
     WAITING,  // The iterator is blocked and the caller must yield.
     COMPLETE  // The iterator is pointing to the end of the buffer.
   };
 
   explicit SourceBufferIterator(SourceBuffer* aOwner)
     : mOwner(aOwner)
     , mState(START)
+    , mChunkCount(0)
+    , mByteCount(0)
   {
     MOZ_ASSERT(aOwner);
     mData.mIterating.mChunk = 0;
     mData.mIterating.mData = nullptr;
     mData.mIterating.mOffset = 0;
-    mData.mIterating.mLength = 0;
+    mData.mIterating.mAvailableLength = 0;
+    mData.mIterating.mNextReadLength = 0;
   }
 
   SourceBufferIterator(SourceBufferIterator&& aOther)
     : mOwner(Move(aOther.mOwner))
     , mState(aOther.mState)
     , mData(aOther.mData)
+    , mChunkCount(aOther.mChunkCount)
+    , mByteCount(aOther.mByteCount)
   { }
 
   ~SourceBufferIterator();
 
-  SourceBufferIterator& operator=(SourceBufferIterator&& aOther)
-  {
-    mOwner = Move(aOther.mOwner);
-    mState = aOther.mState;
-    mData = aOther.mData;
-    return *this;
-  }
+  SourceBufferIterator& operator=(SourceBufferIterator&& aOther);
 
   /**
    * Returns true if there are no more than @aBytes remaining in the
    * SourceBuffer. If the SourceBuffer is not yet complete, returns false.
    */
   bool RemainingBytesIsNoMoreThan(size_t aBytes) const;
 
   /**
-   * Advances the iterator through the SourceBuffer if possible. If not,
+   * Advances the iterator through the SourceBuffer if possible. Advances no
+   * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as
+   * possible.)
+   *
+   * This is a wrapper around AdvanceOrScheduleResume() that makes it clearer at
+   * the callsite when the no resuming is intended.
+   *
+   * @return State::READY if the iterator was successfully advanced.
+   *         State::WAITING if the iterator could not be advanced because it's
+   *           at the end of the underlying SourceBuffer, but the SourceBuffer
+   *           may still receive additional data.
+   *         State::COMPLETE if the iterator could not be advanced because it's
+   *           at the end of the underlying SourceBuffer and the SourceBuffer is
+   *           marked complete (i.e., it will never receive any additional
+   *           data).
+   */
+  State Advance(size_t aRequestedBytes)
+  {
+    return AdvanceOrScheduleResume(aRequestedBytes, nullptr);
+  }
+
+  /**
+   * Advances the iterator through the SourceBuffer if possible. Advances no
+   * more than @aRequestedBytes bytes. (Use SIZE_MAX to advance as much as
+   * possible.) If advancing is not possible and @aConsumer is not null,
    * arranges to call the @aConsumer's Resume() method when more data is
    * available.
+   *
+   * @return State::READY if the iterator was successfully advanced.
+   *         State::WAITING if the iterator could not be advanced because it's
+   *           at the end of the underlying SourceBuffer, but the SourceBuffer
+   *           may still receive additional data. @aConsumer's Resume() method
+   *           will be called when additional data is available.
+   *         State::COMPLETE if the iterator could not be advanced because it's
+   *           at the end of the underlying SourceBuffer and the SourceBuffer is
+   *           marked complete (i.e., it will never receive any additional
+   *           data).
    */
-  State AdvanceOrScheduleResume(IResumable* aConsumer);
+  State AdvanceOrScheduleResume(size_t aRequestedBytes, IResumable* aConsumer);
 
   /// If at the end, returns the status passed to SourceBuffer::Complete().
   nsresult CompletionStatus() const
   {
     MOZ_ASSERT(mState == COMPLETE,
                "Calling CompletionStatus() in the wrong state");
     return mState == COMPLETE ? mData.mAtEnd.mStatus : NS_OK;
   }
@@ -132,36 +165,66 @@ public:
     return mState == READY ? mData.mIterating.mData + mData.mIterating.mOffset
                            : nullptr;
   }
 
   /// If we're ready to read, returns the length of the new data.
   size_t Length() const
   {
     MOZ_ASSERT(mState == READY, "Calling Length() in the wrong state");
-    return mState == READY ? mData.mIterating.mLength : 0;
+    return mState == READY ? mData.mIterating.mNextReadLength : 0;
   }
 
+  /// @return a count of the chunks we've advanced through.
+  uint32_t ChunkCount() const { return mChunkCount; }
+
+  /// @return a count of the bytes in all chunks we've advanced through.
+  size_t ByteCount() const { return mByteCount; }
+
 private:
   friend class SourceBuffer;
 
   SourceBufferIterator(const SourceBufferIterator&) = delete;
   SourceBufferIterator& operator=(const SourceBufferIterator&) = delete;
 
   bool HasMore() const { return mState != COMPLETE; }
 
+  State AdvanceFromLocalBuffer(size_t aRequestedBytes)
+  {
+    MOZ_ASSERT(mState == READY, "Advancing in the wrong state");
+    MOZ_ASSERT(mData.mIterating.mAvailableLength > 0,
+               "The local buffer shouldn't be empty");
+    MOZ_ASSERT(mData.mIterating.mNextReadLength == 0,
+               "Advancing without consuming previous data");
+
+    mData.mIterating.mNextReadLength =
+      std::min(mData.mIterating.mAvailableLength, aRequestedBytes);
+
+    return READY;
+  }
+
   State SetReady(uint32_t aChunk, const char* aData,
-                size_t aOffset, size_t aLength)
+                 size_t aOffset, size_t aAvailableLength,
+                 size_t aRequestedBytes)
   {
     MOZ_ASSERT(mState != COMPLETE);
+    mState = READY;
+
+    // Update state.
     mData.mIterating.mChunk = aChunk;
     mData.mIterating.mData = aData;
     mData.mIterating.mOffset = aOffset;
-    mData.mIterating.mLength = aLength;
-    return mState = READY;
+    mData.mIterating.mAvailableLength = aAvailableLength;
+
+    // Update metrics.
+    mChunkCount++;
+    mByteCount += aAvailableLength;
+
+    // Attempt to advance by the requested number of bytes.
+    return AdvanceFromLocalBuffer(aRequestedBytes);
   }
 
   State SetWaiting()
   {
     MOZ_ASSERT(mState != COMPLETE);
     MOZ_ASSERT(mState != WAITING, "Did we get a spurious wakeup somehow?");
     return mState = WAITING;
   }
@@ -181,22 +244,26 @@ private:
    * states START, READY, and WAITING) and the status the SourceBuffer was
    * completed with if we're in state COMPLETE.
    */
   union {
     struct {
       uint32_t mChunk;
       const char* mData;
       size_t mOffset;
-      size_t mLength;
+      size_t mAvailableLength;
+      size_t mNextReadLength;
     } mIterating;
     struct {
       nsresult mStatus;
     } mAtEnd;
   } mData;
+
+  uint32_t mChunkCount;  // Count of chunks we've advanced through.
+  size_t mByteCount;     // Count of bytes in all chunks we've advanced through.
 };
 
 /**
  * SourceBuffer is a parallel data structure used for storing image source
  * (compressed) data.
  *
  * SourceBuffer is a single producer, multiple consumer data structure. The
  * single producer calls Append() to append data to the buffer. In parallel,
@@ -251,16 +318,28 @@ public:
   //////////////////////////////////////////////////////////////////////////////
   // Consumer methods.
   //////////////////////////////////////////////////////////////////////////////
 
   /// Returns an iterator to this SourceBuffer.
   SourceBufferIterator Iterator();
 
 
+  //////////////////////////////////////////////////////////////////////////////
+  // Consumer methods.
+  //////////////////////////////////////////////////////////////////////////////
+
+  /**
+   * The minimum chunk capacity we'll allocate, if we don't know the correct
+   * capacity (which would happen because ExpectLength() wasn't called or gave
+   * us the wrong value). This is only exposed for use by tests; if normal code
+   * is using this, it's doing something wrong.
+   */
+  static const size_t MIN_CHUNK_CAPACITY = 4096;
+
 private:
   friend class SourceBufferIterator;
 
   ~SourceBuffer();
 
   //////////////////////////////////////////////////////////////////////////////
   // Chunk type and chunk-related methods.
   //////////////////////////////////////////////////////////////////////////////
@@ -332,16 +411,17 @@ private:
   //////////////////////////////////////////////////////////////////////////////
 
   void AddWaitingConsumer(IResumable* aConsumer);
   void ResumeWaitingConsumers();
 
   typedef SourceBufferIterator::State State;
 
   State AdvanceIteratorOrScheduleResume(SourceBufferIterator& aIterator,
+                                        size_t aRequestedBytes,
                                         IResumable* aConsumer);
   bool RemainingBytesIsNoMoreThan(const SourceBufferIterator& aIterator,
                                   size_t aBytes) const;
 
   void OnIteratorRelease();
 
   //////////////////////////////////////////////////////////////////////////////
   // Helper methods.
@@ -351,18 +431,16 @@ private:
   bool IsEmpty();
   bool IsLastChunk(uint32_t aChunk);
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Member variables.
   //////////////////////////////////////////////////////////////////////////////
 
-  static const size_t MIN_CHUNK_CAPACITY = 4096;
-
   /// All private members are protected by mMutex.
   mutable Mutex mMutex;
 
   /// The data in this SourceBuffer, stored as a series of Chunks.
   FallibleTArray<Chunk> mChunks;
 
   /// Consumers which are waiting to be notified when new data is available.
   nsTArray<RefPtr<IResumable>> mWaitingConsumers;
--- a/image/StreamingLexer.h
+++ b/image/StreamingLexer.h
@@ -9,39 +9,43 @@
  * image decoders without worrying about the details of how the data is arriving
  * from the network.
  */
 
 #ifndef mozilla_image_StreamingLexer_h
 #define mozilla_image_StreamingLexer_h
 
 #include <algorithm>
+#include <cstdint>
 #include "mozilla/Assertions.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Variant.h"
 #include "mozilla/Vector.h"
 
 namespace mozilla {
 namespace image {
 
 /// Buffering behaviors for StreamingLexer transitions.
 enum class BufferingStrategy
 {
   BUFFERED,   // Data will be buffered and processed in one chunk.
   UNBUFFERED  // Data will be processed as it arrives, in multiple chunks.
 };
 
-/// The result of a call to StreamingLexer::Lex().
+/// Possible terminal states for the lexer.
 enum class TerminalState
 {
   SUCCESS,
   FAILURE
 };
 
+/// The result of a call to StreamingLexer::Lex().
+typedef Variant<TerminalState> LexerResult;
+
 /**
  * LexerTransition is a type used to give commands to the lexing framework.
  * Code that uses StreamingLexer can create LexerTransition values using the
  * static methods on Transition, and then return them to the lexing framework
  * for execution.
  */
 template <typename State>
 class LexerTransition
@@ -253,161 +257,172 @@ private:
  * 1198451 lands, since we can then just return a function representing the next
  * state directly.
  */
 template <typename State, size_t InlineBufferSize = 16>
 class StreamingLexer
 {
 public:
   explicit StreamingLexer(LexerTransition<State> aStartState)
-    : mTransition(aStartState)
+    : mTransition(TerminalState::FAILURE)
     , mToReadUnbuffered(0)
-  { }
+  {
+    SetTransition(aStartState);
+  }
 
   template <typename Func>
-  Maybe<TerminalState> Lex(const char* aInput, size_t aLength, Func aFunc)
+  Maybe<TerminalState> Lex(SourceBufferIterator& aIterator,
+                           IResumable* aOnResume,
+                           Func aFunc)
   {
     if (mTransition.NextStateIsTerminal()) {
       // We've already reached a terminal state. We never deliver any more data
       // in this case; just return the terminal state again immediately.
       return Some(mTransition.NextStateAsTerminal());
     }
 
-    if (mToReadUnbuffered > 0) {
-      // We're continuing an unbuffered read.
+    Maybe<LexerResult> result;
+    do {
+      // Figure out how much we need to read.
+      const size_t toRead = mTransition.Buffering() == BufferingStrategy::UNBUFFERED
+                          ? mToReadUnbuffered
+                          : mTransition.Size() - mBuffer.length();
+
+      // Attempt to advance the iterator by |toRead| bytes.
+      switch (aIterator.AdvanceOrScheduleResume(toRead, aOnResume)) {
+        case SourceBufferIterator::WAITING:
+          // We can't continue because the rest of the data hasn't arrived from
+          // the network yet. We don't have to do anything special; the
+          // SourceBufferIterator will ensure that |aOnResume| gets called when
+          // more data is available.
+          return Nothing();
+
+        case SourceBufferIterator::COMPLETE:
+          // Normally even if the data is truncated, we want decoding to
+          // succeed so we can display whatever we got. However, if the
+          // SourceBuffer was completed with a failing status, we want to fail.
+          // This happens only in exceptional situations like SourceBuffer
+          // itself encountering a failure due to OOM.
+          result = SetTransition(NS_SUCCEEDED(aIterator.CompletionStatus())
+                 ? Transition::TerminateSuccess()
+                 : Transition::TerminateFailure());
+          break;
 
-      MOZ_ASSERT(mBuffer.empty(),
-                 "Shouldn't be continuing an unbuffered read and a buffered "
-                 "read at the same time");
+        case SourceBufferIterator::READY:
+          // Process the new data that became available.
+          MOZ_ASSERT(aIterator.Data());
+
+          result = mTransition.Buffering() == BufferingStrategy::UNBUFFERED
+                 ? UnbufferedRead(aIterator, aFunc)
+                 : BufferedRead(aIterator, aFunc);
+          break;
+
+        default:
+          MOZ_ASSERT_UNREACHABLE("Unknown SourceBufferIterator state");
+          result = SetTransition(Transition::TerminateFailure());
+      }
+    } while (!result);
 
-      size_t toRead = std::min(mToReadUnbuffered, aLength);
+    // Map |LexerResult| onto the old |Maybe<TerminalState>| API.
+    return result->is<TerminalState>() ? Some(result->as<TerminalState>())
+                                       : Nothing();
+  }
 
+private:
+  template <typename Func>
+  Maybe<LexerResult> UnbufferedRead(SourceBufferIterator& aIterator, Func aFunc)
+  {
+    MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED);
+    MOZ_ASSERT(mBuffer.empty(),
+               "Buffered read at the same time as unbuffered read?");
+
+    if (mToReadUnbuffered > 0) {
       // Call aFunc with the unbuffered state to indicate that we're in the
       // middle of an unbuffered read. We enforce that any state transition
       // passed back to us is either a terminal state or takes us back to the
       // unbuffered state.
       LexerTransition<State> unbufferedTransition =
-        aFunc(mTransition.UnbufferedState(), aInput, toRead);
+        aFunc(mTransition.UnbufferedState(), aIterator.Data(), aIterator.Length());
       if (unbufferedTransition.NextStateIsTerminal()) {
-        mTransition = unbufferedTransition;
-        return Some(mTransition.NextStateAsTerminal());  // Done!
+        return SetTransition(unbufferedTransition);
       }
+
       MOZ_ASSERT(mTransition.UnbufferedState() ==
                    unbufferedTransition.NextState());
 
-      aInput += toRead;
-      aLength -= toRead;
-      mToReadUnbuffered -= toRead;
+      mToReadUnbuffered -= aIterator.Length();
       if (mToReadUnbuffered != 0) {
-        return Nothing();  // Need more input.
-      }
-
-      // We're done with the unbuffered read, so transition to the next state.
-      mTransition = aFunc(mTransition.NextState(), nullptr, 0);
-      if (mTransition.NextStateIsTerminal()) {
-        return Some(mTransition.NextStateAsTerminal());  // Done!
-      }
-    } else if (0 < mBuffer.length()) {
-      // We're continuing a buffered read.
-
-      MOZ_ASSERT(mToReadUnbuffered == 0,
-                 "Shouldn't be continuing an unbuffered read and a buffered "
-                 "read at the same time");
-      MOZ_ASSERT(mBuffer.length() < mTransition.Size(),
-                 "Buffered more than we needed?");
-
-      size_t toRead = std::min(aLength, mTransition.Size() - mBuffer.length());
-
-      if (!mBuffer.append(aInput, toRead)) {
-        return Some(TerminalState::FAILURE);
-      }
-      aInput += toRead;
-      aLength -= toRead;
-      if (mBuffer.length() != mTransition.Size()) {
-        return Nothing();  // Need more input.
-      }
-
-      // We've buffered everything, so transition to the next state.
-      mTransition =
-        aFunc(mTransition.NextState(), mBuffer.begin(), mBuffer.length());
-      mBuffer.clear();
-      if (mTransition.NextStateIsTerminal()) {
-        return Some(mTransition.NextStateAsTerminal());  // Done!
+        return Nothing();  // Keep processing.
       }
     }
 
-    MOZ_ASSERT(mToReadUnbuffered == 0);
-    MOZ_ASSERT(mBuffer.empty());
-
-    // Process states as long as we continue to have enough input to do so.
-    while (mTransition.Size() <= aLength) {
-      size_t toRead = mTransition.Size();
-
-      if (mTransition.Buffering() == BufferingStrategy::BUFFERED) {
-        mTransition = aFunc(mTransition.NextState(), aInput, toRead);
-      } else {
-        MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::UNBUFFERED);
+    // We're done with the unbuffered read, so transition to the next state.
+    return SetTransition(aFunc(mTransition.NextState(), nullptr, 0));
+  }
 
-        // Call aFunc with the unbuffered state to indicate that we're in the
-        // middle of an unbuffered read. We enforce that any state transition
-        // passed back to us is either a terminal state or takes us back to the
-        // unbuffered state.
-        LexerTransition<State> unbufferedTransition =
-          aFunc(mTransition.UnbufferedState(), aInput, toRead);
-        if (unbufferedTransition.NextStateIsTerminal()) {
-          mTransition = unbufferedTransition;
-          return Some(mTransition.NextStateAsTerminal());  // Done!
-        }
-        MOZ_ASSERT(mTransition.UnbufferedState() ==
-                     unbufferedTransition.NextState());
+  template <typename Func>
+  Maybe<LexerResult> BufferedRead(SourceBufferIterator& aIterator, Func aFunc)
+  {
+    MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED);
+    MOZ_ASSERT(mToReadUnbuffered == 0,
+               "Buffered read at the same time as unbuffered read?");
+    MOZ_ASSERT(mBuffer.length() < mTransition.Size() ||
+               (mBuffer.length() == 0 && mTransition.Size() == 0),
+               "Buffered more than we needed?");
 
-        // We're done with the unbuffered read, so transition to the next state.
-        mTransition = aFunc(mTransition.NextState(), nullptr, 0);
-      }
+    // If we have all the data, we don't actually need to buffer anything.
+    if (mBuffer.empty() && aIterator.Length() == mTransition.Size()) {
+      return SetTransition(aFunc(mTransition.NextState(),
+                                 aIterator.Data(),
+                                 aIterator.Length()));
+    }
 
-      aInput += toRead;
-      aLength -= toRead;
-
-      if (mTransition.NextStateIsTerminal()) {
-        return Some(mTransition.NextStateAsTerminal());  // Done!
-      }
+    // We do need to buffer, so make sure the buffer has enough capacity. We
+    // deliberately wait until we know for sure we need to buffer to call
+    // reserve() since it could require memory allocation.
+    if (!mBuffer.reserve(mTransition.Size())) {
+      return SetTransition(Transition::TerminateFailure());
     }
 
-    if (aLength == 0) {
-      // We finished right at a transition point. Just wait for more data.
-      return Nothing();
+    // Append the new data we just got to the buffer.
+    if (!mBuffer.append(aIterator.Data(), aIterator.Length())) {
+      return SetTransition(Transition::TerminateFailure());
+    }
+
+    if (mBuffer.length() != mTransition.Size()) {
+      return Nothing();  // Keep processing.
     }
 
-    // If the next state is unbuffered, deliver what we can and then wait.
-    if (mTransition.Buffering() == BufferingStrategy::UNBUFFERED) {
-      LexerTransition<State> unbufferedTransition =
-        aFunc(mTransition.UnbufferedState(), aInput, aLength);
-      if (unbufferedTransition.NextStateIsTerminal()) {
-        mTransition = unbufferedTransition;
-        return Some(mTransition.NextStateAsTerminal());  // Done!
-      }
-      MOZ_ASSERT(mTransition.UnbufferedState() ==
-                   unbufferedTransition.NextState());
+    // We've buffered everything, so transition to the next state.
+    return SetTransition(aFunc(mTransition.NextState(),
+                               mBuffer.begin(),
+                               mBuffer.length()));
+  }
 
-      mToReadUnbuffered = mTransition.Size() - aLength;
-      return Nothing();  // Need more input.
+  Maybe<LexerResult> SetTransition(const LexerTransition<State>& aTransition)
+  {
+    mTransition = aTransition;
+
+    // Get rid of anything left over from the previous state.
+    mBuffer.clear();
+    mToReadUnbuffered = 0;
+
+    // If we reached a terminal state, let the caller know.
+    if (mTransition.NextStateIsTerminal()) {
+      return Some(LexerResult(mTransition.NextStateAsTerminal()));
     }
 
-    // If the next state is buffered, buffer what we can and then wait.
-    MOZ_ASSERT(mTransition.Buffering() == BufferingStrategy::BUFFERED);
-    if (!mBuffer.reserve(mTransition.Size())) {
-      return Some(TerminalState::FAILURE);  // Done due to allocation failure.
+    // If we're entering an unbuffered state, record how long we'll stay in it.
+    if (mTransition.Buffering() == BufferingStrategy::UNBUFFERED) {
+      mToReadUnbuffered = mTransition.Size();
     }
-    if (!mBuffer.append(aInput, aLength)) {
-      return Some(TerminalState::FAILURE);
-    }
-    return Nothing();  // Need more input.
+
+    return Nothing();  // Keep processing.
   }
 
-private:
   Vector<char, InlineBufferSize> mBuffer;
   LexerTransition<State> mTransition;
   size_t mToReadUnbuffered;
 };
 
 } // namespace image
 } // namespace mozilla
 
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -655,16 +655,21 @@ public:
     return LookupResult(Move(ref), matchType);
   }
 
   bool CanHold(const Cost aCost) const
   {
     return aCost <= mMaxCost;
   }
 
+  size_t MaximumCapacity() const
+  {
+    return size_t(mMaxCost);
+  }
+
   void LockImage(const ImageKey aImageKey)
   {
     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache) {
       cache = new ImageSurfaceCache;
       mImageCaches.Put(aImageKey, cache);
     }
 
@@ -1121,10 +1126,21 @@ SurfaceCache::CollectSizeOfSurfaces(cons
   if (!sInstance) {
     return;
   }
 
   MutexAutoLock lock(sInstance->GetMutex());
   return sInstance->CollectSizeOfSurfaces(aImageKey, aCounters, aMallocSizeOf);
 }
 
+/* static */ size_t
+SurfaceCache::MaximumCapacity()
+{
+  if (!sInstance) {
+    return 0;
+  }
+
+  MutexAutoLock lock(sInstance->GetMutex());
+  return sInstance->MaximumCapacity();
+}
+
 } // namespace image
 } // namespace mozilla
--- a/image/SurfaceCache.h
+++ b/image/SurfaceCache.h
@@ -394,16 +394,22 @@ struct SurfaceCache
    * @param aCounters     An array into which the report for each surface will
    *                      be written.
    * @param aMallocSizeOf A fallback malloc memory reporting function.
    */
   static void CollectSizeOfSurfaces(const ImageKey    aImageKey,
                                     nsTArray<SurfaceMemoryCounter>& aCounters,
                                     MallocSizeOf      aMallocSizeOf);
 
+  /**
+   * @return maximum capacity of the SurfaceCache in bytes. This is only exposed
+   * for use by tests; normal code should use CanHold() instead.
+   */
+  static size_t MaximumCapacity();
+
 private:
   virtual ~SurfaceCache() = 0;  // Forbid instantiation.
 };
 
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_SurfaceCache_h
--- a/image/decoders/nsBMPDecoder.cpp
+++ b/image/decoders/nsBMPDecoder.cpp
@@ -217,25 +217,27 @@ nsBMPDecoder::GetCompressedImageSize() c
 {
   // In the RGB case mImageSize might not be set, so compute it manually.
   MOZ_ASSERT(mPixelRowSize != 0);
   return mH.mCompression == Compression::RGB
        ? mPixelRowSize * AbsoluteHeight()
        : mH.mImageSize;
 }
 
-void
+nsresult
 nsBMPDecoder::BeforeFinishInternal()
 {
   if (!IsMetadataDecode() && !mImageData) {
-    PostDataError();
+    return NS_ERROR_FAILURE;  // No image; something went wrong.
   }
+
+  return NS_OK;
 }
 
-void
+nsresult
 nsBMPDecoder::FinishInternal()
 {
   // We shouldn't be called in error cases.
   MOZ_ASSERT(!HasError(), "Can't call FinishInternal on error!");
 
   // We should never make multiple frames.
   MOZ_ASSERT(GetFrameCount() <= 1, "Multiple BMP frames?");
 
@@ -255,24 +257,33 @@ nsBMPDecoder::FinishInternal()
       mCurrentPos = 0;
       FinishRow();
     }
 
     // Invalidate.
     nsIntRect r(0, 0, mH.mWidth, AbsoluteHeight());
     PostInvalidation(r);
 
-    if (mDoesHaveTransparency) {
-      MOZ_ASSERT(mMayHaveTransparency);
-      PostFrameStop(Opacity::SOME_TRANSPARENCY);
-    } else {
-      PostFrameStop(Opacity::FULLY_OPAQUE);
-    }
+    MOZ_ASSERT_IF(mDoesHaveTransparency, mMayHaveTransparency);
+
+    // We have transparency if we either detected some in the image itself
+    // (i.e., |mDoesHaveTransparency| is true) or we're in an ICO, which could
+    // mean we have an AND mask that provides transparency (i.e., |mIsWithinICO|
+    // is true).
+    // XXX(seth): We can tell when we create the decoder if the AND mask is
+    // present, so we could be more precise about this.
+    const Opacity opacity = mDoesHaveTransparency || mIsWithinICO
+                          ? Opacity::SOME_TRANSPARENCY
+                          : Opacity::FULLY_OPAQUE;
+
+    PostFrameStop(opacity);
     PostDecodeDone();
   }
+
+  return NS_OK;
 }
 
 // ----------------------------------------
 // Actual Data Processing
 // ----------------------------------------
 
 void
 BitFields::Value::Set(uint32_t aMask)
@@ -431,23 +442,21 @@ nsBMPDecoder::FinishRow()
     }
   } else {
     PostInvalidation(IntRect(0, mCurrentRow, mH.mWidth, 1));
   }
   mCurrentRow--;
 }
 
 Maybe<TerminalState>
-nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator)
+nsBMPDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](State aState, const char* aData, size_t aLength) {
     switch (aState) {
       case State::FILE_HEADER:      return ReadFileHeader(aData, aLength);
       case State::INFO_HEADER_SIZE: return ReadInfoHeaderSize(aData, aLength);
       case State::INFO_HEADER_REST: return ReadInfoHeaderRest(aData, aLength);
       case State::BITFIELDS:        return ReadBitfields(aData, aLength);
       case State::COLOR_TABLE:      return ReadColorTable(aData, aLength);
       case State::GAP:              return SkipGap();
@@ -464,17 +473,16 @@ nsBMPDecoder::DoDecode(SourceBufferItera
 
 LexerTransition<nsBMPDecoder::State>
 nsBMPDecoder::ReadFileHeader(const char* aData, size_t aLength)
 {
   mPreGapLength += aLength;
 
   bool signatureOk = aData[0] == 'B' && aData[1] == 'M';
   if (!signatureOk) {
-    PostDataError();
     return Transition::TerminateFailure();
   }
 
   // We ignore the filesize (aData + 2) and reserved (aData + 6) fields.
 
   mH.mDataOffset = LittleEndian::readUint32(aData + 10);
 
   return Transition::To(State::INFO_HEADER_SIZE, BIHSIZE_FIELD_LENGTH);
@@ -491,17 +499,16 @@ nsBMPDecoder::ReadInfoHeaderSize(const c
 
   bool bihSizeOk = mH.mBIHSize == InfoHeaderLength::WIN_V2 ||
                    mH.mBIHSize == InfoHeaderLength::WIN_V3 ||
                    mH.mBIHSize == InfoHeaderLength::WIN_V4 ||
                    mH.mBIHSize == InfoHeaderLength::WIN_V5 ||
                    (mH.mBIHSize >= InfoHeaderLength::OS2_V2_MIN &&
                     mH.mBIHSize <= InfoHeaderLength::OS2_V2_MAX);
   if (!bihSizeOk) {
-    PostDataError();
     return Transition::TerminateFailure();
   }
   // ICO BMPs must have a WinBMPv3 header. nsICODecoder should have already
   // terminated decoding if this isn't the case.
   MOZ_ASSERT_IF(mIsWithinICO, mH.mBIHSize == InfoHeaderLength::WIN_V3);
 
   return Transition::To(State::INFO_HEADER_REST,
                         mH.mBIHSize - BIHSIZE_FIELD_LENGTH);
@@ -546,17 +553,16 @@ nsBMPDecoder::ReadInfoHeaderRest(const c
 
   // BMPs with negative width are invalid. Also, reject extremely wide images
   // to keep the math sane. And reject INT_MIN as a height because you can't
   // get its absolute value (because -INT_MIN is one more than INT_MAX).
   const int32_t k64KWidth = 0x0000FFFF;
   bool sizeOk = 0 <= mH.mWidth && mH.mWidth <= k64KWidth &&
                 mH.mHeight != INT_MIN;
   if (!sizeOk) {
-    PostDataError();
     return Transition::TerminateFailure();
   }
 
   // Check mBpp and mCompression.
   bool bppCompressionOk =
     (mH.mCompression == Compression::RGB &&
       (mH.mBpp ==  1 || mH.mBpp ==  4 || mH.mBpp ==  8 ||
        mH.mBpp == 16 || mH.mBpp == 24 || mH.mBpp == 32)) ||
@@ -565,24 +571,21 @@ nsBMPDecoder::ReadInfoHeaderRest(const c
     (mH.mCompression == Compression::BITFIELDS &&
       // For BITFIELDS compression we require an exact match for one of the
       // WinBMP BIH sizes; this clearly isn't an OS2 BMP.
       (mH.mBIHSize == InfoHeaderLength::WIN_V3 ||
        mH.mBIHSize == InfoHeaderLength::WIN_V4 ||
        mH.mBIHSize == InfoHeaderLength::WIN_V5) &&
       (mH.mBpp == 16 || mH.mBpp == 32));
   if (!bppCompressionOk) {
-    PostDataError();
     return Transition::TerminateFailure();
   }
 
-  // Post our size to the superclass.
-  uint32_t absHeight = AbsoluteHeight();
-  PostSize(mH.mWidth, absHeight);
-  mCurrentRow = absHeight;
+  // Initialize our current row to the top of the image.
+  mCurrentRow = AbsoluteHeight();
 
   // Round it up to the nearest byte count, then pad to 4-byte boundary.
   // Compute this even for a metadate decode because GetCompressedImageSize()
   // relies on it.
   mPixelRowSize = (mH.mBpp * mH.mWidth + 7) / 8;
   uint32_t surplus = mPixelRowSize % 4;
   if (surplus != 0) {
     mPixelRowSize += 4 - surplus;
@@ -640,16 +643,19 @@ nsBMPDecoder::ReadBitfields(const char* 
     mH.mCompression == Compression::RLE8 ||
     mH.mCompression == Compression::RLE4 ||
     (mH.mCompression == Compression::BITFIELDS &&
      mBitFields.mAlpha.IsPresent());
   if (mMayHaveTransparency) {
     PostHasTransparency();
   }
 
+  // Post our size to the superclass.
+  PostSize(mH.mWidth, AbsoluteHeight());
+
   // We've now read all the headers. If we're doing a metadata decode, we're
   // done.
   if (IsMetadataDecode()) {
     return Transition::TerminateSuccess();
   }
 
   // Set up the color table, if present; it'll be filled in by ReadColorTable().
   if (mH.mBpp <= 8) {
@@ -710,17 +716,16 @@ nsBMPDecoder::ReadColorTable(const char*
   // We know how many bytes we've read so far (mPreGapLength) and we know the
   // offset of the pixel data (mH.mDataOffset), so we can determine the length
   // of the gap (possibly zero) between the color table and the pixel data.
   //
   // If the gap is negative the file must be malformed (e.g. mH.mDataOffset
   // points into the middle of the color palette instead of past the end) and
   // we give up.
   if (mPreGapLength > mH.mDataOffset) {
-    PostDataError();
     return Transition::TerminateFailure();
   }
 
   uint32_t gapLength = mH.mDataOffset - mPreGapLength;
   return Transition::ToUnbuffered(State::AFTER_GAP, State::GAP, gapLength);
 }
 
 LexerTransition<nsBMPDecoder::State>
--- a/image/decoders/nsBMPDecoder.h
+++ b/image/decoders/nsBMPDecoder.h
@@ -137,26 +137,20 @@ public:
   /// Mark this BMP as being within an ICO file. Only used for testing purposes
   /// because the ICO-specific constructor does this marking automatically.
   void SetIsWithinICO() { mIsWithinICO = true; }
 
   /// Did the BMP file have alpha data of any kind? (Only use this after the
   /// bitmap has been fully decoded.)
   bool HasTransparency() const { return mDoesHaveTransparency; }
 
-  /// Force transparency from outside. (Used by the ICO decoder.)
-  void SetHasTransparency()
-  {
-    mMayHaveTransparency = true;
-    mDoesHaveTransparency = true;
-  }
-
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
-  virtual void BeforeFinishInternal() override;
-  virtual void FinishInternal() override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
+  nsresult BeforeFinishInternal() override;
+  nsresult FinishInternal() override;
 
 private:
   friend class DecoderFactory;
 
   enum class State {
     FILE_HEADER,
     INFO_HEADER_SIZE,
     INFO_HEADER_REST,
--- a/image/decoders/nsGIFDecoder2.cpp
+++ b/image/decoders/nsGIFDecoder2.cpp
@@ -95,29 +95,31 @@ nsGIFDecoder2::nsGIFDecoder2(RasterImage
   mGIFStruct.loop_count = 1;
 }
 
 nsGIFDecoder2::~nsGIFDecoder2()
 {
   free(mGIFStruct.local_colormap);
 }
 
-void
+nsresult
 nsGIFDecoder2::FinishInternal()
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!");
 
   // If the GIF got cut off, handle it anyway
   if (!IsMetadataDecode() && mGIFOpen) {
     if (mCurrentFrameIndex == mGIFStruct.images_decoded) {
       EndImageFrame();
     }
     PostDecodeDone(mGIFStruct.loop_count - 1);
     mGIFOpen = false;
   }
+
+  return NS_OK;
 }
 
 void
 nsGIFDecoder2::FlushImageData()
 {
   Maybe<SurfaceInvalidRect> invalidRect = mPipe.TakeInvalidRect();
   if (!invalidRect) {
     return;
@@ -450,23 +452,21 @@ ConvertColormap(uint32_t* aColormap, uin
   // NB: can't use 32-bit reads, they might read off the end of the buffer
   while (c--) {
     from -= 3;
     *--to = gfxPackedPixel(0xFF, from[0], from[1], from[2]);
   }
 }
 
 Maybe<TerminalState>
-nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator)
+nsGIFDecoder2::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](State aState, const char* aData, size_t aLength) {
     switch(aState) {
       case State::GIF_HEADER:
         return ReadGIFHeader(aData);
       case State::SCREEN_DESCRIPTOR:
         return ReadScreenDescriptor(aData);
       case State::GLOBAL_COLOR_TABLE:
         return ReadGlobalColorTable(aData, aLength);
--- a/image/decoders/nsGIFDecoder2.h
+++ b/image/decoders/nsGIFDecoder2.h
@@ -19,18 +19,19 @@ class RasterImage;
 //////////////////////////////////////////////////////////////////////
 // nsGIFDecoder2 Definition
 
 class nsGIFDecoder2 : public Decoder
 {
 public:
   ~nsGIFDecoder2();
 
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
-  virtual void FinishInternal() override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
+  nsresult FinishInternal() override;
   virtual Telemetry::ID SpeedHistogram() override;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   explicit nsGIFDecoder2(RasterImage* aImage);
 
--- a/image/decoders/nsICODecoder.cpp
+++ b/image/decoders/nsICODecoder.cpp
@@ -49,73 +49,73 @@ nsICODecoder::GetNumColors()
     }
   }
   return numColors;
 }
 
 nsICODecoder::nsICODecoder(RasterImage* aImage)
   : Decoder(aImage)
   , mLexer(Transition::To(ICOState::HEADER, ICOHEADERSIZE))
-  , mDoNotResume(WrapNotNull(new DoNotResume))
   , mBiggestResourceColorDepth(0)
   , mBestResourceDelta(INT_MIN)
   , mBestResourceColorDepth(0)
   , mNumIcons(0)
   , mCurrIcon(0)
   , mBPP(0)
   , mMaskRowSize(0)
   , mCurrMaskLine(0)
   , mIsCursor(false)
   , mHasMaskAlpha(false)
 { }
 
-void
+nsresult
 nsICODecoder::FinishInternal()
 {
   // We shouldn't be called in error cases
   MOZ_ASSERT(!HasError(), "Shouldn't call FinishInternal after error!");
 
-  GetFinalStateFromContainedDecoder();
+  return GetFinalStateFromContainedDecoder();
 }
 
-void
+nsresult
 nsICODecoder::FinishWithErrorInternal()
 {
-  GetFinalStateFromContainedDecoder();
+  return GetFinalStateFromContainedDecoder();
 }
 
-void
+nsresult
 nsICODecoder::GetFinalStateFromContainedDecoder()
 {
   if (!mContainedDecoder) {
-    return;
+    return NS_OK;
   }
 
   MOZ_ASSERT(mContainedSourceBuffer,
              "Should have a SourceBuffer if we have a decoder");
 
   // Let the contained decoder finish up if necessary.
   if (!mContainedSourceBuffer->IsComplete()) {
     mContainedSourceBuffer->Complete(NS_OK);
-    if (NS_FAILED(mContainedDecoder->Decode(mDoNotResume))) {
-      PostDataError();
-    }
+    mContainedDecoder->Decode();
   }
 
   // Make our state the same as the state of the contained decoder.
   mDecodeDone = mContainedDecoder->GetDecodeDone();
-  mDataError = mDataError || mContainedDecoder->HasDataError();
-  mFailCode = NS_SUCCEEDED(mFailCode) ? mContainedDecoder->GetDecoderError()
-                                      : mFailCode;
   mDecodeAborted = mContainedDecoder->WasAborted();
   mProgress |= mContainedDecoder->TakeProgress();
   mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect());
   mCurrentFrame = mContainedDecoder->GetCurrentFrameRef();
 
-  MOZ_ASSERT(HasError() || !mCurrentFrame || mCurrentFrame->IsFinished());
+  // Propagate errors.
+  nsresult rv = HasError() || mContainedDecoder->HasError()
+              ? NS_ERROR_FAILURE
+              : NS_OK;
+
+  MOZ_ASSERT(NS_FAILED(rv) || !mCurrentFrame || mCurrentFrame->IsFinished());
+  return rv;
 }
 
 bool
 nsICODecoder::CheckAndFixBitmapSize(int8_t* aBIH)
 {
   // Get the width from the BMP file information header. This is
   // (unintuitively) a signed integer; see the documentation at:
   //
@@ -567,25 +567,16 @@ nsICODecoder::FinishMask()
     // Iterate through the alpha values, copying from mask to image.
     MOZ_ASSERT(mMaskBuffer);
     MOZ_ASSERT(bmpDecoder->GetImageDataLength() > 0);
     for (size_t i = 3 ; i < bmpDecoder->GetImageDataLength() ; i += 4) {
       imageData[i] = mMaskBuffer[i];
     }
   }
 
-  // If the mask contained any transparent pixels, record that fact.
-  if (mHasMaskAlpha) {
-    PostHasTransparency();
-
-    RefPtr<nsBMPDecoder> bmpDecoder =
-      static_cast<nsBMPDecoder*>(mContainedDecoder.get());
-    bmpDecoder->SetHasTransparency();
-  }
-
   return Transition::To(ICOState::FINISHED_RESOURCE, 0);
 }
 
 LexerTransition<ICOState>
 nsICODecoder::FinishResource()
 {
   // Make sure the actual size of the resource matches the size in the directory
   // entry. If not, we consider the image corrupt.
@@ -593,23 +584,21 @@ nsICODecoder::FinishResource()
       mContainedDecoder->GetSize() != GetRealSize()) {
     return Transition::TerminateFailure();
   }
 
   return Transition::TerminateSuccess();
 }
 
 Maybe<TerminalState>
-nsICODecoder::DoDecode(SourceBufferIterator& aIterator)
+nsICODecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](ICOState aState, const char* aData, size_t aLength) {
     switch (aState) {
       case ICOState::HEADER:
         return ReadHeader(aData);
       case ICOState::DIR_ENTRY:
         return ReadDirEntry(aData);
       case ICOState::SKIP_TO_RESOURCE:
         return Transition::ContinueUnbuffered(ICOState::SKIP_TO_RESOURCE);
@@ -644,31 +633,31 @@ nsICODecoder::WriteToContainedDecoder(co
 {
   MOZ_ASSERT(mContainedDecoder);
   MOZ_ASSERT(mContainedSourceBuffer);
 
   // Append the provided data to the SourceBuffer that the contained decoder is
   // reading from.
   mContainedSourceBuffer->Append(aBuffer, aCount);
 
+  bool succeeded = true;
+
   // Write to the contained decoder. If we run out of data, the ICO decoder will
   // get resumed when there's more data available, as usual, so we don't need
   // the contained decoder to get resumed too. To avoid that, we provide an
   // IResumable which just does nothing.
-  if (NS_FAILED(mContainedDecoder->Decode(mDoNotResume))) {
-    PostDataError();
+  if (NS_FAILED(mContainedDecoder->Decode())) {
+    succeeded = false;
   }
 
-  // Make our state the same as the state of the contained decoder.
+  // Make our state the same as the state of the contained decoder, and
+  // propagate errors.
   mProgress |= mContainedDecoder->TakeProgress();
   mInvalidRect.UnionRect(mInvalidRect, mContainedDecoder->TakeInvalidRect());
-  if (mContainedDecoder->HasDataError()) {
-    PostDataError();
-  }
-  if (mContainedDecoder->HasDecoderError()) {
-    PostDecoderError(mContainedDecoder->GetDecoderError());
+  if (mContainedDecoder->HasError()) {
+    succeeded = false;
   }
 
-  return !HasError();
+  return succeeded;
 }
 
 } // namespace image
 } // namespace mozilla
--- a/image/decoders/nsICODecoder.h
+++ b/image/decoders/nsICODecoder.h
@@ -6,17 +6,16 @@
 
 #ifndef mozilla_image_decoders_nsICODecoder_h
 #define mozilla_image_decoders_nsICODecoder_h
 
 #include "StreamingLexer.h"
 #include "Decoder.h"
 #include "imgFrame.h"
 #include "mozilla/gfx/2D.h"
-#include "mozilla/NotNull.h"
 #include "nsBMPDecoder.h"
 #include "nsPNGDecoder.h"
 #include "ICOFileHeaders.h"
 
 namespace mozilla {
 namespace image {
 
 class RasterImage;
@@ -65,32 +64,33 @@ public:
   gfx::IntSize GetRealSize() const
   {
     return gfx::IntSize(GetRealWidth(), GetRealHeight());
   }
 
   /// @return The offset from the beginning of the ICO to the first resource.
   size_t FirstResourceOffset() const;
 
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
-  virtual void FinishInternal() override;
-  virtual void FinishWithErrorInternal() override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
+  nsresult FinishInternal() override;
+  nsresult FinishWithErrorInternal() override;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   explicit nsICODecoder(RasterImage* aImage);
 
   // Writes to the contained decoder and sets the appropriate errors
   // Returns true if there are no errors.
   bool WriteToContainedDecoder(const char* aBuffer, uint32_t aCount);
 
   // Gets decoder state from the contained decoder so it's visible externally.
-  void GetFinalStateFromContainedDecoder();
+  nsresult GetFinalStateFromContainedDecoder();
 
   /**
    * Verifies that the width and height values in @aBIH are valid and match the
    * values we read from the ICO directory entry. If everything looks OK, the
    * height value in @aBIH is updated to compensate for the AND mask, which the
    * underlying BMP decoder doesn't know about.
    *
    * @return true if the width and height values in @aBIH are valid and correct.
@@ -106,32 +106,19 @@ private:
   LexerTransition<ICOState> ReadPNG(const char* aData, uint32_t aLen);
   LexerTransition<ICOState> ReadBIH(const char* aData);
   LexerTransition<ICOState> ReadBMP(const char* aData, uint32_t aLen);
   LexerTransition<ICOState> PrepareForMask();
   LexerTransition<ICOState> ReadMaskRow(const char* aData);
   LexerTransition<ICOState> FinishMask();
   LexerTransition<ICOState> FinishResource();
 
-  // A helper implementation of IResumable which just does nothing; see
-  // WriteToContainedDecoder() for more details.
-  class DoNotResume final : public IResumable
-  {
-  public:
-    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DoNotResume, override)
-    void Resume() override { }
-
-  private:
-    virtual ~DoNotResume() { }
-  };
-
   StreamingLexer<ICOState, 32> mLexer; // The lexer.
   RefPtr<Decoder> mContainedDecoder; // Either a BMP or PNG decoder.
   RefPtr<SourceBuffer> mContainedSourceBuffer;  // SourceBuffer for mContainedDecoder.
-  NotNull<RefPtr<IResumable>> mDoNotResume;  // IResumable helper for SourceBuffer.
   UniquePtr<uint8_t[]> mMaskBuffer;    // A temporary buffer for the alpha mask.
   char mBIHraw[bmp::InfoHeaderLength::WIN_ICO]; // The bitmap information header.
   IconDirEntry mDirEntry;              // The dir entry for the selected resource.
   gfx::IntSize mBiggestResourceSize;   // Used to select the intrinsic size.
   gfx::IntSize mBiggestResourceHotSpot; // Used to select the intrinsic size.
   uint16_t mBiggestResourceColorDepth; // Used to select the intrinsic size.
   int32_t mBestResourceDelta;          // Used to select the best resource.
   uint16_t mBestResourceColorDepth;    // Used to select the best resource.
--- a/image/decoders/nsIconDecoder.cpp
+++ b/image/decoders/nsIconDecoder.cpp
@@ -22,23 +22,21 @@ nsIconDecoder::nsIconDecoder(RasterImage
 {
   // Nothing to do
 }
 
 nsIconDecoder::~nsIconDecoder()
 { }
 
 Maybe<TerminalState>
-nsIconDecoder::DoDecode(SourceBufferIterator& aIterator)
+nsIconDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](State aState, const char* aData, size_t aLength) {
     switch (aState) {
       case State::HEADER:
         return ReadHeader(aData);
       case State::ROW_OF_PIXELS:
         return ReadRowOfPixels(aData, aLength);
       case State::FINISH:
         return Finish();
--- a/image/decoders/nsIconDecoder.h
+++ b/image/decoders/nsIconDecoder.h
@@ -32,17 +32,18 @@ class RasterImage;
 //
 ////////////////////////////////////////////////////////////////////////////////
 
 class nsIconDecoder : public Decoder
 {
 public:
   virtual ~nsIconDecoder();
 
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   explicit nsIconDecoder(RasterImage* aImage);
 
   enum class State {
--- a/image/decoders/nsJPEGDecoder.cpp
+++ b/image/decoders/nsJPEGDecoder.cpp
@@ -123,34 +123,33 @@ nsJPEGDecoder::~nsJPEGDecoder()
 }
 
 Telemetry::ID
 nsJPEGDecoder::SpeedHistogram()
 {
   return Telemetry::IMAGE_DECODE_SPEED_JPEG;
 }
 
-void
+nsresult
 nsJPEGDecoder::InitInternal()
 {
   mCMSMode = gfxPlatform::GetCMSMode();
   if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
     mCMSMode = eCMSMode_Off;
   }
 
   // We set up the normal JPEG error routines, then override error_exit.
   mInfo.err = jpeg_std_error(&mErr.pub);
   //   mInfo.err = jpeg_std_error(&mErr.pub);
   mErr.pub.error_exit = my_error_exit;
   // Establish the setjmp return context for my_error_exit to use.
   if (setjmp(mErr.setjmp_buffer)) {
-    // If we get here, the JPEG code has signaled an error.
-    // We need to clean up the JPEG object, close the input file, and return.
-    PostDecoderError(NS_ERROR_FAILURE);
-    return;
+    // If we get here, the JPEG code has signaled an error, and initialization
+    // has failed.
+    return NS_ERROR_FAILURE;
   }
 
   // Step 1: allocate and initialize JPEG decompression object
   jpeg_create_decompress(&mInfo);
   // Set the source manager
   mInfo.src = &mSourceMgr;
 
   // Step 2: specify data source (eg, a file)
@@ -161,37 +160,39 @@ nsJPEGDecoder::InitInternal()
   mSourceMgr.skip_input_data = skip_input_data;
   mSourceMgr.resync_to_restart = jpeg_resync_to_restart;
   mSourceMgr.term_source = term_source;
 
   // Record app markers for ICC data
   for (uint32_t m = 0; m < 16; m++) {
     jpeg_save_markers(&mInfo, JPEG_APP0 + m, 0xFFFF);
   }
+
+  return NS_OK;
 }
 
-void
+nsresult
 nsJPEGDecoder::FinishInternal()
 {
   // If we're not in any sort of error case, force our state to JPEG_DONE.
   if ((mState != JPEG_DONE && mState != JPEG_SINK_NON_JPEG_TRAILER) &&
       (mState != JPEG_ERROR) &&
       !IsMetadataDecode()) {
     mState = JPEG_DONE;
   }
+
+  return NS_OK;
 }
 
 Maybe<TerminalState>
-nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator)
+nsJPEGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](State aState, const char* aData, size_t aLength) {
     switch (aState) {
       case State::JPEG_DATA:
         return ReadJPEGData(aData, aLength);
       case State::FINISHED_JPEG_DATA:
         return FinishedJPEGData();
     }
     MOZ_CRASH("Unknown State");
@@ -212,17 +213,16 @@ nsJPEGDecoder::ReadJPEGData(const char* 
     if (error_code == NS_ERROR_FAILURE) {
       // Error due to corrupt data. Make sure that we don't feed any more data
       // to libjpeg-turbo.
       mState = JPEG_SINK_NON_JPEG_TRAILER;
       MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
              ("} (setjmp returned NS_ERROR_FAILURE)"));
     } else {
       // Error for another reason. (Possibly OOM.)
-      PostDecoderError(error_code);
       mState = JPEG_ERROR;
       MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
              ("} (setjmp returned an error)"));
     }
 
     return Transition::TerminateFailure();
   }
 
@@ -296,34 +296,32 @@ nsJPEGDecoder::ReadJPEGData(const char* 
           break;
         case JCS_CMYK:
         case JCS_YCCK:
             // qcms doesn't support cmyk
             mismatch = true;
           break;
         default:
           mState = JPEG_ERROR;
-          PostDataError();
           MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
                  ("} (unknown colorpsace (1))"));
           return Transition::TerminateFailure();
       }
 
       if (!mismatch) {
         qcms_data_type type;
         switch (mInfo.out_color_space) {
           case JCS_GRAYSCALE:
             type = QCMS_DATA_GRAY_8;
             break;
           case JCS_RGB:
             type = QCMS_DATA_RGB_8;
             break;
           default:
             mState = JPEG_ERROR;
-            PostDataError();
             MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
                    ("} (unknown colorpsace (2))"));
             return Transition::TerminateFailure();
         }
 #if 0
         // We don't currently support CMYK profiles. The following
         // code dealt with lcms types. Add something like this
         // back when we gain support for CMYK.
@@ -372,17 +370,16 @@ nsJPEGDecoder::ReadJPEGData(const char* 
           break;
         case JCS_CMYK:
         case JCS_YCCK:
           // libjpeg can convert from YCCK to CMYK, but not to RGB
           mInfo.out_color_space = JCS_CMYK;
           break;
         default:
           mState = JPEG_ERROR;
-          PostDataError();
           MOZ_LOG(sJPEGDecoderAccountingLog, LogLevel::Debug,
                  ("} (unknown colorpsace (3))"));
           return Transition::TerminateFailure();
       }
     }
 
     // Don't allocate a giant and superfluous memory buffer
     // when not doing a progressive decode.
--- a/image/decoders/nsJPEGDecoder.h
+++ b/image/decoders/nsJPEGDecoder.h
@@ -52,19 +52,20 @@ class nsJPEGDecoder : public Decoder
 public:
   virtual ~nsJPEGDecoder();
 
   virtual void SetSampleSize(int aSampleSize) override
   {
     mSampleSize = aSampleSize;
   }
 
-  virtual void InitInternal() override;
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
-  virtual void FinishInternal() override;
+  nsresult InitInternal() override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
+  nsresult FinishInternal() override;
 
   virtual Telemetry::ID SpeedHistogram() override;
   void NotifyDone();
 
 protected:
   Orientation ReadOrientationFromEXIF();
   void OutputScanlines(bool* suspend);
 
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -257,17 +257,17 @@ nsPNGDecoder::EndImageFrame()
   if (format == gfx::SurfaceFormat::B8G8R8X8) {
     opacity = Opacity::FULLY_OPAQUE;
   }
 
   PostFrameStop(opacity, mAnimInfo.mDispose, mAnimInfo.mTimeout,
                 mAnimInfo.mBlend, Some(mFrameRect));
 }
 
-void
+nsresult
 nsPNGDecoder::InitInternal()
 {
   mCMSMode = gfxPlatform::GetCMSMode();
   if (GetSurfaceFlags() & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
     mCMSMode = eCMSMode_Off;
   }
   mDisablePremultipliedAlpha =
     bool(GetSurfaceFlags() & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
@@ -293,25 +293,23 @@ nsPNGDecoder::InitInternal()
 
   // Initialize the container's source image header
   // Always decode to 24 bit pixdepth
 
   mPNG = png_create_read_struct(PNG_LIBPNG_VER_STRING,
                                 nullptr, nsPNGDecoder::error_callback,
                                 nsPNGDecoder::warning_callback);
   if (!mPNG) {
-    PostDecoderError(NS_ERROR_OUT_OF_MEMORY);
-    return;
+    return NS_ERROR_OUT_OF_MEMORY;
   }
 
   mInfo = png_create_info_struct(mPNG);
   if (!mInfo) {
-    PostDecoderError(NS_ERROR_OUT_OF_MEMORY);
     png_destroy_read_struct(&mPNG, nullptr, nullptr);
-    return;
+    return NS_ERROR_OUT_OF_MEMORY;
   }
 
 #ifdef PNG_HANDLE_AS_UNKNOWN_SUPPORTED
   // Ignore unused chunks
   if (mCMSMode == eCMSMode_Off || IsMetadataDecode()) {
     png_set_keep_unknown_chunks(mPNG, 1, color_chunks, 2);
   }
 
@@ -341,26 +339,25 @@ nsPNGDecoder::InitInternal()
 #endif
 
   // use this as libpng "progressive pointer" (retrieve in callbacks)
   png_set_progressive_read_fn(mPNG, static_cast<png_voidp>(this),
                               nsPNGDecoder::info_callback,
                               nsPNGDecoder::row_callback,
                               nsPNGDecoder::end_callback);
 
+  return NS_OK;
 }
 
 Maybe<TerminalState>
-nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator)
+nsPNGDecoder::DoDecode(SourceBufferIterator& aIterator, IResumable* aOnResume)
 {
   MOZ_ASSERT(!HasError(), "Shouldn't call DoDecode after error!");
-  MOZ_ASSERT(aIterator.Data());
-  MOZ_ASSERT(aIterator.Length() > 0);
 
-  return mLexer.Lex(aIterator.Data(), aIterator.Length(),
+  return mLexer.Lex(aIterator, aOnResume,
                     [=](State aState, const char* aData, size_t aLength) {
     switch (aState) {
       case State::PNG_DATA:
         return ReadPNGData(aData, aLength);
       case State::FINISHED_PNG_DATA:
         return FinishedPNGData();
     }
     MOZ_CRASH("Unknown State");
--- a/image/decoders/nsPNGDecoder.h
+++ b/image/decoders/nsPNGDecoder.h
@@ -17,18 +17,19 @@ namespace mozilla {
 namespace image {
 class RasterImage;
 
 class nsPNGDecoder : public Decoder
 {
 public:
   virtual ~nsPNGDecoder();
 
-  virtual void InitInternal() override;
-  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator) override;
+  nsresult InitInternal() override;
+  Maybe<TerminalState> DoDecode(SourceBufferIterator& aIterator,
+                                IResumable* aOnResume) override;
   virtual Telemetry::ID SpeedHistogram() override;
 
   /// @return true if this PNG is a valid ICO resource.
   bool IsValidICO() const;
 
 private:
   friend class DecoderFactory;
 
--- a/image/test/gtest/Common.cpp
+++ b/image/test/gtest/Common.cpp
@@ -528,16 +528,25 @@ ImageTestCase GreenFirstFrameAnimatedPNG
 }
 
 ImageTestCase CorruptTestCase()
 {
   return ImageTestCase("corrupt.jpg", "image/jpeg", IntSize(100, 100),
                        TEST_CASE_HAS_ERROR);
 }
 
+ImageTestCase CorruptBMPWithTruncatedHeader()
+{
+  // This BMP has a header which is truncated right between the BIH and the
+  // bitfields, which is a particularly error-prone place w.r.t. the BMP decoder
+  // state machine.
+  return ImageTestCase("invalid-truncated-metadata.bmp", "image/bmp",
+                       IntSize(100, 100), TEST_CASE_HAS_ERROR);
+}
+
 ImageTestCase CorruptICOWithBadBMPWidthTestCase()
 {
   // This ICO contains a BMP icon which has a width that doesn't match the size
   // listed in the corresponding ICO directory entry.
   return ImageTestCase("corrupt-with-bad-bmp-width.ico", "image/x-icon",
                        IntSize(100, 100), TEST_CASE_HAS_ERROR);
 }
 
--- a/image/test/gtest/Common.h
+++ b/image/test/gtest/Common.h
@@ -10,16 +10,17 @@
 
 #include "gtest/gtest.h"
 
 #include "mozilla/Maybe.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/gfx/2D.h"
 #include "Decoder.h"
 #include "gfxColor.h"
+#include "imgITools.h"
 #include "nsCOMPtr.h"
 #include "SurfacePipe.h"
 #include "SurfacePipeFactory.h"
 
 class nsIInputStream;
 
 namespace mozilla {
 namespace image {
@@ -94,16 +95,33 @@ struct BGRAColor
   uint8_t mAlpha;
 };
 
 
 ///////////////////////////////////////////////////////////////////////////////
 // General Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
+/**
+ * A RAII class that ensure that ImageLib services are available. Any tests that
+ * require ImageLib to be initialized (for example, any test that uses the
+ * SurfaceCache; see image::EnsureModuleInitialized() for the full list) can
+ * use this class to ensure that ImageLib services are available. Failure to do
+ * so can result in strange, non-deterministic failures.
+ */
+struct AutoInitializeImageLib
+{
+  AutoInitializeImageLib()
+  {
+    // Ensure that ImageLib services are initialized.
+    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
+    EXPECT_TRUE(imgTools != nullptr);
+  }
+};
+
 /// Loads a file from the current directory. @return an nsIInputStream for it.
 already_AddRefed<nsIInputStream> LoadFile(const char* aRelativePath);
 
 /**
  * @returns true if every pixel of @aSurface is @aColor.
  * 
  * If @aFuzz is nonzero, a tolerance of @aFuzz is allowed in each color
  * component. This may be necessary for tests that involve JPEG images or
@@ -165,16 +183,47 @@ bool PalettedRectIsSolidColor(Decoder* a
 /**
  * @returns true if the pixels in @aRow of @aSurface match the pixels given in
  * @aPixels.
  */
 bool RowHasPixels(gfx::SourceSurface* aSurface,
                   int32_t aRow,
                   const std::vector<BGRAColor>& aPixels);
 
+// ExpectNoResume is an IResumable implementation for use by tests that expect
+// Resume() to never get called.
+class ExpectNoResume final : public IResumable
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExpectNoResume, override)
+
+  void Resume() override { FAIL() << "Resume() should not get called"; }
+
+private:
+  ~ExpectNoResume() override { }
+};
+
+// CountResumes is an IResumable implementation for use by tests that expect
+// Resume() to get called a certain number of times.
+class CountResumes : public IResumable
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CountResumes, override)
+
+  CountResumes() : mCount(0) { }
+
+  void Resume() override { mCount++; }
+  uint32_t Count() const { return mCount; }
+
+private:
+  ~CountResumes() override { }
+
+  uint32_t mCount;
+};
+
 
 ///////////////////////////////////////////////////////////////////////////////
 // SurfacePipe Helpers
 ///////////////////////////////////////////////////////////////////////////////
 
 /**
  * Creates a decoder with no data associated with, suitable for testing code
  * that requires a decoder to initialize or to allocate surfaces but doesn't
@@ -335,16 +384,17 @@ ImageTestCase GreenJPGTestCase();
 ImageTestCase GreenBMPTestCase();
 ImageTestCase GreenICOTestCase();
 ImageTestCase GreenIconTestCase();
 
 ImageTestCase GreenFirstFrameAnimatedGIFTestCase();
 ImageTestCase GreenFirstFrameAnimatedPNGTestCase();
 
 ImageTestCase CorruptTestCase();
+ImageTestCase CorruptBMPWithTruncatedHeader();
 ImageTestCase CorruptICOWithBadBMPWidthTestCase();
 ImageTestCase CorruptICOWithBadBMPHeightTestCase();
 
 ImageTestCase TransparentPNGTestCase();
 ImageTestCase TransparentGIFTestCase();
 ImageTestCase FirstFramePaddingGIFTestCase();
 ImageTestCase NoFrameDelayGIFTestCase();
 ImageTestCase ExtraImageSubBlocksAnimatedGIFTestCase();
--- a/image/test/gtest/TestDecodeToSurface.cpp
+++ b/image/test/gtest/TestDecodeToSurface.cpp
@@ -82,23 +82,18 @@ RunDecodeToSurface(const ImageTestCase& 
   thread->Shutdown();
 
   // Explicitly release the SourceSurface on the main thread.
   surface = nullptr;
 }
 
 class ImageDecodeToSurface : public ::testing::Test
 {
-  protected:
-  static void SetUpTestCase()
-  {
-    // Ensure that ImageLib services are initialized.
-    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
-    EXPECT_TRUE(imgTools != nullptr);
-  }
+protected:
+  AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDecodeToSurface, PNG) { RunDecodeToSurface(GreenPNGTestCase()); }
 TEST_F(ImageDecodeToSurface, GIF) { RunDecodeToSurface(GreenGIFTestCase()); }
 TEST_F(ImageDecodeToSurface, JPG) { RunDecodeToSurface(GreenJPGTestCase()); }
 TEST_F(ImageDecodeToSurface, BMP) { RunDecodeToSurface(GreenBMPTestCase()); }
 TEST_F(ImageDecodeToSurface, ICO) { RunDecodeToSurface(GreenICOTestCase()); }
 TEST_F(ImageDecodeToSurface, Icon) { RunDecodeToSurface(GreenIconTestCase()); }
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -201,23 +201,18 @@ CheckDownscaleDuringDecode(const ImageTe
     EXPECT_TRUE(RowsAreSolidColor(surface, 6, 3, BGRAColor::Red(), /* aFuzz = */ 27));
     EXPECT_TRUE(RowsAreSolidColor(surface, 11, 3, BGRAColor::Green(), /* aFuzz = */ 47));
     EXPECT_TRUE(RowsAreSolidColor(surface, 16, 4, BGRAColor::Red(), /* aFuzz = */ 27));
   });
 }
 
 class ImageDecoders : public ::testing::Test
 {
-  protected:
-  static void SetUpTestCase()
-  {
-    // Ensure that ImageLib services are initialized.
-    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
-    EXPECT_TRUE(imgTools != nullptr);
-  }
+protected:
+  AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDecoders, PNGSingleChunk)
 {
   CheckDecoderSingleChunk(GreenPNGTestCase());
 }
 
 TEST_F(ImageDecoders, PNGMultiChunk)
@@ -335,16 +330,26 @@ TEST_F(ImageDecoders, CorruptSingleChunk
   CheckDecoderSingleChunk(CorruptTestCase());
 }
 
 TEST_F(ImageDecoders, CorruptMultiChunk)
 {
   CheckDecoderMultiChunk(CorruptTestCase());
 }
 
+TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderSingleChunk)
+{
+  CheckDecoderSingleChunk(CorruptBMPWithTruncatedHeader());
+}
+
+TEST_F(ImageDecoders, CorruptBMPWithTruncatedHeaderMultiChunk)
+{
+  CheckDecoderMultiChunk(CorruptBMPWithTruncatedHeader());
+}
+
 TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthSingleChunk)
 {
   CheckDecoderSingleChunk(CorruptICOWithBadBMPWidthTestCase());
 }
 
 TEST_F(ImageDecoders, CorruptICOWithBadBMPWidthMultiChunk)
 {
   CheckDecoderMultiChunk(CorruptICOWithBadBMPWidthTestCase());
--- a/image/test/gtest/TestDeinterlacingFilter.cpp
+++ b/image/test/gtest/TestDeinterlacingFilter.cpp
@@ -56,23 +56,18 @@ AssertConfiguringDeinterlacingFilterFail
   AssertConfiguringPipelineFails(decoder,
                                  DeinterlacingConfig<uint32_t> { /* mProgressiveDisplay = */ true},
                                  SurfaceConfig { decoder, 0, aSize,
                                                  SurfaceFormat::B8G8R8A8, false });
 }
 
 class ImageDeinterlacingFilter : public ::testing::Test
 {
-  protected:
-  static void SetUpTestCase()
-  {
-    // Ensure that ImageLib services are initialized.
-    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
-    EXPECT_TRUE(imgTools != nullptr);
-  }
+protected:
+  AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDeinterlacingFilter, WritePixels100_100)
 {
   WithDeinterlacingFilter(IntSize(100, 100), /* aProgressiveDisplay = */ true,
                           [](Decoder* aDecoder, SurfaceFilter* aFilter) {
     CheckWritePixels(aDecoder, aFilter,
                      /* aOutputRect = */ Some(IntRect(0, 0, 100, 100)),
--- a/image/test/gtest/TestMetadata.cpp
+++ b/image/test/gtest/TestMetadata.cpp
@@ -130,23 +130,18 @@ CheckMetadata(const ImageTestCase& aTest
   // discover during the metadata decode, unless the image is animated.
   EXPECT_TRUE(!(fullProgress & FLAG_HAS_TRANSPARENCY) ||
               (metadataProgress & FLAG_HAS_TRANSPARENCY) ||
               (fullProgress & FLAG_IS_ANIMATED));
 }
 
 class ImageDecoderMetadata : public ::testing::Test
 {
-  protected:
-  static void SetUpTestCase()
-  {
-    // Ensure that ImageLib services are initialized.
-    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
-    EXPECT_TRUE(imgTools != nullptr);
-  }
+protected:
+  AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageDecoderMetadata, PNG) { CheckMetadata(GreenPNGTestCase()); }
 TEST_F(ImageDecoderMetadata, TransparentPNG) { CheckMetadata(TransparentPNGTestCase()); }
 TEST_F(ImageDecoderMetadata, GIF) { CheckMetadata(GreenGIFTestCase()); }
 TEST_F(ImageDecoderMetadata, TransparentGIF) { CheckMetadata(TransparentGIFTestCase()); }
 TEST_F(ImageDecoderMetadata, JPG) { CheckMetadata(GreenJPGTestCase()); }
 TEST_F(ImageDecoderMetadata, BMP) { CheckMetadata(GreenBMPTestCase()); }
new file mode 100644
--- /dev/null
+++ b/image/test/gtest/TestSourceBuffer.cpp
@@ -0,0 +1,810 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <algorithm>
+#include <cstdint>
+
+#include "mozilla/Move.h"
+#include "SourceBuffer.h"
+#include "SurfaceCache.h"
+
+using namespace mozilla;
+using namespace mozilla::image;
+
+using std::min;
+
+void
+ExpectChunkAndByteCount(const SourceBufferIterator& aIterator,
+                        uint32_t aChunks,
+                        size_t aBytes)
+{
+  EXPECT_EQ(aChunks, aIterator.ChunkCount());
+  EXPECT_EQ(aBytes, aIterator.ByteCount());
+}
+
+void
+ExpectRemainingBytes(const SourceBufferIterator& aIterator, size_t aBytes)
+{
+  EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes));
+  EXPECT_TRUE(aIterator.RemainingBytesIsNoMoreThan(aBytes + 1));
+
+  if (aBytes > 0) {
+    EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(0));
+    EXPECT_FALSE(aIterator.RemainingBytesIsNoMoreThan(aBytes - 1));
+  }
+}
+
+char
+GenerateByte(size_t aIndex)
+{
+  uint8_t byte = aIndex % 256;
+  return *reinterpret_cast<char*>(&byte);
+}
+
+void
+GenerateData(char* aOutput, size_t aOffset, size_t aLength)
+{
+  for (size_t i = 0; i < aLength; ++i) {
+    aOutput[i] = GenerateByte(aOffset + i);
+  }
+}
+
+void
+GenerateData(char* aOutput, size_t aLength)
+{
+  GenerateData(aOutput, 0, aLength);
+}
+
+void
+CheckData(const char* aData, size_t aOffset, size_t aLength)
+{
+  for (size_t i = 0; i < aLength; ++i) {
+    ASSERT_EQ(GenerateByte(aOffset + i), aData[i]);
+  }
+}
+
+enum class AdvanceMode
+{
+  eAdvanceAsMuchAsPossible,
+  eAdvanceByLengthExactly
+};
+
+class ImageSourceBuffer : public ::testing::Test
+{
+public:
+  ImageSourceBuffer()
+    : mSourceBuffer(new SourceBuffer)
+    , mExpectNoResume(new ExpectNoResume)
+    , mCountResumes(new CountResumes)
+  {
+    GenerateData(mData, sizeof(mData));
+    EXPECT_FALSE(mSourceBuffer->IsComplete());
+  }
+
+protected:
+  void CheckedAppendToBuffer(const char* aData, size_t aLength)
+  {
+    EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->Append(aData, aLength)));
+  }
+
+  void CheckedAppendToBufferLastByteForLength(size_t aLength)
+  {
+    const char lastByte = GenerateByte(aLength);
+    CheckedAppendToBuffer(&lastByte, 1);
+  }
+
+  void CheckedAppendToBufferInChunks(size_t aChunkLength, size_t aTotalLength)
+  {
+    char* data = new char[aChunkLength];
+
+    size_t bytesWritten = 0;
+    while (bytesWritten < aTotalLength) {
+      GenerateData(data, bytesWritten, aChunkLength);
+      size_t toWrite = min(aChunkLength, aTotalLength - bytesWritten);
+      CheckedAppendToBuffer(data, toWrite);
+      bytesWritten += toWrite;
+    }
+
+    delete[] data;
+  }
+
+  void CheckedCompleteBuffer(nsresult aCompletionStatus = NS_OK)
+  {
+    mSourceBuffer->Complete(aCompletionStatus);
+    EXPECT_TRUE(mSourceBuffer->IsComplete());
+  }
+
+  void CheckedCompleteBuffer(SourceBufferIterator& aIterator,
+                             size_t aLength,
+                             nsresult aCompletionStatus = NS_OK)
+  {
+    CheckedCompleteBuffer(aCompletionStatus);
+    ExpectRemainingBytes(aIterator, aLength);
+  }
+
+  void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator,
+                                       size_t aLength,
+                                       uint32_t aChunks,
+                                       size_t aTotalLength,
+                                       AdvanceMode aAdvanceMode
+                                         = AdvanceMode::eAdvanceAsMuchAsPossible)
+  {
+    const size_t advanceBy = aAdvanceMode == AdvanceMode::eAdvanceAsMuchAsPossible
+                           ? SIZE_MAX
+                           : aLength;
+
+    auto state = aIterator.AdvanceOrScheduleResume(advanceBy, mExpectNoResume);
+    ASSERT_EQ(SourceBufferIterator::READY, state);
+    EXPECT_TRUE(aIterator.Data());
+    EXPECT_EQ(aLength, aIterator.Length());
+
+    ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength);
+  }
+
+  void CheckedAdvanceIteratorStateOnly(SourceBufferIterator& aIterator,
+                                       size_t aLength)
+  {
+    CheckedAdvanceIteratorStateOnly(aIterator, aLength, 1, aLength);
+  }
+
+  void CheckedAdvanceIterator(SourceBufferIterator& aIterator,
+                              size_t aLength,
+                              uint32_t aChunks,
+                              size_t aTotalLength,
+                              AdvanceMode aAdvanceMode
+                                = AdvanceMode::eAdvanceAsMuchAsPossible)
+  {
+    // Check that the iterator is in the expected state.
+    CheckedAdvanceIteratorStateOnly(aIterator, aLength, aChunks,
+                                    aTotalLength, aAdvanceMode);
+
+    // Check that we read the expected data. To do this, we need to compute our
+    // offset in the SourceBuffer, but fortunately that's pretty easy: it's the
+    // total number of bytes the iterator has advanced through, minus the length
+    // of the current chunk.
+    const size_t offset = aIterator.ByteCount() - aIterator.Length();
+    CheckData(aIterator.Data(), offset, aIterator.Length());
+  }
+
+  void CheckedAdvanceIterator(SourceBufferIterator& aIterator, size_t aLength)
+  {
+    CheckedAdvanceIterator(aIterator, aLength, 1, aLength);
+  }
+
+  void CheckIteratorMustWait(SourceBufferIterator& aIterator,
+                             IResumable* aOnResume)
+  {
+    auto state = aIterator.AdvanceOrScheduleResume(1, aOnResume);
+    EXPECT_EQ(SourceBufferIterator::WAITING, state);
+  }
+
+  void CheckIteratorIsComplete(SourceBufferIterator& aIterator,
+                               uint32_t aChunks,
+                               size_t aTotalLength,
+                               nsresult aCompletionStatus = NS_OK)
+  {
+    ASSERT_TRUE(mSourceBuffer->IsComplete());
+    auto state = aIterator.AdvanceOrScheduleResume(1, mExpectNoResume);
+    ASSERT_EQ(SourceBufferIterator::COMPLETE, state);
+    EXPECT_EQ(aCompletionStatus, aIterator.CompletionStatus());
+    ExpectRemainingBytes(aIterator, 0);
+    ExpectChunkAndByteCount(aIterator, aChunks, aTotalLength);
+  }
+
+  void CheckIteratorIsComplete(SourceBufferIterator& aIterator,
+                               size_t aTotalLength)
+  {
+    CheckIteratorIsComplete(aIterator, 1, aTotalLength);
+  }
+
+  AutoInitializeImageLib mInit;
+  char mData[9];
+  RefPtr<SourceBuffer> mSourceBuffer;
+  RefPtr<ExpectNoResume> mExpectNoResume;
+  RefPtr<CountResumes> mCountResumes;
+};
+
+TEST_F(ImageSourceBuffer, InitialState)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // RemainingBytesIsNoMoreThan() should always return false in the initial
+  // state, since we can't know the answer until Complete() has been called.
+  EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(0));
+  EXPECT_FALSE(iterator.RemainingBytesIsNoMoreThan(SIZE_MAX));
+
+  // We haven't advanced our iterator at all, so its counters should be zero.
+  ExpectChunkAndByteCount(iterator, 0, 0);
+
+  // Attempt to advance; we should fail, and end up in the WAITING state. We
+  // expect no resumes because we don't actually append anything to the
+  // SourceBuffer in this test.
+  CheckIteratorMustWait(iterator, mExpectNoResume);
+}
+
+TEST_F(ImageSourceBuffer, ZeroLengthBufferAlwaysFails)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Complete the buffer without writing to it, providing a successful
+  // completion status.
+  CheckedCompleteBuffer(iterator, 0);
+
+  // Completing a buffer without writing to it results in an automatic failure;
+  // make sure that the actual completion status we get from the iterator
+  // reflects this.
+  CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE);
+}
+
+TEST_F(ImageSourceBuffer, CompleteSuccess)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write a single byte to the buffer and complete the buffer. (We have to
+  // write at least one byte because completing a zero length buffer always
+  // fails; see the ZeroLengthBufferAlwaysFails test.)
+  CheckedAppendToBuffer(mData, 1);
+  CheckedCompleteBuffer(iterator, 1);
+
+  // We should be able to advance once (to read the single byte) and then should
+  // reach the COMPLETE state with a successful status.
+  CheckedAdvanceIterator(iterator, 1);
+  CheckIteratorIsComplete(iterator, 1);
+}
+
+TEST_F(ImageSourceBuffer, CompleteFailure)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write a single byte to the buffer and complete the buffer. (We have to
+  // write at least one byte because completing a zero length buffer always
+  // fails; see the ZeroLengthBufferAlwaysFails test.)
+  CheckedAppendToBuffer(mData, 1);
+  CheckedCompleteBuffer(iterator, 1, NS_ERROR_FAILURE);
+
+  // Advance the iterator. Because a failing status is propagated to the
+  // iterator as soon as it advances, we won't be able to read the single byte
+  // that we wrote above; we go directly into the COMPLETE state.
+  CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_FAILURE);
+}
+
+TEST_F(ImageSourceBuffer, Append)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write test data to the buffer.
+  EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))));
+  CheckedAppendToBuffer(mData, sizeof(mData));
+  CheckedCompleteBuffer(iterator, sizeof(mData));
+
+  // Verify that we can read it back via the iterator, and that the final state
+  // is what we expect.
+  CheckedAdvanceIterator(iterator, sizeof(mData));
+  CheckIteratorIsComplete(iterator, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, HugeAppendFails)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // We should fail to append anything bigger than what the SurfaceCache can
+  // hold, so use the SurfaceCache's maximum capacity to calculate what a
+  // "massive amount of data" (see below) consists of on this platform.
+  ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX);
+  const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1;
+
+  // Attempt to write a massive amount of data and verify that it fails. (We'd
+  // get a buffer overrun during the test if it succeeds, but if it succeeds
+  // that's the least of our problems.)
+  EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, hugeSize)));
+  EXPECT_TRUE(mSourceBuffer->IsComplete());
+  CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY);
+}
+
+TEST_F(ImageSourceBuffer, AppendFromInputStream)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Construct an input stream with some arbitrary data. (We use test data from
+  // one of the decoder tests.)
+  nsCOMPtr<nsIInputStream> inputStream = LoadFile(GreenPNGTestCase().mPath);
+  ASSERT_TRUE(inputStream != nullptr);
+
+  // Figure out how much data we have.
+  uint64_t length;
+  ASSERT_TRUE(NS_SUCCEEDED(inputStream->Available(&length)));
+
+  // Write test data to the buffer.
+  EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->AppendFromInputStream(inputStream,
+                                                                length)));
+  CheckedCompleteBuffer(iterator, length);
+
+  // Verify that the iterator sees the appropriate amount of data.
+  CheckedAdvanceIteratorStateOnly(iterator, length);
+  CheckIteratorIsComplete(iterator, length);
+}
+
+TEST_F(ImageSourceBuffer, AppendAfterComplete)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write test data to the buffer.
+  EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(sizeof(mData))));
+  CheckedAppendToBuffer(mData, sizeof(mData));
+  CheckedCompleteBuffer(iterator, sizeof(mData));
+
+  // Verify that we can read it back via the iterator, and that the final state
+  // is what we expect.
+  CheckedAdvanceIterator(iterator, sizeof(mData));
+  CheckIteratorIsComplete(iterator, sizeof(mData));
+
+  // Write more data to the completed buffer.
+  EXPECT_TRUE(NS_FAILED(mSourceBuffer->Append(mData, sizeof(mData))));
+
+  // Try to read with a new iterator and verify that the new data got ignored.
+  SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
+  CheckedAdvanceIterator(iterator2, sizeof(mData));
+  CheckIteratorIsComplete(iterator2, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, MinChunkCapacity)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write test data to the buffer using many small appends. Since
+  // ExpectLength() isn't being called, we should be able to write up to
+  // SourceBuffer::MIN_CHUNK_CAPACITY bytes without a second chunk being
+  // allocated.
+  CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+  // Verify that the iterator sees the appropriate amount of data.
+  CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+  // Write one more byte; we expect to see that it triggers an allocation.
+  CheckedAppendToBufferLastByteForLength(SourceBuffer::MIN_CHUNK_CAPACITY);
+  CheckedCompleteBuffer(iterator, 1);
+
+  // Verify that the iterator sees the new byte and a new chunk has been
+  // allocated.
+  CheckedAdvanceIterator(iterator, 1, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1);
+  CheckIteratorIsComplete(iterator, 2, SourceBuffer::MIN_CHUNK_CAPACITY + 1);
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthDoesNotShrinkBelowMinCapacity)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the buffer,
+  // but call ExpectLength() first to make SourceBuffer expect only a single
+  // byte. We expect this to still result in only one chunk, because
+  // regardless of ExpectLength() we won't allocate a chunk smaller than
+  // MIN_CHUNK_CAPACITY bytes.
+  EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(1)));
+  CheckedAppendToBufferInChunks(10, SourceBuffer::MIN_CHUNK_CAPACITY);
+  CheckedCompleteBuffer(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+  // Verify that the iterator sees a single chunk.
+  CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+  CheckIteratorIsComplete(iterator, 1, SourceBuffer::MIN_CHUNK_CAPACITY);
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthGrowsAboveMinCapacity)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer, calling ExpectLength() with the correct length first. We expect
+  // this to result in only one chunk, because ExpectLength() allows us to
+  // allocate a larger first chunk than MIN_CHUNK_CAPACITY bytes.
+  const size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY;
+  EXPECT_TRUE(NS_SUCCEEDED(mSourceBuffer->ExpectLength(length)));
+  CheckedAppendToBufferInChunks(10, length);
+
+  // Verify that the iterator sees a single chunk.
+  CheckedAdvanceIterator(iterator, length);
+
+  // Write one more byte; we expect to see that it triggers an allocation.
+  CheckedAppendToBufferLastByteForLength(length);
+  CheckedCompleteBuffer(iterator, 1);
+
+  // Verify that the iterator sees the new byte and a new chunk has been
+  // allocated.
+  CheckedAdvanceIterator(iterator, 1, 2, length + 1);
+  CheckIteratorIsComplete(iterator, 2, length + 1);
+}
+
+TEST_F(ImageSourceBuffer, HugeExpectLengthFails)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // ExpectLength() should fail if the length is bigger than what the
+  // SurfaceCache can hold, so use the SurfaceCache's maximum capacity to
+  // calculate what a "massive amount of data" (see below) consists of on this
+  // platform.
+  ASSERT_LT(SurfaceCache::MaximumCapacity(), SIZE_MAX);
+  const size_t hugeSize = SurfaceCache::MaximumCapacity() + 1;
+
+  // Attempt to write a massive amount of data and verify that it fails. (We'd
+  // get a buffer overrun during the test if it succeeds, but if it succeeds
+  // that's the least of our problems.)
+  EXPECT_TRUE(NS_FAILED(mSourceBuffer->ExpectLength(hugeSize)));
+  EXPECT_TRUE(mSourceBuffer->IsComplete());
+  CheckIteratorIsComplete(iterator, 0, 0, NS_ERROR_OUT_OF_MEMORY);
+}
+
+TEST_F(ImageSourceBuffer, LargeAppendsAllocateOnlyOneChunk)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Write two times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer in a single Append() call. We expect this to result in only one
+  // chunk even though ExpectLength() wasn't called, because we should always
+  // allocate a new chunk large enough to store the data we have at hand.
+  constexpr size_t length = 2 * SourceBuffer::MIN_CHUNK_CAPACITY;
+  char data[length];
+  GenerateData(data, sizeof(data));
+  CheckedAppendToBuffer(data, length);
+
+  // Verify that the iterator sees a single chunk.
+  CheckedAdvanceIterator(iterator, length);
+
+  // Write one more byte; we expect to see that it triggers an allocation.
+  CheckedAppendToBufferLastByteForLength(length);
+  CheckedCompleteBuffer(iterator, 1);
+
+  // Verify that the iterator sees the new byte and a new chunk has been
+  // allocated.
+  CheckedAdvanceIterator(iterator, 1, 2, length + 1);
+  CheckIteratorIsComplete(iterator, 2, length + 1);
+}
+
+TEST_F(ImageSourceBuffer, LargeAppendsAllocateAtMostOneChunk)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Allocate some data we'll use below.
+  constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2;
+  constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = firstWriteLength + secondWriteLength;
+  char data[totalLength];
+  GenerateData(data, sizeof(data));
+
+  // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer in a single Append() call. This should fill half of the first chunk.
+  CheckedAppendToBuffer(data, firstWriteLength);
+
+  // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer in a single Append() call. We expect this to result in the first of
+  // the first chunk being filled and a new chunk being allocated for the
+  // remainder.
+  CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength);
+
+  // Verify that the iterator sees a MIN_CHUNK_CAPACITY-length chunk.
+  CheckedAdvanceIterator(iterator, SourceBuffer::MIN_CHUNK_CAPACITY);
+
+  // Verify that the iterator sees a second chunk of the length we expect.
+  const size_t expectedSecondChunkLength =
+    totalLength - SourceBuffer::MIN_CHUNK_CAPACITY;
+  CheckedAdvanceIterator(iterator, expectedSecondChunkLength, 2, totalLength);
+
+  // Write one more byte; we expect to see that it triggers an allocation.
+  CheckedAppendToBufferLastByteForLength(totalLength);
+  CheckedCompleteBuffer(iterator, 1);
+
+  // Verify that the iterator sees the new byte and a new chunk has been
+  // allocated.
+  CheckedAdvanceIterator(iterator, 1, 3, totalLength + 1);
+  CheckIteratorIsComplete(iterator, 3, totalLength + 1);
+}
+
+TEST_F(ImageSourceBuffer, CompactionHappensWhenBufferIsComplete)
+{
+  constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = 2 * chunkLength;
+
+  // Write enough data to create two chunks.
+  CheckedAppendToBufferInChunks(chunkLength, totalLength);
+
+  {
+    SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+    // Verify that the iterator sees two chunks.
+    CheckedAdvanceIterator(iterator, chunkLength);
+    CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength);
+  }
+
+  // Complete the buffer, which should trigger compaction implicitly.
+  CheckedCompleteBuffer();
+
+  {
+    SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+    // Verify that compaction happened and there's now only one chunk.
+    CheckedAdvanceIterator(iterator, totalLength);
+    CheckIteratorIsComplete(iterator, 1, totalLength);
+  }
+}
+
+TEST_F(ImageSourceBuffer, CompactionIsDelayedWhileIteratorsExist)
+{
+  constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = 2 * chunkLength;
+
+  {
+    SourceBufferIterator outerIterator = mSourceBuffer->Iterator();
+
+    {
+      SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+      // Write enough data to create two chunks.
+      CheckedAppendToBufferInChunks(chunkLength, totalLength);
+      CheckedCompleteBuffer(iterator, totalLength);
+
+      // Verify that the iterator sees two chunks. Since there are live
+      // iterators, compaction shouldn't have happened when we completed the
+      // buffer.
+      CheckedAdvanceIterator(iterator, chunkLength);
+      CheckedAdvanceIterator(iterator, chunkLength, 2, totalLength);
+      CheckIteratorIsComplete(iterator, 2, totalLength);
+    }
+
+    // Now |iterator| has been destroyed, but |outerIterator| still exists, so
+    // we expect no compaction to have occurred at this point.
+    CheckedAdvanceIterator(outerIterator, chunkLength);
+    CheckedAdvanceIterator(outerIterator, chunkLength, 2, totalLength);
+    CheckIteratorIsComplete(outerIterator, 2, totalLength);
+  }
+
+  // Now all iterators have been destroyed. Since the buffer was already
+  // complete, we expect compaction to happen implicitly here.
+
+  {
+    SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+    // Verify that compaction happened and there's now only one chunk.
+    CheckedAdvanceIterator(iterator, totalLength);
+    CheckIteratorIsComplete(iterator, 1, totalLength);
+  }
+}
+
+TEST_F(ImageSourceBuffer, SourceBufferIteratorsCanBeMoved)
+{
+  constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = 2 * chunkLength;
+
+  // Write enough data to create two chunks. We create an iterator here to make
+  // sure that compaction doesn't happen during the test.
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+  CheckedAppendToBufferInChunks(chunkLength, totalLength);
+  CheckedCompleteBuffer(iterator, totalLength);
+
+  auto GetIterator = [&]{
+    SourceBufferIterator lambdaIterator = mSourceBuffer->Iterator();
+    CheckedAdvanceIterator(lambdaIterator, chunkLength);
+    return lambdaIterator;
+  };
+
+  // Move-construct |movedIterator| from the iterator returned from
+  // GetIterator() and check that its state is as we expect.
+  SourceBufferIterator movedIterator = Move(GetIterator());
+  EXPECT_TRUE(movedIterator.Data());
+  EXPECT_EQ(chunkLength, movedIterator.Length());
+  ExpectChunkAndByteCount(movedIterator, 1, chunkLength);
+
+  // Make sure that we can advance the iterator.
+  CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength);
+
+  // Make sure that the iterator handles completion properly.
+  CheckIteratorIsComplete(movedIterator, 2, totalLength);
+
+  // Move-assign |movedIterator| from the iterator returned from
+  // GetIterator() and check that its state is as we expect.
+  movedIterator = Move(GetIterator());
+  EXPECT_TRUE(movedIterator.Data());
+  EXPECT_EQ(chunkLength, movedIterator.Length());
+  ExpectChunkAndByteCount(movedIterator, 1, chunkLength);
+
+  // Make sure that we can advance the iterator.
+  CheckedAdvanceIterator(movedIterator, chunkLength, 2, totalLength);
+
+  // Make sure that the iterator handles completion properly.
+  CheckIteratorIsComplete(movedIterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkAdvance)
+{
+  constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = 2 * chunkLength;
+
+  // Write enough data to create two chunks. We create our iterator here to make
+  // sure that compaction doesn't happen during the test.
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+  CheckedAppendToBufferInChunks(chunkLength, totalLength);
+  CheckedCompleteBuffer(iterator, totalLength);
+
+  // Advance through the first chunk. The chunk count should not increase.
+  // We check that by always passing 1 for the |aChunks| parameter of
+  // CheckedAdvanceIteratorStateOnly(). We have to call CheckData() manually
+  // because the offset calculation in CheckedAdvanceIterator() assumes that
+  // we're advancing a chunk at a time.
+  size_t offset = 0;
+  while (offset < chunkLength) {
+    CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+    CheckData(iterator.Data(), offset++, iterator.Length());
+  }
+
+  // Read the first byte of the second chunk. This is the point at which we
+  // can't advance within the same chunk, so the chunk count should increase. We
+  // check that by passing 2 for the |aChunks| parameter of
+  // CheckedAdvanceIteratorStateOnly().
+  CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+                                  AdvanceMode::eAdvanceByLengthExactly);
+  CheckData(iterator.Data(), offset++, iterator.Length());
+
+  // Read the rest of the second chunk. The chunk count should not increase.
+  while (offset < totalLength) {
+    CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+    CheckData(iterator.Data(), offset++, iterator.Length());
+  }
+
+  // Make sure we reached the end.
+  CheckIteratorIsComplete(iterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvance)
+{
+  constexpr size_t chunkLength = SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = 2 * chunkLength;
+
+  // Write enough data to create two chunks. We create our iterator here to make
+  // sure that compaction doesn't happen during the test.
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+  CheckedAppendToBufferInChunks(chunkLength, totalLength);
+  CheckedCompleteBuffer(iterator, totalLength);
+
+  // Make an initial zero-length advance. Although a zero-length advance
+  // normally won't cause us to read a chunk from the SourceBuffer, we'll do so
+  // if the iterator is in the initial state to keep the invariant that
+  // SourceBufferIterator in the READY state always returns a non-null pointer
+  // from Data().
+  CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength,
+                                  AdvanceMode::eAdvanceByLengthExactly);
+
+  // Advance through the first chunk. As in the |SubchunkAdvance| test, the
+  // chunk count should not increase. We do a zero-length advance after each
+  // normal advance to ensure that zero-length advances do not change the
+  // iterator's position or cause a new chunk to be read.
+  size_t offset = 0;
+  while (offset < chunkLength) {
+    CheckedAdvanceIteratorStateOnly(iterator, 1, 1, chunkLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+    CheckData(iterator.Data(), offset++, iterator.Length());
+    CheckedAdvanceIteratorStateOnly(iterator, 0, 1, chunkLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+  }
+
+  // Read the first byte of the second chunk. This is the point at which we
+  // can't advance within the same chunk, so the chunk count should increase. As
+  // before, we do a zero-length advance afterward.
+  CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+                                  AdvanceMode::eAdvanceByLengthExactly);
+  CheckData(iterator.Data(), offset++, iterator.Length());
+  CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength,
+                                  AdvanceMode::eAdvanceByLengthExactly);
+
+  // Read the rest of the second chunk. The chunk count should not increase. As
+  // before, we do a zero-length advance after each normal advance.
+  while (offset < totalLength) {
+    CheckedAdvanceIteratorStateOnly(iterator, 1, 2, totalLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+    CheckData(iterator.Data(), offset++, iterator.Length());
+    CheckedAdvanceIteratorStateOnly(iterator, 0, 2, totalLength,
+                                    AdvanceMode::eAdvanceByLengthExactly);
+  }
+
+  // Make sure we reached the end.
+  CheckIteratorIsComplete(iterator, 2, totalLength);
+}
+
+TEST_F(ImageSourceBuffer, SubchunkZeroByteAdvanceWithNoData)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that advancing by zero bytes still makes us enter the WAITING state.
+  // This is because if we entered the READY state before reading any data at
+  // all, we'd break the invariant that SourceBufferIterator::Data() always
+  // returns a non-null pointer in the READY state.
+  auto state = iterator.AdvanceOrScheduleResume(0, mCountResumes);
+  EXPECT_EQ(SourceBufferIterator::WAITING, state);
+
+  // Call Complete(). This should trigger a resume.
+  CheckedCompleteBuffer();
+  EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, NullIResumable)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that we can't advance.
+  CheckIteratorMustWait(iterator, nullptr);
+
+  // Append to the buffer, which would cause a resume if we had passed a
+  // non-null IResumable.
+  CheckedAppendToBuffer(mData, sizeof(mData));
+  CheckedCompleteBuffer(iterator, sizeof(mData));
+}
+
+TEST_F(ImageSourceBuffer, AppendTriggersResume)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that we can't advance.
+  CheckIteratorMustWait(iterator, mCountResumes);
+
+  // Call Append(). This should trigger a resume.
+  mSourceBuffer->Append(mData, sizeof(mData));
+  EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, OnlyOneResumeTriggeredPerAppend)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that we can't advance.
+  CheckIteratorMustWait(iterator, mCountResumes);
+
+  // Allocate some data we'll use below.
+  constexpr size_t firstWriteLength = SourceBuffer::MIN_CHUNK_CAPACITY / 2;
+  constexpr size_t secondWriteLength = 3 * SourceBuffer::MIN_CHUNK_CAPACITY;
+  constexpr size_t totalLength = firstWriteLength + secondWriteLength;
+  char data[totalLength];
+  GenerateData(data, sizeof(data));
+
+  // Write half of SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer in a single Append() call. This should fill half of the first chunk.
+  // This should trigger a resume.
+  CheckedAppendToBuffer(data, firstWriteLength);
+  EXPECT_EQ(1u, mCountResumes->Count());
+
+  // Advance past the new data and wait again.
+  CheckedAdvanceIterator(iterator, firstWriteLength);
+  CheckIteratorMustWait(iterator, mCountResumes);
+
+  // Write three times SourceBuffer::MIN_CHUNK_CAPACITY bytes of test data to the
+  // buffer in a single Append() call. We expect this to result in the first of
+  // the first chunk being filled and a new chunk being allocated for the
+  // remainder. Even though two chunks are getting written to here, only *one*
+  // resume should get triggered, for a total of two in this test.
+  CheckedAppendToBuffer(data + firstWriteLength, secondWriteLength);
+  EXPECT_EQ(2u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, CompleteTriggersResume)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that we can't advance.
+  CheckIteratorMustWait(iterator, mCountResumes);
+
+  // Call Complete(). This should trigger a resume.
+  CheckedCompleteBuffer();
+  EXPECT_EQ(1u, mCountResumes->Count());
+}
+
+TEST_F(ImageSourceBuffer, ExpectLengthDoesNotTriggerResume)
+{
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
+
+  // Check that we can't advance.
+  CheckIteratorMustWait(iterator, mExpectNoResume);
+
+  // Call ExpectLength(). If this triggers a resume, |mExpectNoResume| will
+  // ensure that the test fails.
+  mSourceBuffer->ExpectLength(1000);
+}
--- a/image/test/gtest/TestStreamingLexer.cpp
+++ b/image/test/gtest/TestStreamingLexer.cpp
@@ -14,248 +14,454 @@ enum class TestState
 {
   ONE,
   TWO,
   THREE,
   UNBUFFERED
 };
 
 void
-CheckData(const char* aData, size_t aLength)
+CheckLexedData(const char* aData, size_t aLength, size_t aExpectedLength)
 {
-  EXPECT_TRUE(aLength == 3);
-  EXPECT_EQ(1, aData[0]);
-  EXPECT_EQ(2, aData[1]);
-  EXPECT_EQ(3, aData[2]);
+  EXPECT_TRUE(aLength == aExpectedLength);
+
+  for (size_t i = 0; i < aLength; ++i) {
+    EXPECT_EQ(aData[i], char((i % 3) + 1));
+  }
 }
 
 LexerTransition<TestState>
 DoLex(TestState aState, const char* aData, size_t aLength)
 {
   switch (aState) {
     case TestState::ONE:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::To(TestState::TWO, 3);
     case TestState::TWO:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::To(TestState::THREE, 3);
     case TestState::THREE:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::TerminateSuccess();
     default:
-      MOZ_CRASH("Unknown TestState");
+      MOZ_CRASH("Unexpected or unhandled TestState");
   }
 }
 
 LexerTransition<TestState>
 DoLexWithUnbuffered(TestState aState, const char* aData, size_t aLength,
                     Vector<char>& aUnbufferedVector)
 {
   switch (aState) {
     case TestState::ONE:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
     case TestState::UNBUFFERED:
       EXPECT_TRUE(aLength <= 3);
       EXPECT_TRUE(aUnbufferedVector.append(aData, aLength));
       return Transition::ContinueUnbuffered(TestState::UNBUFFERED);
     case TestState::TWO:
-      CheckData(aUnbufferedVector.begin(), aUnbufferedVector.length());
+      CheckLexedData(aUnbufferedVector.begin(), aUnbufferedVector.length(), 3);
       return Transition::To(TestState::THREE, 3);
     case TestState::THREE:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::TerminateSuccess();
     default:
-      MOZ_CRASH("Unknown TestState");
+      MOZ_CRASH("Unexpected or unhandled TestState");
   }
 }
 
 LexerTransition<TestState>
 DoLexWithUnbufferedTerminate(TestState aState, const char* aData, size_t aLength)
 {
   switch (aState) {
     case TestState::ONE:
-      CheckData(aData, aLength);
+      CheckLexedData(aData, aLength, 3);
       return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 3);
     case TestState::UNBUFFERED:
       return Transition::TerminateSuccess();
     default:
-      MOZ_CRASH("Unknown TestState");
+      MOZ_CRASH("Unexpected or unhandled TestState");
+  }
+}
+
+LexerTransition<TestState>
+DoLexWithZeroLengthStates(TestState aState, const char* aData, size_t aLength)
+{
+  switch (aState) {
+    case TestState::ONE:
+      EXPECT_TRUE(aLength == 0);
+      return Transition::To(TestState::TWO, 0);
+    case TestState::TWO:
+      EXPECT_TRUE(aLength == 0);
+      return Transition::To(TestState::THREE, 9);
+    case TestState::THREE:
+      CheckLexedData(aData, aLength, 9);
+      return Transition::TerminateSuccess();
+    default:
+      MOZ_CRASH("Unexpected or unhandled TestState");
   }
 }
 
-TEST(ImageStreamingLexer, SingleChunk)
+LexerTransition<TestState>
+DoLexWithZeroLengthStatesUnbuffered(TestState aState,
+                                    const char* aData,
+                                    size_t aLength)
+{
+  switch (aState) {
+    case TestState::ONE:
+      EXPECT_TRUE(aLength == 0);
+      return Transition::ToUnbuffered(TestState::TWO, TestState::UNBUFFERED, 0);
+    case TestState::TWO:
+      EXPECT_TRUE(aLength == 0);
+      return Transition::To(TestState::THREE, 9);
+    case TestState::THREE:
+      CheckLexedData(aData, aLength, 9);
+      return Transition::TerminateSuccess();
+    case TestState::UNBUFFERED:
+      ADD_FAILURE() << "Should not enter zero-length unbuffered state";
+      return Transition::TerminateFailure();
+    default:
+      MOZ_CRASH("Unexpected or unhandled TestState");
+  }
+}
+
+class ImageStreamingLexer : public ::testing::Test
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
+public:
+  ImageStreamingLexer()
+    : mLexer(Transition::To(TestState::ONE, 3))
+    , mSourceBuffer(new SourceBuffer)
+    , mIterator(mSourceBuffer->Iterator())
+    , mExpectNoResume(new ExpectNoResume)
+    , mCountResumes(new CountResumes)
+  { }
+
+protected:
+  AutoInitializeImageLib mInit;
+  const char mData[9] { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
+  StreamingLexer<TestState> mLexer;
+  RefPtr<SourceBuffer> mSourceBuffer;
+  SourceBufferIterator mIterator;
+  RefPtr<ExpectNoResume> mExpectNoResume;
+  RefPtr<CountResumes> mCountResumes;
+};
 
+TEST_F(ImageStreamingLexer, ZeroLengthData)
+{
+  // Test a zero-length input.
+  mSourceBuffer->Complete(NS_OK);
+
+  Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
+  EXPECT_TRUE(result.isSome());
+  EXPECT_EQ(Some(TerminalState::FAILURE), result);
+}
+
+TEST_F(ImageStreamingLexer, SingleChunk)
+{
   // Test delivering all the data at once.
-  Maybe<TerminalState> result = lexer.Lex(data, sizeof(data), DoLex);
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
+
+  Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::SUCCESS), result);
 }
 
-TEST(ImageStreamingLexer, SingleChunkWithUnbuffered)
+TEST_F(ImageStreamingLexer, SingleChunkWithUnbuffered)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
   Vector<char> unbufferedVector;
 
   // Test delivering all the data at once.
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
+
   Maybe<TerminalState> result =
-    lexer.Lex(data, sizeof(data),
-              [&](TestState aState, const char* aData, size_t aLength) {
+    mLexer.Lex(mIterator, mExpectNoResume,
+               [&](TestState aState, const char* aData, size_t aLength) {
       return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
   });
+
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::SUCCESS), result);
 }
 
-TEST(ImageStreamingLexer, ChunkPerState)
+TEST_F(ImageStreamingLexer, ChunkPerState)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
-
   // Test delivering in perfectly-sized chunks, one per state.
   for (unsigned i = 0; i < 3; ++i) {
-    Maybe<TerminalState> result = lexer.Lex(data + 3 * i, 3, DoLex);
+    mSourceBuffer->Append(mData + 3 * i, 3);
+    Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex);
 
     if (i == 2) {
       EXPECT_TRUE(result.isSome());
       EXPECT_EQ(Some(TerminalState::SUCCESS), result);
     } else {
       EXPECT_TRUE(result.isNothing());
     }
   }
+
+  EXPECT_EQ(2u, mCountResumes->Count());
+  mSourceBuffer->Complete(NS_OK);
 }
 
-TEST(ImageStreamingLexer, ChunkPerStateWithUnbuffered)
+TEST_F(ImageStreamingLexer, ChunkPerStateWithUnbuffered)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
   Vector<char> unbufferedVector;
 
   // Test delivering in perfectly-sized chunks, one per state.
   for (unsigned i = 0; i < 3; ++i) {
+    mSourceBuffer->Append(mData + 3 * i, 3);
     Maybe<TerminalState> result =
-      lexer.Lex(data + 3 * i, 3,
-                [&](TestState aState, const char* aData, size_t aLength) {
+      mLexer.Lex(mIterator, mCountResumes,
+                 [&](TestState aState, const char* aData, size_t aLength) {
         return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
     });
 
     if (i == 2) {
       EXPECT_TRUE(result.isSome());
       EXPECT_EQ(Some(TerminalState::SUCCESS), result);
     } else {
       EXPECT_TRUE(result.isNothing());
     }
   }
+
+  EXPECT_EQ(2u, mCountResumes->Count());
+  mSourceBuffer->Complete(NS_OK);
 }
 
-TEST(ImageStreamingLexer, OneByteChunks)
+TEST_F(ImageStreamingLexer, OneByteChunks)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
-
   // Test delivering in one byte chunks.
   for (unsigned i = 0; i < 9; ++i) {
-    Maybe<TerminalState> result = lexer.Lex(data + i, 1, DoLex);
+    mSourceBuffer->Append(mData + i, 1);
+    Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex);
 
     if (i == 8) {
       EXPECT_TRUE(result.isSome());
       EXPECT_EQ(Some(TerminalState::SUCCESS), result);
     } else {
       EXPECT_TRUE(result.isNothing());
     }
   }
+
+  EXPECT_EQ(8u, mCountResumes->Count());
+  mSourceBuffer->Complete(NS_OK);
 }
 
-TEST(ImageStreamingLexer, OneByteChunksWithUnbuffered)
+TEST_F(ImageStreamingLexer, OneByteChunksWithUnbuffered)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
   Vector<char> unbufferedVector;
 
   // Test delivering in one byte chunks.
   for (unsigned i = 0; i < 9; ++i) {
+    mSourceBuffer->Append(mData + i, 1);
     Maybe<TerminalState> result =
-      lexer.Lex(data + i, 1,
-                [&](TestState aState, const char* aData, size_t aLength) {
+      mLexer.Lex(mIterator, mCountResumes,
+                 [&](TestState aState, const char* aData, size_t aLength) {
         return DoLexWithUnbuffered(aState, aData, aLength, unbufferedVector);
     });
 
     if (i == 8) {
       EXPECT_TRUE(result.isSome());
       EXPECT_EQ(Some(TerminalState::SUCCESS), result);
     } else {
       EXPECT_TRUE(result.isNothing());
     }
   }
+
+  EXPECT_EQ(8u, mCountResumes->Count());
+  mSourceBuffer->Complete(NS_OK);
+}
+
+TEST_F(ImageStreamingLexer, ZeroLengthState)
+{
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
+
+  // Create a special StreamingLexer for this test because we want the first
+  // state to be zero length.
+  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 0));
+
+  Maybe<TerminalState> result =
+    lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStates);
+
+  EXPECT_TRUE(result.isSome());
+  EXPECT_EQ(Some(TerminalState::SUCCESS), result);
 }
 
-TEST(ImageStreamingLexer, TerminateSuccess)
+TEST_F(ImageStreamingLexer, ZeroLengthStateWithUnbuffered)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
+
+  // Create a special StreamingLexer for this test because we want the first
+  // state to be both zero length and unbuffered.
+  StreamingLexer<TestState> lexer(Transition::ToUnbuffered(TestState::ONE,
+                                                           TestState::UNBUFFERED,
+                                                           0));
+
+  Maybe<TerminalState> result =
+    lexer.Lex(mIterator, mExpectNoResume, DoLexWithZeroLengthStatesUnbuffered);
+
+  EXPECT_TRUE(result.isSome());
+  EXPECT_EQ(Some(TerminalState::SUCCESS), result);
+}
+
+TEST_F(ImageStreamingLexer, TerminateSuccess)
+{
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
 
   // Test that Terminate is "sticky".
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
   Maybe<TerminalState> result =
-    lexer.Lex(data, sizeof(data),
-              [&](TestState aState, const char* aData, size_t aLength) {
+    mLexer.Lex(iterator, mExpectNoResume,
+               [&](TestState aState, const char* aData, size_t aLength) {
       EXPECT_TRUE(aState == TestState::ONE);
       return Transition::TerminateSuccess();
   });
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::SUCCESS), result);
 
+  SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
   result =
-    lexer.Lex(data, sizeof(data),
-              [&](TestState aState, const char* aData, size_t aLength) {
+    mLexer.Lex(iterator2, mExpectNoResume,
+               [&](TestState aState, const char* aData, size_t aLength) {
       EXPECT_TRUE(false);  // Shouldn't get here.
       return Transition::TerminateFailure();
   });
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::SUCCESS), result);
 }
 
-TEST(ImageStreamingLexer, TerminateFailure)
+TEST_F(ImageStreamingLexer, TerminateFailure)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
+  mSourceBuffer->Append(mData, sizeof(mData));
+  mSourceBuffer->Complete(NS_OK);
 
   // Test that Terminate is "sticky".
+  SourceBufferIterator iterator = mSourceBuffer->Iterator();
   Maybe<TerminalState> result =
-    lexer.Lex(data, sizeof(data),
-              [&](TestState aState, const char* aData, size_t aLength) {
+    mLexer.Lex(iterator, mExpectNoResume,
+               [&](TestState aState, const char* aData, size_t aLength) {
       EXPECT_TRUE(aState == TestState::ONE);
       return Transition::TerminateFailure();
   });
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::FAILURE), result);
 
+  SourceBufferIterator iterator2 = mSourceBuffer->Iterator();
   result =
-    lexer.Lex(data, sizeof(data),
-              [&](TestState aState, const char* aData, size_t aLength) {
+    mLexer.Lex(iterator2, mExpectNoResume,
+               [&](TestState aState, const char* aData, size_t aLength) {
       EXPECT_TRUE(false);  // Shouldn't get here.
       return Transition::TerminateFailure();
   });
   EXPECT_TRUE(result.isSome());
   EXPECT_EQ(Some(TerminalState::FAILURE), result);
 }
 
-TEST(ImageStreamingLexer, TerminateUnbuffered)
+TEST_F(ImageStreamingLexer, TerminateUnbuffered)
 {
-  StreamingLexer<TestState> lexer(Transition::To(TestState::ONE, 3));
-  char data[9] = { 1, 2, 3, 1, 2, 3, 1, 2, 3 };
-
   // Test that Terminate works during an unbuffered read.
   for (unsigned i = 0; i < 9; ++i) {
+    mSourceBuffer->Append(mData + i, 1);
     Maybe<TerminalState> result =
-      lexer.Lex(data + i, 1, DoLexWithUnbufferedTerminate);
+      mLexer.Lex(mIterator, mCountResumes, DoLexWithUnbufferedTerminate);
 
     if (i > 2) {
       EXPECT_TRUE(result.isSome());
       EXPECT_EQ(Some(TerminalState::SUCCESS), result);
     } else {
       EXPECT_TRUE(result.isNothing());
     }
   }
+
+  // We expect 3 resumes because TestState::ONE consumes 3 bytes and then
+  // transitions to TestState::UNBUFFERED, which calls TerminateSuccess() as
+  // soon as it receives a single byte. That's four bytes total,  which are
+  // delivered one at a time, requiring 3 resumes.
+  EXPECT_EQ(3u, mCountResumes->Count());
+
+  mSourceBuffer->Complete(NS_OK);
 }
+
+TEST_F(ImageStreamingLexer, SourceBufferImmediateComplete)
+{
+  // Test calling SourceBuffer::Complete() without appending any data. This
+  // causes the SourceBuffer to automatically have a failing completion status,
+  // no matter what you pass, so we expect TerminalState::FAILURE below.
+  mSourceBuffer->Complete(NS_OK);
+
+  Maybe<TerminalState> result = mLexer.Lex(mIterator, mExpectNoResume, DoLex);
+
+  EXPECT_TRUE(result.isSome());
+  EXPECT_EQ(Some(TerminalState::FAILURE), result);
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedSuccess)
+{
+  // Test that calling SourceBuffer::Complete() with a successful status results
+  // in an immediate TerminalState::SUCCESS result.
+  for (unsigned i = 0; i < 9; ++i) {
+    if (i < 2) {
+      mSourceBuffer->Append(mData + i, 1);
+    } else if (i == 2) {
+      mSourceBuffer->Complete(NS_OK);
+    }
+
+    Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex);
+
+    if (i >= 2) {
+      EXPECT_TRUE(result.isSome());
+      EXPECT_EQ(Some(TerminalState::SUCCESS), result);
+    } else {
+      EXPECT_TRUE(result.isNothing());
+    }
+  }
+
+  EXPECT_EQ(2u, mCountResumes->Count());
+}
+
+TEST_F(ImageStreamingLexer, SourceBufferTruncatedFailure)
+{
+  // Test that calling SourceBuffer::Complete() with a failing status results in
+  // an immediate TerminalState::FAILURE result.
+  for (unsigned i = 0; i < 9; ++i) {
+    if (i < 2) {
+      mSourceBuffer->Append(mData + i, 1);
+    } else if (i == 2) {
+      mSourceBuffer->Complete(NS_ERROR_FAILURE);
+    }
+
+    Maybe<TerminalState> result = mLexer.Lex(mIterator, mCountResumes, DoLex);
+
+    if (i >= 2) {
+      EXPECT_TRUE(result.isSome());
+      EXPECT_EQ(Some(TerminalState::FAILURE), result);
+    } else {
+      EXPECT_TRUE(result.isNothing());
+    }
+  }
+
+  EXPECT_EQ(2u, mCountResumes->Count());
+}
+
+TEST_F(ImageStreamingLexer, NoSourceBufferResumable)
+{
+  // Test delivering in one byte chunks with no IResumable.
+  for (unsigned i = 0; i < 9; ++i) {
+    mSourceBuffer->Append(mData + i, 1);
+    Maybe<TerminalState> result = mLexer.Lex(mIterator, nullptr, DoLex);
+
+    if (i == 8) {
+      EXPECT_TRUE(result.isSome());
+      EXPECT_EQ(Some(TerminalState::SUCCESS), result);
+    } else {
+      EXPECT_TRUE(result.isNothing());
+    }
+  }
+
+  mSourceBuffer->Complete(NS_OK);
+}
--- a/image/test/gtest/TestSurfacePipeIntegration.cpp
+++ b/image/test/gtest/TestSurfacePipeIntegration.cpp
@@ -126,23 +126,18 @@ CheckPalettedSurfacePipeMethodResults(Su
   aPipe->ResetToFirstRow();
   EXPECT_FALSE(aPipe->IsSurfaceFinished());
   invalidRect = aPipe->TakeInvalidRect();
   EXPECT_TRUE(invalidRect.isNothing());
 }
 
 class ImageSurfacePipeIntegration : public ::testing::Test
 {
-  protected:
-  static void SetUpTestCase()
-  {
-    // Ensure that ImageLib services are initialized.
-    nsCOMPtr<imgITools> imgTools = do_CreateInstance("@mozilla.org/image/tools;1");
-    EXPECT_TRUE(imgTools != nullptr);
-  }
+protected:
+  AutoInitializeImageLib mInit;
 };
 
 TEST_F(ImageSurfacePipeIntegration, SurfacePipe)
 {
   // Test that SurfacePipe objects can be initialized and move constructed.
   SurfacePipe pipe = TestSurfacePipeFactory::SimpleSurfacePipe();
 
   // Test that SurfacePipe objects can be move assigned.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..228c5c99923bbc940c6178ed08637c86cf38bdc4
GIT binary patch
literal 54
qc${<cb&6nO00Ac;)&OD$Mh1otK$?+3fPon#0hE6MWV`@#AOHZEfCUQx
--- a/image/test/gtest/moz.build
+++ b/image/test/gtest/moz.build
@@ -10,16 +10,17 @@ UNIFIED_SOURCES = [
     'Common.cpp',
     'TestADAM7InterpolatingFilter.cpp',
     'TestCopyOnWrite.cpp',
     'TestDecoders.cpp',
     'TestDecodeToSurface.cpp',
     'TestDeinterlacingFilter.cpp',
     'TestMetadata.cpp',
     'TestRemoveFrameRectFilter.cpp',
+    'TestSourceBuffer.cpp',
     'TestStreamingLexer.cpp',
     'TestSurfaceSink.cpp',
 ]
 
 if CONFIG['MOZ_ENABLE_SKIA']:
     UNIFIED_SOURCES += [
         'TestDownscalingFilter.cpp',
         'TestSurfacePipeIntegration.cpp',
@@ -45,16 +46,17 @@ TEST_HARNESS_FILES.gtest += [
     'first-frame-green.png',
     'first-frame-padding.gif',
     'green.bmp',
     'green.gif',
     'green.ico',
     'green.icon',
     'green.jpg',
     'green.png',
+    'invalid-truncated-metadata.bmp',
     'no-frame-delay.gif',
     'rle4.bmp',
     'rle8.bmp',
     'transparent-ico-with-and-mask.ico',
     'transparent-if-within-ico.bmp',
     'transparent.gif',
     'transparent.png',
 ]
--- a/intl/icu_sources_data.py
+++ b/intl/icu_sources_data.py
@@ -14,21 +14,23 @@ from __future__ import print_function
 
 import glob
 import os
 import shutil
 import subprocess
 import sys
 import tempfile
 
+from mozpack import path as mozpath
+
 
 def find_source_file(dir, filename):
     base = os.path.splitext(filename)[0]
     for ext in ('.cpp', '.c'):
-        f = os.path.join(dir, base + ext)
+        f = mozpath.join(dir, base + ext)
         if os.path.isfile(f):
             return f
     raise Exception("Couldn't find source file for: %s" % filename)
 
 
 def get_sources_from_makefile(makefile):
     import pymake.parser
     from pymake.parserdata import SetVariable
@@ -48,106 +50,109 @@ def write_sources(mozbuild, sources):
                 'DO NOT EDIT\n' +
                 'SOURCES += [\n')
         f.write(''.join("   '/%s',\n" % s for s in sources))
         f.write(']\n')
 
 
 def update_sources(topsrcdir):
     print('Updating ICU sources lists...')
-    sys.path.append(os.path.join(topsrcdir, 'build/pymake'))
+    sys.path.append(mozpath.join(topsrcdir, 'build/pymake'))
     for d in ['common', 'i18n']:
-        makefile = os.path.join(topsrcdir,
+        makefile = mozpath.join(topsrcdir,
                                 'intl/icu/source/%s/Makefile.in' % d)
-        mozbuild = os.path.join(topsrcdir,
+        mozbuild = mozpath.join(topsrcdir,
                                 'config/external/icu/%s/sources.mozbuild' % d)
-        sources = [os.path.relpath(s, topsrcdir)
+        sources = [mozpath.relpath(s, topsrcdir)
                    for s in get_sources_from_makefile(makefile)]
         write_sources(mozbuild, sources)
 
 
 def try_run(name, command, cwd=None, **kwargs):
-    with tempfile.NamedTemporaryFile(prefix=name, delete=False) as f:
-        if subprocess.call(command,
-                           stdout=f,
-                           stderr=subprocess.STDOUT,
-                           cwd=cwd,
-                           **kwargs) == 0:
-            os.unlink(f.name)
-            return True
-    print('''Error running "{}" in directory {}
-See output in {}'''.format(' '.join(command), cwd, f.name),
-          file=sys.stderr)
-    return False
+    try:
+        with tempfile.NamedTemporaryFile(prefix=name, delete=False) as f:
+            subprocess.check_call(command, cwd=cwd, stdout=f,
+                                stderr=subprocess.STDOUT, **kwargs)
+    except subprocess.CalledProcessError:
+        print('''Error running "{}" in directory {}
+    See output in {}'''.format(' '.join(command), cwd, f.name),
+            file=sys.stderr)
+        return False
+    else:
+        os.unlink(f.name)
+        return True
 
 
 def get_data_file(data_dir):
-    files = glob.glob(os.path.join(data_dir, 'icudt*.dat'))
+    files = glob.glob(mozpath.join(data_dir, 'icudt*.dat'))
     return files[0] if files else None
 
 
 def update_data_file(topsrcdir):
     objdir = tempfile.mkdtemp(prefix='icu-obj-')
-    configure = os.path.join(topsrcdir, 'intl/icu/source/configure')
+    configure = mozpath.join(topsrcdir, 'intl/icu/source/configure')
     env = dict(os.environ)
     # bug 1262101 - these should be shared with the moz.build files
     env.update({
         'CPPFLAGS': ('-DU_NO_DEFAULT_INCLUDE_UTF_HEADERS=1 ' +
                      '-DUCONFIG_NO_LEGACY_CONVERSION ' +
                      '-DUCONFIG_NO_TRANSLITERATION ' +
                      '-DUCONFIG_NO_REGULAR_EXPRESSIONS ' +
                      '-DUCONFIG_NO_BREAK_ITERATION ' +
                      '-DU_CHARSET_IS_UTF8')
     })
     print('Running ICU configure...')
     if not try_run(
             'icu-configure',
-            [configure,
+            ['sh', configure,
              '--with-data-packaging=archive',
              '--enable-static',
              '--disable-shared',
              '--disable-extras',
              '--disable-icuio',
              '--disable-layout',
              '--disable-tests',
              '--disable-samples',
              '--disable-strict'],
             cwd=objdir,
             env=env):
         return False
     print('Running ICU make...')
     if not try_run('icu-make', ['make'], cwd=objdir):
         return False
     print('Copying ICU data file...')
-    tree_data_path = os.path.join(topsrcdir,
+    tree_data_path = mozpath.join(topsrcdir,
                                   'config/external/icu/data/')
     old_data_file = get_data_file(tree_data_path)
     if not old_data_file:
         print('Error: no ICU data file in %s' % tree_data_path,
               file=sys.stderr)
         return False
-    new_data_file = get_data_file(os.path.join(objdir, 'data/out'))
+    new_data_file = get_data_file(mozpath.join(objdir, 'data/out'))
     if not new_data_file:
         print('Error: no ICU data in ICU objdir', file=sys.stderr)
         return False
     if os.path.basename(old_data_file) != os.path.basename(new_data_file):
         # Data file name has the major version number embedded.
         os.unlink(old_data_file)
     shutil.copy(new_data_file, tree_data_path)
-    shutil.rmtree(objdir)
+    try:
+        shutil.rmtree(objdir)
+    except:
+        print('Warning: failed to remove %s' % objdir, file=sys.stderr)
     return True
 
 
 def main():
     if len(sys.argv) != 2:
         print('Usage: icu_sources_data.py <mozilla topsrcdir>',
               file=sys.stderr)
         sys.exit(1)
 
-    topsrcdir = os.path.abspath(sys.argv[1])
+    topsrcdir = mozpath.abspath(sys.argv[1])
     update_sources(topsrcdir)
     if not update_data_file(topsrcdir):
         print('Error updating ICU data file', file=sys.stderr)
         sys.exit(1)
 
 
 if __name__ == '__main__':
     main()