Merge inbound to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 25 Mar 2016 22:07:49 -0400
changeset 290473 8a4359ad909fe0cffcfea512770483ffdf8cd4e6
parent 290472 d4ccb3062261c79044eb88f7f63d3afd80740979 (current diff)
parent 290416 9ebccbbb04a65870d49627fa8ca701c494974cef (diff)
child 290474 f3d7946a5cb969f875790b8685092b841985d320
child 290667 3abb7e15e20f7375a6b070ad82101133961e77d7
child 290674 3488451848cee7a57caddc0cc71d2bb8e469a601
push id74255
push userryanvm@gmail.com
push dateSat, 26 Mar 2016 02:10:26 +0000
treeherdermozilla-inbound@f3d7946a5cb9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
8a4359ad909f / 48.0a1 / 20160326030430 / files
nightly linux64
8a4359ad909f / 48.0a1 / 20160326030430 / files
nightly mac
8a4359ad909f / 48.0a1 / 20160326030430 / files
nightly win32
8a4359ad909f / 48.0a1 / 20160326030430 / files
nightly win64
8a4359ad909f / 48.0a1 / 20160326030430 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to m-c. a=merge
js/src/gdb/Makefile.in
security/manager/ssl/tests/unit/test_pkcs11_list.js
testing/taskcluster/tasks/builds/dbg_linux32_clobber.yml
testing/taskcluster/tasks/builds/dbg_linux64_clobber.yml
testing/taskcluster/tasks/builds/dbg_macosx64_clobber.yml
testing/taskcluster/tasks/builds/linux32_clobber.yml
testing/taskcluster/tasks/builds/linux64_clobber.yml
testing/taskcluster/tasks/builds/opt_linux32_clobber.yml
testing/taskcluster/tasks/builds/opt_linux64_clobber.yml
testing/taskcluster/tasks/builds/opt_linux64_st-an_clobber.yml
testing/taskcluster/tasks/builds/opt_macosx64_clobber.yml
testing/taskcluster/tasks/builds/opt_macosx64_st-an_clobber.yml
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -872,18 +872,18 @@ pref("font.size.inflation.disabledInMast
 pref("memory.free_dirty_pages", true);
 
 // Enable the Linux-specific, system-wide memory reporter.
 pref("memory.system_memory_reporter", true);
 
 // Don't dump memory reports on OOM, by default.
 pref("memory.dump_reports_on_oom", false);
 
-pref("layout.imagevisibility.numscrollportwidths", 1);
-pref("layout.imagevisibility.numscrollportheights", 1);
+pref("layout.framevisibility.numscrollportwidths", 1);
+pref("layout.framevisibility.numscrollportheights", 1);
 
 // Enable native identity (persona/browserid)
 pref("dom.identity.enabled", true);
 
 // Wait up to this much milliseconds when orientation changed
 pref("layers.orientation.sync.timeout", 1000);
 
 // Animate the orientation change
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3348,17 +3348,17 @@ const DOMLinkHandler = {
   receiveMessage: function (aMsg) {
     switch (aMsg.name) {
       case "Link:AddFeed":
         let link = {type: aMsg.data.type, href: aMsg.data.href, title: aMsg.data.title};
         FeedHandler.addFeed(link, aMsg.target);
         break;
 
       case "Link:SetIcon":
-        return this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal);
+        this.setIcon(aMsg.target, aMsg.data.url, aMsg.data.loadingPrincipal);
         break;
 
       case "Link:AddSearch":
         this.addSearch(aMsg.target, aMsg.data.engine, aMsg.data.url);
         break;
     }
   },
 
--- a/browser/base/content/test/general/browser_bug408415.js
+++ b/browser/base/content/test/general/browser_bug408415.js
@@ -1,21 +1,45 @@
-function test() {
-  waitForExplicitFinish();
-
+add_task(function* test() {
   let testPath = getRootDirectory(gTestPath);
 
-  let tab = gBrowser.addTab(testPath + "file_with_favicon.html");
+  yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+    function* (tabBrowser) {
+      const URI = testPath + "file_with_favicon.html";
+      const expectedIcon = testPath + "file_generic_favicon.ico";
 
-  tab.linkedBrowser.addEventListener("DOMContentLoaded", function() {
-    tab.linkedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, true);
+      let got_favicon = Promise.defer();
+      let listener = {
+        onLinkIconAvailable(browser, iconURI) {
+          if (got_favicon && iconURI && browser === tabBrowser) {
+            got_favicon.resolve(iconURI);
+            got_favicon = null;
+          }
+        }
+      };
+      gBrowser.addTabsProgressListener(listener);
 
-    let expectedIcon = testPath + "file_generic_favicon.ico";
+      BrowserTestUtils.loadURI(tabBrowser, URI);
+
+      let iconURI = yield got_favicon.promise;
+      is(iconURI, expectedIcon, "Correct icon before pushState.");
 
-    is(gBrowser.getIcon(tab), expectedIcon, "Correct icon before hash change.");
-    tab.linkedBrowser.contentWindow.location.href += "#foo";
-    is(gBrowser.getIcon(tab), expectedIcon, "Correct icon after hash change.");
+      got_favicon = Promise.defer();
+      got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e);
+      yield ContentTask.spawn(tabBrowser, null, function() {
+        content.location.href += "#foo";
+      });
 
-    gBrowser.removeTab(tab);
+      // We've navigated and shouldn't get a call to onLinkIconAvailable.
+      TestUtils.executeSoon(() => {
+        got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser)));
+      });
+      try {
+        yield got_favicon.promise;
+      } catch (e) {
+        iconURI = e;
+      }
+      is(iconURI, expectedIcon, "Correct icon after pushState.");
 
-    finish();
-  }, true);
-}
+      gBrowser.removeTabsProgressListener(listener);
+    });
+});
+
--- a/browser/base/content/test/general/browser_bug550565.js
+++ b/browser/base/content/test/general/browser_bug550565.js
@@ -1,21 +1,44 @@
-function test() {
-  waitForExplicitFinish();
-
+add_task(function* test() {
   let testPath = getRootDirectory(gTestPath);
 
-  let tab = gBrowser.addTab(testPath + "file_with_favicon.html");
+  yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" },
+    function* (tabBrowser) {
+      const URI = testPath + "file_with_favicon.html";
+      const expectedIcon = testPath + "file_generic_favicon.ico";
 
-  tab.linkedBrowser.addEventListener("DOMContentLoaded", function() {
-    tab.linkedBrowser.removeEventListener("DOMContentLoaded", arguments.callee, true);
+      let got_favicon = Promise.defer();
+      let listener = {
+        onLinkIconAvailable(browser, iconURI) {
+          if (got_favicon && iconURI && browser === tabBrowser) {
+            got_favicon.resolve(iconURI);
+            got_favicon = null;
+          }
+        }
+      };
+      gBrowser.addTabsProgressListener(listener);
 
-    let expectedIcon = testPath + "file_generic_favicon.ico";
+      BrowserTestUtils.loadURI(tabBrowser, URI);
+
+      let iconURI = yield got_favicon.promise;
+      is(iconURI, expectedIcon, "Correct icon before pushState.");
 
-    is(gBrowser.getIcon(tab), expectedIcon, "Correct icon before pushState.");
-    tab.linkedBrowser.contentWindow.history.pushState("page2", "page2", "page2");
-    is(gBrowser.getIcon(tab), expectedIcon, "Correct icon after pushState.");
+      got_favicon = Promise.defer();
+      got_favicon.promise.then(() => { ok(false, "shouldn't be called"); }, (e) => e);
+      yield ContentTask.spawn(tabBrowser, null, function() {
+        content.history.pushState("page2", "page2", "page2");
+      });
 
-    gBrowser.removeTab(tab);
+      // We've navigated and shouldn't get a call to onLinkIconAvailable.
+      TestUtils.executeSoon(() => {
+        got_favicon.reject(gBrowser.getIcon(gBrowser.getTabForBrowser(tabBrowser)));
+      });
+      try {
+        yield got_favicon.promise;
+      } catch (e) {
+        iconURI = e;
+      }
+      is(iconURI, expectedIcon, "Correct icon after pushState.");
 
-    finish();
-  }, true);
-}
+      gBrowser.removeTabsProgressListener(listener);
+    });
+});
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -277,20 +277,16 @@
     <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;"
               accesskey="&warnCloseMultipleTabs.accesskey;"
               preference="browser.tabs.warnOnClose"/>
 
     <checkbox id="warnOpenMany" label="&warnOpenManyTabs.label;"
               accesskey="&warnOpenManyTabs.accesskey;"
               preference="browser.tabs.warnOnOpen"/>
 
-    <checkbox id="restoreOnDemand" label="&restoreTabsOnDemand.label;"
-              accesskey="&restoreTabsOnDemand.accesskey;"
-              preference="browser.sessionstore.restore_on_demand"/>
-
     <checkbox id="switchToNewTabs" label="&switchToNewTabs.label;"
               accesskey="&switchToNewTabs.accesskey;"
               preference="browser.tabs.loadInBackground"/>
 
 #ifdef XP_WIN
     <checkbox id="showTabsInTaskbar" label="&showTabsInTaskbar.label;"
               accesskey="&showTabsInTaskbar.accesskey;"
               preference="browser.taskbar.previews.enable"/>
--- a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
@@ -6,17 +6,14 @@
 <!ENTITY newWindowsAsTabs.accesskey   "t">
 
 <!ENTITY warnCloseMultipleTabs.label  "Warn me when closing multiple tabs">
 <!ENTITY warnCloseMultipleTabs.accesskey  "m">
 
 <!ENTITY warnOpenManyTabs.label       "Warn me when opening multiple tabs might slow down &brandShortName;">
 <!ENTITY warnOpenManyTabs.accesskey   "d">
 
-<!ENTITY restoreTabsOnDemand.label        "Don’t load tabs until selected">
-<!ENTITY restoreTabsOnDemand.accesskey    "u">
-
 <!ENTITY switchToNewTabs.label        "When I open a link in a new tab, switch to it immediately">
 <!ENTITY switchToNewTabs.accesskey    "h">
 
 <!ENTITY showTabsInTaskbar.label          "Show tab previews in the Windows taskbar">
 <!ENTITY showTabsInTaskbar.accesskey      "k">
 <!ENTITY tabsGroup.label          "Tabs">
--- a/browser/modules/ContentLinkHandler.jsm
+++ b/browser/modules/ContentLinkHandler.jsm
@@ -66,57 +66,56 @@ this.ContentLinkHandler = {
                                             {type: link.type,
                                              href: link.href,
                                              title: link.title});
               feedAdded = true;
             }
           }
           break;
         case "icon":
-          if (!iconAdded) {
-            if (!Services.prefs.getBoolPref("browser.chrome.site_icons"))
-              break;
+          if (iconAdded || !Services.prefs.getBoolPref("browser.chrome.site_icons"))
+            break;
 
-            var uri = this.getLinkIconURI(link);
-            if (!uri)
-              break;
+          var uri = this.getLinkIconURI(link);
+          if (!uri)
+            break;
 
-            // Telemetry probes for measuring the sizes attribute
-            // usage and available dimensions.
-            let sizeHistogramTypes = Services.telemetry.
-                                     getHistogramById("LINK_ICON_SIZES_ATTR_USAGE");
-            let sizeHistogramDimension = Services.telemetry.
-                                         getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION");
-            let sizesType;
-            if (link.sizes.length) {
-              for (let size of link.sizes) {
-                if (size.toLowerCase() == "any") {
-                  sizesType = SIZES_TELEMETRY_ENUM.ANY;
+          // Telemetry probes for measuring the sizes attribute
+          // usage and available dimensions.
+          let sizeHistogramTypes = Services.telemetry.
+                                   getHistogramById("LINK_ICON_SIZES_ATTR_USAGE");
+          let sizeHistogramDimension = Services.telemetry.
+                                       getHistogramById("LINK_ICON_SIZES_ATTR_DIMENSION");
+          let sizesType;
+          if (link.sizes.length) {
+            for (let size of link.sizes) {
+              if (size.toLowerCase() == "any") {
+                sizesType = SIZES_TELEMETRY_ENUM.ANY;
+                break;
+              } else {
+                let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
+                let values = re.exec(size);
+                if (values && values.length > 1) {
+                  sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
+                  sizeHistogramDimension.add(parseInt(values[1]));
+                } else {
+                  sizesType = SIZES_TELEMETRY_ENUM.INVALID;
                   break;
-                } else {
-                  let re = /^([1-9][0-9]*)x[1-9][0-9]*$/i;
-                  let values = re.exec(size);
-                  if (values && values.length > 1) {
-                    sizesType = SIZES_TELEMETRY_ENUM.DIMENSION;
-                    sizeHistogramDimension.add(parseInt(values[1]));
-                  } else {
-                    sizesType = SIZES_TELEMETRY_ENUM.INVALID;
-                    break;
-                  }
                 }
               }
-            } else {
-              sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
             }
-            sizeHistogramTypes.add(sizesType);
+          } else {
+            sizesType = SIZES_TELEMETRY_ENUM.NO_SIZES;
+          }
+          sizeHistogramTypes.add(sizesType);
 
-            [iconAdded] = chromeGlobal.sendSyncMessage(
-                            "Link:SetIcon",
-                            {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
-          }
+          chromeGlobal.sendAsyncMessage(
+            "Link:SetIcon",
+            {url: uri.spec, loadingPrincipal: link.ownerDocument.nodePrincipal});
+          iconAdded = true;
           break;
         case "search":
           if (!searchAdded && event.type == "DOMLinkAdded") {
             var type = link.type && link.type.toLowerCase();
             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
 
             let re = /^(?:https?|ftp):/i;
             if (type == "application/opensearchdescription+xml" && link.title &&
--- a/build/moz.configure/checks.configure
+++ b/build/moz.configure/checks.configure
@@ -16,26 +16,30 @@
 #       ret = foo
 #       sys.stdout.write(ret + '\n')
 #       return ret
 # This can be combined with e.g. @depends:
 #   @depends(some_option)
 #   @checking('for something')
 #   def check(value):
 #       ...
+# An optional callback can be given, that will be used to format the returned
+# value when displaying it.
 @template
-def checking(what):
+def checking(what, callback=None):
     def decorator(func):
         @advanced
         def wrapped(*args, **kwargs):
             import sys
             print('checking', what, end='... ')
             sys.stdout.flush()
             ret = func(*args, **kwargs)
-            if ret is True:
+            if callback:
+                print(callback(ret))
+            elif ret is True:
                 print('yes')
             elif ret is False:
                 print('no')
             else:
                 print(ret)
             sys.stdout.flush()
             return ret
         return wrapped
@@ -46,40 +50,37 @@ def checking(what):
 #   check('PROG', ('a', 'b'))
 # will look for 'a' or 'b' in $PATH, and set_config PROG to the one
 # it can find. If PROG is already set from the environment or command line,
 # use that value instead.
 @template
 def check_prog(var, progs, allow_missing=False):
     option(env=var, nargs=1, help='Path to the %s program' % var.lower())
 
-    not_found = 'not found'
     if not (isinstance(progs, tuple) or isinstance(progs, list)):
         configure_error('progs should be a list or tuple!')
     progs = list(progs)
 
     @depends(var)
-    @checking('for %s' % var.lower())
+    @checking('for %s' % var.lower(), lambda x: x or 'not found')
     def check(value):
         if value:
             progs[:] = value
         for prog in progs:
             result = find_program(prog)
             if result:
                 return result
-        return not_found
 
     @depends(check, var)
     @advanced
     def postcheck(value, raw_value):
-        if value is not_found and (not allow_missing or raw_value):
+        if value is None and (not allow_missing or raw_value):
             from mozbuild.shellutil import quote
             error('Cannot find %s (tried: %s)'
                   % (var.lower(), ', '.join(quote(p) for p in progs)))
-        return None if value is not_found else value
 
-    @depends(postcheck)
+    @depends(check)
     def normalized_for_config(value):
         return ':' if value is None else value
 
     set_config(var, normalized_for_config)
 
-    return postcheck
+    return check
--- a/build/moz.configure/init.configure
+++ b/build/moz.configure/init.configure
@@ -1,15 +1,16 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include('util.configure')
+include('checks.configure')
 
 option(env='DIST', nargs=1, help='DIST directory')
 
 # Do not allow objdir == srcdir builds.
 # ==============================================================
 @depends('--help', 'DIST')
 def check_build_environment(help, dist):
     topobjdir = os.path.realpath(os.path.abspath('.'))
@@ -459,30 +460,32 @@ def split_triplet(triplet):
 def config_sub(shell, triplet):
     import subprocess
     config_sub = os.path.join(os.path.dirname(__file__), '..',
                               'autoconf', 'config.sub')
     return subprocess.check_output([shell, config_sub, triplet]).strip()
 
 
 @depends('--host', shell)
+@checking('for host system type', lambda h: h.alias)
 @advanced
 def host(value, shell):
     if not value:
         import subprocess
         config_guess = os.path.join(os.path.dirname(__file__), '..',
                                     'autoconf', 'config.guess')
         host = subprocess.check_output([shell, config_guess]).strip()
     else:
         host = value[0]
 
     return split_triplet(config_sub(shell, host))
 
 
 @depends('--target', host, shell)
+@checking('for target system type', lambda t: t.alias)
 def target(value, host, shell):
     if not value:
         return host
     return split_triplet(config_sub(shell, value[0]))
 
 
 @depends(host, target)
 def host_and_target_for_old_configure(host, target):
--- a/dom/base/nsIImageLoadingContent.idl
+++ b/dom/base/nsIImageLoadingContent.idl
@@ -1,22 +1,30 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "imgINotificationObserver.idl"
 
+%{C++
+#include "mozilla/Maybe.h"
+#include "Visibility.h"
+%}
+
 interface imgIRequest;
 interface nsIChannel;
 interface nsIStreamListener;
 interface nsIURI;
 interface nsIDocument;
 interface nsIFrame;
 
+[ref] native MaybeOnNonvisible(const mozilla::Maybe<mozilla::OnNonvisible>);
+native Visibility(mozilla::Visibility);
+
 /**
  * This interface represents a content node that loads images.  The interface
  * exists to allow getting information on the images that the content node
  * loads and to allow registration of observers for the image loads.
  *
  * Implementors of this interface should handle all the mechanics of actually
  * loading an image -- getting the URI, checking with content policies and
  * the security manager to see whether loading the URI is allowed, performing
@@ -27,17 +35,17 @@ interface nsIFrame;
  * the currently loaded image will start a "pending" request which will
  * become current only when the image is loaded.  It is the responsibility of
  * observers to check which request they are getting notifications for.
  *
  * Please make sure to update the MozImageLoadingContent WebIDL
  * interface to mirror this interface when changing it.
  */
 
-[scriptable, builtinclass, uuid(770f7d84-c917-42d7-bf8d-d1b70649e733)]
+[scriptable, builtinclass, uuid(0357123d-9224-4d12-a47e-868c32689777)]
 interface nsIImageLoadingContent : imgINotificationObserver
 {
   /**
    * Request types.  Image loading content nodes attempt to do atomic
    * image changes when the image url is changed.  This means that
    * when the url changes the new image load will start, but the old
    * image will remain the "current" request until the new image is
    * fully loaded.  At that point, the old "current" request will be
@@ -164,23 +172,22 @@ interface nsIImageLoadingContent : imgIN
   /**
    * The intrinsic size and width of this content. May differ from actual image
    * size due to things like responsive image density.
    */
   readonly attribute unsigned long    naturalWidth;
   readonly attribute unsigned long    naturalHeight;
 
   /**
-   * A visible count is stored, if it is non-zero then this image is considered
-   * visible. These methods increment, decrement, or return the visible count.
+   * Called by layout to announce when the frame associated with this content
+   * has changed its visibility state.
    *
-   * @param aNonvisibleAction What to do if the image's visibility count is now
-   *                          zero. If ON_NONVISIBLE_NO_ACTION, nothing will be
-   *                          done. If ON_NONVISIBLE_REQUEST_DISCARD, the image
-   *                          will be asked to discard its surfaces if possible.
+   * @param aNewVisibility    The new visibility state.
+   * @param aNonvisibleAction A requested action if the frame has become
+   *                          nonvisible. If Nothing(), no action is
+   *                          requested. If DISCARD_IMAGES is specified, the
+   *                          frame is requested to ask any images it's
+   *                          associated with to discard their surfaces if
+   *                          possible.
    */
-  [noscript, notxpcom] void IncrementVisibleCount();
-  [noscript, notxpcom] void DecrementVisibleCount(in uint32_t aNonvisibleAction);
-  [noscript, notxpcom] uint32_t GetVisibleCount();
-
-  const long ON_NONVISIBLE_NO_ACTION = 0;
-  const long ON_NONVISIBLE_REQUEST_DISCARD = 1;
+  [noscript, notxpcom] void onVisibilityChange(in Visibility aNewVisibility,
+                                               in MaybeOnNonvisible aNonvisibleAction);
 };
--- a/dom/base/nsINode.cpp
+++ b/dom/base/nsINode.cpp
@@ -721,34 +721,34 @@ nsINode::GetBaseURIObject() const
 }
 
 void
 nsINode::LookupPrefix(const nsAString& aNamespaceURI, nsAString& aPrefix)
 {
   Element *element = GetNameSpaceElement();
   if (element) {
     // XXX Waiting for DOM spec to list error codes.
-  
+
     // Trace up the content parent chain looking for the namespace
     // declaration that defines the aNamespaceURI namespace. Once found,
     // return the prefix (i.e. the attribute localName).
     for (nsIContent* content = element; content;
          content = content->GetParent()) {
       uint32_t attrCount = content->GetAttrCount();
-  
+
       for (uint32_t i = 0; i < attrCount; ++i) {
         const nsAttrName* name = content->GetAttrNameAt(i);
-  
+
         if (name->NamespaceEquals(kNameSpaceID_XMLNS) &&
             content->AttrValueIs(kNameSpaceID_XMLNS, name->LocalName(),
                                  aNamespaceURI, eCaseMatters)) {
           // If the localName is "xmlns", the prefix we output should be
           // null.
           nsIAtom *localName = name->LocalName();
-  
+
           if (localName != nsGkAtoms::xmlns) {
             localName->ToString(aPrefix);
           }
           else {
             SetDOMStringToNull(aPrefix);
           }
           return;
         }
@@ -967,17 +967,23 @@ nsINode::CompareDocumentPosition(nsINode
 
   // We hit the end of one of the parent chains without finding a difference
   // between the chains. That must mean that one node is an ancestor of the
   // other. The one with the shortest chain must be the ancestor.
   return pos1 < pos2 ?
     (nsIDOMNode::DOCUMENT_POSITION_PRECEDING |
      nsIDOMNode::DOCUMENT_POSITION_CONTAINS) :
     (nsIDOMNode::DOCUMENT_POSITION_FOLLOWING |
-     nsIDOMNode::DOCUMENT_POSITION_CONTAINED_BY);    
+     nsIDOMNode::DOCUMENT_POSITION_CONTAINED_BY);
+}
+
+bool
+nsINode::IsSameNode(nsINode *other)
+{
+  return other == this;
 }
 
 bool
 nsINode::IsEqualNode(nsINode* aOther)
 {
   if (!aOther) {
     return false;
   }
@@ -1015,17 +1021,17 @@ nsINode::IsEqualNode(nsINode* aOther)
         for (uint32_t i = 0; i < attrCount; ++i) {
           const nsAttrName* attrName = element1->GetAttrNameAt(i);
 #ifdef DEBUG
           bool hasAttr =
 #endif
           element1->GetAttr(attrName->NamespaceID(), attrName->LocalName(),
                             string1);
           NS_ASSERTION(hasAttr, "Why don't we have an attr?");
-    
+
           if (!element2->AttrValueIs(attrName->NamespaceID(),
                                      attrName->LocalName(),
                                      string1,
                                      eCaseMatters)) {
             return false;
           }
         }
         break;
@@ -1051,36 +1057,36 @@ nsINode::IsEqualNode(nsINode* aOther)
         break;
       case nsIDOMNode::ATTRIBUTE_NODE:
       {
         NS_ASSERTION(node1 == this && node2 == aOther,
                      "Did we come upon an attribute node while walking a "
                      "subtree?");
         node1->GetNodeValue(string1);
         node2->GetNodeValue(string2);
-        
+
         // Returning here as to not bother walking subtree. And there is no
         // risk that we're half way through walking some other subtree since
         // attribute nodes doesn't appear in subtrees.
         return string1.Equals(string2);
       }
       case nsIDOMNode::DOCUMENT_TYPE_NODE:
       {
         nsCOMPtr<nsIDOMDocumentType> docType1 = do_QueryInterface(node1);
         nsCOMPtr<nsIDOMDocumentType> docType2 = do_QueryInterface(node2);
-    
+
         NS_ASSERTION(docType1 && docType2, "Why don't we have a document type node?");
 
         // Public ID
         docType1->GetPublicId(string1);
         docType2->GetPublicId(string2);
         if (!string1.Equals(string2)) {
           return false;
         }
-    
+
         // System ID
         docType1->GetSystemId(string1);
         docType2->GetSystemId(string2);
         if (!string1.Equals(string2)) {
           return false;
         }
 
         break;
@@ -1114,17 +1120,17 @@ nsINode::IsEqualNode(nsINode* aOther)
           node2 = node2->GetNextSibling();
           break;
         }
 
         if (node2->GetNextSibling()) {
           // node2 has a nextSibling, but node1 doesn't
           return false;
         }
-        
+
         node1 = node1->GetParentNode();
         node2 = node2->GetParentNode();
         NS_ASSERTION(node1 && node2, "no parent while walking subtree");
       }
     }
   } while(node2);
 
   return false;
@@ -1790,17 +1796,17 @@ bool IsAllowedAsChild(nsIContent* aNewCh
         return true;
       }
 
       int32_t doctypeIndex = aParent->IndexOf(docTypeContent);
       int32_t insertIndex = aParent->IndexOf(aRefChild);
 
       // Now we're OK in the following two cases only:
       // 1) We're replacing something that's not before the doctype
-      // 2) We're inserting before something that comes after the doctype 
+      // 2) We're inserting before something that comes after the doctype
       return aIsReplace ? (insertIndex >= doctypeIndex) :
         insertIndex > doctypeIndex;
     }
   case nsIDOMNode::DOCUMENT_TYPE_NODE :
     {
       if (!aParent->IsNodeOfType(nsINode::eDOCUMENT)) {
         // doctypes only allowed under documents
         return false;
@@ -2031,17 +2037,17 @@ nsINode::ReplaceOrInsertBefore(bool aRep
         mb.SetPrevSibling(oldParent->GetChildAt(removeIndex - 1));
         mb.SetNextSibling(oldParent->GetChildAt(removeIndex));
       }
     }
 
     // We expect one mutation (the removal) to have happened.
     if (guard.Mutated(1)) {
       // XBL destructors, yuck.
-      
+
       // Verify that nodeToInsertBefore, if non-null, is still our child.  If
       // it's not, there's no way we can do this insert sanely; just bail out.
       if (nodeToInsertBefore && nodeToInsertBefore->GetParent() != this) {
         aError.Throw(NS_ERROR_DOM_HIERARCHY_REQUEST_ERR);
         return nullptr;
       }
 
       // Verify that newContent has no parent.
@@ -2107,17 +2113,17 @@ nsINode::ReplaceOrInsertBefore(bool aRep
       for (uint32_t i = count; i > 0;) {
         newContent->RemoveChildAt(--i, true);
       }
     }
 
     // We expect |count| removals
     if (guard.Mutated(count)) {
       // XBL destructors, yuck.
-      
+
       // Verify that nodeToInsertBefore, if non-null, is still our child.  If
       // it's not, there's no way we can do this insert sanely; just bail out.
       if (nodeToInsertBefore && nodeToInsertBefore->GetParent() != this) {
         aError.Throw(NS_ERROR_DOM_HIERARCHY_REQUEST_ERR);
         return nullptr;
       }
 
       // Verify that all the things in fragChildren have no parent.
@@ -2139,17 +2145,17 @@ nsINode::ReplaceOrInsertBefore(bool aRep
         return nullptr;
       }
 
       // Recompute nodeToInsertBefore, just in case.
       if (aReplace) {
         nodeToInsertBefore = aRefChild->GetNextSibling();
       } else {
         nodeToInsertBefore = aRefChild;
-      }      
+      }
 
       // And verify that newContent is still allowed as our child.  Sadly, we
       // need to reimplement the relevant part of IsAllowedAsChild() because
       // now our nodes are in an array and all.  If you change this code,
       // change the code there.
       if (IsNodeOfType(nsINode::eDOCUMENT)) {
         bool sawElement = false;
         for (uint32_t i = 0; i < count; ++i) {
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -720,17 +720,17 @@ public:
    * child of |this|.
    *
    * @throws NS_ERROR_OUT_OF_MEMORY in some cases (from BindToTree).
    */
   nsresult AppendChildTo(nsIContent* aKid, bool aNotify)
   {
     return InsertChildAt(aKid, GetChildCount(), aNotify);
   }
-  
+
   /**
    * Remove a child from this node.  This method handles calling UnbindFromTree
    * on the child appropriately.
    *
    * @param aIndex the index of the child to remove
    * @param aNotify whether to notify the document (current document for
    *        nsIContent, and |this| for nsIDocument) that the remove has
    *        occurred
@@ -883,17 +883,17 @@ public:
    *                       is not set.
    * @return               the property. Null if the property is not set
    *                       (though a null return value does not imply the
    *                       property was not set, i.e. it can be set to null).
    */
   virtual void* UnsetProperty(uint16_t aCategory,
                               nsIAtom *aPropertyName,
                               nsresult *aStatus = nullptr);
-  
+
   bool HasProperties() const
   {
     return HasFlag(NODE_HAS_PROPERTIES);
   }
 
   /**
    * Return the principal of this node.  This is guaranteed to never be a null
    * pointer.
@@ -915,17 +915,17 @@ public:
    * Get the parent nsINode for this node. This can be either an nsIContent,
    * an nsIDocument or an nsIAttribute.
    * @return the parent node
    */
   nsINode* GetParentNode() const
   {
     return mParent;
   }
-  
+
   /**
    * Get the parent nsINode for this node if it is an Element.
    * @return the parent node
    */
   mozilla::dom::Element* GetParentElement() const
   {
     return mParent && mParent->IsElement() ? mParent->AsElement() : nullptr;
   }
@@ -1250,17 +1250,17 @@ public:
    */
 protected:
   nsIURI* GetExplicitBaseURI() const {
     if (HasExplicitBaseURI()) {
       return static_cast<nsIURI*>(GetProperty(nsGkAtoms::baseURIProperty));
     }
     return nullptr;
   }
-  
+
 public:
   void GetTextContent(nsAString& aTextContent,
                       mozilla::ErrorResult& aError)
   {
     GetTextContentInternal(aTextContent, aError);
   }
   void SetTextContent(const nsAString& aTextContent,
                       mozilla::ErrorResult& aError)
@@ -1305,17 +1305,17 @@ public:
    * @return aResult the previously registered object for aKey on this node, if
    *                 any
    */
   nsIVariant* GetUserData(const nsAString& aKey);
 
   nsresult GetUserData(const nsAString& aKey, nsIVariant** aResult)
   {
     NS_IF_ADDREF(*aResult = GetUserData(aKey));
-  
+
     return NS_OK;
   }
 
   void LookupPrefix(const nsAString& aNamespace, nsAString& aResult);
   bool IsDefaultNamespace(const nsAString& aNamespaceURI)
   {
     nsAutoString defaultNamespace;
     LookupNamespaceURI(EmptyString(), defaultNamespace);
@@ -1466,17 +1466,17 @@ private:
     // that is in a Selection.
     NodeIsCommonAncestorForRangeInSelection,
     // Set if the node is a descendant of a node with the above bit set.
     NodeIsDescendantOfCommonAncestorForRangeInSelection,
     // Set if CanSkipInCC check has been done for this subtree root.
     NodeIsCCMarkedRoot,
     // Maybe set if this node is in black subtree.
     NodeIsCCBlackTree,
-    // Maybe set if the node is a root of a subtree 
+    // Maybe set if the node is a root of a subtree
     // which needs to be kept in the purple buffer.
     NodeIsPurpleRoot,
     // Set if the node has an explicit base URI stored
     NodeHasExplicitBaseURI,
     // Set if the element has some style states locked
     ElementHasLockedStyleStates,
     // Set if element has pointer locked
     ElementHasPointerLock,
@@ -1768,16 +1768,17 @@ public:
   }
   nsINode* ReplaceChild(nsINode& aNode, nsINode& aChild,
                         mozilla::ErrorResult& aError)
   {
     return ReplaceOrInsertBefore(true, &aNode, &aChild, aError);
   }
   nsINode* RemoveChild(nsINode& aChild, mozilla::ErrorResult& aError);
   already_AddRefed<nsINode> CloneNode(bool aDeep, mozilla::ErrorResult& aError);
+  bool IsSameNode(nsINode* aNode);
   bool IsEqualNode(nsINode* aNode);
   void GetNamespaceURI(nsAString& aNamespaceURI) const
   {
     mNodeInfo->GetNamespaceURI(aNamespaceURI);
   }
 #ifdef MOZILLA_INTERNAL_API
   void GetPrefix(nsAString& aPrefix)
   {
--- a/dom/base/nsImageLoadingContent.cpp
+++ b/dom/base/nsImageLoadingContent.cpp
@@ -89,34 +89,33 @@ nsImageLoadingContent::nsImageLoadingCon
     // mBroken starts out true, since an image without a URI is broken....
     mBroken(true),
     mUserDisabled(false),
     mSuppressed(false),
     mNewRequestsWillNeedAnimationReset(false),
     mStateChangerDepth(0),
     mCurrentRequestRegistered(false),
     mPendingRequestRegistered(false),
-    mFrameCreateCalled(false),
-    mVisibleCount(0)
+    mFrameCreateCalled(false)
 {
   if (!nsContentUtils::GetImgLoaderForChannel(nullptr, nullptr)) {
     mLoadingEnabled = false;
   }
 
   bool isInconsistent;
   mMostRecentRequestChange = TimeStamp::ProcessCreation(isInconsistent);
 }
 
 void
 nsImageLoadingContent::DestroyImageLoadingContent()
 {
   // Cancel our requests so they won't hold stale refs to us
   // NB: Don't ask to discard the images here.
-  ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_NO_ACTION);
-  ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_NO_ACTION);
+  ClearCurrentRequest(NS_BINDING_ABORTED);
+  ClearPendingRequest(NS_BINDING_ABORTED);
 }
 
 nsImageLoadingContent::~nsImageLoadingContent()
 {
   NS_ASSERTION(!mCurrentRequest && !mPendingRequest,
                "DestroyImageLoadingContent not called");
   NS_ASSERTION(!mObserverList.mObserver && !mObserverList.mNext,
                "Observers still registered?");
@@ -259,39 +258,46 @@ ImageIsAnimated(imgIRequest* aRequest)
   }
 
   return false;
 }
 
 void
 nsImageLoadingContent::OnUnlockedDraw()
 {
-  if (mVisibleCount > 0) {
-    // We should already be marked as visible, there is nothing more we can do.
-    return;
-  }
-
-  // It's OK for non-animated images to wait until the next image visibility
+  // It's OK for non-animated images to wait until the next frame visibility
   // update to become locked. (And that's preferable, since in the case of
   // scrolling it keeps memory usage minimal.) For animated images, though, we
   // want to mark them visible right away so we can call
   // IncrementAnimationConsumers() on them and they'll start animating.
   if (!ImageIsAnimated(mCurrentRequest) && !ImageIsAnimated(mPendingRequest)) {
     return;
   }
 
-  nsPresContext* presContext = GetFramePresContext();
-  if (!presContext)
+  nsIFrame* frame = GetOurPrimaryFrame();
+  if (!frame) {
     return;
+  }
+
+  if (frame->GetVisibility() == Visibility::APPROXIMATELY_VISIBLE) {
+    // This frame is already marked visible; there's nothing to do.
+    return;
+  }
+
+  nsPresContext* presContext = frame->PresContext();
+  if (!presContext) {
+    return;
+  }
 
   nsIPresShell* presShell = presContext->PresShell();
-  if (!presShell)
+  if (!presShell) {
     return;
+  }
 
-  presShell->EnsureImageInVisibleList(this);
+  presShell->EnsureFrameInApproximatelyVisibleList(frame);
 }
 
 nsresult
 nsImageLoadingContent::OnImageIsAnimated(imgIRequest *aRequest)
 {
   bool* requestFlag = GetRegisteredFlagForRequest(aRequest);
   if (requestFlag) {
     nsLayoutUtils::RegisterImageRequest(GetFramePresContext(),
@@ -465,21 +471,16 @@ nsImageLoadingContent::CurrentRequestHas
 
 NS_IMETHODIMP_(void)
 nsImageLoadingContent::FrameCreated(nsIFrame* aFrame)
 {
   NS_ASSERTION(aFrame, "aFrame is null");
 
   mFrameCreateCalled = true;
 
-  if (aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP)) {
-    // Assume all images in popups are visible.
-    IncrementVisibleCount();
-  }
-
   TrackImage(mCurrentRequest);
   TrackImage(mPendingRequest);
 
   // We need to make sure that our image request is registered, if it should
   // be registered.
   nsPresContext* presContext = aFrame->PresContext();
   if (mCurrentRequest) {
     nsLayoutUtils::RegisterImageRequestIfAnimated(presContext, mCurrentRequest,
@@ -513,23 +514,17 @@ nsImageLoadingContent::FrameDestroyed(ns
                                           &mPendingRequestRegistered);
   }
 
   UntrackImage(mCurrentRequest);
   UntrackImage(mPendingRequest);
 
   nsIPresShell* presShell = presContext ? presContext->GetPresShell() : nullptr;
   if (presShell) {
-    presShell->RemoveImageFromVisibleList(this);
-  }
-
-  if (aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP)) {
-    // We assume all images in popups are visible, so this decrement balances
-    // out the increment in FrameCreated above.
-    DecrementVisibleCount(ON_NONVISIBLE_NO_ACTION);
+    presShell->RemoveFrameFromApproximatelyVisibleList(aFrame);
   }
 }
 
 /* static */
 nsContentPolicyType
 nsImageLoadingContent::PolicyTypeForLoad(ImageLoadType aImageLoadType)
 {
   if (aImageLoadType == eImageLoadType_Imageset) {
@@ -733,44 +728,16 @@ nsImageLoadingContent::UnblockOnload(img
   nsIDocument* doc = GetOurCurrentDoc();
   if (doc) {
     doc->UnblockOnload(false);
   }
 
   return NS_OK;
 }
 
-void
-nsImageLoadingContent::IncrementVisibleCount()
-{
-  mVisibleCount++;
-  if (mVisibleCount == 1) {
-    TrackImage(mCurrentRequest);
-    TrackImage(mPendingRequest);
-  }
-}
-
-void
-nsImageLoadingContent::DecrementVisibleCount(uint32_t aNonvisibleAction)
-{
-  NS_ASSERTION(mVisibleCount > 0, "visible count should be positive here");
-  mVisibleCount--;
-
-  if (mVisibleCount == 0) {
-    UntrackImage(mCurrentRequest, aNonvisibleAction);
-    UntrackImage(mPendingRequest, aNonvisibleAction);
-  }
-}
-
-uint32_t
-nsImageLoadingContent::GetVisibleCount()
-{
-  return mVisibleCount;
-}
-
 /*
  * Non-interface methods
  */
 
 nsresult
 nsImageLoadingContent::LoadImage(const nsAString& aNewURI,
                                  bool aForce,
                                  bool aNotify,
@@ -1071,31 +1038,31 @@ nsImageLoadingContent::UpdateImageState(
   NS_ASSERTION(thisContent->IsElement(), "Not an element?");
   thisContent->AsElement()->UpdateState(aNotify);
 }
 
 void
 nsImageLoadingContent::CancelImageRequests(bool aNotify)
 {
   AutoStateChanger changer(this, aNotify);
-  ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD);
-  ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD);
+  ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
+  ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
 }
 
 nsresult
 nsImageLoadingContent::UseAsPrimaryRequest(imgRequestProxy* aRequest,
                                            bool aNotify,
                                            ImageLoadType aImageLoadType)
 {
   // Our state will change. Watch it.
   AutoStateChanger changer(this, aNotify);
 
   // Get rid if our existing images
-  ClearPendingRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD);
-  ClearCurrentRequest(NS_BINDING_ABORTED, ON_NONVISIBLE_REQUEST_DISCARD);
+  ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
+  ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DISCARD_IMAGES));
 
   // Clone the request we were given.
   RefPtr<imgRequestProxy>& req = PrepareNextRequest(aImageLoadType);
   nsresult rv = aRequest->Clone(this, getter_AddRefs(req));
   if (NS_SUCCEEDED(rv)) {
     TrackImage(req);
   } else {
     MOZ_ASSERT(!req, "Shouldn't have non-null request here");
@@ -1223,25 +1190,28 @@ nsImageLoadingContent::SetBlockedRequest
   MOZ_ASSERT(!NS_CP_ACCEPTED(aContentDecision), "Blocked but not?");
 
   // We do some slightly illogical stuff here to maintain consistency with
   // old behavior that people probably depend on. Even in the case where the
   // new image is blocked, the old one should really be canceled with the
   // reason "image source changed". However, apparently there's some abuse
   // over in nsImageFrame where the displaying of the "broken" icon for the
   // next image depends on the cancel reason of the previous image. ugh.
-  ClearPendingRequest(NS_ERROR_IMAGE_BLOCKED, ON_NONVISIBLE_REQUEST_DISCARD);
+  // XXX(seth): So shouldn't we fix nsImageFrame?!
+  ClearPendingRequest(NS_ERROR_IMAGE_BLOCKED,
+                      Some(OnNonvisible::DISCARD_IMAGES));
 
   // For the blocked case, we only want to cancel the existing current request
   // if size is not available. bz says the web depends on this behavior.
   if (!HaveSize(mCurrentRequest)) {
 
     mImageBlockingStatus = aContentDecision;
     uint32_t keepFlags = mCurrentRequestFlags & REQUEST_IS_IMAGESET;
-    ClearCurrentRequest(NS_ERROR_IMAGE_BLOCKED, ON_NONVISIBLE_REQUEST_DISCARD);
+    ClearCurrentRequest(NS_ERROR_IMAGE_BLOCKED,
+                        Some(OnNonvisible::DISCARD_IMAGES));
 
     // We still want to remember what URI we were and if it was an imageset,
     // despite not having an actual request. These are both cleared as part of
     // ClearCurrentRequest() before a new request is started.
     mCurrentURI = aURI;
     mCurrentRequestFlags = keepFlags;
   }
 }
@@ -1250,17 +1220,17 @@ RefPtr<imgRequestProxy>&
 nsImageLoadingContent::PrepareCurrentRequest(ImageLoadType aImageLoadType)
 {
   // Blocked images go through SetBlockedRequest, which is a separate path. For
   // everything else, we're unblocked.
   mImageBlockingStatus = nsIContentPolicy::ACCEPT;
 
   // Get rid of anything that was there previously.
   ClearCurrentRequest(NS_ERROR_IMAGE_SRC_CHANGED,
-                      ON_NONVISIBLE_REQUEST_DISCARD);
+                      Some(OnNonvisible::DISCARD_IMAGES));
 
   if (mNewRequestsWillNeedAnimationReset) {
     mCurrentRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET;
   }
 
   if (aImageLoadType == eImageLoadType_Imageset) {
     mCurrentRequestFlags |= REQUEST_IS_IMAGESET;
   }
@@ -1269,17 +1239,17 @@ nsImageLoadingContent::PrepareCurrentReq
   return mCurrentRequest;
 }
 
 RefPtr<imgRequestProxy>&
 nsImageLoadingContent::PreparePendingRequest(ImageLoadType aImageLoadType)
 {
   // Get rid of anything that was there previously.
   ClearPendingRequest(NS_ERROR_IMAGE_SRC_CHANGED,
-                      ON_NONVISIBLE_REQUEST_DISCARD);
+                      Some(OnNonvisible::DISCARD_IMAGES));
 
   if (mNewRequestsWillNeedAnimationReset) {
     mPendingRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET;
   }
 
   if (aImageLoadType == eImageLoadType_Imageset) {
     mPendingRequestFlags |= REQUEST_IS_IMAGESET;
   }
@@ -1334,17 +1304,17 @@ nsImageLoadingContent::MakePendingReques
   mPendingRequest = nullptr;
   mCurrentRequestFlags = mPendingRequestFlags;
   mPendingRequestFlags = 0;
   ResetAnimationIfNeeded();
 }
 
 void
 nsImageLoadingContent::ClearCurrentRequest(nsresult aReason,
-                                           uint32_t aNonvisibleAction)
+                                           const Maybe<OnNonvisible>& aNonvisibleAction)
 {
   if (!mCurrentRequest) {
     // Even if we didn't have a current request, we might have been keeping
     // a URI and flags as a placeholder for a failed load. Clear that now.
     mCurrentURI = nullptr;
     mCurrentRequestFlags = 0;
     return;
   }
@@ -1360,17 +1330,17 @@ nsImageLoadingContent::ClearCurrentReque
   UntrackImage(mCurrentRequest, aNonvisibleAction);
   mCurrentRequest->CancelAndForgetObserver(aReason);
   mCurrentRequest = nullptr;
   mCurrentRequestFlags = 0;
 }
 
 void
 nsImageLoadingContent::ClearPendingRequest(nsresult aReason,
-                                           uint32_t aNonvisibleAction)
+                                           const Maybe<OnNonvisible>& aNonvisibleAction)
 {
   if (!mPendingRequest)
     return;
 
   // Deregister this image from the refresh driver so it no longer receives
   // notifications.
   nsLayoutUtils::DeregisterImageRequest(GetFramePresContext(), mPendingRequest,
                                         &mPendingRequestRegistered);
@@ -1447,83 +1417,106 @@ nsImageLoadingContent::UnbindFromTree(bo
   UntrackImage(mCurrentRequest);
   UntrackImage(mPendingRequest);
 
   if (mCurrentRequestFlags & REQUEST_BLOCKS_ONLOAD)
     doc->UnblockOnload(false);
 }
 
 void
+nsImageLoadingContent::OnVisibilityChange(Visibility aNewVisibility,
+                                          const Maybe<OnNonvisible>& aNonvisibleAction)
+{
+  switch (aNewVisibility) {
+    case Visibility::APPROXIMATELY_VISIBLE:
+      TrackImage(mCurrentRequest);
+      TrackImage(mPendingRequest);
+      break;
+
+    case Visibility::APPROXIMATELY_NONVISIBLE:
+      UntrackImage(mCurrentRequest, aNonvisibleAction);
+      UntrackImage(mPendingRequest, aNonvisibleAction);
+      break;
+
+    case Visibility::UNTRACKED:
+      MOZ_ASSERT_UNREACHABLE("Shouldn't notify for untracked visibility");
+      break;
+  }
+}
+
+void
 nsImageLoadingContent::TrackImage(imgIRequest* aImage)
 {
   if (!aImage)
     return;
 
   MOZ_ASSERT(aImage == mCurrentRequest || aImage == mPendingRequest,
              "Why haven't we heard of this request?");
 
   nsIDocument* doc = GetOurCurrentDoc();
-  if (doc && (mFrameCreateCalled || GetOurPrimaryFrame()) &&
-      (mVisibleCount > 0)) {
+  if (!doc) {
+    return;
+  }
 
-    if (mVisibleCount == 1) {
-      // Since we're becoming visible, request a decode.
-      nsImageFrame* f = do_QueryFrame(GetOurPrimaryFrame());
-      if (f) {
-        f->MaybeDecodeForPredictedSize();
-      }
-    }
+  // We only want to track this request if we're visible. Ordinarily we check
+  // the visible count, but that requires a frame; in cases where
+  // GetOurPrimaryFrame() cannot obtain a frame (e.g. <feImage>), we assume
+  // we're visible if FrameCreated() was called.
+  nsIFrame* frame = GetOurPrimaryFrame();
+  if ((frame && frame->GetVisibility() == Visibility::APPROXIMATELY_NONVISIBLE) ||
+      (!frame && !mFrameCreateCalled)) {
+    return;
+  }
 
-    if (aImage == mCurrentRequest && !(mCurrentRequestFlags & REQUEST_IS_TRACKED)) {
-      mCurrentRequestFlags |= REQUEST_IS_TRACKED;
-      doc->AddImage(mCurrentRequest);
-    }
-    if (aImage == mPendingRequest && !(mPendingRequestFlags & REQUEST_IS_TRACKED)) {
-      mPendingRequestFlags |= REQUEST_IS_TRACKED;
-      doc->AddImage(mPendingRequest);
-    }
+  if (aImage == mCurrentRequest && !(mCurrentRequestFlags & REQUEST_IS_TRACKED)) {
+    mCurrentRequestFlags |= REQUEST_IS_TRACKED;
+    doc->AddImage(mCurrentRequest);
+  }
+  if (aImage == mPendingRequest && !(mPendingRequestFlags & REQUEST_IS_TRACKED)) {
+    mPendingRequestFlags |= REQUEST_IS_TRACKED;
+    doc->AddImage(mPendingRequest);
   }
 }
 
 void
 nsImageLoadingContent::UntrackImage(imgIRequest* aImage,
-                                    uint32_t aNonvisibleAction
-                                      /* = ON_NONVISIBLE_NO_ACTION */)
+                                    const Maybe<OnNonvisible>& aNonvisibleAction
+                                      /* = Nothing() */)
 {
   if (!aImage)
     return;
 
   MOZ_ASSERT(aImage == mCurrentRequest || aImage == mPendingRequest,
              "Why haven't we heard of this request?");
 
   // We may not be in the document.  If we outlived our document that's fine,
   // because the document empties out the tracker and unlocks all locked images
   // on destruction.  But if we were never in the document we may need to force
   // discarding the image here, since this is the only chance we have.
   nsIDocument* doc = GetOurCurrentDoc();
   if (aImage == mCurrentRequest) {
     if (doc && (mCurrentRequestFlags & REQUEST_IS_TRACKED)) {
       mCurrentRequestFlags &= ~REQUEST_IS_TRACKED;
       doc->RemoveImage(mCurrentRequest,
-                       (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD)
+                       aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)
                          ? nsIDocument::REQUEST_DISCARD
                          : 0);
-    } else if (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) {
+    } else if (aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)) {
       // If we're not in the document we may still need to be discarded.
       aImage->RequestDiscard();
     }
   }
   if (aImage == mPendingRequest) {
     if (doc && (mPendingRequestFlags & REQUEST_IS_TRACKED)) {
       mPendingRequestFlags &= ~REQUEST_IS_TRACKED;
       doc->RemoveImage(mPendingRequest,
-                       (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD)
+                       aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)
                          ? nsIDocument::REQUEST_DISCARD
                          : 0);
-    } else if (aNonvisibleAction == ON_NONVISIBLE_REQUEST_DISCARD) {
+    } else if (aNonvisibleAction == Some(OnNonvisible::DISCARD_IMAGES)) {
       // If we're not in the document we may still need to be discarded.
       aImage->RequestDiscard();
     }
   }
 }
 
 
 void
--- a/dom/base/nsImageLoadingContent.h
+++ b/dom/base/nsImageLoadingContent.h
@@ -36,16 +36,21 @@ class imgRequestProxy;
 #ifdef LoadImage
 // Undefine LoadImage to prevent naming conflict with Windows.
 #undef LoadImage
 #endif
 
 class nsImageLoadingContent : public nsIImageLoadingContent,
                               public imgIOnloadBlocker
 {
+  template <typename T> using Maybe = mozilla::Maybe<T>;
+  using Nothing = mozilla::Nothing;
+  using OnNonvisible = mozilla::OnNonvisible;
+  using Visibility = mozilla::Visibility;
+
   /* METHODS */
 public:
   nsImageLoadingContent();
   virtual ~nsImageLoadingContent();
 
   NS_DECL_IMGINOTIFICATIONOBSERVER
   NS_DECL_NSIIMAGELOADINGCONTENT
   NS_DECL_IMGIONLOADBLOCKER
@@ -313,18 +318,20 @@ protected:
   void MakePendingRequestCurrent();
 
   /**
    * Cancels and nulls-out the "current" and "pending" requests if they exist.
    * 
    * @param aNonvisibleAction An action to take if the image is no longer
    *                          visible as a result; see |UntrackImage|.
    */
-  void ClearCurrentRequest(nsresult aReason, uint32_t aNonvisibleAction);
-  void ClearPendingRequest(nsresult aReason, uint32_t aNonvisibleAction);
+  void ClearCurrentRequest(nsresult aReason,
+                           const Maybe<OnNonvisible>& aNonvisibleAction = Nothing());
+  void ClearPendingRequest(nsresult aReason,
+                           const Maybe<OnNonvisible>& aNonvisibleAction = Nothing());
 
   /**
    * Retrieve a pointer to the 'registered with the refresh driver' flag for
    * which a particular image request corresponds.
    *
    * @returns A pointer to the boolean flag for a given image request, or
    *          |nullptr| if the request is not either |mPendingRequest| or
    *          |mCurrentRequest|.
@@ -343,24 +350,26 @@ protected:
    */
   static bool HaveSize(imgIRequest *aImage);
 
   /**
    * Adds/Removes a given imgIRequest from our document's tracker.
    *
    * No-op if aImage is null.
    *
-   * @param aNonvisibleAction What to do if the image's visibility count is now
-   *                          zero. If ON_NONVISIBLE_NO_ACTION, nothing will be
-   *                          done. If ON_NONVISIBLE_REQUEST_DISCARD, the image
-   *                          will be asked to discard its surfaces if possible.
+   * @param aNonvisibleAction A requested action if the frame has become
+   *                          nonvisible. If Nothing(), no action is
+   *                          requested. If DISCARD_IMAGES is specified, the
+   *                          frame is requested to ask any images it's
+   *                          associated with to discard their surfaces if
+   *                          possible.
    */
   void TrackImage(imgIRequest* aImage);
   void UntrackImage(imgIRequest* aImage,
-                    uint32_t aNonvisibleAction = ON_NONVISIBLE_NO_ACTION);
+                    const Maybe<OnNonvisible>& aNonvisibleAction = Nothing());
 
   /* MEMBERS */
   RefPtr<imgRequestProxy> mCurrentRequest;
   RefPtr<imgRequestProxy> mPendingRequest;
   uint32_t mCurrentRequestFlags;
   uint32_t mPendingRequestFlags;
 
   enum {
@@ -434,13 +443,11 @@ private:
 
   // Flags to indicate whether each of the current and pending requests are
   // registered with the refresh driver.
   bool mCurrentRequestRegistered;
   bool mPendingRequestRegistered;
 
   // True when FrameCreate has been called but FrameDestroy has not.
   bool mFrameCreateCalled;
-
-  uint32_t mVisibleCount;
 };
 
 #endif // nsImageLoadingContent_h__
--- a/dom/bindings/CallbackObject.cpp
+++ b/dom/bindings/CallbackObject.cpp
@@ -260,41 +260,23 @@ CallbackObject::CallSetup::~CallSetup()
           needToDealWithException = false;
         }
       }
     }
 
     if (needToDealWithException) {
       // Either we're supposed to report our exceptions, or we're supposed to
       // re-throw them but we failed to get the exception value.  Either way,
-      // just report the pending exception, if any.
-      //
-      // XXXbz FIXME: bug 979525 means we don't always JS_SaveFrameChain here,
-      // so we need to go ahead and do that.  This is also the reason we don't
-      // just rely on ~AutoJSAPI reporting the exception for us.  I think if we
-      // didn't need to JS_SaveFrameChain here, we could just rely on that.
-      JS::Rooted<JSObject*> oldGlobal(mCx, JS::CurrentGlobalOrNull(mCx));
-      MOZ_ASSERT(oldGlobal, "How can we not have a global here??");
-      bool saved = JS_SaveFrameChain(mCx);
-      // Make sure the JSAutoCompartment goes out of scope before the
-      // JS_RestoreFrameChain call!
-      {
-        JSAutoCompartment ac(mCx, oldGlobal);
-        MOZ_ASSERT(!JS::DescribeScriptedCaller(mCx),
-                   "Our comment above about JS_SaveFrameChain having been "
-                   "called is a lie?");
-        mAutoEntryScript->ReportException();
-      }
-      if (saved) {
-        JS_RestoreFrameChain(mCx);
-      }
-
+      // we'll just report the pending exception, if any, once ~mAutoEntryScript
+      // runs.  Note that we've already run ~mAc, effectively, so we don't have
+      // to worry about ordering here.
       if (mErrorResult.IsJSContextException()) {
         // XXXkhuey bug 1117269.
-        // This isn't true anymore ... so throw something else.
+        // This won't be true anymore because we will report the exception on
+        // the JSContext ... so throw something else.
         mErrorResult.Throw(NS_ERROR_UNEXPECTED);
       }
     }
   }
 
   mAutoIncumbentScript.reset();
   mAutoEntryScript.reset();
 
--- a/dom/indexedDB/IDBDatabase.cpp
+++ b/dom/indexedDB/IDBDatabase.cpp
@@ -34,26 +34,25 @@
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/dom/ipc/BlobChild.h"
 #include "mozilla/dom/ipc/nsIRemoteBlob.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/ipc/FileDescriptor.h"
 #include "mozilla/ipc/InputStreamParams.h"
 #include "mozilla/ipc/InputStreamUtils.h"
 #include "nsCOMPtr.h"
-#include "nsContentUtils.h"
-#include "nsIConsoleService.h"
 #include "nsIDocument.h"
 #include "nsIObserver.h"
 #include "nsIObserverService.h"
 #include "nsIScriptError.h"
 #include "nsISupportsPrimitives.h"
 #include "nsThreadUtils.h"
 #include "ProfilerHelpers.h"
 #include "ReportInternalError.h"
+#include "ScriptErrorHelper.h"
 #include "nsQueryObject.h"
 
 // Include this last to avoid path problems on Windows.
 #include "ActorsChild.h"
 
 namespace mozilla {
 namespace dom {
 
@@ -127,60 +126,16 @@ private:
 #ifdef DEBUG
     mDatabase = nullptr;
 #endif
   }
 };
 
 } // namespace
 
-class IDBDatabase::LogWarningRunnable final
-  : public nsRunnable
-{
-  nsCString mMessageName;
-  nsString mFilename;
-  uint32_t mLineNumber;
-  uint32_t mColumnNumber;
-  uint64_t mInnerWindowID;
-  bool mIsChrome;
-
-public:
-  LogWarningRunnable(const char* aMessageName,
-                     const nsAString& aFilename,
-                     uint32_t aLineNumber,
-                     uint32_t aColumnNumber,
-                     bool aIsChrome,
-                     uint64_t aInnerWindowID)
-    : mMessageName(aMessageName)
-    , mFilename(aFilename)
-    , mLineNumber(aLineNumber)
-    , mColumnNumber(aColumnNumber)
-    , mInnerWindowID(aInnerWindowID)
-    , mIsChrome(aIsChrome)
-  {
-    MOZ_ASSERT(!NS_IsMainThread());
-  }
-
-  static void
-  LogWarning(const char* aMessageName,
-             const nsAString& aFilename,
-             uint32_t aLineNumber,
-             uint32_t aColumnNumber,
-             bool aIsChrome,
-             uint64_t aInnerWindowID);
-
-  NS_DECL_ISUPPORTS_INHERITED
-
-private:
-  ~LogWarningRunnable()
-  { }
-
-  NS_DECL_NSIRUNNABLE
-};
-
 class IDBDatabase::Observer final
   : public nsIObserver
 {
   IDBDatabase* mWeakDatabase;
   const uint64_t mWindowId;
 
 public:
   Observer(IDBDatabase* aDatabase, uint64_t aWindowId)
@@ -1252,33 +1207,23 @@ void
 IDBDatabase::LogWarning(const char* aMessageName,
                         const nsAString& aFilename,
                         uint32_t aLineNumber,
                         uint32_t aColumnNumber)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(aMessageName);
 
-  if (NS_IsMainThread()) {
-    LogWarningRunnable::LogWarning(aMessageName,
-                                   aFilename,
-                                   aLineNumber,
-                                   aColumnNumber,
-                                   mFactory->IsChrome(),
-                                   mFactory->InnerWindowID());
-  } else {
-    RefPtr<LogWarningRunnable> runnable =
-      new LogWarningRunnable(aMessageName,
-                             aFilename,
-                             aLineNumber,
-                             aColumnNumber,
-                             mFactory->IsChrome(),
-                             mFactory->InnerWindowID());
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable)));
-  }
+  ScriptErrorHelper::DumpLocalizedMessage(nsDependentCString(aMessageName),
+                                          aFilename,
+                                          aLineNumber,
+                                          aColumnNumber,
+                                          nsIScriptError::warningFlag,
+                                          mFactory->IsChrome(),
+                                          mFactory->InnerWindowID());
 }
 
 NS_IMPL_ADDREF_INHERITED(IDBDatabase, IDBWrapperCache)
 NS_IMPL_RELEASE_INHERITED(IDBDatabase, IDBWrapperCache)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(IDBDatabase)
 NS_INTERFACE_MAP_END_INHERITING(IDBWrapperCache)
 
@@ -1351,95 +1296,16 @@ CancelableRunnableWrapper::Cancel()
   if (mRunnable) {
     mRunnable = nullptr;
     return NS_OK;
   }
 
   return NS_ERROR_UNEXPECTED;
 }
 
-// static
-void
-IDBDatabase::
-LogWarningRunnable::LogWarning(const char* aMessageName,
-                               const nsAString& aFilename,
-                               uint32_t aLineNumber,
-                               uint32_t aColumnNumber,
-                               bool aIsChrome,
-                               uint64_t aInnerWindowID)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(aMessageName);
-
-  nsXPIDLString localizedMessage;
-  if (NS_WARN_IF(NS_FAILED(
-    nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
-                                       aMessageName,
-                                       localizedMessage)))) {
-    return;
-  }
-
-  nsAutoCString category;
-  if (aIsChrome) {
-    category.AssignLiteral("chrome ");
-  } else {
-    category.AssignLiteral("content ");
-  }
-  category.AppendLiteral("javascript");
-
-  nsCOMPtr<nsIConsoleService> consoleService =
-    do_GetService(NS_CONSOLESERVICE_CONTRACTID);
-  MOZ_ASSERT(consoleService);
-
-  nsCOMPtr<nsIScriptError> scriptError =
-    do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
-  MOZ_ASSERT(consoleService);
-
-  if (aInnerWindowID) {
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
-      scriptError->InitWithWindowID(localizedMessage,
-                                    aFilename,
-                                    /* aSourceLine */ EmptyString(),
-                                    aLineNumber,
-                                    aColumnNumber,
-                                    nsIScriptError::warningFlag,
-                                    category,
-                                    aInnerWindowID)));
-  } else {
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
-      scriptError->Init(localizedMessage,
-                        aFilename,
-                        /* aSourceLine */ EmptyString(),
-                        aLineNumber,
-                        aColumnNumber,
-                        nsIScriptError::warningFlag,
-                        category.get())));
-  }
-
-  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError)));
-}
-
-NS_IMPL_ISUPPORTS_INHERITED0(IDBDatabase::LogWarningRunnable, nsRunnable)
-
-NS_IMETHODIMP
-IDBDatabase::
-LogWarningRunnable::Run()
-{
-  MOZ_ASSERT(NS_IsMainThread());
-
-  LogWarning(mMessageName.get(),
-             mFilename,
-             mLineNumber,
-             mColumnNumber,
-             mIsChrome,
-             mInnerWindowID);
-
-  return NS_OK;
-}
-
 NS_IMPL_ISUPPORTS(IDBDatabase::Observer, nsIObserver)
 
 NS_IMETHODIMP
 IDBDatabase::
 Observer::Observe(nsISupports* aSubject,
                   const char* aTopic,
                   const char16_t* aData)
 {
--- a/dom/indexedDB/IDBDatabase.h
+++ b/dom/indexedDB/IDBDatabase.h
@@ -48,19 +48,16 @@ class PBackgroundIDBDatabaseFileChild;
 
 class IDBDatabase final
   : public IDBWrapperCache
 {
   typedef mozilla::dom::indexedDB::DatabaseSpec DatabaseSpec;
   typedef mozilla::dom::StorageType StorageType;
   typedef mozilla::dom::quota::PersistenceType PersistenceType;
 
-  class LogWarningRunnable;
-  friend class LogWarningRunnable;
-
   class Observer;
   friend class Observer;
 
   // The factory must be kept alive when IndexedDB is used in multiple
   // processes. If it dies then the entire actor tree will be destroyed with it
   // and the world will explode.
   RefPtr<IDBFactory> mFactory;
 
--- a/dom/indexedDB/IndexedDatabaseManager.cpp
+++ b/dom/indexedDB/IndexedDatabaseManager.cpp
@@ -36,16 +36,17 @@
 
 #include "FileInfo.h"
 #include "FileManager.h"
 #include "IDBEvents.h"
 #include "IDBFactory.h"
 #include "IDBKeyRange.h"
 #include "IDBRequest.h"
 #include "ProfilerHelpers.h"
+#include "ScriptErrorHelper.h"
 #include "WorkerScope.h"
 #include "WorkerPrivate.h"
 
 // Bindings for ResolveConstructors
 #include "mozilla/dom/IDBCursorBinding.h"
 #include "mozilla/dom/IDBDatabaseBinding.h"
 #include "mozilla/dom/IDBFactoryBinding.h"
 #include "mozilla/dom/IDBIndexBinding.h"
@@ -533,55 +534,24 @@ IndexedDatabaseManager::CommonPostHandle
       status = nsEventStatus_eIgnore;
     }
   }
 
   if (status == nsEventStatus_eConsumeNoDefault) {
     return NS_OK;
   }
 
-  nsAutoCString category;
-  if (aFactory->IsChrome()) {
-    category.AssignLiteral("chrome ");
-  } else {
-    category.AssignLiteral("content ");
-  }
-  category.AppendLiteral("javascript");
-
   // Log the error to the error console.
-  nsCOMPtr<nsIConsoleService> consoleService =
-    do_GetService(NS_CONSOLESERVICE_CONTRACTID);
-  MOZ_ASSERT(consoleService);
-
-  nsCOMPtr<nsIScriptError> scriptError =
-    do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
-  MOZ_ASSERT(consoleService);
-
-  if (uint64_t innerWindowID = aFactory->InnerWindowID()) {
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
-      scriptError->InitWithWindowID(errorName,
-                                    init.mFilename,
-                                    /* aSourceLine */ EmptyString(),
-                                    init.mLineno,
-                                    init.mColno,
-                                    nsIScriptError::errorFlag,
-                                    category,
-                                    innerWindowID)));
-  } else {
-    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
-      scriptError->Init(errorName,
-                        init.mFilename,
-                        /* aSourceLine */ EmptyString(),
-                        init.mLineno,
-                        init.mColno,
-                        nsIScriptError::errorFlag,
-                        category.get())));
-  }
-
-  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError)));
+  ScriptErrorHelper::Dump(errorName,
+                          init.mFilename,
+                          init.mLineno,
+                          init.mColno,
+                          nsIScriptError::errorFlag,
+                          aFactory->IsChrome(),
+                          aFactory->InnerWindowID());
 
   return NS_OK;
 }
 
 // static
 bool
 IndexedDatabaseManager::DefineIndexedDB(JSContext* aCx,
                                         JS::Handle<JSObject*> aGlobal)
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/ScriptErrorHelper.cpp
@@ -0,0 +1,249 @@
+/* -*- 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 "ScriptErrorHelper.h"
+
+#include "MainThreadUtils.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+namespace {
+
+class ScriptErrorRunnable final : public nsRunnable
+{
+  nsString mMessage;
+  nsCString mMessageName;
+  nsString mFilename;
+  uint32_t mLineNumber;
+  uint32_t mColumnNumber;
+  uint32_t mSeverityFlag;
+  uint64_t mInnerWindowID;
+  bool mIsChrome;
+
+public:
+  ScriptErrorRunnable(const nsAString& aMessage,
+                      const nsAString& aFilename,
+                      uint32_t aLineNumber,
+                      uint32_t aColumnNumber,
+                      uint32_t aSeverityFlag,
+                      bool aIsChrome,
+                      uint64_t aInnerWindowID)
+    : mMessage(aMessage)
+    , mFilename(aFilename)
+    , mLineNumber(aLineNumber)
+    , mColumnNumber(aColumnNumber)
+    , mSeverityFlag(aSeverityFlag)
+    , mInnerWindowID(aInnerWindowID)
+    , mIsChrome(aIsChrome)
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+    mMessageName.SetIsVoid(true);
+  }
+
+  ScriptErrorRunnable(const nsACString& aMessageName,
+                      const nsAString& aFilename,
+                      uint32_t aLineNumber,
+                      uint32_t aColumnNumber,
+                      uint32_t aSeverityFlag,
+                      bool aIsChrome,
+                      uint64_t aInnerWindowID)
+    : mMessageName(aMessageName)
+    , mFilename(aFilename)
+    , mLineNumber(aLineNumber)
+    , mColumnNumber(aColumnNumber)
+    , mSeverityFlag(aSeverityFlag)
+    , mInnerWindowID(aInnerWindowID)
+    , mIsChrome(aIsChrome)
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+    mMessage.SetIsVoid(true);
+  }
+
+  static void
+  DumpLocalizedMessage(const nsCString& aMessageName,
+                       const nsAString& aFilename,
+                       uint32_t aLineNumber,
+                       uint32_t aColumnNumber,
+                       uint32_t aSeverityFlag,
+                       bool aIsChrome,
+                       uint64_t aInnerWindowID)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(!aMessageName.IsEmpty());
+
+    nsXPIDLString localizedMessage;
+    if (NS_WARN_IF(NS_FAILED(
+      nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
+                                         aMessageName.get(),
+                                         localizedMessage)))) {
+      return;
+    }
+
+    Dump(localizedMessage,
+         aFilename,
+         aLineNumber,
+         aColumnNumber,
+         aSeverityFlag,
+         aIsChrome,
+         aInnerWindowID);
+  }
+
+  static void
+  Dump(const nsAString& aMessage,
+       const nsAString& aFilename,
+       uint32_t aLineNumber,
+       uint32_t aColumnNumber,
+       uint32_t aSeverityFlag,
+       bool aIsChrome,
+       uint64_t aInnerWindowID)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+
+    nsAutoCString category;
+    if (aIsChrome) {
+      category.AssignLiteral("chrome ");
+    } else {
+      category.AssignLiteral("content ");
+    }
+    category.AppendLiteral("javascript");
+
+    nsCOMPtr<nsIConsoleService> consoleService =
+      do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+    MOZ_ASSERT(consoleService);
+
+    nsCOMPtr<nsIScriptError> scriptError =
+      do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
+    MOZ_ASSERT(scriptError);
+
+    if (aInnerWindowID) {
+      MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+        scriptError->InitWithWindowID(aMessage,
+                                      aFilename,
+                                      /* aSourceLine */ EmptyString(),
+                                      aLineNumber,
+                                      aColumnNumber,
+                                      aSeverityFlag,
+                                      category,
+                                      aInnerWindowID)));
+    } else {
+      MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+        scriptError->Init(aMessage,
+                          aFilename,
+                          /* aSourceLine */ EmptyString(),
+                          aLineNumber,
+                          aColumnNumber,
+                          aSeverityFlag,
+                          category.get())));
+    }
+
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(consoleService->LogMessage(scriptError)));
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mMessage.IsVoid() != mMessageName.IsVoid());
+
+    if (!mMessage.IsVoid()) {
+      Dump(mMessage,
+           mFilename,
+           mLineNumber,
+           mColumnNumber,
+           mSeverityFlag,
+           mIsChrome,
+           mInnerWindowID);
+      return NS_OK;
+    }
+
+    DumpLocalizedMessage(mMessageName,
+                         mFilename,
+                         mLineNumber,
+                         mColumnNumber,
+                         mSeverityFlag,
+                         mIsChrome,
+                         mInnerWindowID);
+
+    return NS_OK;
+  }
+
+private:
+  virtual ~ScriptErrorRunnable() {}
+};
+
+} // namespace
+
+namespace mozilla {
+namespace dom {
+namespace indexedDB {
+
+/*static*/ void
+ScriptErrorHelper::Dump(const nsAString& aMessage,
+                        const nsAString& aFilename,
+                        uint32_t aLineNumber,
+                        uint32_t aColumnNumber,
+                        uint32_t aSeverityFlag,
+                        bool aIsChrome,
+                        uint64_t aInnerWindowID)
+{
+  if (NS_IsMainThread()) {
+    ScriptErrorRunnable::Dump(aMessage,
+                              aFilename,
+                              aLineNumber,
+                              aColumnNumber,
+                              aSeverityFlag,
+                              aIsChrome,
+                              aInnerWindowID);
+  } else {
+    RefPtr<ScriptErrorRunnable> runnable =
+      new ScriptErrorRunnable(aMessage,
+                              aFilename,
+                              aLineNumber,
+                              aColumnNumber,
+                              aSeverityFlag,
+                              aIsChrome,
+                              aInnerWindowID);
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable)));
+  }
+}
+
+/*static*/ void
+ScriptErrorHelper::DumpLocalizedMessage(const nsACString& aMessageName,
+                                        const nsAString& aFilename,
+                                        uint32_t aLineNumber,
+                                        uint32_t aColumnNumber,
+                                        uint32_t aSeverityFlag,
+                                        bool aIsChrome,
+                                        uint64_t aInnerWindowID)
+{
+  if (NS_IsMainThread()) {
+    ScriptErrorRunnable::DumpLocalizedMessage(nsAutoCString(aMessageName),
+                                              aFilename,
+                                              aLineNumber,
+                                              aColumnNumber,
+                                              aSeverityFlag,
+                                              aIsChrome,
+                                              aInnerWindowID);
+  } else {
+    RefPtr<ScriptErrorRunnable> runnable =
+      new ScriptErrorRunnable(aMessageName,
+                              aFilename,
+                              aLineNumber,
+                              aColumnNumber,
+                              aSeverityFlag,
+                              aIsChrome,
+                              aInnerWindowID);
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable)));
+  }
+}
+
+} // namespace indexedDB
+} // namespace dom
+} // namespace mozilla
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/ScriptErrorHelper.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_indexeddb_scripterrorhelper_h__
+#define mozilla_dom_indexeddb_scripterrorhelper_h__
+
+class nsAString;
+
+namespace mozilla {
+namespace dom {
+namespace indexedDB {
+
+// Helper to report a script error to the main thread.
+class ScriptErrorHelper
+{
+public:
+  static void Dump(const nsAString& aMessage,
+                   const nsAString& aFilename,
+                   uint32_t aLineNumber,
+                   uint32_t aColumnNumber,
+                   uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */
+                   bool aIsChrome,
+                   uint64_t aInnerWindowID);
+
+  static void DumpLocalizedMessage(const nsACString& aMessageName,
+                                   const nsAString& aFilename,
+                                   uint32_t aLineNumber,
+                                   uint32_t aColumnNumber,
+                                   uint32_t aSeverityFlag, /* nsIScriptError::xxxFlag */
+                                   bool aIsChrome,
+                                   uint64_t aInnerWindowID);
+};
+
+} // namespace indexedDB
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_indexeddb_scripterrorhelper_h__
\ No newline at end of file
--- a/dom/indexedDB/moz.build
+++ b/dom/indexedDB/moz.build
@@ -62,16 +62,17 @@ UNIFIED_SOURCES += [
     'IDBObjectStore.cpp',
     'IDBRequest.cpp',
     'IDBTransaction.cpp',
     'IDBWrapperCache.cpp',
     'IndexedDatabaseManager.cpp',
     'KeyPath.cpp',
     'PermissionRequestBase.cpp',
     'ReportInternalError.cpp',
+    'ScriptErrorHelper.cpp',
 ]
 
 SOURCES += [
     'ActorsParent.cpp', # This file is huge.
     'Key.cpp', # We disable a warning on this file only
 ]
 
 IPDL_SOURCES += [
--- a/dom/interfaces/core/nsIDOMNode.idl
+++ b/dom/interfaces/core/nsIDOMNode.idl
@@ -3,21 +3,21 @@
  * 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 "domstubs.idl"
 
 interface nsIVariant;
 
 /**
- * The nsIDOMNode interface is the primary datatype for the entire 
+ * The nsIDOMNode interface is the primary datatype for the entire
  * Document Object Model.
  * It represents a single node in the document tree.
  *
- * For more information on this interface please see 
+ * For more information on this interface please see
  * http://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html
  */
 
 [uuid(cc35b412-009b-46a3-9be0-76448f12548d)]
 interface nsIDOMNode : nsISupports
 {
   const unsigned short      ELEMENT_NODE       = 1;
   const unsigned short      ATTRIBUTE_NODE     = 2;
@@ -41,20 +41,20 @@ interface nsIDOMNode : nsISupports
   readonly attribute nsIDOMElement    parentElement;
   readonly attribute nsIDOMNodeList   childNodes;
   readonly attribute nsIDOMNode       firstChild;
   readonly attribute nsIDOMNode       lastChild;
   readonly attribute nsIDOMNode       previousSibling;
   readonly attribute nsIDOMNode       nextSibling;
   // Modified in DOM Level 2:
   readonly attribute nsIDOMDocument   ownerDocument;
-  nsIDOMNode                insertBefore(in nsIDOMNode newChild, 
+  nsIDOMNode                insertBefore(in nsIDOMNode newChild,
                                          in nsIDOMNode refChild)
                                           raises(DOMException);
-  nsIDOMNode                replaceChild(in nsIDOMNode newChild, 
+  nsIDOMNode                replaceChild(in nsIDOMNode newChild,
                                          in nsIDOMNode oldChild)
                                           raises(DOMException);
   nsIDOMNode                removeChild(in nsIDOMNode oldChild)
                                          raises(DOMException);
   nsIDOMNode                appendChild(in nsIDOMNode newChild)
                                          raises(DOMException);
   boolean                   hasChildNodes();
   // Modified in DOM Level 4:
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -1251,20 +1251,16 @@ void
 MediaDecoderStateMachine::SetDormant(bool aDormant)
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (IsShutdown()) {
     return;
   }
 
-  if (!mReader) {
-    return;
-  }
-
   if (mMetadataRequest.Exists()) {
     if (mPendingDormant && mPendingDormant.ref() != aDormant && !aDormant) {
       // We already have a dormant request pending; the new request would have
       // resumed from dormant, we can just cancel any pending dormant requests.
       mPendingDormant.reset();
     } else {
       mPendingDormant = Some(aDormant);
     }
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -1055,17 +1055,23 @@ RTCPeerConnection.prototype = {
     }
     this._checkClosed();
     this._senders.forEach(sender => {
       if (sender.track == track) {
         throw new this._win.DOMException("already added.",
                                          "InvalidParameterError");
       }
     });
-    this._impl.addTrack(track, stream);
+    try {
+      this._impl.addTrack(track, stream);
+    } catch (e if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED)) {
+      throw new this._win.DOMException(
+          "track in constructed stream not yet supported (see Bug 1259236).",
+          "NotSupportedError");
+    }
     let sender = this._win.RTCRtpSender._create(this._win,
                                                 new RTCRtpSender(this, track,
                                                                  stream));
     this._senders.push(sender);
     return sender;
   },
 
   removeTrack: function(sender) {
--- a/dom/media/mediasink/DecodedStream.cpp
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/CheckedInt.h"
 #include "mozilla/gfx/Point.h"
+#include "mozilla/SyncRunnable.h"
 
 #include "AudioSegment.h"
 #include "DecodedStream.h"
 #include "MediaData.h"
 #include "MediaQueue.h"
 #include "MediaStreamGraph.h"
 #include "OutputStreamManager.h"
 #include "SharedBuffer.h"
@@ -264,42 +265,61 @@ DecodedStream::Start(int64_t aStartTime,
 
   mStartTime.emplace(aStartTime);
   mInfo = aInfo;
   mPlaying = true;
   ConnectListener();
 
   class R : public nsRunnable {
     typedef MozPromiseHolder<GenericPromise> Promise;
-    typedef decltype(&DecodedStream::CreateData) Method;
   public:
-    R(DecodedStream* aThis, Method aMethod, PlaybackInfoInit&& aInit, Promise&& aPromise)
-      : mThis(aThis), mMethod(aMethod), mInit(Move(aInit))
+    R(PlaybackInfoInit&& aInit, Promise&& aPromise, OutputStreamManager* aManager)
+      : mInit(Move(aInit)), mOutputStreamManager(aManager)
     {
       mPromise = Move(aPromise);
     }
     NS_IMETHOD Run() override
     {
-      (mThis->*mMethod)(Move(mInit), Move(mPromise));
+      MOZ_ASSERT(NS_IsMainThread());
+      // No need to create a source stream when there are no output streams. This
+      // happens when RemoveOutput() is called immediately after StartPlayback().
+      if (!mOutputStreamManager->Graph()) {
+        // Resolve the promise to indicate the end of playback.
+        mPromise.Resolve(true, __func__);
+        return NS_OK;
+      }
+      mData = MakeUnique<DecodedStreamData>(
+        mOutputStreamManager, Move(mInit), Move(mPromise));
       return NS_OK;
     }
+    UniquePtr<DecodedStreamData> ReleaseData()
+    {
+      return Move(mData);
+    }
   private:
-    RefPtr<DecodedStream> mThis;
-    Method mMethod;
     PlaybackInfoInit mInit;
     Promise mPromise;
+    RefPtr<OutputStreamManager> mOutputStreamManager;
+    UniquePtr<DecodedStreamData> mData;
   };
 
   MozPromiseHolder<GenericPromise> promise;
   mFinishPromise = promise.Ensure(__func__);
   PlaybackInfoInit init {
     aStartTime, aInfo
   };
-  nsCOMPtr<nsIRunnable> r = new R(this, &DecodedStream::CreateData, Move(init), Move(promise));
-  AbstractThread::MainThread()->Dispatch(r.forget());
+  nsCOMPtr<nsIRunnable> r = new R(Move(init), Move(promise), mOutputStreamManager);
+  nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+  SyncRunnable::DispatchToThread(mainThread, r);
+  mData = static_cast<R*>(r.get())->ReleaseData();
+
+  if (mData) {
+    mData->SetPlaying(mPlaying);
+    SendData();
+  }
 }
 
 void
 DecodedStream::Stop()
 {
   AssertOwnerThread();
   MOZ_ASSERT(mStartTime.isSome(), "playback not started.");
 
@@ -338,87 +358,16 @@ DecodedStream::DestroyData(UniquePtr<Dec
   DecodedStreamData* data = aData.release();
   nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
     delete data;
   });
   AbstractThread::MainThread()->Dispatch(r.forget());
 }
 
 void
-DecodedStream::CreateData(PlaybackInfoInit&& aInit, MozPromiseHolder<GenericPromise>&& aPromise)
-{
-  MOZ_ASSERT(NS_IsMainThread());
-
-  // No need to create a source stream when there are no output streams. This
-  // happens when RemoveOutput() is called immediately after StartPlayback().
-  if (!mOutputStreamManager->Graph()) {
-    // Resolve the promise to indicate the end of playback.
-    aPromise.Resolve(true, __func__);
-    return;
-  }
-
-  auto data = new DecodedStreamData(mOutputStreamManager, Move(aInit), Move(aPromise));
-
-  class R : public nsRunnable {
-    typedef void(DecodedStream::*Method)(UniquePtr<DecodedStreamData>);
-  public:
-    R(DecodedStream* aThis, Method aMethod, DecodedStreamData* aData)
-      : mThis(aThis), mMethod(aMethod), mData(aData) {}
-    NS_IMETHOD Run() override
-    {
-      (mThis->*mMethod)(Move(mData));
-      return NS_OK;
-    }
-  private:
-    virtual ~R()
-    {
-      // mData is not transferred when dispatch fails and Run() is not called.
-      // We need to dispatch a task to ensure DecodedStreamData is destroyed
-      // properly on the main thread.
-      if (mData) {
-        DecodedStreamData* data = mData.release();
-        nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-          delete data;
-        });
-        // We are in tail dispatching phase. Don't call
-        // AbstractThread::MainThread()->Dispatch() to avoid reentrant
-        // AutoTaskDispatcher.
-        NS_DispatchToMainThread(r.forget());
-      }
-    }
-    RefPtr<DecodedStream> mThis;
-    Method mMethod;
-    UniquePtr<DecodedStreamData> mData;
-  };
-
-  // Post a message to ensure |mData| is only updated on the worker thread.
-  // Note this could fail when MDSM begin to shut down the worker thread.
-  nsCOMPtr<nsIRunnable> r = new R(this, &DecodedStream::OnDataCreated, data);
-  mOwnerThread->Dispatch(r.forget(), AbstractThread::DontAssertDispatchSuccess);
-}
-
-void
-DecodedStream::OnDataCreated(UniquePtr<DecodedStreamData> aData)
-{
-  AssertOwnerThread();
-  MOZ_ASSERT(!mData, "Already created.");
-
-  // Start to send data to the stream immediately
-  if (mStartTime.isSome()) {
-    aData->SetPlaying(mPlaying);
-    mData = Move(aData);
-    SendData();
-    return;
-  }
-
-  // Playback has ended. Destroy aData which is not needed anymore.
-  DestroyData(Move(aData));
-}
-
-void
 DecodedStream::SetPlaying(bool aPlaying)
 {
   AssertOwnerThread();
 
   // Resume/pause matters only when playback started.
   if (mStartTime.isNothing()) {
     return;
   }
--- a/dom/media/mediasink/DecodedStream.h
+++ b/dom/media/mediasink/DecodedStream.h
@@ -61,19 +61,17 @@ public:
   void Stop() override;
   bool IsStarted() const override;
   bool IsPlaying() const override;
 
 protected:
   virtual ~DecodedStream();
 
 private:
-  void CreateData(PlaybackInfoInit&& aInit, MozPromiseHolder<GenericPromise>&& aPromise);
   void DestroyData(UniquePtr<DecodedStreamData> aData);
-  void OnDataCreated(UniquePtr<DecodedStreamData> aData);
   void AdvanceTracks();
   void SendAudio(double aVolume, bool aIsSameOrigin);
   void SendVideo(bool aIsSameOrigin);
   void SendData();
 
   void AssertOwnerThread() const {
     MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
   }
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -415,16 +415,27 @@ var listenUntil = (target, eventName, on
     var result = onFire();
     if (result) {
       target.removeEventListener(eventName, callback, false);
       resolve(result);
     }
   }, false));
 };
 
+/* Test that a function throws the right error */
+function mustThrowWith(msg, reason, f) {
+  try {
+    f();
+    ok(false, msg + " must throw");
+  } catch (e) {
+    is(e.name, reason, msg + " must throw: " + e.message);
+  }
+};
+
+
 /*** Test control flow methods */
 
 /**
  * Generates a callback function fired only under unexpected circumstances
  * while running the tests. The generated function kills off the test as well
  * gracefully.
  *
  * @param {String} [message]
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -65,16 +65,17 @@ skip-if = (toolkit == 'gonk' || buildapp
 [test_getUserMedia_stopAudioStreamWithFollowupAudio.html]
 [test_getUserMedia_stopVideoAudioStream.html]
 [test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html]
 [test_getUserMedia_stopVideoStream.html]
 [test_getUserMedia_stopVideoStreamWithFollowupVideo.html]
 [test_getUserMedia_peerIdentity.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g(Bug 1021776, too --ing slow on b2g)
 [test_peerConnection_addIceCandidate.html]
+[test_peerConnection_addTrack.html]
 [test_peerConnection_basicAudio.html]
 skip-if = toolkit == 'gonk' # B2G emulator is too slow to handle a two-way audio call reliably
 [test_peerConnection_basicAudioRequireEOC.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_basicAudioPcmaPcmuOnly.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
 [test_peerConnection_basicAudioDynamicPtMissingRtpmap.html]
 skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g (Bug 1059867)
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addTrack.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+  createHTML({
+    bug: "1259236",
+    title: "PeerConnection addTrack errors",
+    visible: true
+  });
+
+  runNetworkTest(function() {
+    navigator.mediaDevices.getUserMedia({ video: true })
+    .then(gumStream => {
+      let newStream = new MediaStream(gumStream.getTracks());
+
+      mustThrowWith("pc.addTrack a track from a constructed MediaStream",
+                    "NotSupportedError",
+                    () => new RTCPeerConnection().addTrack(newStream.getTracks()[0],
+                                                           newStream));
+    })
+    .catch(e => ok(false, "unexpected failure: " + e))
+    .then(networkTestFinished);
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
+++ b/dom/media/tests/mochitest/test_peerConnection_promiseSendOnly.html
@@ -14,25 +14,16 @@
 
   var pc1 = new RTCPeerConnection();
   var pc2 = new RTCPeerConnection();
 
   var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
   pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
   pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
 
-  function mustThrowWith(msg, reason, f) {
-    try {
-      f();
-      ok(false, msg + " must throw");
-    } catch (e) {
-      is(e.name, reason, msg + " must throw: " + e.message);
-    }
-  };
-
   var v1, v2;
   var delivered = new Promise(resolve => pc2.ontrack = e => {
     // Test RTCTrackEvent here.
     ok(e.streams.length > 0, "has streams");
     ok(e.streams[0].getTracks().some(track => track == e.track), "has track");
     ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver");
     if (e.streams[0].getTracks().length == 2) {
       // Test RTCTrackEvent required args here.
--- a/dom/plugins/base/nsPluginStreamListenerPeer.cpp
+++ b/dom/plugins/base/nsPluginStreamListenerPeer.cpp
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsPluginStreamListenerPeer.h"
 #include "nsIContentPolicy.h"
 #include "nsContentPolicyUtils.h"
 #include "nsIDOMElement.h"
 #include "nsIStreamConverterService.h"
+#include "nsIStreamLoader.h"
 #include "nsIHttpChannel.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIFileChannel.h"
 #include "nsMimeTypes.h"
 #include "nsISupportsPrimitives.h"
 #include "nsNetCID.h"
 #include "nsPluginInstanceOwner.h"
 #include "nsPluginLogging.h"
@@ -29,17 +30,17 @@
 #include "nsContentUtils.h"
 #include "nsNetUtil.h"
 #include "nsPluginNativeWindow.h"
 #include "GeckoProfiler.h"
 #include "nsPluginInstanceOwner.h"
 #include "nsDataHashtable.h"
 #include "nsNullPrincipal.h"
 
-#define MAGIC_REQUEST_CONTEXT 0x01020304
+#define BYTERANGE_REQUEST_CONTEXT 0x01020304
 
 // nsPluginByteRangeStreamListener
 
 class nsPluginByteRangeStreamListener
   : public nsIStreamListener
   , public nsIInterfaceRequestor
 {
 public:
@@ -50,28 +51,28 @@ public:
   NS_DECL_NSISTREAMLISTENER
   NS_DECL_NSIINTERFACEREQUESTOR
 
 private:
   virtual ~nsPluginByteRangeStreamListener();
 
   nsCOMPtr<nsIStreamListener> mStreamConverter;
   nsWeakPtr mWeakPtrPluginStreamListenerPeer;
-  bool mRemoveMagicNumber;
+  bool mRemoveByteRangeRequest;
 };
 
 NS_IMPL_ISUPPORTS(nsPluginByteRangeStreamListener,
                   nsIRequestObserver,
                   nsIStreamListener,
                   nsIInterfaceRequestor)
 
 nsPluginByteRangeStreamListener::nsPluginByteRangeStreamListener(nsIWeakReference* aWeakPtr)
 {
   mWeakPtrPluginStreamListenerPeer = aWeakPtr;
-  mRemoveMagicNumber = false;
+  mRemoveByteRangeRequest = false;
 }
 
 nsPluginByteRangeStreamListener::~nsPluginByteRangeStreamListener()
 {
   mStreamConverter = 0;
   mWeakPtrPluginStreamListenerPeer = 0;
 }
 
@@ -144,17 +145,17 @@ nsPluginByteRangeStreamListener::OnStart
     if (!wantsAllNetworkStreams){
       return NS_ERROR_FAILURE;
     }
   }
 
   // if server cannot continue with byte range (206 status) and sending us whole object (200 status)
   // reset this seekable stream & try serve it to plugin instance as a file
   mStreamConverter = finalStreamListener;
-  mRemoveMagicNumber = true;
+  mRemoveByteRangeRequest = true;
 
   rv = pslp->ServeStreamAsFile(request, ctxt);
   return rv;
 }
 
 NS_IMETHODIMP
 nsPluginByteRangeStreamListener::OnStopRequest(nsIRequest *request, nsISupports *ctxt,
                                                nsresult status)
@@ -168,25 +169,25 @@ nsPluginByteRangeStreamListener::OnStopR
 
   nsPluginStreamListenerPeer *pslp =
     static_cast<nsPluginStreamListenerPeer*>(finalStreamListener.get());
   bool found = pslp->mRequests.RemoveObject(request);
   if (!found) {
     NS_ERROR("OnStopRequest received for untracked byte-range request!");
   }
 
-  if (mRemoveMagicNumber) {
-    // remove magic number from container
+  if (mRemoveByteRangeRequest) {
+    // remove byte range request from container
     nsCOMPtr<nsISupportsPRUint32> container = do_QueryInterface(ctxt);
     if (container) {
-      uint32_t magicNumber = 0;
-      container->GetData(&magicNumber);
-      if (magicNumber == MAGIC_REQUEST_CONTEXT) {
+      uint32_t byteRangeRequest = 0;
+      container->GetData(&byteRangeRequest);
+      if (byteRangeRequest == BYTERANGE_REQUEST_CONTEXT) {
         // to allow properly finish nsPluginStreamListenerPeer->OnStopRequest()
-        // set it to something that is not the magic number.
+        // set it to something that is not the byte range request.
         container->SetData(0);
       }
     } else {
       NS_WARNING("Bad state of nsPluginByteRangeStreamListener");
     }
   }
 
   return mStreamConverter->OnStopRequest(request, ctxt, status);
@@ -655,16 +656,73 @@ nsPluginStreamListenerPeer::MakeByteRang
   // get rid of possible trailing comma
   string.Trim(",", false);
 
   rangeRequest = string;
   *numRequests  = requestCnt;
   return;
 }
 
+// XXX: Converting the channel within nsPluginStreamListenerPeer
+// to use asyncOpen2() and do not want to touch the fragile logic
+// of byte range requests. Hence we just introduce this lightweight
+// wrapper to proxy the context.
+class PluginContextProxy final : public nsIStreamListener
+{
+public:
+  NS_DECL_ISUPPORTS
+
+  PluginContextProxy(nsIStreamListener *aListener, nsISupports* aContext)
+    : mListener(aListener)
+    , mContext(aContext)
+  {
+    MOZ_ASSERT(aListener);
+    MOZ_ASSERT(aContext);
+  }
+
+  NS_IMETHOD
+  OnDataAvailable(nsIRequest* aRequest,
+                  nsISupports* aContext,
+                  nsIInputStream *aIStream,
+                  uint64_t aSourceOffset,
+                  uint32_t aLength) override
+  {
+    // Proxy OnDataAvailable using the internal context
+    return mListener->OnDataAvailable(aRequest,
+                                      mContext,
+                                      aIStream,
+                                      aSourceOffset,
+                                      aLength);
+  }
+
+  NS_IMETHOD
+  OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) override
+  {
+    // Proxy OnStartRequest using the internal context
+    return mListener->OnStartRequest(aRequest, mContext);
+  }
+
+  NS_IMETHOD
+  OnStopRequest(nsIRequest* aRequest, nsISupports* aContext,
+                nsresult aStatusCode) override
+  {
+    // Proxy OnStopRequest using the inernal context
+    return mListener->OnStopRequest(aRequest,
+                                    mContext,
+                                    aStatusCode);
+  }
+
+private:
+  ~PluginContextProxy() {}
+  nsCOMPtr<nsIStreamListener> mListener;
+  nsCOMPtr<nsISupports> mContext;
+};
+
+NS_IMPL_ISUPPORTS(PluginContextProxy, nsIStreamListener)
+
 nsresult
 nsPluginStreamListenerPeer::RequestRead(NPByteRange* rangeList)
 {
   nsAutoCString rangeString;
   int32_t numRequests;
 
   MakeByteRangeString(rangeList, rangeString, &numRequests);
 
@@ -687,32 +745,29 @@ nsPluginStreamListenerPeer::RequestRead(
   nsCOMPtr<nsILoadGroup> loadGroup = do_QueryReferent(mWeakPtrChannelLoadGroup);
 
   nsCOMPtr<nsIChannel> channel;
   nsCOMPtr<nsINode> requestingNode(do_QueryInterface(element));
   if (requestingNode) {
     rv = NS_NewChannel(getter_AddRefs(channel),
                        mURL,
                        requestingNode,
-                       nsILoadInfo::SEC_NORMAL,
+                       nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                        nsIContentPolicy::TYPE_OTHER,
                        loadGroup,
                        callbacks,
                        nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
   }
   else {
-    // in this else branch we really don't know where the load is coming
-    // from and in fact should use something better than just using
-    // a nullPrincipal as the loadingPrincipal.
-    nsCOMPtr<nsIPrincipal> principal = nsNullPrincipal::Create();
-    NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE);
+    // In this else branch we really don't know where the load is coming
+    // from. Let's fall back to using the SystemPrincipal for such Plugins.
     rv = NS_NewChannel(getter_AddRefs(channel),
                        mURL,
-                       principal,
-                       nsILoadInfo::SEC_NORMAL,
+                       nsContentUtils::GetSystemPrincipal(),
+                       nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                        nsIContentPolicy::TYPE_OTHER,
                        loadGroup,
                        callbacks,
                        nsIChannel::LOAD_BYPASS_SERVICE_WORKER);
   }
 
   if (NS_FAILED(rv))
     return rv;
@@ -738,26 +793,26 @@ nsPluginStreamListenerPeer::RequestRead(
     nsWeakPtr weakpeer =
     do_GetWeakReference(static_cast<nsISupportsWeakReference*>(this));
     converter = new nsPluginByteRangeStreamListener(weakpeer);
   }
 
   mPendingRequests += numRequests;
 
   nsCOMPtr<nsISupportsPRUint32> container = do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv);
-  if (NS_FAILED(rv))
-    return rv;
-  rv = container->SetData(MAGIC_REQUEST_CONTEXT);
-  if (NS_FAILED(rv))
-    return rv;
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = container->SetData(BYTERANGE_REQUEST_CONTEXT);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = channel->AsyncOpen(converter, container);
-  if (NS_SUCCEEDED(rv))
-    TrackRequest(channel);
-  return rv;
+  RefPtr<PluginContextProxy> pluginContextProxy =
+    new PluginContextProxy(converter, container);
+  rv = channel->AsyncOpen2(pluginContextProxy);
+  NS_ENSURE_SUCCESS(rv, rv);
+  TrackRequest(channel);
+  return NS_OK;
 }
 
 nsresult
 nsPluginStreamListenerPeer::GetStreamOffset(int32_t* result)
 {
   *result = mStreamOffset;
   return NS_OK;
 }
@@ -837,22 +892,22 @@ NS_IMETHODIMP nsPluginStreamListenerPeer
     MOZ_ASSERT(false, "Received OnDataAvailable for untracked request.");
     return NS_ERROR_UNEXPECTED;
   }
 
   if (mRequestFailed)
     return NS_ERROR_FAILURE;
 
   if (mAbort) {
-    uint32_t magicNumber = 0;  // set it to something that is not the magic number.
+    uint32_t byteRangeRequest = 0;  // set it to something that is not the byte range request.
     nsCOMPtr<nsISupportsPRUint32> container = do_QueryInterface(aContext);
     if (container)
-      container->GetData(&magicNumber);
+      container->GetData(&byteRangeRequest);
 
-    if (magicNumber != MAGIC_REQUEST_CONTEXT) {
+    if (byteRangeRequest != BYTERANGE_REQUEST_CONTEXT) {
       // this is not one of our range requests
       mAbort = false;
       return NS_BINDING_ABORTED;
     }
   }
 
   nsresult rv = NS_OK;
 
@@ -975,19 +1030,19 @@ NS_IMETHODIMP nsPluginStreamListenerPeer
 
   // if we still have pending stuff to do, lets not close the plugin socket.
   if (--mPendingRequests > 0)
     return NS_OK;
 
   // we keep our connections around...
   nsCOMPtr<nsISupportsPRUint32> container = do_QueryInterface(aContext);
   if (container) {
-    uint32_t magicNumber = 0;  // set it to something that is not the magic number.
-    container->GetData(&magicNumber);
-    if (magicNumber == MAGIC_REQUEST_CONTEXT) {
+    uint32_t byteRangeRequest = 0;  // something other than the byte range request.
+    container->GetData(&byteRangeRequest);
+    if (byteRangeRequest == BYTERANGE_REQUEST_CONTEXT) {
       // this is one of our range requests
       return NS_OK;
     }
   }
 
   if (!mPStreamListener)
     return NS_ERROR_FAILURE;
 
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -11,16 +11,24 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
 
+// The default Push service implementation.
+XPCOMUtils.defineLazyGetter(this, "PushService", function() {
+  const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
+                                  {});
+  PushService.init();
+  return PushService;
+});
+
 // Observer notification topics for system subscriptions. These are duplicated
 // and used in `PushNotifier.cpp`. They're exposed on `nsIPushService` instead
 // of `nsIPushNotifier` so that JS callers only need to import this service.
 const OBSERVER_TOPIC_PUSH = "push-message";
 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
 
 /**
  * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
@@ -94,24 +102,16 @@ function PushServiceParent() {
   PushServiceBase.call(this);
 }
 
 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
 
 XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
   "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
 
-XPCOMUtils.defineLazyGetter(PushServiceParent.prototype, "_service",
-  function() {
-    const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
-                                    {});
-    PushService.init();
-    return PushService;
-});
-
 Object.assign(PushServiceParent.prototype, {
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
 
   _messages: [
     "Push:Register",
     "Push:Registration",
     "Push:Unregister",
     "Push:Clear",
@@ -159,21 +159,21 @@ Object.assign(PushServiceParent.prototyp
     }, error => {
       callback.onClear(Cr.NS_ERROR_FAILURE);
     }).catch(Cu.reportError);
   },
 
   // nsIPushQuotaManager methods
 
   notificationForOriginShown(origin) {
-    this._service.notificationForOriginShown(origin);
+    this.service.notificationForOriginShown(origin);
   },
 
   notificationForOriginClosed(origin) {
-    this._service.notificationForOriginClosed(origin);
+    this.service.notificationForOriginClosed(origin);
   },
 
   receiveMessage(message) {
     if (!this._isValidMessage(message)) {
       return;
     }
     let {name, principal, target, data} = message;
     if (name === "Push:NotificationForOriginShown") {
@@ -196,17 +196,17 @@ Object.assign(PushServiceParent.prototyp
     }, error => {
       sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
         requestID: data.requestID,
       });
     }).catch(Cu.reportError);
   },
 
   _handleReady() {
-    this._service.init();
+    this.service.init();
   },
 
   _toPageRecord(principal, data) {
     if (!data.scope) {
       throw new Error("Invalid page record: missing scope");
     }
     if (!principal) {
       throw new Error("Invalid page record: missing principal");
@@ -223,53 +223,63 @@ Object.assign(PushServiceParent.prototyp
     data.originAttributes =
       ChromeUtils.originAttributesToSuffix(principal.originAttributes);
 
     return data;
   },
 
   _handleRequest(name, principal, data) {
     if (name == "Push:Clear") {
-      return this._service.clear(data);
+      return this.service.clear(data);
     }
 
     let pageRecord;
     try {
       pageRecord = this._toPageRecord(principal, data);
     } catch (e) {
       return Promise.reject(e);
     }
 
     if (name === "Push:Register") {
-      return this._service.register(pageRecord);
+      return this.service.register(pageRecord);
     }
     if (name === "Push:Registration") {
-      return this._service.registration(pageRecord);
+      return this.service.registration(pageRecord);
     }
     if (name === "Push:Unregister") {
-      return this._service.unregister(pageRecord);
+      return this.service.unregister(pageRecord);
     }
 
     return Promise.reject(new Error("Invalid request: unknown name"));
   },
 
   _getResponseName(requestName, suffix) {
     let name = requestName.slice("Push:".length);
     return "PushService:" + name + ":" + suffix;
   },
 
   // Methods used for mocking in tests.
 
   replaceServiceBackend(options) {
-    this._service.changeTestServer(options.serverURI, options);
+    this.service.changeTestServer(options.serverURI, options);
   },
 
   restoreServiceBackend() {
     var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
-    this._service.changeTestServer(defaultServerURL);
+    this.service.changeTestServer(defaultServerURL);
+  },
+});
+
+// Used to replace the implementation with a mock.
+Object.defineProperty(PushServiceParent.prototype, "service", {
+  get() {
+    return this._service || PushService;
+  },
+  set(impl) {
+    this._service = impl;
   },
 });
 
 /**
  * The content process implementation of `nsIPushService`. This version
  * uses the child message manager to forward calls to the parent process.
  * The parent Push service instance handles the request, and responds with a
  * message containing the result.
--- a/dom/push/PushCrypto.jsm
+++ b/dom/push/PushCrypto.jsm
@@ -9,17 +9,25 @@ const Cu = Components.utils;
 
 Cu.importGlobalProperties(['crypto']);
 
 this.EXPORTED_SYMBOLS = ['PushCrypto', 'concatArray',
                          'getCryptoParams',
                          'base64UrlDecode'];
 
 var UTF8 = new TextEncoder('utf-8');
-var ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// Legacy encryption scheme (draft-thomson-http-encryption-02).
+var AESGCM128_ENCODING = 'aesgcm128';
+var AESGCM128_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm128');
+
+// New encryption scheme (draft-ietf-httpbis-encryption-encoding-01).
+var AESGCM_ENCODING = 'aesgcm';
+var AESGCM_ENCRYPT_INFO = UTF8.encode('Content-Encoding: aesgcm');
+
 var NONCE_INFO = UTF8.encode('Content-Encoding: nonce');
 var AUTH_INFO = UTF8.encode('Content-Encoding: auth\0'); // note nul-terminus
 var P256DH_INFO = UTF8.encode('P-256\0');
 var ECDH_KEY = { name: 'ECDH', namedCurve: 'P-256' };
 // A default keyid with a name that won't conflict with a real keyid.
 var DEFAULT_KEYID = '';
 
 function getEncryptionKeyParams(encryptKeyField) {
@@ -48,36 +56,50 @@ function getEncryptionParams(encryptFiel
 }
 
 this.getCryptoParams = function(headers) {
   if (!headers) {
     return null;
   }
 
   var requiresAuthenticationSecret = true;
-  var keymap = getEncryptionKeyParams(headers.crypto_key);
-  if (!keymap) {
-    requiresAuthenticationSecret = false;
-    keymap = getEncryptionKeyParams(headers.encryption_key);
+  var keymap;
+  var padSize;
+  if (headers.encoding == AESGCM_ENCODING) {
+    // aesgcm uses the Crypto-Key header, 2 bytes for the pad length, and an
+    // authentication secret.
+    keymap = getEncryptionKeyParams(headers.crypto_key);
+    padSize = 2;
+  } else if (headers.encoding == AESGCM128_ENCODING) {
+    // aesgcm128 uses Crypto-Key or Encryption-Key, and 1 byte for the pad
+    // length.
+    keymap = getEncryptionKeyParams(headers.crypto_key);
+    padSize = 1;
     if (!keymap) {
-      return null;
+      // Encryption-Key header indicates unauthenticated encryption.
+      requiresAuthenticationSecret = false;
+      keymap = getEncryptionKeyParams(headers.encryption_key);
     }
   }
+  if (!keymap) {
+    return null;
+  }
+
   var enc = getEncryptionParams(headers.encryption);
   if (!enc) {
     return null;
   }
   var dh = keymap[enc.keyid || DEFAULT_KEYID];
   var salt = enc.salt;
   var rs = (enc.rs)? parseInt(enc.rs, 10) : 4096;
 
-  if (!dh || !salt || isNaN(rs) || (rs <= 1)) {
+  if (!dh || !salt || isNaN(rs) || (rs <= padSize)) {
     return null;
   }
-  return {dh, salt, rs, auth: requiresAuthenticationSecret};
+  return {dh, salt, rs, auth: requiresAuthenticationSecret, padSize};
 }
 
 var parseHeaderFieldParams = (m, v) => {
   var i = v.indexOf('=');
   if (i >= 0) {
     // A quoted string with internal quotes is invalid for all the possible
     // values of this header field.
     m[v.substring(0, i).trim()] = v.substring(i + 1).trim()
@@ -190,18 +212,18 @@ this.PushCrypto = {
     return crypto.subtle.generateKey(ECDH_KEY, true, ['deriveBits'])
       .then(cryptoKey =>
          Promise.all([
            crypto.subtle.exportKey('raw', cryptoKey.publicKey),
            crypto.subtle.exportKey('jwk', cryptoKey.privateKey)
          ]));
   },
 
-  decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey,
-            aSalt, aRs, aAuthenticationSecret) {
+  decodeMsg(aData, aPrivateKey, aPublicKey, aSenderPublicKey, aSalt, aRs,
+            aAuthenticationSecret, aPadSize) {
 
     if (aData.byteLength === 0) {
       // Zero length messages will be passed as null.
       return Promise.resolve(null);
     }
 
     // The last chunk of data must be less than aRs, if it is not return an
     // error.
@@ -214,31 +236,34 @@ this.PushCrypto = {
       crypto.subtle.importKey('raw', senderKey, ECDH_KEY,
                               false, ['deriveBits']),
       crypto.subtle.importKey('jwk', aPrivateKey, ECDH_KEY,
                               false, ['deriveBits'])
     ])
     .then(([appServerKey, subscriptionPrivateKey]) =>
           crypto.subtle.deriveBits({ name: 'ECDH', public: appServerKey },
                                    subscriptionPrivateKey, 256))
-    .then(ikm => this._deriveKeyAndNonce(new Uint8Array(ikm),
+    .then(ikm => this._deriveKeyAndNonce(aPadSize,
+                                         new Uint8Array(ikm),
                                          base64UrlDecode(aSalt),
                                          aPublicKey,
                                          senderKey,
                                          aAuthenticationSecret))
     .then(r =>
       // AEAD_AES_128_GCM expands ciphertext to be 16 octets longer.
       Promise.all(chunkArray(aData, aRs + 16).map((slice, index) =>
-        this._decodeChunk(slice, index, r[1], r[0]))))
+        this._decodeChunk(aPadSize, slice, index, r[1], r[0]))))
     .then(r => concatArray(r));
   },
 
-  _deriveKeyAndNonce(ikm, salt, receiverKey, senderKey, authenticationSecret) {
+  _deriveKeyAndNonce(padSize, ikm, salt, receiverKey, senderKey,
+                     authenticationSecret) {
     var kdfPromise;
     var context;
+    var encryptInfo;
     // The authenticationSecret, when present, is mixed with the ikm using HKDF.
     // This is its primary purpose.  However, since the authentication secret
     // was added at the same time that the info string was changed, we also use
     // its presence to change how the final info string is calculated:
     //
     // 1. When there is no authenticationSecret, the context string is simply
     // "Content-Encoding: <blah>". This corresponds to old, deprecated versions
     // of the content encoding.  This should eventually be removed: bug 1230038.
@@ -255,48 +280,72 @@ this.PushCrypto = {
 
       // We also use the presence of the authentication secret to indicate that
       // we have extra context to add to the info parameter.
       context = concatArray([
         new Uint8Array([0]), P256DH_INFO,
         this._encodeLength(receiverKey), receiverKey,
         this._encodeLength(senderKey), senderKey
       ]);
+      // Finally, we use the pad size to infer the content encoding.
+      encryptInfo = padSize == 2 ? AESGCM_ENCRYPT_INFO :
+                                   AESGCM128_ENCRYPT_INFO;
     } else {
+      if (padSize == 2) {
+        throw new Error("aesgcm encoding requires an authentication secret");
+      }
       kdfPromise = Promise.resolve(new hkdf(salt, ikm));
       context = new Uint8Array(0);
+      encryptInfo = AESGCM128_ENCRYPT_INFO;
     }
     return kdfPromise.then(kdf => Promise.all([
-      kdf.extract(concatArray([ENCRYPT_INFO, context]), 16)
+      kdf.extract(concatArray([encryptInfo, context]), 16)
         .then(gcmBits => crypto.subtle.importKey('raw', gcmBits, 'AES-GCM', false,
                                                  ['decrypt'])),
       kdf.extract(concatArray([NONCE_INFO, context]), 12)
     ]));
   },
 
   _encodeLength(buffer) {
     return new Uint8Array([0, buffer.byteLength]);
   },
 
-  _decodeChunk(aSlice, aIndex, aNonce, aKey) {
+  _decodeChunk(aPadSize, aSlice, aIndex, aNonce, aKey) {
     let params = {
       name: 'AES-GCM',
       iv: generateNonce(aNonce, aIndex)
     };
     return crypto.subtle.decrypt(params, aKey, aSlice)
-      .then(decoded => {
-        decoded = new Uint8Array(decoded);
-        if (decoded.length == 0) {
-          return Promise.reject(new Error('Decoded array is too short!'));
-        } else if (decoded[0] > decoded.length) {
-          return Promise.reject(new Error ('Padding is wrong!'));
-        } else {
-          // All padded bytes must be zero except the first one.
-          for (var i = 1; i <= decoded[0]; i++) {
-            if (decoded[i] != 0) {
-              return Promise.reject(new Error('Padding is wrong!'));
-            }
-          }
-          return decoded.slice(decoded[0] + 1);
-        }
-      });
-  }
+      .then(decoded => this._unpadChunk(aPadSize, new Uint8Array(decoded)));
+  },
+
+  /**
+   * Removes padding from a decrypted chunk.
+   *
+   * @param {Number} padSize The size of the padding length prepended to each
+   *  chunk. For aesgcm, the padding length is expressed as a 16-bit unsigned
+   *  big endian integer. For aesgcm128, the padding is an 8-bit integer.
+   * @param {Uint8Array} decoded The decrypted, padded chunk.
+   * @returns {Uint8Array} The chunk with padding removed.
+   */
+  _unpadChunk(padSize, decoded) {
+    if (padSize < 1 || padSize > 2) {
+      throw new Error('Unsupported pad size');
+    }
+    if (decoded.length < padSize) {
+      throw new Error('Decoded array is too short!');
+    }
+    var pad = decoded[0];
+    if (padSize == 2) {
+      pad = (pad << 8) | decoded[1];
+    }
+    if (pad > decoded.length) {
+      throw new Error ('Padding is wrong!');
+    }
+    // All padded bytes must be zero except the first one.
+    for (var i = padSize; i <= pad; i++) {
+      if (decoded[i] !== 0) {
+        throw new Error('Padding is wrong!');
+      }
+    }
+    return decoded.slice(pad + padSize);
+  },
 };
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -806,17 +806,18 @@ this.PushService = {
       if (cryptoParams) {
         decodedPromise = PushCrypto.decodeMsg(
           message,
           record.p256dhPrivateKey,
           record.p256dhPublicKey,
           cryptoParams.dh,
           cryptoParams.salt,
           cryptoParams.rs,
-          cryptoParams.auth ? record.authenticationSecret : null
+          cryptoParams.auth ? record.authenticationSecret : null,
+          cryptoParams.padSize
         );
       } else {
         decodedPromise = Promise.resolve(null);
       }
       return decodedPromise.then(message => {
         if (shouldNotify) {
           notified = this._notifyApp(record, message);
         }
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -109,16 +109,17 @@ this.PushServiceAndroidGCM = {
       let message = null;
       let cryptoParams = null;
 
       if (data.message && data.enc && (data.enckey || data.cryptokey)) {
         let headers = {
           encryption_key: data.enckey,
           crypto_key: data.cryptokey,
           encryption: data.enc,
+          encoding: data.con,
         };
         cryptoParams = getCryptoParams(headers);
         // Ciphertext is (urlsafe) Base 64 encoded.
         message = base64UrlDecode(data.message);
       }
 
       console.debug("Delivering message to main PushService:", message, cryptoParams);
       this._mainPushService.receivedPushMessage(
--- a/dom/push/PushServiceHttp2.jsm
+++ b/dom/push/PushServiceHttp2.jsm
@@ -150,16 +150,17 @@ PushChannelListener.prototype = {
       aStatusCode);
     if (Components.isSuccessCode(aStatusCode) &&
         this._mainListener &&
         this._mainListener._pushService) {
       let headers = {
         encryption_key: getHeaderField(aRequest, "Encryption-Key"),
         crypto_key: getHeaderField(aRequest, "Crypto-Key"),
         encryption: getHeaderField(aRequest, "Encryption"),
+        encoding: getHeaderField(aRequest, "Content-Encoding"),
       };
       let cryptoParams = getCryptoParams(headers);
       let msg = concatArray(this._message);
 
       this._mainListener._pushService._pushChannelOnStop(this._mainListener.uri,
                                                          this._ackUri,
                                                          msg,
                                                          cryptoParams);
--- a/dom/push/test/mockpushserviceparent.js
+++ b/dom/push/test/mockpushserviceparent.js
@@ -43,17 +43,17 @@ MockWebSocketParent.prototype = {
 
   asyncOpen(uri, origin, windowId, listener, context) {
     this._listener = listener;
     this._context = context;
     waterfall(() => this._listener.onStart(this._context));
   },
 
   sendMsg(msg) {
-    sendAsyncMessage("client-msg", msg);
+    sendAsyncMessage("socket-client-msg", msg);
   },
 
   close() {
     waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
   },
 
   serverSendMsg(msg) {
     waterfall(() => this._listener.onMessageAvailable(this._context, msg),
@@ -78,17 +78,17 @@ MockNetworkInfo.prototype = {
 };
 
 var pushService = Cc["@mozilla.org/push/Service;1"].
                   getService(Ci.nsIPushService).
                   wrappedJSObject;
 
 var mockWebSocket;
 
-addMessageListener("setup", function () {
+addMessageListener("socket-setup", function () {
   mockWebSocket = new Promise((resolve, reject) => {
     var mockSocket = null;
     pushService.replaceServiceBackend({
       serverURI: "wss://push.example.org/",
       networkInfo: new MockNetworkInfo(),
       makeWebSocket(uri) {
         if (!mockSocket) {
           mockSocket = new MockWebSocketParent(uri);
@@ -96,20 +96,77 @@ addMessageListener("setup", function () 
         }
 
         return mockSocket;
       }
     });
   });
 });
 
-addMessageListener("teardown", function () {
+addMessageListener("socket-teardown", function () {
   mockWebSocket.then(socket => {
     socket.close();
     pushService.restoreServiceBackend();
   });
 });
 
-addMessageListener("server-msg", function (msg) {
+addMessageListener("socket-server-msg", function (msg) {
   mockWebSocket.then(socket => {
     socket.serverSendMsg(msg);
   });
 });
+
+var MockService = {
+  requestID: 1,
+  resolvers: new Map(),
+
+  sendRequest(name, params) {
+    return new Promise((resolve, reject) => {
+      let id = this.requestID++;
+      this.resolvers.set(id, { resolve, reject });
+      sendAsyncMessage("service-request", {
+        name: name,
+        id: id,
+        params: params,
+      });
+    });
+  },
+
+  handleResponse(response) {
+    if (!this.resolvers.has(response.id)) {
+      Cu.reportError(`Unexpected response for request ${response.id}`);
+      return;
+    }
+    let resolver = this.resolvers.get(response.id);
+    this.resolvers.delete(response.id);
+    if (response.error) {
+      resolver.reject(response.error);
+    } else {
+      resolver.resolve(response.result);
+    }
+  },
+
+  init() {},
+
+  register(pageRecord) {
+    return this.sendRequest("register", pageRecord);
+  },
+
+  registration(pageRecord) {
+    return this.sendRequest("registration", pageRecord);
+  },
+
+  unregister(pageRecord) {
+    return this.sendRequest("unregister", pageRecord);
+  },
+};
+
+addMessageListener("service-replace", function () {
+  pushService.service = MockService;
+});
+
+addMessageListener("service-restore", function () {
+  pushService.service = null;
+});
+
+addMessageListener("service-response", function (response) {
+  MockService.handleResponse(response);
+});
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -37,17 +37,17 @@ http://creativecommons.org/licenses/publ
       channelID,
       status: 200,
       pushEndpoint: "https://example.com/endpoint/1"
     }));
   };
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(mockSocket);
+    yield setupPrefsAndMockSocket(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_multiple_register.html
+++ b/dom/push/test/test_multiple_register.html
@@ -114,14 +114,14 @@ http://creativecommons.org/licenses/publ
     .then(getEndpoint)
     .then(unregisterPushNotification)
     .then(unregister)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_different_scope.html
+++ b/dom/push/test/test_multiple_register_different_scope.html
@@ -109,14 +109,14 @@ http://creativecommons.org/licenses/publ
             .then(_ => unregister(swrB))
         )
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_during_service_activation.html
+++ b/dom/push/test/test_multiple_register_during_service_activation.html
@@ -52,18 +52,18 @@ http://creativecommons.org/licenses/publ
       }, err => {
         ok(false, "could not register for push notification");
         throw err;
       });
   }
 
   function setupMultipleSubscriptions(swr) {
     // We need to do this to restart service so that a queue will be formed.
-    teardownMockPushService();
-    setupMockPushService(new MockWebSocket());
+    teardownMockPushSocket();
+    setupMockPushSocket(new MockWebSocket());
 
     return Promise.all([
       subscribe(swr),
       subscribe(swr)
     ]).then(a => {
       ok(a[0].endpoint == a[1].endpoint, "setupMultipleSubscriptions - Got the same endpoint back.");
       return a[0];
     }, err => {
@@ -94,14 +94,14 @@ http://creativecommons.org/licenses/publ
         .then(sub => unsubscribe(sub))
         .then(_ => unregister(swr))
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_permissions.html
+++ b/dom/push/test/test_permissions.html
@@ -26,17 +26,17 @@ http://creativecommons.org/licenses/publ
 <script class="testbody" type="text/javascript">
 
   function debug(str) {
   //  console.log(str + "\n");
   }
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(new MockWebSocket());
+    yield setupPrefsAndMockSocket(new MockWebSocket());
     yield setPushPermission(false);
 
     var url = "worker.js" + "?" + Math.random();
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   add_task(function* denySubscribe() {
     try {
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -41,17 +41,17 @@ http://creativecommons.org/licenses/publ
       channelID,
       status: 200,
       pushEndpoint: "https://example.com/endpoint/1"
     }));
   };
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(mockSocket);
+    yield setupPrefsAndMockSocket(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_serviceworker_lifetime.html
+++ b/dom/push/test/test_serviceworker_lifetime.html
@@ -330,14 +330,14 @@
       .then(subTest(test3))
       .then(unregisterPushNotification)
       .then(unregister)
       .catch(function(e) {
         ok(false, "Some test failed with error " + e)
       }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(mockSocket).then(_ => runTest());
+  setupPrefsAndMockSocket(mockSocket).then(_ => runTest());
   SpecialPowers.addPermission('desktop-notification', true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_subscription_change.html
+++ b/dom/push/test/test_subscription_change.html
@@ -22,17 +22,17 @@ http://creativecommons.org/licenses/publ
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="text/javascript">
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(new MockWebSocket());
+    yield setupPrefsAndMockSocket(new MockWebSocket());
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_try_registering_offline_disabled.html
+++ b/dom/push/test/test_try_registering_offline_disabled.html
@@ -291,14 +291,14 @@ http://creativecommons.org/licenses/publ
     .then(_ => runTest2())
     .then(_ => runTest3())
     .then(_ => runTest4())
     .then(_ => runTest5())
     .then(_ => runTest6())
     .then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_unregister.html
+++ b/dom/push/test/test_unregister.html
@@ -75,15 +75,15 @@ http://creativecommons.org/licenses/publ
     .then(unregisterPushNotification)
     .then(unregisterAgain)
     .then(unregisterSW)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
 
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -1,31 +1,70 @@
 (function (g) {
   "use strict";
 
   let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
   let chromeScript = SpecialPowers.loadChromeScript(url);
 
+  /**
+   * Replaces `PushService.jsm` with a mock implementation that handles requests
+   * from the DOM API. This allows tests to simulate local errors and error
+   * reporting, bypassing the `PushService.jsm` machinery.
+   */
+  function replacePushService(mockService) {
+    chromeScript.sendSyncMessage("service-replace");
+    chromeScript.addMessageListener("service-request", function(msg) {
+      let promise;
+      try {
+        let handler = mockService[msg.name];
+        promise = Promise.resolve(handler(msg.params));
+      } catch (error) {
+        promise = Promise.reject(error);
+      }
+      promise.then(result => {
+        chromeScript.sendAsyncMessage("service-response", {
+          id: msg.id,
+          result: result,
+        });
+      }, error => {
+        chromeScript.sendAsyncMessage("service-response", {
+          id: msg.id,
+          error: error,
+        });
+      });
+    });
+  }
+
+  function restorePushService() {
+    chromeScript.sendSyncMessage("service-restore");
+  }
+
   let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
 
   let currentMockSocket = null;
 
-  function setupMockPushService(mockWebSocket) {
+  /**
+   * Sets up a mock connection for the WebSocket backend. This only replaces
+   * the transport layer; `PushService.jsm` still handles DOM API requests,
+   * observes permission changes, writes to IndexedDB, and notifies service
+   * workers of incoming push messages.
+   */
+  function setupMockPushSocket(mockWebSocket) {
     currentMockSocket = mockWebSocket;
     currentMockSocket._isActive = true;
-    chromeScript.sendSyncMessage("setup");
-    chromeScript.addMessageListener("client-msg", function(msg) {
+    chromeScript.sendSyncMessage("socket-setup");
+    chromeScript.addMessageListener("socket-client-msg", function(msg) {
       mockWebSocket.handleMessage(msg);
     });
   }
 
-  function teardownMockPushService() {
+  function teardownMockPushSocket() {
     if (currentMockSocket) {
       currentMockSocket._isActive = false;
-      chromeScript.sendSyncMessage("teardown");
+      chromeScript.sendSyncMessage("socket-teardown");
     }
   }
 
   /**
    * Minimal implementation of web sockets for use in testing. Forwards
    * messages to a mock web socket in the parent process that is used
    * by the push service.
    */
@@ -85,58 +124,70 @@
         break;
       default:
         throw new Error("Unexpected message: " + messageType);
       }
     },
 
     serverSendMsg(msg) {
       if (this._isActive) {
-        chromeScript.sendAsyncMessage("server-msg", msg);
+        chromeScript.sendAsyncMessage("socket-server-msg", msg);
       }
     },
   };
 
   g.MockWebSocket = MockWebSocket;
-  g.setupMockPushService = setupMockPushService;
-  g.teardownMockPushService = teardownMockPushService;
+  g.setupMockPushSocket = setupMockPushSocket;
+  g.teardownMockPushSocket = teardownMockPushSocket;
+  g.replacePushService = replacePushService;
+  g.restorePushService = restorePushService;
 }(this));
 
 // Remove permissions and prefs when the test finishes.
 SimpleTest.registerCleanupFunction(() => {
   new Promise(resolve => {
     SpecialPowers.flushPermissions(_ => {
       SpecialPowers.flushPrefEnv(resolve);
     });
   }).then(_ => {
-    teardownMockPushService();
+    teardownMockPushSocket();
+    restorePushService();
   });
 });
 
 function setPushPermission(allow) {
   return new Promise(resolve => {
     SpecialPowers.pushPermissions([
       { type: "desktop-notification", allow, context: document },
       ], resolve);
   });
 }
 
-function setupPrefsAndMock(mockSocket) {
+function setupPrefs() {
   return new Promise(resolve => {
-    setupMockPushService(mockSocket);
     SpecialPowers.pushPrefEnv({"set": [
       ["dom.push.enabled", true],
       ["dom.push.connection.enabled", true],
       ["dom.serviceWorkers.exemptFromPerDomainMax", true],
       ["dom.serviceWorkers.enabled", true],
       ["dom.serviceWorkers.testing.enabled", true]
       ]}, resolve);
   });
 }
 
+function setupPrefsAndReplaceService(mockService) {
+  replacePushService(mockService);
+  return setupPrefs();
+}
+
+function setupPrefsAndMockSocket(mockSocket) {
+  setupMockPushSocket(mockSocket);
+  return setupPrefs();
+}
+
 function injectControlledFrame(target = document.body) {
   return new Promise(function(res, rej) {
     var iframe = document.createElement("iframe");
     iframe.src = "/tests/dom/push/test/frame.html";
 
     var controlledFrame = {
       remove() {
         target.removeChild(iframe);
--- a/dom/push/test/xpcshell/head.js
+++ b/dom/push/test/xpcshell/head.js
@@ -17,18 +17,16 @@ Cu.import('resource://gre/modules/Object
 XPCOMUtils.defineLazyModuleGetter(this, 'PlacesTestUtils',
                                   'resource://testing-common/PlacesTestUtils.jsm');
 XPCOMUtils.defineLazyServiceGetter(this, 'PushServiceComponent',
                                    '@mozilla.org/push/Service;1', 'nsIPushService');
 
 const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
 const servicePrefs = new Preferences('dom.push.');
 
-const DEFAULT_TIMEOUT = 5000;
-
 const WEBSOCKET_CLOSE_GOING_AWAY = 1001;
 
 var isParent = Cc['@mozilla.org/xre/runtime;1']
                  .getService(Ci.nsIXULRuntime).processType ==
                  Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
 
 // Stop and clean up after the PushService.
 Services.obs.addObserver(function observe(subject, topic, data) {
@@ -94,41 +92,16 @@ function promiseObserverNotification(top
       }
       Services.obs.removeObserver(observe, topic, false);
       resolve({subject, data});
     }, topic, false);
   });
 }
 
 /**
- * Waits for a promise to settle. Returns a rejected promise if the promise
- * is not resolved or rejected within the given delay.
- *
- * @param {Promise} promise The pending promise.
- * @param {Number} delay The time to wait before rejecting the promise.
- * @param {String} [message] The rejection message if the promise times out.
- * @returns {Promise} A promise that settles with the value of the pending
- *  promise, or rejects if the pending promise times out.
- */
-function waitForPromise(promise, delay, message = 'Timed out waiting on promise') {
-  let timeoutDefer = Promise.defer();
-  let id = setTimeout(() => timeoutDefer.reject(new Error(message)), delay);
-  return Promise.race([
-    promise.then(value => {
-      clearTimeout(id);
-      return value;
-    }, error => {
-      clearTimeout(id);
-      throw error;
-    }),
-    timeoutDefer.promise
-  ]);
-}
-
-/**
  * Wraps an object in a proxy that traps property gets and returns stubs. If
  * the stub is a function, the original value will be passed as the first
  * argument. If the original value is a function, the proxy returns a wrapper
  * that calls the stub; otherwise, the stub is called as a getter.
  *
  * @param {Object} target The object to wrap.
  * @param {Object} stubs An object containing stubbed values and functions.
  * @returns {Proxy} A proxy that returns stubs for property gets.
--- a/dom/push/test/xpcshell/test_clear_origin_data.js
+++ b/dom/push/test/xpcshell/test_clear_origin_data.js
@@ -132,11 +132,10 @@ add_task(function* test_webapps_cleardat
   // Removes all records for all scopes with the same app ID, where
   // `inIsolatedMozBrowser` is true.
   yield clearForPattern(testRecords, { appId: 2, inIsolatedMozBrowser: true });
 
   // Removes all records where `inIsolatedMozBrowser` is true.
   yield clearForPattern(testRecords, { inIsolatedMozBrowser: true });
 
   equal(testRecords.length, 0, 'Should remove all test records');
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister');
+  yield unregisterPromise;
 });
new file mode 100644
--- /dev/null
+++ b/dom/push/test/xpcshell/test_crypto.js
@@ -0,0 +1,245 @@
+'use strict';
+
+const {
+  base64UrlDecode,
+  getCryptoParams,
+  PushCrypto,
+} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* test_crypto_getCryptoParams() {
+  let testData = [
+  // These headers should parse correctly.
+  {
+    desc: 'aesgcm with multiple keys',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI',
+      encryption: 'keyid=p256dh;salt=upk1yFkp1xI',
+    },
+    params: {
+      dh: 'Iy1Je2Kv11A',
+      salt: 'upk1yFkp1xI',
+      rs: 4096,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm with quoted key param',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh="byfHbUffc-k"',
+      encryption: 'salt=C11AvAsp6Gc',
+    },
+    params: {
+      dh: 'byfHbUffc-k',
+      salt: 'C11AvAsp6Gc',
+      rs: 4096,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm with Crypto-Key and rs = 24',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh="ybuT4VDz-Bg"',
+      encryption: 'salt=H7U7wcIoIKs; rs=24',
+    },
+    params: {
+      dh: 'ybuT4VDz-Bg',
+      salt: 'H7U7wcIoIKs',
+      rs: 24,
+      auth: true,
+      padSize: 2,
+    },
+  }, {
+    desc: 'aesgcm128 with Encryption-Key and rs = 2',
+    headers: {
+      encoding: 'aesgcm128',
+      encryption_key: 'keyid=legacy; dh=LqrDQuVl9lY',
+      encryption: 'keyid=legacy; salt=YngI8B7YapM; rs=2',
+    },
+    params: {
+      dh: 'LqrDQuVl9lY',
+      salt: 'YngI8B7YapM',
+      rs: 2,
+      auth: false,
+      padSize: 1,
+    },
+  }, {
+    desc: 'aesgcm128 with Encryption-Key',
+    headers: {
+      encoding: 'aesgcm128',
+      encryption_key: 'keyid=v2; dh=VA6wmY1IpiE',
+      encryption: 'keyid=v2; salt=khtpyXhpDKM',
+    },
+    params: {
+      dh: 'VA6wmY1IpiE',
+      salt: 'khtpyXhpDKM',
+      rs: 4096,
+      auth: false,
+      padSize: 1,
+    }
+  }, {
+    desc: 'aesgcm128 with Crypto-Key',
+    headers: {
+      encoding: 'aesgcm128',
+      crypto_key: 'keyid=v2; dh=VA6wmY1IpiE',
+      encryption: 'keyid=v2; salt=F0Im7RtGgNY',
+    },
+    params: {
+      dh: 'VA6wmY1IpiE',
+      salt: 'F0Im7RtGgNY',
+      rs: 4096,
+      auth: true,
+      padSize: 1,
+    },
+  },
+
+  // These headers should be rejected.
+  {
+    desc: 'Invalid encoding',
+    headers: {
+      encoding: 'nonexistent',
+    },
+    params: null,
+  }, {
+    desc: 'Invalid record size',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh=pbmv1QkcEDY',
+      encryption: 'dh=Esao8aTBfIk;rs=bad',
+    },
+    params: null,
+  }, {
+    desc: 'Insufficiently large record size',
+    headers: {
+      encoding: 'aesgcm',
+      crypto_key: 'dh=fK0EXaw5IU8',
+      encryption: 'salt=orbLLmlbJfM;rs=1',
+    },
+    params: null,
+  }, {
+    desc: 'aesgcm with Encryption-Key',
+    headers: {
+      encoding: 'aesgcm',
+      encryption_key: 'dh=FplK5KkvUF0',
+      encryption: 'salt=p6YHhFF3BQY',
+    },
+    params: null,
+  }];
+
+  for (let test of testData) {
+    let params = getCryptoParams(test.headers);
+    deepEqual(params, test.params, test.desc);
+  }
+});
+
+add_task(function* test_crypto_decodeMsg() {
+  let privateKey = {
+    "crv": "P-256",
+    "d": "4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg",
+    "ext": true,
+    "key_ops": ["deriveBits"],
+    "kty":"EC",
+    "x":"sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4",
+    "y":"vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs",
+  };
+  let publicKey = base64UrlDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs');
+
+  let expectedSuccesses = [{
+    desc: 'padSize = 2, rs = 24, pad = 0',
+    result: 'Some message',
+    data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
+    senderPublicKey: 'BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
+    salt: 'zCU18Rw3A5aB_Xi-vfixmA',
+    rs: 24,
+    authSecret: 'aTDc6JebzR6eScy2oLo4RQ',
+    padSize: 2,
+  }, {
+    desc: 'padSize = 2, rs = 8, pad = 16',
+    result: 'Yet another message',
+    data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg',
+    senderPublicKey: 'BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
+    salt: 'ZFhzj0S-n29g9P2p4-I7tA',
+    rs: 8,
+    authSecret: '6plwZnSpVUbF7APDXus3UQ',
+    padSize: 2,
+  }, {
+    desc: 'padSize = 1, rs = 4096, pad = 2',
+    result: 'aesgcm128 encrypted message',
+    data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE',
+    senderPublicKey: 'BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
+    salt: 'btxxUtclbmgcc30b9rT3Bg',
+    rs: 4096,
+    padSize: 1,
+  }, {
+    desc: 'padSize = 2, rs = 3, pad = 0',
+    result: 'Small record size',
+    data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM',
+    senderPublicKey: 'BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
+    salt: '5LIDBXbvkBvvb7ZdD-T4PQ',
+    rs: 3,
+    authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ',
+    padSize: 2,
+  }, {
+    desc: 'padSize = 1, rs = 4096, auth secret, pad = 8',
+    result: 'aesgcm128 with auth secret',
+    data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r',
+    senderPublicKey: 'BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
+    salt: 'aGBpoKklLtrLcAUCcCr7JQ',
+    rs: 4096,
+    authSecret: 'Sxb6u0gJIhGEogyLawjmCw',
+    padSize: 1,
+  }];
+  for (let test of expectedSuccesses) {
+    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
+    let result = yield PushCrypto.decodeMsg(base64UrlDecode(test.data),
+                                            privateKey, publicKey,
+                                            test.senderPublicKey, test.salt,
+                                            test.rs, authSecret, test.padSize);
+    let decoder = new TextDecoder('utf-8');
+    equal(decoder.decode(new Uint8Array(result)), test.result, test.desc);
+  }
+
+  let expectedFailures = [{
+    desc: 'Missing padding',
+    data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg',
+    senderPublicKey: 'BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
+    salt: 'Czx2i18rar8XWOXAVDnUuw',
+    rs: 4096,
+    padSize: 1,
+  }, {
+    desc: 'padSize > rs',
+    data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu',
+    senderPublicKey: 'BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
+    salt: 'NQVTKhB0rpL7ZzKkotTGlA',
+    rs: 1,
+    authSecret: '6plwZnSpVUbF7APDXus3UQ',
+    padSize: 2,
+  }, {
+    desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret',
+    data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8',
+    senderPublicKey: 'BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+    salt: 'c6JQl9eJ0VvwrUVCQDxY7Q',
+    rs: 4096,
+    authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
+    padSize: 2,
+  }, {
+    desc: 'Truncated input',
+    data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
+    rs: 25,
+  }];
+  for (let test of expectedFailures) {
+    let authSecret = test.authSecret ? base64UrlDecode(test.authSecret) : null;
+    yield rejects(
+      PushCrypto.decodeMsg(base64UrlDecode(test.data), privateKey, publicKey,
+                           test.senderPublicKey, test.salt, test.rs,
+                           authSecret, test.padSize),
+      test.desc
+    );
+  }
+});
--- a/dom/push/test/xpcshell/test_drop_expired.js
+++ b/dom/push/test/xpcshell/test_drop_expired.js
@@ -115,37 +115,34 @@ add_task(function* setUp() {
             status: 200,
             uaid: userAgentID,
           }));
         },
       });
     },
   });
 
-  yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for subscription change event on startup');
+  yield subChangePromise;
 });
 
 add_task(function* test_site_visited() {
   let subChangePromise = promiseObserverNotification(
     PushServiceComponent.subscriptionChangeTopic,
     (subject, data) => data == 'https://example.xyz/expired-quota-exceeded'
   );
 
   yield visitURI(quotaURI, Date.now());
   PushService.observe(null, 'idle-daily', '');
 
-  yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for subscription change event after visit');
+  yield subChangePromise;
 });
 
 add_task(function* test_perm_restored() {
   let subChangePromise = promiseObserverNotification(
     PushServiceComponent.subscriptionChangeTopic,
     (subject, data) => data == 'https://example.info/expired-perm-revoked'
   );
 
   Services.perms.add(permURI, 'desktop-notification',
     Ci.nsIPermissionManager.ALLOW_ACTION);
 
-  yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for subscription change event after permission');
+  yield subChangePromise;
 });
--- a/dom/push/test/xpcshell/test_notification_ack.js
+++ b/dom/push/test/xpcshell/test_notification_ack.js
@@ -113,13 +113,11 @@ add_task(function* test_notification_ack
           default:
             ok(false, 'Unexpected acknowledgement ' + acks);
           }
         }
       });
     }
   });
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
-  yield waitForPromise(ackPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for multiple acknowledgements');
+  yield notifyPromise;
+  yield ackPromise;
 });
--- a/dom/push/test/xpcshell/test_notification_data.js
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -109,60 +109,62 @@ add_task(function* test_notification_ack
         onACK(request) {
           if (ackDone) {
             ackDone(request.updates);
           }
         }
       });
     }
   });
-  yield waitForPromise(setupDonePromise, DEFAULT_TIMEOUT,
-                       'Timed out waiting for notifications');
+  yield setupDonePromise;
 });
 
 add_task(function* test_notification_ack_data() {
   let allTestData = [
     {
       channelID: 'subscription1',
       version: 'v1',
       send: {
         headers: {
           encryption_key: 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
           encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+          encoding: 'aesgcm128',
         },
         data: 'NwrrOWPxLE8Sv5Rr0Kep7n0-r_j3rsYrUw_CXPo',
         version: 'v1',
       },
       receive: {
         scope: 'https://example.com/page/1',
         data: 'Some message'
       }
     },
     {
       channelID: 'subscription2',
       version: 'v2',
       send: {
         headers: {
           encryption_key: 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
           encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+          encoding: 'aesgcm128',
         },
         data: 'Zt9dEdqgHlyAL_l83385aEtb98ZBilz5tgnGgmwEsl5AOCNgesUUJ4p9qUU',
       },
       receive: {
         scope: 'https://example.com/page/2',
         data: 'Some message'
       }
     },
     {
       channelID: 'subscription3',
       version: 'v3',
       send: {
         headers: {
           encryption_key: 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
           encryption: 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+          encoding: 'aesgcm128',
         },
         data: 'LKru3ZzxBZuAxYtsaCfaj_fehkrIvqbVd1iSwnwAUgnL-cTeDD-83blxHXTq7r0z9ydTdMtC3UjAcWi8LMnfY-BFzi0qJAjGYIikDA',
       },
       receive: {
         scope: 'https://example.com/page/3',
         data: 'Some message'
       }
     },
@@ -170,48 +172,68 @@ add_task(function* test_notification_ack
     // header field.  No padding or record size changes.
     {
       channelID: 'subscription1',
       version: 'v4',
       send: {
         headers: {
           crypto_key: 'keyid=v4;dh="BHqG01j7rOfp12BEDzxWXxlCaU4cdOx2DZAwCt3QuzEsnXN9lCna9QmZCkVpXsx7sAlaEmtl_VfF1lHlFS7XWcA"',
           encryption: 'keyid="v4";salt="X5-iy5rzhm4naNmMHdSYJw"',
+          encoding: 'aesgcm128',
         },
         data: '7YlxyNlZsNX4UNknHxzTqFrcrzz58W95uXBa0iY',
       },
       receive: {
         scope: 'https://example.com/page/1',
         data: 'Some message'
       }
     },
+    // A message encoded with `aesgcm` (2 bytes of padding).
+    {
+      channelID: 'subscription1',
+      version: 'v5',
+      send: {
+        headers: {
+          crypto_key: 'dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"',
+          encryption: 'salt="C14Wb7rQTlXzrgcPHtaUzw"',
+          encoding: 'aesgcm',
+        },
+        data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe',
+      },
+      receive: {
+        scope: 'https://example.com/page/1',
+        data: 'Another message'
+      }
+    },
     // A message with 17 bytes of padding and rs of 24
     {
       channelID: 'subscription2',
       version: 'v5',
       send: {
         headers: {
           crypto_key: 'keyid="v5"; dh="BJhyKIH5P30YUKn1bolj_LMnael1-KZT_aGXgD2CRspBfv9gcUhVAmpxToZrw7QQEKl9K83b3zcqNY6G_dFhEsI"',
           encryption: 'keyid=v5;salt="bLmqCy550eK1Ao41tD7orA";rs=24',
+          encoding: 'aesgcm128',
         },
         data: 'SQDlDg1ftLkM_ruZlmyB2bk9L78HYtkcbA-y4-uAxwL-G4KtOA-J-A_rJ007Vi6NUkQe9K4kSZeIBrIUpmGv',
       },
       receive: {
         scope: 'https://example.com/page/2',
         data: 'Some message'
       }
     },
     // A message without key identifiers.
     {
       channelID: 'subscription3',
       version: 'v6',
       send: {
         headers: {
           crypto_key: 'dh="BEgnDmVw9Gcn1fWA5t53Jtpsgfewk_pzsjSc_PBPpPmROWGQA2v8ESrSsQgosNXx0o-uMMhi9tDAUeks3380kd8"',
           encryption: 'salt=T9DM8bNxuMHRVTn4LzkJDQ',
+          encoding: 'aesgcm128',
         },
         data: '7KUCi0dBBJbWmsYTqEqhFrgTv4ZOo_BmQRQ_2kY',
       },
       receive: {
         scope: 'https://example.com/page/3',
         data: 'Some message'
       }
     },
@@ -239,15 +261,12 @@ add_task(function* test_notification_ack
     msg.messageType = 'notification';
     msg.channelID = testData.channelID;
     msg.version = testData.version;
     server.serverSendMsg(JSON.stringify(msg));
 
     return Promise.all([messageReceived, ackReceived]);
   };
 
-  yield waitForPromise(
-    allTestData.reduce((p, testData) => {
-      return p.then(_ => sendAndReceive(testData));
-    }, Promise.resolve()),
-    DEFAULT_TIMEOUT,
-    'Timed out waiting for message exchange to complete');
+  yield allTestData.reduce((p, testData) => {
+    return p.then(_ => sendAndReceive(testData));
+  }, Promise.resolve());
 });
--- a/dom/push/test/xpcshell/test_notification_duplicate.js
+++ b/dom/push/test/xpcshell/test_notification_duplicate.js
@@ -68,20 +68,18 @@ add_task(function* test_notification_dup
             }]
           }));
         },
         onACK: ackDone
       });
     }
   });
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
-  yield waitForPromise(ackPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for stale acknowledgement');
+  yield notifyPromise;
+  yield ackPromise;
 
   let staleRecord = yield db.getByKeyID(
     '8d2d9400-3597-4c5a-8a38-c546b0043bcc');
   strictEqual(staleRecord.version, 2, 'Wrong stale record version');
 
   let updatedRecord = yield db.getByKeyID(
     '27d1e393-03ef-4c72-a5e6-9e890dfccad0');
   strictEqual(updatedRecord.version, 3, 'Wrong updated record version');
--- a/dom/push/test/xpcshell/test_notification_error.js
+++ b/dom/push/test/xpcshell/test_notification_error.js
@@ -82,28 +82,23 @@ add_task(function* test_notification_err
         },
         // Should acknowledge all received updates, even if updating
         // IndexedDB fails.
         onACK: ackDone
       });
     }
   });
 
-  yield waitForPromise(
-    notifyPromise,
-    DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications'
-  );
+  yield notifyPromise;
   ok(scopes.includes('https://example.com/a'),
     'Missing scope for notification A');
   ok(scopes.includes('https://example.com/c'),
     'Missing scope for notification C');
 
-  yield waitForPromise(ackPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for acknowledgements');
+  yield ackPromise;
 
   let aRecord = yield db.getByIdentifiers({scope: 'https://example.com/a',
                                            originAttributes: originAttributes });
   equal(aRecord.channelID, 'f04f1e46-9139-4826-b2d1-9411b0821283',
     'Wrong channel ID for record A');
   strictEqual(aRecord.version, 2,
     'Should return the new version for record A');
 
--- a/dom/push/test/xpcshell/test_notification_http2.js
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 'use strict';
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+const {base64UrlDecode} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
 
 var prefs;
 var tlsProfile;
 var pushEnabled;
 var pushConnectionEnabled;
 
 var serverPort = -1;
 
@@ -51,16 +52,18 @@ function run_test() {
 add_task(function* test_pushNotifications() {
 
   // /pushNotifications/subscription1 will send a message with no rs and padding
   // length 1.
   // /pushNotifications/subscription2 will send a message with no rs and padding
   // length 16.
   // /pushNotifications/subscription3 will send a message with rs equal 24 and
   // padding length 16.
+  // /pushNotifications/subscription4 will send a message with no rs and padding
+  // length 256.
 
   let db = PushServiceHttp2.newPushDB();
   do_register_cleanup(() => {
     return db.drop().then(_ => db.close());
   });
 
   var serverURL = "https://localhost:" + serverPort;
 
@@ -116,16 +119,36 @@ add_task(function* test_pushNotification
       kty: 'EC',
       x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
       y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
     },
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     quota: Infinity,
     systemRecord: true,
+  }, {
+    subscriptionUri: serverURL + '/pushNotifications/subscription4',
+    pushEndpoint: serverURL + '/pushEndpoint4',
+    pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
+    scope: 'https://example.com/page/4',
+    p256dhPublicKey: base64UrlDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'),
+    p256dhPrivateKey: {
+      crv: 'P-256',
+      d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
+      ext: true,
+      key_ops: ["deriveBits"],
+      kty: 'EC',
+      x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
+      y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
+    },
+    authenticationSecret: base64UrlDecode('cwDVC1iwAn8E37mkR3tMSg'),
+    originAttributes: ChromeUtils.originAttributesToSuffix(
+      { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+    quota: Infinity,
+    systemRecord: true,
   }];
 
   for (let record of records) {
     yield db.put(record);
   }
 
   let notifyPromise = Promise.all([
     promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
@@ -143,25 +166,31 @@ add_task(function* test_pushNotification
       }
     }),
     promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
       var message = subject.QueryInterface(Ci.nsIPushMessage);
       if (message && (data == "https://example.com/page/3")){
         equal(message.text(), "Some message", "decoded message is incorrect");
         return true;
       }
-    })
+    }),
+    promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+      var message = subject.QueryInterface(Ci.nsIPushMessage);
+      if (message && (data == "https://example.com/page/4")){
+        equal(message.text(), "Yet another message", "decoded message is incorrect");
+        return true;
+      }
+    }),
   ]);
 
   PushService.init({
     serverURI: serverURL,
     db
   });
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
+  yield notifyPromise;
 });
 
 add_task(function* test_complete() {
   prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
   prefs.setBoolPref("dom.push.enabled", pushEnabled);
   prefs.setBoolPref("dom.push.connection.enabled", pushConnectionEnabled);
 });
--- a/dom/push/test/xpcshell/test_notification_incomplete.js
+++ b/dom/push/test/xpcshell/test_notification_incomplete.js
@@ -103,18 +103,17 @@ add_task(function* test_notification_inc
         },
         onACK() {
           ok(false, 'Should not acknowledge malformed updates');
         }
       });
     }
   });
 
-  yield waitForPromise(notificationPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for incomplete notifications');
+  yield notificationPromise;
 
   let storeRecords = yield db.getAllKeyIDs();
   storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) =>
     compareAscending(a, b));
   recordsAreEqual(records, storeRecords);
 });
 
 function recordIsEqual(a, b) {
--- a/dom/push/test/xpcshell/test_notification_version_string.js
+++ b/dom/push/test/xpcshell/test_notification_version_string.js
@@ -52,23 +52,18 @@ add_task(function* test_notification_ver
             }]
           }));
         },
         onACK: ackDone
       });
     }
   });
 
-  let {subject: notification, data: scope} = yield waitForPromise(
-    notifyPromise,
-    DEFAULT_TIMEOUT,
-    'Timed out waiting for string notification'
-  );
+  let {subject: notification, data: scope} = yield notifyPromise;
   equal(notification, null, 'Unexpected data for Simple Push message');
 
-  yield waitForPromise(ackPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for string acknowledgement');
+  yield ackPromise;
 
   let storeRecord = yield db.getByKeyID(
     '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
   strictEqual(storeRecord.version, 4, 'Wrong record version');
   equal(storeRecord.quota, Infinity, 'Wrong quota');
 });
--- a/dom/push/test/xpcshell/test_permissions.js
+++ b/dom/push/test/xpcshell/test_permissions.js
@@ -119,29 +119,27 @@ add_task(function* setUp() {
             'Dropped unexpected channel ID ' + request.channelID);
           delete unregisterDefers[request.channelID];
           resolve();
         },
         onACK(request) {},
       });
     }
   });
-  yield waitForPromise(handshakePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for handshake');
+  yield handshakePromise;
 });
 
 add_task(function* test_permissions_allow_added() {
   let subChangePromise = promiseSubscriptionChanges(1);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.info', 'ALLOW_ACTION'),
     'added'
   );
-  let notifiedScopes = yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-      'Timed out waiting for notifications after adding allow');
+  let notifiedScopes = yield subChangePromise;
 
   deepEqual(notifiedScopes, [
     'https://example.info/page/2',
   ], 'Wrong scopes after adding allow');
 
   let record = yield db.getByKeyID('active-allow');
   equal(record.quota, 16,
     'Should reset quota for active records after adding allow');
@@ -154,18 +152,17 @@ add_task(function* test_permissions_allo
   let unregisterPromise = new Promise(resolve => unregisterDefers[
     'active-allow'] = resolve);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.info', 'ALLOW_ACTION'),
     'deleted'
   );
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister after deleting allow');
+  yield unregisterPromise;
 
   let record = yield db.getByKeyID('active-allow');
   ok(record.isExpired(),
     'Should expire active record after deleting allow');
 });
 
 add_task(function* test_permissions_deny_added() {
   let unregisterPromise = Promise.all([
@@ -174,18 +171,17 @@ add_task(function* test_permissions_deny
     new Promise(resolve => unregisterDefers[
       'active-deny-added-2'] = resolve),
   ]);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.net', 'DENY_ACTION'),
     'added'
   );
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications after adding deny');
+  yield unregisterPromise;
 
   let isExpired = yield allExpired(
     'active-deny-added-1',
     'expired-deny-added'
   );
   ok(isExpired, 'Should expire all registrations after adding deny');
 });
 
@@ -205,18 +201,17 @@ add_task(function* test_permissions_deny
 add_task(function* test_permissions_allow_changed() {
   let subChangePromise = promiseSubscriptionChanges(3);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.net', 'ALLOW_ACTION'),
     'changed'
   );
 
-  let notifiedScopes = yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications after changing to allow');
+  let notifiedScopes = yield subChangePromise;
 
   deepEqual(notifiedScopes, [
     'https://example.net/eggs',
     'https://example.net/green',
     'https://example.net/ham'
   ], 'Wrong scopes after changing to allow');
 
   let droppedRecords = yield Promise.all([
@@ -232,18 +227,17 @@ add_task(function* test_permissions_deny
   let unregisterPromise = new Promise(resolve => unregisterDefers[
     'active-deny-changed'] = resolve);
 
   yield PushService._onPermissionChange(
     makePushPermission('https://example.xyz', 'DENY_ACTION'),
     'changed'
   );
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister after changing to deny');
+  yield unregisterPromise;
 
   let record = yield db.getByKeyID('active-deny-changed');
   ok(record.isExpired(),
     'Should expire active record after changing to allow');
 });
 
 add_task(function* test_permissions_clear() {
   let records = yield db.getAllKeyIDs();
@@ -254,16 +248,15 @@ add_task(function* test_permissions_clea
     'never-expires',
   ], 'Wrong records in database before clearing');
 
   let unregisterPromise = new Promise(resolve => unregisterDefers[
       'drop-on-clear'] = resolve);
 
   yield PushService._onPermissionChange(null, 'cleared');
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister requests after clearing permissions');
+  yield unregisterPromise;
 
   records = yield db.getAllKeyIDs();
   deepEqual(records.map(record => record.keyID).sort(), [
     'never-expires',
   ], 'Unrestricted registrations should not be dropped');
 });
--- a/dom/push/test/xpcshell/test_quota_exceeded.js
+++ b/dom/push/test/xpcshell/test_quota_exceeded.js
@@ -127,17 +127,15 @@ add_task(function* test_expiration_origi
         },
         // We expect to receive acks, but don't care about their
         // contents.
         onACK(request) {},
       });
     },
   });
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister request');
+  yield unregisterPromise;
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
+  yield notifyPromise;
 
   let expiredRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
   strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
 });
--- a/dom/push/test/xpcshell/test_quota_observer.js
+++ b/dom/push/test/xpcshell/test_quota_observer.js
@@ -93,20 +93,18 @@ add_task(function* test_expiration_histo
           equal(request.channelID, '379c0668-8323-44d2-a315-4ee83f1a9ee9', 'Dropped wrong channel ID');
           unregisterDone();
         },
         onACK(request) {},
       });
     }
   });
 
-  yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for subscription change event on startup');
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister request');
+  yield subChangePromise;
+  yield unregisterPromise;
 
   let expiredRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
   strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
 
   let notifiedScopes = [];
   subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
     notifiedScopes.push(data);
     return notifiedScopes.length == 2;
@@ -129,18 +127,17 @@ add_task(function* test_expiration_histo
     uri: 'https://example.com/another-page',
     title: 'Infrequently-visited page',
     visitDate: Date.now() * 1000,
     transition: Ci.nsINavHistoryService.TRANSITION_LINK
   });
   Services.obs.notifyObservers(null, 'idle-daily', '');
 
   // And we should receive notifications for both scopes.
-  yield waitForPromise(subChangePromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for subscription change events');
+  yield subChangePromise;
   deepEqual(notifiedScopes.sort(), [
     'https://example.com/auctions',
     'https://example.com/deals'
   ], 'Wrong scopes for subscription changes');
 
   let aRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
   ok(!aRecord, 'Should drop expired record');
 
--- a/dom/push/test/xpcshell/test_quota_with_notification.js
+++ b/dom/push/test/xpcshell/test_quota_with_notification.js
@@ -98,17 +98,15 @@ add_task(function* test_expiration_origi
         },
         // We expect to receive acks, but don't care about their
         // contents.
         onACK(request) {},
       });
     },
   });
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
+  yield notifyPromise;
 
-  yield waitForPromise(updateQuotaPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for quota to be updated');
+  yield updateQuotaPromise;
 
   let expiredRecord = yield db.getByKeyID('f56645a9-1f32-4655-92ad-ddc37f6d54fb');
   notStrictEqual(expiredRecord.quota, 0, 'Expired record not updated');
 });
--- a/dom/push/test/xpcshell/test_reconnect_retry.js
+++ b/dom/push/test/xpcshell/test_reconnect_retry.js
@@ -63,12 +63,12 @@ add_task(function* test_reconnect_retry(
   let retryEndpoint = 'https://example.org/push/' + channelID;
   equal(registration.endpoint, retryEndpoint, 'Wrong endpoint for retried request');
 
   registration = yield PushService.register({
     scope: 'https://example.com/page/2',
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
   });
-  notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request')
+  notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request');
 
   equal(registers, 3, 'Wrong registration count');
 });
--- a/dom/push/test/xpcshell/test_register_case.js
+++ b/dom/push/test/xpcshell/test_register_case.js
@@ -38,24 +38,20 @@ add_task(function* test_register_case() 
             status: 200,
             pushEndpoint: 'https://example.com/update/case'
           }));
         }
       });
     }
   });
 
-  let newRecord = yield waitForPromise(
-    PushService.register({
-      scope: 'https://example.net/case',
-      originAttributes: ChromeUtils.originAttributesToSuffix(
-        { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
-    }),
-    DEFAULT_TIMEOUT,
-    'Mixed-case register response timed out'
-  );
+  let newRecord = yield PushService.register({
+    scope: 'https://example.net/case',
+    originAttributes: ChromeUtils.originAttributesToSuffix(
+      { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+  });
   equal(newRecord.endpoint, 'https://example.com/update/case',
     'Wrong push endpoint in registration record');
 
   let record = yield db.getByPushEndpoint('https://example.com/update/case');
   equal(record.scope, 'https://example.net/case',
     'Wrong scope in database record');
 });
--- a/dom/push/test/xpcshell/test_register_flush.js
+++ b/dom/push/test/xpcshell/test_register_flush.js
@@ -75,22 +75,20 @@ add_task(function* test_register_flush()
 
   let newRecord = yield PushService.register({
     scope: 'https://example.com/page/2',
     originAttributes: '',
   });
   equal(newRecord.endpoint, 'https://example.org/update/2',
     'Wrong push endpoint in record');
 
-  let {data: scope} = yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notification');
+  let {data: scope} = yield notifyPromise;
   equal(scope, 'https://example.com/page/1', 'Wrong notification scope');
 
-  yield waitForPromise(ackPromise, DEFAULT_TIMEOUT,
-     'Timed out waiting for acknowledgements');
+  yield ackPromise;
 
   let prevRecord = yield db.getByKeyID(
     '9bcc7efb-86c7-4457-93ea-e24e6eb59b74');
   equal(prevRecord.pushEndpoint, 'https://example.org/update/1',
     'Wrong existing push endpoint');
   strictEqual(prevRecord.version, 3,
     'Should record version updates sent before register responses');
 
--- a/dom/push/test/xpcshell/test_register_invalid_json.js
+++ b/dom/push/test/xpcshell/test_register_invalid_json.js
@@ -49,12 +49,11 @@ add_task(function* test_register_invalid
     PushService.register({
       scope: 'https://example.net/page/invalid-json',
       originAttributes: ChromeUtils.originAttributesToSuffix(
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for invalid JSON response'
   );
 
-  yield waitForPromise(helloPromise, DEFAULT_TIMEOUT,
-    'Reconnect after invalid JSON response timed out');
+  yield helloPromise;
   equal(registers, 1, 'Wrong register count');
 });
--- a/dom/push/test/xpcshell/test_register_no_id.js
+++ b/dom/push/test/xpcshell/test_register_no_id.js
@@ -53,12 +53,11 @@ add_task(function* test_register_no_id()
     PushService.register({
       scope: 'https://example.com/incomplete',
       originAttributes: ChromeUtils.originAttributesToSuffix(
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for incomplete register response'
   );
 
-  yield waitForPromise(helloPromise, DEFAULT_TIMEOUT,
-    'Reconnect after incomplete register response timed out');
+  yield helloPromise;
   equal(registers, 1, 'Wrong register count');
 });
--- a/dom/push/test/xpcshell/test_register_request_queue.js
+++ b/dom/push/test/xpcshell/test_register_request_queue.js
@@ -48,16 +48,15 @@ add_task(function* test_register_request
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
   });
   let secondRegister = PushService.register({
     scope: 'https://example.com/page/1',
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
   });
 
-  yield waitForPromise(Promise.all([
+  yield Promise.all([
     rejects(firstRegister, 'Should time out the first request'),
     rejects(secondRegister, 'Should time out the second request')
-  ]), DEFAULT_TIMEOUT, 'Queued requests did not time out');
+  ]);
 
-  yield waitForPromise(helloPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for reconnect');
+  yield helloPromise;
 });
--- a/dom/push/test/xpcshell/test_register_rollback.js
+++ b/dom/push/test/xpcshell/test_register_rollback.js
@@ -76,13 +76,12 @@ add_task(function* test_register_rollbac
       scope: 'https://example.com/storage-error',
       originAttributes: ChromeUtils.originAttributesToSuffix(
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for unregister database failure'
   );
 
   // Should send an out-of-band unregister request.
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Unregister request timed out');
+  yield unregisterPromise;
   equal(handshakes, 1, 'Wrong handshake count');
   equal(registers, 1, 'Wrong register count');
 });
--- a/dom/push/test/xpcshell/test_register_timeout.js
+++ b/dom/push/test/xpcshell/test_register_timeout.js
@@ -78,15 +78,11 @@ add_task(function* test_register_timeout
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for request timeout'
   );
 
   let record = yield db.getByKeyID(channelID);
   ok(!record, 'Should not store records for timed-out responses');
 
-  yield waitForPromise(
-    timeoutPromise,
-    DEFAULT_TIMEOUT,
-    'Reconnect timed out'
-  );
+  yield timeoutPromise;
   equal(registers, 1, 'Should not handle timed-out register requests');
 });
--- a/dom/push/test/xpcshell/test_register_wrong_id.js
+++ b/dom/push/test/xpcshell/test_register_wrong_id.js
@@ -59,12 +59,11 @@ add_task(function* test_register_wrong_i
     PushService.register({
       scope: 'https://example.com/mismatched',
       originAttributes: ChromeUtils.originAttributesToSuffix(
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for mismatched register reply'
   );
 
-  yield waitForPromise(helloPromise, DEFAULT_TIMEOUT,
-    'Reconnect after mismatched register reply timed out');
+  yield helloPromise;
   equal(registers, 1, 'Wrong register count');
 });
--- a/dom/push/test/xpcshell/test_register_wrong_type.js
+++ b/dom/push/test/xpcshell/test_register_wrong_type.js
@@ -53,12 +53,11 @@ add_task(function* test_register_wrong_t
     PushService.register({
       scope: 'https://example.com/mistyped',
       originAttributes: ChromeUtils.originAttributesToSuffix(
         { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
     }),
     'Expected error for non-string channel ID'
   );
 
-  yield waitForPromise(helloPromise, DEFAULT_TIMEOUT,
-    'Reconnect after sending non-string channel ID timed out');
+  yield helloPromise;
   equal(registers, 1, 'Wrong register count');
 });
--- a/dom/push/test/xpcshell/test_registration_success.js
+++ b/dom/push/test/xpcshell/test_registration_success.js
@@ -59,21 +59,17 @@ add_task(function* test_registration_suc
             uaid: userAgentID
           }));
           handshakeDone();
         }
       });
     }
   });
 
-  yield waitForPromise(
-    handshakePromise,
-    DEFAULT_TIMEOUT,
-    'Timed out waiting for handshake'
-  );
+  yield handshakePromise;
 
   let registration = yield PushService.registration({
     scope: 'https://example.net/a',
     originAttributes: '',
   });
   equal(
     registration.endpoint,
     'https://example.com/update/same-manifest/1',
--- a/dom/push/test/xpcshell/test_retry_ws.js
+++ b/dom/push/test/xpcshell/test_retry_ws.js
@@ -53,18 +53,14 @@ add_task(function* test_ws_retry() {
             return;
           }
           this.serverInterrupt();
         },
       });
     },
   });
 
-  yield waitForPromise(
-    handshakePromise,
-    45000,
-    'Timed out waiting for successful handshake'
-  );
+  yield handshakePromise;
   [25, 50, 100, 200, 400, 800, 1600, 3200, 6400, 10000].forEach(function(minDelay, index) {
     ok(alarmDelays[index] >= minDelay, `Should wait at least ${
       minDelay}ms before attempt ${index + 1}`);
   });
 });
--- a/dom/push/test/xpcshell/test_unregister_error.js
+++ b/dom/push/test/xpcshell/test_unregister_error.js
@@ -60,11 +60,10 @@ add_task(function* test_unregister_error
     scope: 'https://example.net/page/failure',
     originAttributes: '',
   });
 
   let result = yield db.getByKeyID(channelID);
   ok(!result, 'Deleted push record exists');
 
   // Make sure we send a request to the server.
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister');
+  yield unregisterPromise;
 });
--- a/dom/push/test/xpcshell/test_unregister_invalid_json.js
+++ b/dom/push/test/xpcshell/test_unregister_invalid_json.js
@@ -77,11 +77,10 @@ add_task(function* test_unregister_inval
     originAttributes: ChromeUtils.originAttributesToSuffix(
       { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
   });
   record = yield db.getByKeyID(
     '057caa8f-9b99-47ff-891c-adad18ce603e');
   ok(!record,
     'Failed to delete unregistered record after receiving invalid JSON');
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister');
+  yield unregisterPromise;
 });
--- a/dom/push/test/xpcshell/test_unregister_success.js
+++ b/dom/push/test/xpcshell/test_unregister_success.js
@@ -55,11 +55,10 @@ add_task(function* test_unregister_succe
 
   yield PushService.unregister({
     scope: 'https://example.com/page/unregister-success',
     originAttributes: '',
   });
   let record = yield db.getByKeyID(channelID);
   ok(!record, 'Unregister did not remove record');
 
-  yield waitForPromise(unregisterPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for unregister');
+  yield unregisterPromise;
 });
--- a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
+++ b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
@@ -62,16 +62,15 @@ add_task(function* test1() {
   let notifyPromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic,
                                                   _ => true);
 
   PushService.init({
     serverURI: serverURL + "/subscribe",
     db
   });
 
-  yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
-    'Timed out waiting for notifications');
+  yield notifyPromise;
 
   let aRecord = yield db.getByKeyID(serverURL + '/subscriptionNoKey');
   ok(aRecord, 'The record should still be there');
   ok(aRecord.p256dhPublicKey, 'There should be a public key');
   ok(aRecord.p256dhPrivateKey, 'There should be a private key');
 });
--- a/dom/push/test/xpcshell/xpcshell.ini
+++ b/dom/push/test/xpcshell/xpcshell.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 head = head.js head-http2.js
 tail =
 # Push notifications and alarms are currently disabled on Android.
 skip-if = toolkit == 'android'
 
 [test_clear_origin_data.js]
+[test_crypto.js]
 [test_drop_expired.js]
 [test_handler_service.js]
 support-files = PushServiceHandler.js PushServiceHandler.manifest
 [test_notification_ack.js]
 [test_notification_data.js]
 [test_notification_duplicate.js]
 [test_notification_error.js]
 [test_notification_incomplete.js]
--- a/dom/security/nsContentSecurityManager.cpp
+++ b/dom/security/nsContentSecurityManager.cpp
@@ -100,16 +100,24 @@ DoSOPChecks(nsIURI* aURI, nsILoadInfo* a
   return NS_OK;
 }
 
 static nsresult
 DoCORSChecks(nsIChannel* aChannel, nsILoadInfo* aLoadInfo,
              nsCOMPtr<nsIStreamListener>& aInAndOutListener)
 {
   MOZ_RELEASE_ASSERT(aInAndOutListener, "can not perform CORS checks without a listener");
+
+  // No need to set up CORS if TriggeringPrincipal is the SystemPrincipal.
+  // For example, allow user stylesheets to load XBL from external files
+  // without requiring CORS.
+  if (nsContentUtils::IsSystemPrincipal(aLoadInfo->TriggeringPrincipal())) {
+    return NS_OK;
+  }
+
   nsIPrincipal* loadingPrincipal = aLoadInfo->LoadingPrincipal();
   RefPtr<nsCORSListenerProxy> corsListener =
     new nsCORSListenerProxy(aInAndOutListener,
                             loadingPrincipal,
                             aLoadInfo->GetCookiePolicy() ==
                               nsILoadInfo::SEC_COOKIES_INCLUDE);
   // XXX: @arg: DataURIHandling::Allow
   // lets use  DataURIHandling::Allow for now and then decide on callsite basis. see also:
@@ -471,20 +479,21 @@ nsContentSecurityManager::CheckChannel(n
   // CORS mode is handled by nsCORSListenerProxy
   if (securityMode == nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS) {
     if (NS_HasBeenCrossOrigin(aChannel)) {
       loadInfo->MaybeIncreaseTainting(LoadTainting::CORS);
     }
     return NS_OK;
   }
 
-  // Allow the load if TriggeringPrincipal is the SystemPrincipal which
-  // is e.g. necessary to allow user user stylesheets to load XBL from
-  // external files.
-  if (nsContentUtils::IsSystemPrincipal(loadInfo->TriggeringPrincipal())) {
+  // Allow subresource loads if TriggeringPrincipal is the SystemPrincipal.
+  // For example, allow user stylesheets to load XBL from external files.
+  if (nsContentUtils::IsSystemPrincipal(loadInfo->TriggeringPrincipal()) &&
+      loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_DOCUMENT &&
+      loadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_SUBDOCUMENT) {
     return NS_OK;
   }
 
   // if none of the REQUIRE_SAME_ORIGIN flags are set, then SOP does not apply
   if ((securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS) ||
       (securityMode == nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED)) {
     rv = DoSOPChecks(uri, loadInfo, aChannel);
     NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/webidl/Node.webidl
+++ b/dom/webidl/Node.webidl
@@ -67,16 +67,18 @@ interface Node : EventTarget {
   Node replaceChild(Node node, Node child);
   [Throws]
   Node removeChild(Node child);
   void normalize();
 
   [Throws]
   Node cloneNode(optional boolean deep = false);
   [Pure]
+  boolean isSameNode(Node? node);
+  [Pure]
   boolean isEqualNode(Node? node);
 
   const unsigned short DOCUMENT_POSITION_DISCONNECTED = 0x01;
   const unsigned short DOCUMENT_POSITION_PRECEDING = 0x02;
   const unsigned short DOCUMENT_POSITION_FOLLOWING = 0x04;
   const unsigned short DOCUMENT_POSITION_CONTAINS = 0x08;
   const unsigned short DOCUMENT_POSITION_CONTAINED_BY = 0x10;
   const unsigned short DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; // historical
--- a/ipc/glue/BackgroundImpl.cpp
+++ b/ipc/glue/BackgroundImpl.cpp
@@ -336,21 +336,17 @@ class ChildImpl final : public Backgroun
   // This is only modified on the main thread. It is a FIFO queue for actors
   // that are in the process of construction.
   static StaticAutoPtr<nsTArray<nsCOMPtr<nsIEventTarget>>> sPendingTargets;
 
   // This is only modified on the main thread. It prevents us from trying to
   // create the background thread after application shutdown has started.
   static bool sShutdownHasStarted;
 
-#ifdef RELEASE_BUILD
-#ifdef DEBUG
-  nsIThread* mBoundThread;
-#endif
-#else
+#if defined(DEBUG) || !defined(RELEASE_BUILD)
   nsIThread* mBoundThread;
 #endif
 
 #ifdef DEBUG
   bool mActorDestroyed;
 #endif
 
 public:
@@ -377,17 +373,19 @@ public:
 
   void
   AssertActorDestroyed()
   {
     MOZ_ASSERT(mActorDestroyed, "ChildImpl::ActorDestroy not called in time");
   }
 
   ChildImpl()
+#if defined(DEBUG) || !defined(RELEASE_BUILD)
   : mBoundThread(nullptr)
+#endif
 #ifdef DEBUG
   , mActorDestroyed(false)
 #endif
   {
     AssertIsOnMainThread();
   }
 
   NS_INLINE_DECL_REFCOUNTING(ChildImpl)
--- a/js/examples/jorendb.js
+++ b/js/examples/jorendb.js
@@ -20,30 +20,101 @@
  */
 
 // Debugger state.
 var focusedFrame = null;
 var topFrame = null;
 var debuggeeValues = {};
 var nextDebuggeeValueIndex = 1;
 var lastExc = null;
+var todo = [];
+var activeTask;
+var options = { 'pretty': true,
+                'emacs': (os.getenv('EMACS') == 't') };
+var rerun = true;
 
 // Cleanup functions to run when we next re-enter the repl.
 var replCleanups = [];
 
+// Redirect debugger printing functions to go to the original output
+// destination, unaffected by any redirects done by the debugged script.
+var initialOut = os.file.redirect();
+var initialErr = os.file.redirectErr();
+
+function wrap(global, name) {
+    var orig = global[name];
+    global[name] = function(...args) {
+
+        var oldOut = os.file.redirect(initialOut);
+        var oldErr = os.file.redirectErr(initialErr);
+        try {
+            return orig.apply(global, args);
+        } finally {
+            os.file.redirect(oldOut);
+            os.file.redirectErr(oldErr);
+        }
+    };
+}
+wrap(this, 'print');
+wrap(this, 'printErr');
+wrap(this, 'putstr');
+
 // Convert a debuggee value v to a string.
 function dvToString(v) {
     return (typeof v !== 'object' || v === null) ? uneval(v) : "[object " + v.class + "]";
 }
 
-function showDebuggeeValue(dv) {
+function summaryObject(dv) {
+    var obj = {};
+    for (var name of dv.getOwnPropertyNames()) {
+        var v = dv.getOwnPropertyDescriptor(name).value;
+        if (v instanceof Debugger.Object) {
+            v = "(...)";
+        }
+        obj[name] = v;
+    }
+    return obj;
+}
+
+function debuggeeValueToString(dv, style) {
     var dvrepr = dvToString(dv);
+    if (!style.pretty || (typeof dv !== 'object'))
+        return [dvrepr, undefined];
+
+    if (dv.class == "Error") {
+        let errval = debuggeeGlobalWrapper.executeInGlobalWithBindings("$" + i + ".toString()", debuggeeValues);
+        return [dvrepr, errval.return];
+    }
+
+    if (style.brief)
+        return [dvrepr, JSON.stringify(summaryObject(dv), null, 4)];
+
+    let str = debuggeeGlobalWrapper.executeInGlobalWithBindings("JSON.stringify(v, null, 4)", {v: dv});
+    if ('throw' in str) {
+        if (style.noerror)
+            return [dvrepr, undefined];
+
+        let substyle = {};
+        Object.assign(substyle, style);
+        substyle.noerror = true;
+        return [dvrepr, debuggeeValueToString(str.throw, substyle)];
+    }
+
+    return [dvrepr, str.return];
+}
+
+// Problem! Used to do [object Object] followed by details. Now just details?
+
+function showDebuggeeValue(dv, style={pretty: options.pretty}) {
     var i = nextDebuggeeValueIndex++;
     debuggeeValues["$" + i] = dv;
-    print("$" + i + " = " + dvrepr);
+    let [brief, full] = debuggeeValueToString(dv, style);
+    print("$" + i + " = " + brief);
+    if (full !== undefined)
+        print(full);
 }
 
 Object.defineProperty(Debugger.Frame.prototype, "num", {
     configurable: true,
     enumerable: false,
     get: function () {
             var i = 0;
             for (var f = topFrame; f && f !== this; f = f.older)
@@ -65,16 +136,26 @@ Debugger.Frame.prototype.positionDescrip
         var line = this.script.getOffsetLocation(this.offset).lineNumber;
         if (this.script.url)
             return this.script.url + ":" + line;
         return "line " + line;
     }
     return null;
 }
 
+Debugger.Frame.prototype.location = function () {
+    if (this.script) {
+        var { lineNumber, columnNumber, isEntryPoint } = this.script.getOffsetLocation(this.offset);
+        if (this.script.url)
+            return this.script.url + ":" + lineNumber;
+        return null;
+    }
+    return null;
+}
+
 Debugger.Frame.prototype.fullDescription = function fullDescription() {
     var fr = this.frameDescription();
     var pos = this.positionDescription();
     if (pos)
         return fr + ", " + pos;
     return fr;
 }
 
@@ -116,16 +197,60 @@ function saveExcursion(fn) {
     try {
         return fn();
     } finally {
         topFrame = tf;
         focusedFrame = ff;
     }
 }
 
+function parseArgs(str) {
+    return str.split(" ");
+}
+
+function describedRv(r, desc) {
+    desc = "[" + desc + "] ";
+    if (r === undefined) {
+        print(desc + "Returning undefined");
+    } else if (r === null) {
+        print(desc + "Returning null");
+    } else if (r.length === undefined) {
+        print(desc + "Returning object " + JSON.stringify(r));
+    } else {
+        print(desc + "Returning length-" + r.length + " list");
+        if (r.length > 0) {
+            print("  " + r[0]);
+        }
+    }
+    return r;
+}
+
+// Rerun the program (reloading it from the file)
+function runCommand(args) {
+    print("Restarting program");
+    if (args)
+        activeTask.scriptArgs = parseArgs(args);
+    rerun = true;
+    for (var f = topFrame; f; f = f.older) {
+        print(f.script.url + ":" + f.script.getOffsetLine(f.offset) +" was " + f.onPop);
+        if (f.older) {
+            f.onPop = function() {
+                print("Resumifying " + this.script.url + ":" + this.script.getOffsetLine(this.offset));
+                return null;
+            };
+        } else {
+            f.onPop = function() {
+                return { 'return': 0 };
+            };
+        }
+    }
+    //return describedRv([{ 'return': 0 }], "runCommand");
+    return null;
+}
+
 // Evaluate an expression in the Debugger global
 function evalCommand(expr) {
     eval(expr);
 }
 
 function quitCommand() {
     dbg.enabled = false;
     quit(0);
@@ -133,49 +258,97 @@ function quitCommand() {
 
 function backtraceCommand() {
     if (topFrame === null)
         print("No stack.");
     for (var i = 0, f = topFrame; f; i++, f = f.older)
         showFrame(f, i);
 }
 
-function printCommand(rest) {
+function setCommand(rest) {
+    var space = rest.indexOf(' ');
+    if (space == -1) {
+        print("Invalid set <option> <value> command");
+    } else {
+        var name = rest.substr(0, space);
+        var value = rest.substr(space + 1);
+
+        if (name == 'args') {
+            activeTask.scriptArgs = parseArgs(value);
+        } else {
+            var yes = ["1", "yes", "true", "on"];
+            var no = ["0", "no", "false", "off"];
+
+            if (yes.indexOf(value) !== -1)
+                options[name] = true;
+            else if (no.indexOf(value) !== -1)
+                options[name] = false;
+            else
+                options[name] = value;
+        }
+    }
+}
+
+function split_print_options(s, style) {
+    var m = /^\/(\w+)/.exec(s);
+    if (!m)
+        return [ s, style ];
+    if (m[1].indexOf("p") != -1)
+        style.pretty = true;
+    if (m[1].indexOf("b") != -1)
+        style.brief = true;
+    return [ s.substr(m[0].length).trimLeft(), style ];
+}
+
+function doPrint(expr, style) {
     // This is the real deal.
     var cv = saveExcursion(
         () => focusedFrame == null
-              ? debuggeeGlobalWrapper.executeInGlobalWithBindings(rest, debuggeeValues)
-              : focusedFrame.evalWithBindings(rest, debuggeeValues));
+              ? debuggeeGlobalWrapper.executeInGlobalWithBindings(expr, debuggeeValues)
+              : focusedFrame.evalWithBindings(expr, debuggeeValues));
     if (cv === null) {
         if (!dbg.enabled)
             return [cv];
         print("Debuggee died.");
     } else if ('return' in cv) {
         if (!dbg.enabled)
             return [undefined];
-        showDebuggeeValue(cv.return);
+        showDebuggeeValue(cv.return, style);
     } else {
         if (!dbg.enabled)
             return [cv];
         print("Exception caught. (To rethrow it, type 'throw'.)");
         lastExc = cv.throw;
-        showDebuggeeValue(lastExc);
+        showDebuggeeValue(lastExc, style);
     }
 }
 
+function printCommand(rest) {
+    var [expr, style] = split_print_options(rest, {pretty: options.pretty});
+    return doPrint(expr, style);
+}
+
+function keysCommand(rest) { return doPrint("Object.keys(" + rest + ")"); }
+
 function detachCommand() {
     dbg.enabled = false;
     return [undefined];
 }
 
-function continueCommand() {
+function continueCommand(rest) {
     if (focusedFrame === null) {
         print("No stack.");
         return;
     }
+
+    var match = rest.match(/^(\d+)$/);
+    if (match) {
+        return doStepOrNext({upto:true, stopLine:match[1]});
+    }
+
     return [undefined];
 }
 
 function throwCommand(rest) {
     var v;
     if (focusedFrame !== topFrame) {
         print("To throw, you must select the newest frame (use 'frame 0').");
         return;
@@ -215,46 +388,51 @@ function frameCommand(rest) {
             if (!f.older) {
                 print("There is no frame " + rest + ".");
                 return;
             }
             f.older.younger = f;
             f = f.older;
         }
         focusedFrame = f;
+        updateLocation(focusedFrame);
         showFrame(f, n);
-    } else if (rest !== '') {
-        if (topFrame === null)
+    } else if (rest === '') {
+        if (topFrame === null) {
             print("No stack.");
-        else
+        } else {
+            updateLocation(focusedFrame);
             showFrame();
+        }
     } else {
         print("do what now?");
     }
 }
 
 function upCommand() {
     if (focusedFrame === null)
         print("No stack.");
     else if (focusedFrame.older === null)
         print("Initial frame selected; you cannot go up.");
     else {
         focusedFrame.older.younger = focusedFrame;
         focusedFrame = focusedFrame.older;
+        updateLocation(focusedFrame);
         showFrame();
     }
 }
 
 function downCommand() {
     if (focusedFrame === null)
         print("No stack.");
     else if (!focusedFrame.younger)
         print("Youngest frame selected; you cannot go down.");
     else {
         focusedFrame = focusedFrame.younger;
+        updateLocation(focusedFrame);
         showFrame();
     }
 }
 
 function forcereturnCommand(rest) {
     var v;
     var f = focusedFrame;
     if (f !== topFrame) {
@@ -279,17 +457,17 @@ function forcereturnCommand(rest) {
         }
     }
 }
 
 function printPop(f, c) {
     var fdesc = f.fullDescription();
     if (c.return) {
         print("frame returning (still selected): " + fdesc);
-        showDebuggeeValue(c.return);
+        showDebuggeeValue(c.return, {brief: true});
     } else if (c.throw) {
         print("frame threw exception: " + fdesc);
         showDebuggeeValue(c.throw);
         print("(To rethrow it, type 'throw'.)");
         lastExc = c.throw;
     } else {
         print("frame was terminated: " + fdesc);
     }
@@ -298,41 +476,77 @@ function printPop(f, c) {
 // Set |prop| on |obj| to |value|, but then restore its current value
 // when we next enter the repl.
 function setUntilRepl(obj, prop, value) {
     var saved = obj[prop];
     obj[prop] = value;
     replCleanups.push(function () { obj[prop] = saved; });
 }
 
+function updateLocation(frame) {
+    if (options.emacs) {
+        var loc = frame.location();
+        if (loc)
+            print("\032\032" + loc + ":1");
+    }
+}
+
 function doStepOrNext(kind) {
     var startFrame = topFrame;
     var startLine = startFrame.line;
-    print("stepping in:   " + startFrame.fullDescription());
-    print("starting line: " + uneval(startLine));
+    // print("stepping in:   " + startFrame.fullDescription());
+    // print("starting line: " + uneval(startLine));
 
     function stepPopped(completion) {
         // Note that we're popping this frame; we need to watch for
         // subsequent step events on its caller.
         this.reportedPop = true;
         printPop(this, completion);
         topFrame = focusedFrame = this;
+        if (kind.finish) {
+            // We want to continue, but this frame is going to be invalid as
+            // soon as this function returns, which will make the replCleanups
+            // assert when it tries to access the dead frame's 'onPop'
+            // property. So clear it out now while the frame is still valid,
+            // and trade it for an 'onStep' callback on the frame we're popping to.
+            preReplCleanups();
+            setUntilRepl(this.older, 'onStep', stepStepped);
+            return undefined;
+        }
+        updateLocation(this);
         return repl();
     }
 
     function stepEntered(newFrame) {
         print("entered frame: " + newFrame.fullDescription());
+        updateLocation(newFrame);
         topFrame = focusedFrame = newFrame;
         return repl();
     }
 
     function stepStepped() {
-        print("stepStepped: " + this.fullDescription());
-        // If we've changed frame or line, then report that.
-        if (this !== startFrame || this.line != startLine) {
+        // print("stepStepped: " + this.fullDescription());
+        updateLocation(this);
+        var stop = false;
+
+        if (kind.finish) {
+            // 'finish' set a one-time onStep for stopping at the frame it
+            // wants to return to
+            stop = true;
+        } else if (kind.upto) {
+            // running until a given line is reached
+            if (this.line == kind.stopLine)
+                stop = true;
+        } else {
+            // regular step; stop whenever the line number changes
+            if ((this.line != startLine) || (this != startFrame))
+                stop = true;
+        }
+
+        if (stop) {
             topFrame = focusedFrame = this;
             if (focusedFrame != startFrame)
                 print(focusedFrame.fullDescription());
             return repl();
         }
 
         // Otherwise, let execution continue.
         return undefined;
@@ -342,52 +556,74 @@ function doStepOrNext(kind) {
         setUntilRepl(dbg, 'onEnterFrame', stepEntered);
 
     // If we're stepping after an onPop, watch for steps and pops in the
     // next-older frame; this one is done.
     var stepFrame = startFrame.reportedPop ? startFrame.older : startFrame;
     if (!stepFrame || !stepFrame.script)
         stepFrame = null;
     if (stepFrame) {
-        setUntilRepl(stepFrame, 'onStep', stepStepped);
+        if (!kind.finish)
+            setUntilRepl(stepFrame, 'onStep', stepStepped);
         setUntilRepl(stepFrame, 'onPop',  stepPopped);
     }
 
     // Let the program continue!
     return [undefined];
 }
 
 function stepCommand() { return doStepOrNext({step:true}); }
 function nextCommand() { return doStepOrNext({next:true}); }
+function finishCommand() { return doStepOrNext({finish:true}); }
+
+// FIXME: DOES NOT WORK YET
+function breakpointCommand(where) {
+    print("Sorry, breakpoints don't work yet.");
+    var script = focusedFrame.script;
+    var offsets = script.getLineOffsets(Number(where));
+    if (offsets.length == 0) {
+        print("Unable to break at line " + where);
+        return;
+    }
+    for (var offset of offsets) {
+        script.setBreakpoint(offset, { hit: handleBreakpoint });
+    }
+    print("Set breakpoint in " + script.url + ":" + script.startLine + " at line " + where + ", " + offsets.length);
+}
 
 // Build the table of commands.
 var commands = {};
 var commandArray = [
     backtraceCommand, "bt", "where",
+    breakpointCommand, "b", "break",
     continueCommand, "c",
     detachCommand,
     downCommand, "d",
+    evalCommand, "!",
     forcereturnCommand,
     frameCommand, "f",
+    finishCommand, "fin",
     nextCommand, "n",
     printCommand, "p",
+    keysCommand, "k",
     quitCommand, "q",
+    runCommand, "run",
     stepCommand, "s",
+    setCommand,
     throwCommand, "t",
     upCommand, "u",
     helpCommand, "h",
-    evalCommand, "!",
-    ];
-var last = null;
+];
+var currentCmd = null;
 for (var i = 0; i < commandArray.length; i++) {
     var cmd = commandArray[i];
     if (typeof cmd === "string")
-        commands[cmd] = last;
+        commands[cmd] = currentCmd;
     else
-        last = commands[cmd.name.replace(/Command$/, '')] = cmd;
+        currentCmd = commands[cmd.name.replace(/Command$/, '')] = cmd;
 }
 
 function helpCommand(rest) {
     print("Available commands:");
     var printcmd = function(group) {
         print("  " + group.join(", "));
     }
 
@@ -399,25 +635,33 @@ function helpCommand(rest) {
             if (group.length) printcmd(group);
             group = [ cmd.name.replace(/Command$/, '') ];
         }
     }
     printcmd(group);
 }
 
 // Break cmd into two parts: its first word and everything else. If it begins
-// with punctuation, treat that as a separate word.
+// with punctuation, treat that as a separate word. The first word is
+// terminated with whitespace or the '/' character. So:
+//
+//   print x         => ['print', 'x']
+//   print           => ['print', '']
+//   !print x        => ['!', 'print x']
+//   ?!wtf!?         => ['?', '!wtf!?']
+//   print/b x       => ['print', '/b x']
+//
 function breakcmd(cmd) {
     cmd = cmd.trimLeft();
     if ("!@#$%^&*_+=/?.,<>:;'\"".indexOf(cmd.substr(0, 1)) != -1)
         return [cmd.substr(0, 1), cmd.substr(1).trimLeft()];
-    var m = /\s/.exec(cmd);
+    var m = /\s+|(?=\/)/.exec(cmd);
     if (m === null)
         return [cmd, ''];
-    return [cmd.slice(0, m.index), cmd.slice(m.index).trimLeft()];
+    return [cmd.slice(0, m.index), cmd.slice(m.index + m[0].length)];
 }
 
 function runcmd(cmd) {
     var pieces = breakcmd(cmd);
     if (pieces[0] === "")
         return undefined;
 
     var first = pieces[0], rest = pieces[1];
@@ -430,81 +674,223 @@ function runcmd(cmd) {
     if (cmd.length === 0 && rest !== '') {
         print("this command cannot take an argument");
         return undefined;
     }
 
     return cmd(rest);
 }
 
-function repl() {
+function preReplCleanups() {
     while (replCleanups.length > 0)
         replCleanups.pop()();
+}
+
+var prevcmd = undefined;
+function repl() {
+    preReplCleanups();
 
     var cmd;
     for (;;) {
         putstr("\n" + prompt);
         cmd = readline();
         if (cmd === null)
             return null;
+        else if (cmd === "")
+            cmd = prevcmd;
 
         try {
+            prevcmd = cmd;
             var result = runcmd(cmd);
             if (result === undefined)
-                ; // do nothing
+                ; // do nothing, return to prompt
             else if (Array.isArray(result))
                 return result[0];
+            else if (result === null)
+                return null;
             else
-                throw new Error("Internal error: result of runcmd wasn't array or undefined");
+                throw new Error("Internal error: result of runcmd wasn't array or undefined: " + result);
         } catch (exc) {
             print("*** Internal error: exception in the debugger code.");
             print("    " + exc);
             print(exc.stack);
         }
     }
 }
 
 var dbg = new Debugger();
 dbg.onDebuggerStatement = function (frame) {
     return saveExcursion(function () {
             topFrame = focusedFrame = frame;
             print("'debugger' statement hit.");
             showFrame();
-            return repl();
+            updateLocation(focusedFrame);
+            backtrace();
+            return describedRv(repl(), "debugger.saveExc");
         });
 };
 dbg.onThrow = function (frame, exc) {
     return saveExcursion(function () {
             topFrame = focusedFrame = frame;
             print("Unwinding due to exception. (Type 'c' to continue unwinding.)");
             showFrame();
             print("Exception value is:");
             showDebuggeeValue(exc);
             return repl();
         });
 };
 
+function handleBreakpoint (frame) {
+    print("Breakpoint hit!");
+    return saveExcursion(() => {
+        topFrame = focusedFrame = frame;
+        print("breakpoint hit.");
+        showFrame();
+        updateLocation(focusedFrame);
+        return repl();
+    });
+};
+
 // The depth of jorendb nesting.
 var jorendbDepth;
 if (typeof jorendbDepth == 'undefined') jorendbDepth = 0;
 
 var debuggeeGlobal = newGlobal("new-compartment");
 debuggeeGlobal.jorendbDepth = jorendbDepth + 1;
 var debuggeeGlobalWrapper = dbg.addDebuggee(debuggeeGlobal);
 
 print("jorendb version -0.0");
 prompt = '(' + Array(jorendbDepth+1).join('meta-') + 'jorendb) ';
 
-var args = arguments;
+var args = scriptArgs.slice(0);
+print("INITIAL ARGS: " + args);
+
+// Find the script to run and its arguments. The script may have been given as
+// a plain script name, in which case all remaining arguments belong to the
+// script. Or there may have been any number of arguments to the JS shell,
+// followed by -f scriptName, followed by additional arguments to the JS shell,
+// followed by the script arguments. There may be multiple -e or -f options in
+// the JS shell arguments, and we want to treat each one as a debuggable
+// script.
+//
+// The difficulty is that the JS shell has a mixture of
+//
+//   --boolean
+//
+// and
+//
+//   --value VAL
+//
+// parameters, and there's no way to know whether --option takes an argument or
+// not. We will assume that VAL will never end in .js, or rather that the first
+// argument that does not start with "-" but does end in ".js" is the name of
+// the script.
+//
+// If you need to pass other options and not have them given to the script,
+// pass them before the -f jorendb.js argument. Thus, the safe ways to pass
+// arguments are:
+//
+//   js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)+ -- [script args]
+//   js [JS shell options] -f jorendb.js (-e SCRIPT | -f FILE)* script.js [script args]
+//
+// Additionally, if you want to run a script that is *NOT* debugged, put it in
+// as part of the leading [JS shell options].
+
+
+// Compute actualScriptArgs by finding the script to be run and grabbing every
+// non-script argument. The script may be given by -f scriptname or just plain
+// scriptname. In the latter case, it will be in the global variable
+// 'scriptPath' (and NOT in scriptArgs.)
+var actualScriptArgs = [];
+var scriptSeen;
+
+if (scriptPath !== undefined) {
+    todo.push({
+        'action': 'load',
+        'script': scriptPath,
+    });
+    scriptSeen = true;
+}
+
 while(args.length > 0) {
     var arg = args.shift();
-    if (arg == '-f') {
-        arg = args.shift();
-        debuggeeGlobal.evaluate(read(arg), { fileName: arg, lineNumber: 1 });
-    } else if (arg == '-e') {
-        arg = args.shift();
-        debuggeeGlobal.eval(arg);
+    print("arg: " + arg);
+    if (arg == '-e') {
+        print("  eval");
+        todo.push({
+            'action': 'eval',
+            'code': args.shift()
+        });
+    } else if (arg == '-f') {
+        var script = args.shift();
+        print("  load -f " + script);
+        scriptSeen = true;
+        todo.push({
+            'action': 'load',
+            'script': script,
+        });
+    } else if (arg.indexOf("-") == 0) {
+        if (arg == '--') {
+            print("  pass remaining args to script");
+            actualScriptArgs.push(...args);
+            break;
+        } else if ((args.length > 0) && (args[0].indexOf(".js") + 3 == args[0].length)) {
+            // Ends with .js, assume we are looking at --boolean script.js
+            print("  load script.js after --boolean");
+            todo.push({
+                'action': 'load',
+                'script': args.shift(),
+            });
+            scriptSeen = true;
+        } else {
+            // Does not end with .js, assume we are looking at JS shell arg
+            // --value VAL
+            print("  ignore");
+            args.shift();
+        }
     } else {
-        throw("jorendb does not implement command-line argument '" + arg + "'");
+        if (!scriptSeen) {
+            print("  load general");
+            scriptSeen = true;
+            todo.push({
+                'action': 'load',
+                'script': arg,
+            });
+        } else {
+            print("  arg " + arg);
+            actualScriptArgs.push(arg);
+        }
+    }
+}
+print("jorendb: scriptPath = " + scriptPath);
+print("jorendb: scriptArgs = " + scriptArgs);
+print("jorendb: actualScriptArgs = " + actualScriptArgs);
+
+for (var task of todo) {
+    task['scriptArgs'] = actualScriptArgs;
+}
+
+// If nothing to run, just drop into a repl
+if (todo.length == 0) {
+    todo.push({ 'action': 'repl' });
+}
+
+while (rerun) {
+    print("Top of run loop");
+    rerun = false;
+    for (var task of todo) {
+        activeTask = task;
+        if (task.action == 'eval') {
+            debuggeeGlobal.eval(task.code);
+        } else if (task.action == 'load') {
+            debuggeeGlobal['scriptArgs'] = task.scriptArgs;
+            debuggeeGlobal['scriptPath'] = task.script;
+            print("Loading JavaScript file " + task.script);
+            debuggeeGlobal.evaluate(read(task.script), { 'fileName': task.script, 'lineNumber': 1 });
+        } else if (task.action == 'repl') {
+            repl();
+        }
+        if (rerun)
+            break;
     }
 }
 
-repl();
+quit(0);
--- a/js/src/Makefile.in
+++ b/js/src/Makefile.in
@@ -36,26 +36,16 @@ OS_LDFLAGS += -Wl,-version-script,symver
 
 symverscript: symverscript.in
 	$(call py_action,preprocessor, \
 		-DVERSION='$(subst -,_,$(LIBRARY_NAME))' $< -o $@)
 
 EXTRA_DEPS += symverscript
 endif
 
-export_files = js-config.h
-ifdef HAVE_DTRACE
-export_files += $(CURDIR)/javascript-trace.h
-endif
-
-INSTALL_TARGETS += jsconfig
-jsconfig_FILES = $(export_files)
-jsconfig_DEST = $(DIST)/include
-jsconfig_TARGET := export
-
 include $(topsrcdir)/config/rules.mk
 
 # check_vanilla_allocations.py is tailored to Linux, so only run it there.
 # That should be enough to catch any problems.
 check-vanilla-allocations:
 	$(PYTHON) $(topsrcdir)/config/check_vanilla_allocations.py $(REAL_LIBRARY)
 
 # The "aggressive" variant will likely fail on some compiler/platform
@@ -248,17 +238,17 @@ ETWProvider.res: ETWProvider.rc
 export:: ETWProvider.res
 
 install:: ETWProvider.mof ETWProvider.man
 	$(SYSINSTALL) $^ $(DESTDIR)$(bindir)
 
 endif
 
 ifdef HAVE_DTRACE
-$(CURDIR)/javascript-trace.h: $(srcdir)/devtools/javascript-trace.d
+javascript-trace.h: $(srcdir)/devtools/javascript-trace.d
 	dtrace -x nolibs -h -s $(srcdir)/devtools/javascript-trace.d -o javascript-trace.h.in
 	sed -e 's/if _DTRACE_VERSION/ifdef INCLUDE_MOZILLA_DTRACE/' \
 	    -e '/const/!s/char \*/const char */g' \
 	    javascript-trace.h.in > javascript-trace.h
 
 # We can't automatically generate dependencies on auto-generated headers;
 # we have to list them explicitly.
 $(addsuffix .$(OBJ_SUFFIX),Probes jsinterp jsobj): $(CURDIR)/javascript-trace.h
--- a/js/src/builtin/Promise.h
+++ b/js/src/builtin/Promise.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef builtin_Promise_h
 #define builtin_Promise_h
 
 #include "builtin/SelfHostingDefines.h"
 #include "js/Date.h"
 #include "vm/NativeObject.h"
+#include "vm/Time.h"
 
 namespace js {
 
 class AutoSetNewObjectMetadata;
 
 class PromiseObject : public NativeObject
 {
   public:
--- a/js/src/devtools/rootAnalysis/annotations.js
+++ b/js/src/devtools/rootAnalysis/annotations.js
@@ -71,16 +71,17 @@ var ignoreCallees = {
     "js::Class.trace" : true,
     "js::Class.finalize" : true,
     "JSRuntime.destroyPrincipals" : true,
     "icu_50::UObject.__deleting_dtor" : true, // destructors in ICU code can't cause GC
     "mozilla::CycleCollectedJSRuntime.DescribeCustomObjects" : true, // During tracing, cannot GC.
     "mozilla::CycleCollectedJSRuntime.NoteCustomGCThingXPCOMChildren" : true, // During tracing, cannot GC.
     "PLDHashTableOps.hashKey" : true,
     "z_stream_s.zfree" : true,
+    "z_stream_s.zalloc" : true,
     "GrGLInterface.fCallback" : true,
     "std::strstreambuf._M_alloc_fun" : true,
     "std::strstreambuf._M_free_fun" : true,
     "struct js::gc::Callback<void (*)(JSRuntime*, void*)>.op" : true,
 };
 
 function fieldCallCannotGC(csu, fullfield)
 {
deleted file mode 100644
--- a/js/src/gdb/Makefile.in
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- Mode: makefile -*-
-#
-# 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/.
-
-# Place a GDB Python auto-load file next to the gdb-tests executable, both
-# in the build directory and in the dist/bin directory.
-PP_TARGETS += GDB_AUTOLOAD
-GDB_AUTOLOAD := gdb-tests-gdb.py.in
-GDB_AUTOLOAD_FLAGS := -Dtopsrcdir=$(abspath $(srcdir)/..)
-
-INSTALL_TARGETS += GDB_INSTALL_AUTOLOAD
-GDB_INSTALL_AUTOLOAD_FILES := $(CURDIR)/gdb-tests-gdb.py
-GDB_INSTALL_AUTOLOAD_DEST := $(DIST)/bin
--- a/js/src/gdb/moz.build
+++ b/js/src/gdb/moz.build
@@ -33,8 +33,12 @@ USE_LIBS += [
     'static:js',
 ]
 
 OS_LIBS += CONFIG['MOZ_ZLIB_LIBS']
 
 # This is intended as a temporary workaround to enable VS2015.
 if CONFIG['_MSC_VER']:
     CXXFLAGS += ['-wd4312']
+
+DEFINES['topsrcdir'] = '%s/js/src' % TOPSRCDIR
+FINAL_TARGET_PP_FILES += ['gdb-tests-gdb.py.in']
+OBJDIR_FILES.js.src.gdb += ['!/dist/bin/gdb-tests-gdb.py']
--- a/js/src/jit/arm/Assembler-arm.h
+++ b/js/src/jit/arm/Assembler-arm.h
@@ -906,16 +906,23 @@ class EDtrAddr
   public:
     explicit EDtrAddr(Register r, EDtrOff off)
       : data(RN(r) | off.encode())
     { }
 
     uint32_t encode() const {
         return data;
     }
+#ifdef DEBUG
+    Register maybeOffsetRegister() const {
+        if (data & IsImmEDTR)
+            return InvalidReg;
+        return Register::FromCode(data & 0xf);
+    }
+#endif
 };
 
 class VFPOff
 {
     uint32_t data;
 
   protected:
     explicit VFPOff(datastore::Imm8VFPOffData imm, IsUp_ isup)
--- a/js/src/jit/arm/MacroAssembler-arm.cpp
+++ b/js/src/jit/arm/MacroAssembler-arm.cpp
@@ -1087,16 +1087,18 @@ MacroAssemblerARM::ma_ldrsb(EDtrAddr add
 }
 
 void
 MacroAssemblerARM::ma_ldrd(EDtrAddr addr, Register rt, DebugOnly<Register> rt2,
                            Index mode, Condition cc)
 {
     MOZ_ASSERT((rt.code() & 1) == 0);
     MOZ_ASSERT(rt2.value.code() == rt.code() + 1);
+    MOZ_ASSERT(addr.maybeOffsetRegister() != rt); // Undefined behavior if rm == rt/rt2.
+    MOZ_ASSERT(addr.maybeOffsetRegister() != rt2);
     as_extdtr(IsLoad, 64, true, mode, rt, addr, cc);
 }
 
 void
 MacroAssemblerARM::ma_strh(Register rt, EDtrAddr addr, Index mode, Condition cc)
 {
     as_extdtr(IsStore, 16, false, mode, rt, addr, cc);
 }
@@ -3165,17 +3167,25 @@ void
 MacroAssemblerARMCompat::loadValue(const BaseIndex& addr, ValueOperand val)
 {
     ScratchRegisterScope scratch(asMasm());
 
     if (isValueDTRDCandidate(val) && Abs(addr.offset) <= 255) {
         Register tmpIdx;
         if (addr.offset == 0) {
             if (addr.scale == TimesOne) {
-                tmpIdx = addr.index;
+                // If the offset register is the same as one of the destination
+                // registers, LDRD's behavior is undefined. Use the scratch
+                // register to avoid this.
+                if (val.aliases(addr.index)) {
+                    ma_mov(addr.index, scratch);
+                    tmpIdx = scratch;
+                } else {
+                    tmpIdx = addr.index;
+                }
             } else {
                 ma_lsl(Imm32(addr.scale), addr.index, scratch);
                 tmpIdx = scratch;
             }
             ma_ldrd(EDtrAddr(addr.base, EDtrOffReg(tmpIdx)), val.payloadReg(), val.typeReg());
         } else {
             ma_alu(addr.base, lsl(addr.index, addr.scale), scratch, OpAdd);
             ma_ldrd(EDtrAddr(scratch, EDtrOffImm(addr.offset)),
--- a/js/src/jsapi-tests/Makefile.in
+++ b/js/src/jsapi-tests/Makefile.in
@@ -2,14 +2,8 @@
 #
 # 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/.
 
 ifdef QEMU_EXE
 MOZ_POST_PROGRAM_COMMAND = $(topsrcdir)/build/qemu-wrap --qemu $(QEMU_EXE) --libdir $(CROSS_LIB)
 endif
-
-# Place a GDB Python auto-load file next to the jsapi-tests executable in
-# the build directory.
-PP_TARGETS += JSAPI_TESTS_AUTOLOAD
-JSAPI_TESTS_AUTOLOAD := jsapi-tests-gdb.py.in
-JSAPI_TESTS_AUTOLOAD_FLAGS := -Dtopsrcdir=$(abspath $(srcdir)/..)
--- a/js/src/jsapi-tests/moz.build
+++ b/js/src/jsapi-tests/moz.build
@@ -128,8 +128,11 @@ USE_LIBS += [
     'static:js',
 ]
 
 OS_LIBS += CONFIG['MOZ_ZLIB_LIBS']
 
 # This is intended as a temporary workaround to enable VS2015.
 if CONFIG['_MSC_VER']:
     CXXFLAGS += ['-wd4312']
+
+DEFINES['topsrcdir'] = '%s/js/src' % TOPSRCDIR
+OBJDIR_PP_FILES.js.src['jsapi-tests'] += ['jsapi-tests-gdb.py.in']
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -1345,21 +1345,16 @@ JSFunction::createScriptForLazilyInterpr
     MOZ_ASSERT(fun->isInterpretedLazy());
 
     Rooted<LazyScript*> lazy(cx, fun->lazyScriptOrNull());
     if (lazy) {
         // Trigger a pre barrier on the lazy script being overwritten.
         if (cx->zone()->needsIncrementalBarrier())
             LazyScript::writeBarrierPre(lazy);
 
-        // Suppress GC for now although we should be able to remove this by
-        // making 'lazy' a Rooted<LazyScript*> (which requires adding a
-        // THING_ROOT_LAZY_SCRIPT).
-        AutoSuppressGC suppressGC(cx);
-
         RootedScript script(cx, lazy->maybeScript());
 
         // Only functions without inner functions or direct eval are
         // re-lazified. Functions with either of those are on the static scope
         // chain of their inner functions, or in the case of eval, possibly
         // eval'd inner functions. This prohibits re-lazification as
         // StaticScopeIter queries needsCallObject of those functions, which
         // requires a non-lazy script.  Note that if this ever changes,
--- a/js/src/jsscriptinlines.h
+++ b/js/src/jsscriptinlines.h
@@ -82,19 +82,20 @@ ScriptAndCounts::ScriptAndCounts(ScriptA
 
 void
 SetFrameArgumentsObject(JSContext* cx, AbstractFramePtr frame,
                         HandleScript script, JSObject* argsobj);
 
 inline JSFunction*
 LazyScript::functionDelazifying(JSContext* cx) const
 {
-    if (function_ && !function_->getOrCreateScript(cx))
+    Rooted<const LazyScript*> self(cx, this);
+    if (self->function_ && !self->function_->getOrCreateScript(cx))
         return nullptr;
-    return function_;
+    return self->function_;
 }
 
 } // namespace js
 
 inline JSFunction*
 JSScript::functionDelazifying() const
 {
     if (function_ && function_->isInterpretedLazy()) {
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -72,20 +72,26 @@ if not CONFIG['JS_STANDALONE']:
         '../../config/autoconf-js.mk',
         '../../config/emptyvars-js.mk',
     ]
 
 CONFIGURE_DEFINE_FILES += [
     'js-config.h',
 ]
 
+if CONFIG['HAVE_DTRACE']:
+    GENERATED_FILES += ['javascript-trace.h']
+
+    EXPORTS += ['!javascript-trace.h']
+
 # Changes to internal header files, used externally, massively slow down
 # browser builds.  Don't add new files here unless you know what you're
 # doing!
 EXPORTS += [
+    '!js-config.h',
     'js.msg',
     'jsalloc.h',
     'jsapi.h',
     'jsbytecode.h',
     'jsclist.h',
     'jscpucfg.h',
     'jsfriendapi.h',
     'jsprf.h',
--- a/js/src/old-configure.in
+++ b/js/src/old-configure.in
@@ -968,19 +968,19 @@ case "$target" in
         LDFLAGS=$_SAVE_LDFLAGS
     fi
     MOZ_FIX_LINK_PATHS="-Wl,-executable_path,${DIST}/bin"
     ;;
 
 *-android*|*-linuxandroid*)
     AC_DEFINE(NO_PW_GECOS)
     MOZ_GFX_OPTIMIZE_MOBILE=1
-    MOZ_OPTIMIZE_FLAGS="-O3 -fno-reorder-functions"
+    MOZ_OPTIMIZE_FLAGS="-O3"
     if test -z "$CLANG_CC"; then
-       MOZ_OPTIMIZE_FLAGS="-freorder-blocks $MOZ_OPTIMIZE_FLAGS"
+       MOZ_OPTIMIZE_FLAGS="-freorder-blocks -fno-reorder-functions $MOZ_OPTIMIZE_FLAGS"
     fi
     # The Maemo builders don't know about this flag
     MOZ_ARM_VFP_FLAGS="-mfpu=vfp"
     ;;
 
 *-*linux*)
     # Note: both GNU_CC and INTEL_CC are set when using Intel's C compiler.
     # Similarly for GNU_CXX and INTEL_CXX.
--- a/js/src/shell/Makefile.in
+++ b/js/src/shell/Makefile.in
@@ -3,26 +3,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 ifdef QEMU_EXE
 MOZ_POST_PROGRAM_COMMAND = $(topsrcdir)/build/qemu-wrap --qemu $(QEMU_EXE) --libdir $(CROSS_LIB)
 endif
 
-# Place a GDB Python auto-load file next to the shell executable, both in
-# the build directory and in the dist/bin directory.
-PP_TARGETS += SHELL_AUTOLOAD
-SHELL_AUTOLOAD := js-gdb.py.in
-SHELL_AUTOLOAD_FLAGS := -Dtopsrcdir=$(abspath $(srcdir)/..)
-
-INSTALL_TARGETS += SHELL_INSTALL_AUTOLOAD
-SHELL_INSTALL_AUTOLOAD_FILES := $(CURDIR)/js-gdb.py
-SHELL_INSTALL_AUTOLOAD_DEST := $(DIST)/bin
-
 include $(topsrcdir)/config/rules.mk
 
 # People expect the js shell to wind up in the top-level JS dir.
 libs::
 	$(INSTALL) $(IFLAGS2) $(PROGRAM) ..
 
 GARBAGE += ../$(PROGRAM)
 
--- a/js/src/shell/moz.build
+++ b/js/src/shell/moz.build
@@ -43,8 +43,14 @@ shellmoduleloader.script = '../builtin/e
 shellmoduleloader.inputs = [
     '../js.msg',
     'ModuleLoader.js',
 ]
 
 # This is intended as a temporary workaround to enable VS2015.
 if CONFIG['_MSC_VER']:
     CXXFLAGS += ['-wd4312']
+
+# Place a GDB Python auto-load file next to the shell executable, both in
+# the build directory and in the dist/bin directory.
+DEFINES['topsrcdir'] = '%s/js/src' % TOPSRCDIR
+FINAL_TARGET_PP_FILES += ['js-gdb.py.in']
+OBJDIR_FILES.js.src.shell += ['!/dist/bin/js-gdb.py']
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -5100,19 +5100,21 @@ ChooseScaleAndSetTransform(FrameLayerBui
       } else {
         displaySize = presContext->GetVisibleArea().Size();
       }
       // compute scale using the animation on the container (ignoring
       // its ancestors)
       scale = nsLayoutUtils::ComputeSuitableScaleForAnimation(
                 aContainerFrame, aVisibleRect.Size(),
                 displaySize);
-      // multiply by the scale inherited from ancestors
-      scale.width *= aIncomingScale.mXScale;
-      scale.height *= aIncomingScale.mYScale;
+      // multiply by the scale inherited from ancestors--we use a uniform
+      // scale factor to prevent blurring when the layer is rotated.
+      float incomingScale = std::max(aIncomingScale.mXScale, aIncomingScale.mYScale);
+      scale.width *= incomingScale;
+      scale.height *= incomingScale;
     } else {
       // Scale factors are normalized to a power of 2 to reduce the number of resolution changes
       scale = RoundToFloatPrecision(ThebesMatrix(transform2d).ScaleFactors(true));
       // For frames with a changing transform that's not just a translation,
       // round scale factors up to nearest power-of-2 boundary so that we don't
       // keep having to redraw the content as it scales up and down. Rounding up to nearest
       // power-of-2 boundary ensures we never scale up, only down --- avoiding
       // jaggies. It also ensures we never scale down by more than a factor of 2,
--- a/layout/base/FramePropertyTable.h
+++ b/layout/base/FramePropertyTable.h
@@ -182,21 +182,21 @@ public:
    * you're doing a lookup anyway it would be far more efficient to call Get()
    * or Remove() and check the aFoundResult outparam to find out whether the
    * property is set. Legitimate non-assertion uses include:
    *
    *   - Checking if a frame property is set in cases where that's all we want
    *     to know (i.e., we don't intend to read the actual value or remove the
    *     property).
    *
-   *   - Calling IsSet() before Set() in cases where we don't want to overwrite
+   *   - Calling Has() before Set() in cases where we don't want to overwrite
    *     an existing value for the frame property.
    */
   template<typename T>
-  bool IsSet(const nsIFrame* aFrame, Descriptor<T> aProperty)
+  bool Has(const nsIFrame* aFrame, Descriptor<T> aProperty)
   {
     bool foundResult = false;
     mozilla::Unused << GetInternal(aFrame, aProperty, &foundResult);
     return foundResult;
   }
 
   /**
    * Get a property value for a frame. This requires one hashtable
@@ -384,19 +384,19 @@ public:
 
   template<typename T>
   void Set(Descriptor<T> aProperty, PropertyType<T> aValue) const
   {
     mTable->Set(mFrame, aProperty, aValue);
   }
 
   template<typename T>
-  bool IsSet(Descriptor<T> aProperty) const
+  bool Has(Descriptor<T> aProperty) const
   {
-    return mTable->IsSet(mFrame, aProperty);
+    return mTable->Has(mFrame, aProperty);
   }
 
   template<typename T>
   PropertyType<T> Get(Descriptor<T> aProperty,
                       bool* aFoundResult = nullptr) const
   {
     return mTable->Get(mFrame, aProperty, aFoundResult);
   }
new file mode 100644
--- /dev/null
+++ b/layout/base/crashtests/1234622-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+
+<script>
+
+window.addEventListener("load", function() {
+    setTimeout(function() {
+        window.location = "data:text/html,2";
+    }, 0);
+}, false);
+
+window.addEventListener("pagehide", function() {
+    var x = document.createElement("object");
+    x.setAttribute("data", "data:text/plain,3");
+    document.documentElement.appendChild(x);
+}, false);
+
+</script>
--- a/layout/base/crashtests/crashtests.list
+++ b/layout/base/crashtests/crashtests.list
@@ -463,9 +463,10 @@ load 1043163-1.html
 load 1061028.html
 load 1107508-1.html
 load 1116104.html
 load 1127198-1.html
 load 1140198.html
 pref(layout.css.grid.enabled,true) load 1156588.html
 load 1162813.xul
 load 1163583.html
+load 1234622-1.html
 load 1235467-1.html
--- a/layout/base/nsDisplayList.h
+++ b/layout/base/nsDisplayList.h
@@ -220,17 +220,17 @@ public:
    * @param aMode encodes what the builder is being used for.
    * @param aBuildCaret whether or not we should include the caret in any
    * display lists that we make.
    */
   enum Mode {
     PAINTING,
     EVENT_DELIVERY,
     PLUGIN_GEOMETRY,
-    IMAGE_VISIBILITY,
+    FRAME_VISIBILITY,
     TRANSFORM_COMPUTATION
   };
   nsDisplayListBuilder(nsIFrame* aReferenceFrame, Mode aMode, bool aBuildCaret);
   ~nsDisplayListBuilder();
 
   void SetWillComputePluginGeometry(bool aWillComputePluginGeometry)
   {
     mWillComputePluginGeometry = aWillComputePluginGeometry;
@@ -256,21 +256,23 @@ public:
    * @return true if the display list is being built to compute geometry
    * for plugins.
    */
   bool IsForPluginGeometry() { return mMode == PLUGIN_GEOMETRY; }
   /**
    * @return true if the display list is being built for painting.
    */
   bool IsForPainting() { return mMode == PAINTING; }
+
   /**
-   * @return true if the display list is being built for determining image
+   * @return true if the display list is being built for determining frame
    * visibility.
    */
-  bool IsForImageVisibility() { return mMode == IMAGE_VISIBILITY; }
+  bool IsForFrameVisibility() { return mMode == FRAME_VISIBILITY; }
+
   bool WillComputePluginGeometry() { return mWillComputePluginGeometry; }
   /**
    * @return true if "painting is suppressed" during page load and we
    * should paint only the background of the document.
    */
   bool IsBackgroundOnly() {
     NS_ASSERTION(mPresShellStates.Length() > 0,
                  "don't call this if we're not in a presshell");
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -2457,27 +2457,29 @@ nsDocumentViewer::FindContainerView()
   if (mContainer) {
     nsCOMPtr<nsIDocShell> docShell(mContainer);
     nsCOMPtr<nsPIDOMWindowOuter> pwin(docShell->GetWindow());
     if (pwin) {
       nsCOMPtr<Element> containerElement = pwin->GetFrameElementInternal();
       if (!containerElement) {
         return nullptr;
       }
+
       nsCOMPtr<nsIPresShell> parentPresShell;
-      nsCOMPtr<nsIDocShellTreeItem> parentDocShellItem;
-      docShell->GetParent(getter_AddRefs(parentDocShellItem));
-      if (parentDocShellItem) {
-        nsCOMPtr<nsIDocShell> parentDocShell = do_QueryInterface(parentDocShellItem);
-        parentPresShell = parentDocShell->GetPresShell();
+      nsCOMPtr<nsIDocument> parentDoc = containerElement->GetCurrentDoc();
+      if (parentDoc) {
+        parentPresShell = parentDoc->GetShell();
       }
+
       if (!parentPresShell) {
-        nsCOMPtr<nsIDocument> parentDoc = containerElement->GetCurrentDoc();
-        if (parentDoc) {
-          parentPresShell = parentDoc->GetShell();
+        nsCOMPtr<nsIDocShellTreeItem> parentDocShellItem;
+        docShell->GetParent(getter_AddRefs(parentDocShellItem));
+        if (parentDocShellItem) {
+          nsCOMPtr<nsIDocShell> parentDocShell = do_QueryInterface(parentDocShellItem);
+          parentPresShell = parentDocShell->GetPresShell();
         }
       }
       if (!parentPresShell) {
         NS_WARNING("Subdocument container has no presshell");
       } else {
         nsIFrame* subdocFrame = parentPresShell->GetRealPrimaryFrameFor(containerElement);
         if (subdocFrame) {
           // subdocFrame might not be a subdocument frame; the frame
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -133,20 +133,20 @@ typedef struct CapturingContentInfo {
   // capture should only be allowed during a mousedown event
   bool mAllowed;
   bool mPointerLock;
   bool mRetargetToElement;
   bool mPreventDrag;
   mozilla::StaticRefPtr<nsIContent> mContent;
 } CapturingContentInfo;
 
-// f17842ee-f1f0-4193-814f-70d706b67060
+// a75573d6-34c8-4485-8fb7-edcb6fc70e12
 #define NS_IPRESSHELL_IID \
-{ 0xf17842ee, 0xf1f0, 0x4193, \
-  { 0x81, 0x4f, 0x70, 0xd7, 0x06, 0xb6, 0x70, 0x60 } }
+{ 0xa75573d6, 0x34c8, 0x4485, \
+  { 0x8f, 0xb7, 0xed, 0xcb, 0x6f, 0xc7, 0x0e, 0x12 } }
 
 // debug VerifyReflow flags
 #define VERIFY_REFLOW_ON                    0x01
 #define VERIFY_REFLOW_NOISY                 0x02
 #define VERIFY_REFLOW_ALL                   0x04
 #define VERIFY_REFLOW_DUMP_COMMANDS         0x08
 #define VERIFY_REFLOW_NOISY_RC              0x10
 #define VERIFY_REFLOW_REALLY_NOISY_RC       0x20
@@ -1559,33 +1559,48 @@ public:
     mFontSizeInflationEnabledIsDirty = true;
   }
 
   virtual void AddInvalidateHiddenPresShellObserver(nsRefreshDriver *aDriver) = 0;
 
   void InvalidatePresShellIfHidden();
   void CancelInvalidatePresShellIfHidden();
 
-  // Schedule an update of the list of visible images.
-  virtual void ScheduleImageVisibilityUpdate() = 0;
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Approximate frame visibility tracking public API.
+  //////////////////////////////////////////////////////////////////////////////
 
-  // Clears the current list of visible images on this presshell and replaces it
-  // with images that are in the display list aList.
-  virtual void RebuildImageVisibilityDisplayList(const nsDisplayList& aList) = 0;
-  virtual void RebuildImageVisibility(nsRect* aRect = nullptr,
-                                      bool aRemoveOnly = false) = 0;
+  /// Schedule an update of the list of approximately visible frames "soon".
+  /// This lets the refresh driver know that we want a visibility update in the
+  /// near future. The refresh driver applies its own heuristics and throttling
+  /// to decide when to actually perform the visibility update.
+  virtual void ScheduleApproximateFrameVisibilityUpdateSoon() = 0;
 
-  // Ensures the image is in the list of visible images.
-  virtual void EnsureImageInVisibleList(nsIImageLoadingContent* aImage) = 0;
+  /// Schedule an update of the list of approximately visible frames "now". The
+  /// update runs asynchronously, but it will be posted to the event loop
+  /// immediately. Prefer the "soon" variation of this method when possible, as
+  /// this variation ignores the refresh driver's heuristics.
+  virtual void ScheduleApproximateFrameVisibilityUpdateNow() = 0;
 
-  // Removes the image from the list of visible images if it is present there.
-  virtual void RemoveImageFromVisibleList(nsIImageLoadingContent* aImage) = 0;
+  /// Clears the current list of approximately visible frames on this pres shell
+  /// and replaces it with frames that are in the display list @aList.
+  virtual void RebuildApproximateFrameVisibilityDisplayList(const nsDisplayList& aList) = 0;
+  virtual void RebuildApproximateFrameVisibility(nsRect* aRect = nullptr,
+                                                 bool aRemoveOnly = false) = 0;
 
-  // Whether we should assume all images are visible.
-  virtual bool AssumeAllImagesVisible() = 0;
+  /// Ensures @aFrame is in the list of approximately visible frames.
+  virtual void EnsureFrameInApproximatelyVisibleList(nsIFrame* aFrame) = 0;
+
+  /// Removes @aFrame from the list of approximately visible frames if present.
+  virtual void RemoveFrameFromApproximatelyVisibleList(nsIFrame* aFrame) = 0;
+
+  /// Whether we should assume all frames are visible.
+  virtual bool AssumeAllFramesVisible() = 0;
+
 
   /**
    * Returns whether the document's style set's rule processor for the
    * specified level of the cascade is shared by multiple style sets.
    *
    * @param aSheetType One of the nsIStyleSheetService.*_SHEET constants.
    */
   nsresult HasRuleProcessorUsedByMultipleStyleSets(uint32_t aSheetType,
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -1209,45 +1209,45 @@ nsLayoutUtils::SetDisplayPortMargins(nsI
   nsIFrame* frame = GetScrollFrameFromContent(aContent);
   nsIScrollableFrame* scrollableFrame = frame ? frame->GetScrollTargetFrame() : nullptr;
   if (!scrollableFrame) {
     return true;
   }
 
   scrollableFrame->TriggerDisplayPortExpiration();
 
-  // Display port margins changing means that the set of visible images may
+  // Display port margins changing means that the set of visible frames may
   // have drastically changed. Check if we should schedule an update.
   hadDisplayPort =
-    scrollableFrame->GetDisplayPortAtLastImageVisibilityUpdate(&oldDisplayPort);
-
-  bool needImageVisibilityUpdate = !hadDisplayPort;
+    scrollableFrame->GetDisplayPortAtLastApproximateFrameVisibilityUpdate(&oldDisplayPort);
+
+  bool needVisibilityUpdate = !hadDisplayPort;
   // Check if the total size has changed by a large factor.
-  if (!needImageVisibilityUpdate) {
+  if (!needVisibilityUpdate) {
     if ((newDisplayPort.width > 2 * oldDisplayPort.width) ||
         (oldDisplayPort.width > 2 * newDisplayPort.width) ||
         (newDisplayPort.height > 2 * oldDisplayPort.height) ||
         (oldDisplayPort.height > 2 * newDisplayPort.height)) {
-      needImageVisibilityUpdate = true;
+      needVisibilityUpdate = true;
     }
   }
   // Check if it's moved by a significant amount.
-  if (!needImageVisibilityUpdate) {
+  if (!needVisibilityUpdate) {
     if (nsRect* baseData = static_cast<nsRect*>(aContent->GetProperty(nsGkAtoms::DisplayPortBase))) {
       nsRect base = *baseData;
       if ((std::abs(newDisplayPort.X() - oldDisplayPort.X()) > base.width) ||
           (std::abs(newDisplayPort.XMost() - oldDisplayPort.XMost()) > base.width) ||
           (std::abs(newDisplayPort.Y() - oldDisplayPort.Y()) > base.height) ||
           (std::abs(newDisplayPort.YMost() - oldDisplayPort.YMost()) > base.height)) {
-        needImageVisibilityUpdate = true;
+        needVisibilityUpdate = true;
       }
     }
   }
-  if (needImageVisibilityUpdate) {
-    aPresShell->ScheduleImageVisibilityUpdate();
+  if (needVisibilityUpdate) {
+    aPresShell->ScheduleApproximateFrameVisibilityUpdateNow();
   }
 
   return true;
 }
 
 void
 nsLayoutUtils::SetDisplayPortBase(nsIContent* aContent, const nsRect& aBase)
 {
@@ -8049,87 +8049,16 @@ nsLayoutUtils::GetBoxShadowRectForFrame(
     tmpRect.Inflate(shadow->mSpread);
     tmpRect.Inflate(
       nsContextBoxBlur::GetBlurRadiusMargin(shadow->mRadius, A2D));
     shadows.UnionRect(shadows, tmpRect);
   }
   return shadows;
 }
 
-/* static */ void
-nsLayoutUtils::UpdateImageVisibilityForFrame(nsIFrame* aImageFrame)
-{
-#ifdef DEBUG
-  nsIAtom* type = aImageFrame->GetType();
-  MOZ_ASSERT(type == nsGkAtoms::imageFrame ||
-             type == nsGkAtoms::imageControlFrame ||
-             type == nsGkAtoms::svgImageFrame, "wrong type of frame");
-#endif
-
-  nsCOMPtr<nsIImageLoadingContent> content = do_QueryInterface(aImageFrame->GetContent());
-  if (!content) {
-    return;
-  }
-
-  nsIPresShell* presShell = aImageFrame->PresContext()->PresShell();
-  if (presShell->AssumeAllImagesVisible()) {
-    presShell->EnsureImageInVisibleList(content);
-    return;
-  }
-
-  bool visible = true;
-  nsIFrame* f = aImageFrame->GetParent();
-  nsRect rect = aImageFrame->GetContentRectRelativeToSelf();
-  nsIFrame* rectFrame = aImageFrame;
-  while (f) {
-    nsIScrollableFrame* sf = do_QueryFrame(f);
-    if (sf) {
-      nsRect transformedRect =
-        nsLayoutUtils::TransformFrameRectToAncestor(rectFrame, rect, f);
-      if (!sf->IsRectNearlyVisible(transformedRect)) {
-        visible = false;
-        break;
-      }
-      // Move transformedRect to be contained in the scrollport as best we can
-      // (it might not fit) to pretend that it was scrolled into view.
-      nsRect scrollPort = sf->GetScrollPortRect();
-      if (transformedRect.XMost() > scrollPort.XMost()) {
-        transformedRect.x -= transformedRect.XMost() - scrollPort.XMost();
-      }
-      if (transformedRect.x < scrollPort.x) {
-        transformedRect.x = scrollPort.x;
-      }
-      if (transformedRect.YMost() > scrollPort.YMost()) {
-        transformedRect.y -= transformedRect.YMost() - scrollPort.YMost();
-      }
-      if (transformedRect.y < scrollPort.y) {
-        transformedRect.y = scrollPort.y;
-      }
-      transformedRect.width = std::min(transformedRect.width, scrollPort.width);
-      transformedRect.height = std::min(transformedRect.height, scrollPort.height);
-      rect = transformedRect;
-      rectFrame = f;
-    }
-    nsIFrame* parent = f->GetParent();
-    if (!parent) {
-      parent = nsLayoutUtils::GetCrossDocParentFrame(f);
-      if (parent && parent->PresContext()->IsChrome()) {
-        break;
-      }
-    }
-    f = parent;
-  }
-
-  if (visible) {
-    presShell->EnsureImageInVisibleList(content);
-  } else {
-    presShell->RemoveImageFromVisibleList(content);
-  }
-}
-
 /* static */ bool
 nsLayoutUtils::GetContentViewerSize(nsPresContext* aPresContext,
                                     LayoutDeviceIntSize& aOutSize)
 {
   nsCOMPtr<nsIDocShell> docShell = aPresContext->GetDocShell();
   if (!docShell) {
     return false;
   }
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -2539,24 +2539,16 @@ public:
   TransformToAncestorAndCombineRegions(
     const nsRect& aBounds,
     nsIFrame* aFrame,
     const nsIFrame* aAncestorFrame,
     nsRegion* aPreciseTargetDest,
     nsRegion* aImpreciseTargetDest);
 
   /**
-   * Determine if aImageFrame (which is an nsImageFrame, nsImageControlFrame, or
-   * nsSVGImageFrame) is visible or close to being visible via scrolling and
-   * update the presshell with this knowledge.
-   */
-  static void
-  UpdateImageVisibilityForFrame(nsIFrame* aImageFrame);
-
-  /**
    * Populate aOutSize with the size of the content viewer corresponding
    * to the given prescontext. Return true if the size was set, false
    * otherwise.
    */
   static bool
   GetContentViewerSize(nsPresContext* aPresContext,
                        LayoutDeviceIntSize& aOutSize);
 
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -2204,18 +2204,17 @@ NotifyTabSizeModeChanged(TabParent* aTab
 
 void
 nsPresContext::SizeModeChanged(nsSizeMode aSizeMode)
 {
   if (HasCachedStyleData()) {
     nsContentUtils::CallOnAllRemoteChildren(mDocument->GetWindow(),
                                             NotifyTabSizeModeChanged,
                                             &aSizeMode);
-    MediaFeatureValuesChangedAllDocuments(eRestyle_Subtree,
-                                          NS_STYLE_HINT_REFLOW);
+    MediaFeatureValuesChangedAllDocuments(nsRestyleHint(0));
   }
 }
 
 nsCompatibility
 nsPresContext::CompatibilityMode() const
 {
   return Document()->GetCompatibilityMode();
 }
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -1165,19 +1165,19 @@ PresShell::Destroy()
 
   if (mDelayedPaintTimer) {
     mDelayedPaintTimer->Cancel();
     mDelayedPaintTimer = nullptr;
   }
 
   mSynthMouseMoveEvent.Revoke();
 
-  mUpdateImageVisibilityEvent.Revoke();
-
-  ClearVisibleImagesList(nsIImageLoadingContent::ON_NONVISIBLE_REQUEST_DISCARD);
+  mUpdateApproximateFrameVisibilityEvent.Revoke();
+
+  ClearApproximatelyVisibleFramesList(Some(OnNonvisible::DISCARD_IMAGES));
 
   if (mCaret) {
     mCaret->Terminate();
     mCaret = nullptr;
   }
 
   if (mSelection) {
     mSelection->DisconnectFromPresShell();
@@ -3771,17 +3771,17 @@ PresShell::UnsuppressAndInvalidate()
   }
 
   // now that painting is unsuppressed, focus may be set on the document
   if (nsPIDOMWindowOuter* win = mDocument->GetWindow())
     win->SetReadyForFocus();
 
   if (!mHaveShutDown) {
     SynthesizeMouseMove(false);
-    ScheduleImageVisibilityUpdate();
+    ScheduleApproximateFrameVisibilityUpdateNow();
   }
 }
 
 void
 PresShell::UnsuppressPainting()
 {
   if (mPaintSuppressionTimer) {
     mPaintSuppressionTimer->Cancel();
@@ -5578,97 +5578,83 @@ AddFrameToVisibleRegions(nsIFrame* aFram
 
   CSSIntRegion* regionForView = aVisibleRegions->LookupOrAdd(viewID);
   MOZ_ASSERT(regionForView);
 
   regionForView->OrWith(CSSPixel::FromAppUnitsRounded(frameRectInScrolledFrameSpace));
 }
 
 /* static */ void
-PresShell::MarkImagesInListVisible(const nsDisplayList& aList,
-                                   Maybe<VisibleRegions>& aVisibleRegions)
+PresShell::MarkFramesInListApproximatelyVisible(const nsDisplayList& aList,
+                                                Maybe<VisibleRegions>& aVisibleRegions)
 {
   for (nsDisplayItem* item = aList.GetBottom(); item; item = item->GetAbove()) {
     nsDisplayList* sublist = item->GetChildren();
     if (sublist) {
-      MarkImagesInListVisible(*sublist, aVisibleRegions);
+      MarkFramesInListApproximatelyVisible(*sublist, aVisibleRegions);
+      continue;
+    }
+
+    nsIFrame* frame = item->Frame();
+    MOZ_ASSERT(frame);
+
+    if (!frame->TrackingVisibility()) {
       continue;
     }
-    nsIFrame* f = item->Frame();
-    // We could check the type of the display item, only a handful can hold an
-    // image loading content.
-    // dont bother nscomptr here, it is wasteful
-    nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(f->GetContent()));
-    if (content) {
-      // use the presshell containing the image
-      PresShell* presShell = static_cast<PresShell*>(f->PresContext()->PresShell());
-      uint32_t count = presShell->mVisibleImages.Count();
-      presShell->mVisibleImages.PutEntry(content);
-      if (presShell->mVisibleImages.Count() > count) {
-        // content was added to mVisibleImages, so we need to increment its visible count
-        content->IncrementVisibleCount();
-      }
-
-      AddFrameToVisibleRegions(f, presShell->mViewManager, aVisibleRegions);
-    }
-  }
-}
-
-void
-PresShell::ReportAnyBadState()
+
+    // Use the presshell containing the frame.
+    auto* presShell = static_cast<PresShell*>(frame->PresContext()->PresShell());
+    uint32_t count = presShell->mApproximatelyVisibleFrames.Count();
+    MOZ_ASSERT(!presShell->AssumeAllFramesVisible());
+    presShell->mApproximatelyVisibleFrames.PutEntry(frame);
+    if (presShell->mApproximatelyVisibleFrames.Count() > count) {
+      // The frame was added to mApproximatelyVisibleFrames, so increment its visible count.
+      frame->IncApproximateVisibleCount();
+    }
+
+    AddFrameToVisibleRegions(frame, presShell->mViewManager, aVisibleRegions);
+  }
+}
+
+void
+PresShell::ReportBadStateDuringVisibilityUpdate()
 {
   if (!NS_IsMainThread()) {
-    gfxCriticalNote << "Got null image in image visibility: off-main-thread";
+    gfxCriticalNote << "Got null frame in frame visibility: off-main-thread";
   }
   if (mIsZombie) {
-    gfxCriticalNote << "Got null image in image visibility: mIsZombie";
+    gfxCriticalNote << "Got null frame in frame visibility: mIsZombie";
   }
   if (mIsDestroying) {
-    gfxCriticalNote << "Got null image in image visibility: mIsDestroying";
+    gfxCriticalNote << "Got null frame in frame visibility: mIsDestroying";
   }
   if (mIsReflowing) {
-    gfxCriticalNote << "Got null image in image visibility: mIsReflowing";
+    gfxCriticalNote << "Got null frame in frame visibility: mIsReflowing";
   }
   if (mPaintingIsFrozen) {
-    gfxCriticalNote << "Got null image in image visibility: mPaintingIsFrozen";
+    gfxCriticalNote << "Got null frame in frame visibility: mPaintingIsFrozen";
   }
   if (mForwardingContainer) {
-    gfxCriticalNote << "Got null image in image visibility: mForwardingContainer";
+    gfxCriticalNote << "Got null frame in frame visibility: mForwardingContainer";
   }
   if (mIsNeverPainting) {
-    gfxCriticalNote << "Got null image in image visibility: mIsNeverPainting";
+    gfxCriticalNote << "Got null frame in frame visibility: mIsNeverPainting";
   }
   if (mIsDocumentGone) {
-    gfxCriticalNote << "Got null image in image visibility: mIsDocumentGone";
+    gfxCriticalNote << "Got null frame in frame visibility: mIsDocumentGone";
   }
   if (!nsContentUtils::IsSafeToRunScript()) {
-    gfxCriticalNote << "Got null image in image visibility: not safe to run script";
-  }
-}
-
-void
-PresShell::SetInImageVisibility(bool aState)
-{
-  mInImageVisibility = aState;
-}
-
-static void
-DecrementVisibleCount(nsTHashtable<nsRefPtrHashKey<nsIImageLoadingContent>>& aImages,
-                      uint32_t aNonvisibleAction, PresShell* aPresShell)
-{
-  for (auto iter = aImages.Iter(); !iter.Done(); iter.Next()) {
-    if (MOZ_UNLIKELY(!iter.Get()->GetKey())) {
-      // We are about to crash, annotate crash report with some info that might
-      // help debug the crash (bug 1251150)
-      aPresShell->ReportAnyBadState();
-    }
-    aPresShell->SetInImageVisibility(true);
-    iter.Get()->GetKey()->DecrementVisibleCount(aNonvisibleAction);
-    aPresShell->SetInImageVisibility(false);
-  }
+    gfxCriticalNote << "Got null frame in frame visibility: not safe to run script";
+  }
+}
+
+void
+PresShell::SetInFrameVisibilityUpdate(bool aState)
+{
+  mInFrameVisibilityUpdate = aState;
 }
 
 static void
 NotifyCompositorOfVisibleRegionsChange(PresShell* aPresShell,
                                        const Maybe<VisibleRegions>& aRegions)
 {
   if (!aRegions) {
     return;
@@ -5711,133 +5697,161 @@ NotifyCompositorOfVisibleRegionsChange(P
     MOZ_ASSERT(region);
 
     const ScrollableLayerGuid guid(layersId, presShellId, viewId);
 
     compositorChild->SendNotifyApproximatelyVisibleRegion(guid, *region);
   }
 }
 
-void
-PresShell::RebuildImageVisibilityDisplayList(const nsDisplayList& aList)
-{
-  MOZ_ASSERT(!mImageVisibilityVisited, "already visited?");
-  mImageVisibilityVisited = true;
-  // Remove the entries of the mVisibleImages hashtable and put them in
-  // oldVisibleImages.
-  nsTHashtable< nsRefPtrHashKey<nsIImageLoadingContent> > oldVisibleImages;
-  mVisibleImages.SwapElements(oldVisibleImages);
+/* static */ void
+PresShell::DecApproximateVisibleCount(VisibleFrames& aFrames,
+                                      Maybe<OnNonvisible> aNonvisibleAction
+                                        /* = Nothing() */)
+{
+  for (auto iter = aFrames.Iter(); !iter.Done(); iter.Next()) {
+    nsIFrame* frame = iter.Get()->GetKey();
+
+    if (MOZ_UNLIKELY(!frame)) {
+      // We are about to crash, annotate crash report with some info that might
+      // help debug the crash (bug 1251150)
+      ReportBadStateDuringVisibilityUpdate();
+    }
+
+    SetInFrameVisibilityUpdate(true);
+
+    // Decrement the frame's visible count if we're still tracking its
+    // visibility. (We may not be, if the frame disabled visibility tracking
+    // after we added it to the visible frames list.)
+    if (frame->TrackingVisibility()) {
+      frame->DecApproximateVisibleCount(aNonvisibleAction);
+    }
+
+    SetInFrameVisibilityUpdate(false);
+  }
+}
+
+void
+PresShell::RebuildApproximateFrameVisibilityDisplayList(const nsDisplayList& aList)
+{
+  MOZ_ASSERT(!mApproximateFrameVisibilityVisited, "already visited?");
+  mApproximateFrameVisibilityVisited = true;
+
+  // Remove the entries of the mApproximatelyVisibleFrames hashtable and put
+  // them in oldApproxVisibleFrames.
+  VisibleFrames oldApproximatelyVisibleFrames;
+  mApproximatelyVisibleFrames.SwapElements(oldApproximatelyVisibleFrames);
 
   // If we're visualizing visible regions, create a VisibleRegions object to
   // store information about them. The functions we call will populate this
   // object and send it to the compositor only if it's Some(), so we don't
   // need to check the prefs everywhere.
   Maybe<VisibleRegions> visibleRegions;
   if (gfxPrefs::APZMinimap() && gfxPrefs::APZMinimapVisibilityEnabled()) {
     visibleRegions.emplace();
   }
 
-  MarkImagesInListVisible(aList, visibleRegions);
-
-  DecrementVisibleCount(oldVisibleImages,
-                        nsIImageLoadingContent::ON_NONVISIBLE_NO_ACTION, this);
+  MarkFramesInListApproximatelyVisible(aList, visibleRegions);
+
+  DecApproximateVisibleCount(oldApproximatelyVisibleFrames);
 
   NotifyCompositorOfVisibleRegionsChange(this, visibleRegions);
 }
 
 /* static */ void
-PresShell::ClearImageVisibilityVisited(nsView* aView, bool aClear)
+PresShell::ClearApproximateFrameVisibilityVisited(nsView* aView, bool aClear)
 {
   nsViewManager* vm = aView->GetViewManager();
   if (aClear) {
     PresShell* presShell = static_cast<PresShell*>(vm->GetPresShell());
-    if (!presShell->mImageVisibilityVisited) {
-      presShell->ClearVisibleImagesList(
-        nsIImageLoadingContent::ON_NONVISIBLE_NO_ACTION);
-    }
-    presShell->mImageVisibilityVisited = false;
+    if (!presShell->mApproximateFrameVisibilityVisited) {
+      presShell->ClearApproximatelyVisibleFramesList();
+    }
+    presShell->mApproximateFrameVisibilityVisited = false;
   }
   for (nsView* v = aView->GetFirstChild(); v; v = v->GetNextSibling()) {
-    ClearImageVisibilityVisited(v, v->GetViewManager() != vm);
-  }
-}
-
-void
-PresShell::ClearVisibleImagesList(uint32_t aNonvisibleAction)
-{
-  if (mInImageVisibility) {
-    gfxCriticalNoteOnce << "ClearVisibleImagesList is re-entering on "
+    ClearApproximateFrameVisibilityVisited(v, v->GetViewManager() != vm);
+  }
+}
+
+void
+PresShell::ClearApproximatelyVisibleFramesList(Maybe<OnNonvisible> aNonvisibleAction
+                                                 /* = Nothing() */)
+{
+  if (mInFrameVisibilityUpdate) {
+    gfxCriticalNoteOnce << "ClearApproximatelyVisibleFramesList is re-entering on "
                         << (NS_IsMainThread() ? "" : "non-") << "main thread";
   }
-  DecrementVisibleCount(mVisibleImages, aNonvisibleAction, this);
-  mVisibleImages.Clear();
-}
-
-void
-PresShell::MarkImagesInSubtreeVisible(nsIFrame* aFrame,
-                                      const nsRect& aRect,
-                                      Maybe<VisibleRegions>& aVisibleRegions,
-                                      bool aRemoveOnly /* = false */)
+  DecApproximateVisibleCount(mApproximatelyVisibleFrames, aNonvisibleAction);
+  mApproximatelyVisibleFrames.Clear();
+}
+
+void
+PresShell::MarkFramesInSubtreeApproximatelyVisible(nsIFrame* aFrame,
+                                                   const nsRect& aRect,
+                                                   Maybe<VisibleRegions>& aVisibleRegions,
+                                                   bool aRemoveOnly /* = false */)
 {
   MOZ_ASSERT(aFrame->PresContext()->PresShell() == this, "wrong presshell");
 
-  nsCOMPtr<nsIImageLoadingContent> content(do_QueryInterface(aFrame->GetContent()));
-  if (content && aFrame->StyleVisibility()->IsVisible() &&
-      (!aRemoveOnly || content->GetVisibleCount() > 0)) {
-    uint32_t count = mVisibleImages.Count();
-    mVisibleImages.PutEntry(content);
-    if (mVisibleImages.Count() > count) {
-      // content was added to mVisibleImages, so we need to increment its visible count
-      content->IncrementVisibleCount();
+  if (aFrame->TrackingVisibility() &&
+      aFrame->StyleVisibility()->IsVisible() &&
+      (!aRemoveOnly || aFrame->GetVisibility() == Visibility::APPROXIMATELY_VISIBLE)) {
+    MOZ_ASSERT(!AssumeAllFramesVisible());
+    uint32_t count = mApproximatelyVisibleFrames.Count();
+    mApproximatelyVisibleFrames.PutEntry(aFrame);
+    if (mApproximatelyVisibleFrames.Count() > count) {
+      // The frame was added to mApproximatelyVisibleFrames, so increment its visible count.
+      aFrame->IncApproximateVisibleCount();
     }
 
     AddFrameToVisibleRegions(aFrame, mViewManager, aVisibleRegions);
   }
 
   nsSubDocumentFrame* subdocFrame = do_QueryFrame(aFrame);
   if (subdocFrame) {
     nsIPresShell* presShell = subdocFrame->GetSubdocumentPresShellForPainting(
       nsSubDocumentFrame::IGNORE_PAINT_SUPPRESSION);
-    if (presShell) {
+    if (presShell && !presShell->AssumeAllFramesVisible()) {
       nsRect rect = aRect;
       nsIFrame* root = presShell->GetRootFrame();
       if (root) {
         rect.MoveBy(aFrame->GetOffsetToCrossDoc(root));
       } else {
         rect.MoveBy(-aFrame->GetContentRectRelativeToSelf().TopLeft());
       }
       rect = rect.ScaleToOtherAppUnitsRoundOut(
         aFrame->PresContext()->AppUnitsPerDevPixel(),
         presShell->GetPresContext()->AppUnitsPerDevPixel());
 
-      presShell->RebuildImageVisibility(&rect);
+      presShell->RebuildApproximateFrameVisibility(&rect);
     }
     return;
   }
 
   nsRect rect = aRect;
 
   nsIScrollableFrame* scrollFrame = do_QueryFrame(aFrame);
   if (scrollFrame) {
-    scrollFrame->NotifyImageVisibilityUpdate();
+    scrollFrame->NotifyApproximateFrameVisibilityUpdate();
     nsRect displayPort;
     bool usingDisplayport =
       nsLayoutUtils::GetDisplayPortForVisibilityTesting(
         aFrame->GetContent(), &displayPort, RelativeTo::ScrollFrame);
     if (usingDisplayport) {
       rect = displayPort;
     } else {
       rect = rect.Intersect(scrollFrame->GetScrollPortRect());
     }
     rect = scrollFrame->ExpandRectToNearlyVisible(rect);
   }
 
   bool preserves3DChildren = aFrame->Extend3DContext();
 
-  // we assume all images in popups are visible elsewhere, so we skip them here
+  // We assume all frames in popups are visible, so we skip them here.
   const nsIFrame::ChildListIDs skip(nsIFrame::kPopupList |
                                     nsIFrame::kSelectPopupList);
   for (nsIFrame::ChildListIterator childLists(aFrame);
        !childLists.IsDone(); childLists.Next()) {
     if (skip.Contains(childLists.CurrentID())) {
       continue;
     }
 
@@ -5853,101 +5867,99 @@ PresShell::MarkImagesInSubtreeVisible(ns
           nsRect out;
           if (nsDisplayTransform::UntransformRect(r, overflow, child, nsPoint(0,0), &out)) {
             r = out;
           } else {
             r.SetEmpty();
           }
         }
       }
-      MarkImagesInSubtreeVisible(child, r, aVisibleRegions);
-    }
-  }
-}
-
-void
-PresShell::RebuildImageVisibility(nsRect* aRect,
-                                  bool aRemoveOnly /* = false */)
-{
-  MOZ_ASSERT(!mImageVisibilityVisited, "already visited?");
-  mImageVisibilityVisited = true;
+      MarkFramesInSubtreeApproximatelyVisible(child, r, aVisibleRegions);
+    }
+  }
+}
+
+void
+PresShell::RebuildApproximateFrameVisibility(nsRect* aRect,
+                                             bool aRemoveOnly /* = false */)
+{
+  MOZ_ASSERT(!mApproximateFrameVisibilityVisited, "already visited?");
+  mApproximateFrameVisibilityVisited = true;
 
   nsIFrame* rootFrame = GetRootFrame();
   if (!rootFrame) {
     return;
   }
 
-  if (mInImageVisibility) {
-    gfxCriticalNoteOnce << "RebuildImageVisibility is re-entering on "
+  if (mInFrameVisibilityUpdate) {
+    gfxCriticalNoteOnce << "RebuildApproximateFrameVisibility is re-entering on "
                         << (NS_IsMainThread() ? "" : "non-") << "main thread";
   }
 
-  // Remove the entries of the mVisibleImages hashtable and put them in
-  // oldVisibleImages.
-  nsTHashtable< nsRefPtrHashKey<nsIImageLoadingContent> > oldVisibleImages;
-  mVisibleImages.SwapElements(oldVisibleImages);
+  // Remove the entries of the mApproximatelyVisibleFrames hashtable and put
+  // them in oldApproximatelyVisibleFrames.
+  VisibleFrames oldApproximatelyVisibleFrames;
+  mApproximatelyVisibleFrames.SwapElements(oldApproximatelyVisibleFrames);
 
   // If we're visualizing visible regions, create a VisibleRegions object to
   // store information about them. The functions we call will populate this
   // object and send it to the compositor only if it's Some(), so we don't
   // need to check the prefs everywhere.
   Maybe<VisibleRegions> visibleRegions;
   if (gfxPrefs::APZMinimap() && gfxPrefs::APZMinimapVisibilityEnabled()) {
     visibleRegions.emplace();
   }
 
   nsRect vis(nsPoint(0, 0), rootFrame->GetSize());
   if (aRect) {
     vis = *aRect;
   }
 
-  MarkImagesInSubtreeVisible(rootFrame, vis, visibleRegions, aRemoveOnly);
-
-  DecrementVisibleCount(oldVisibleImages,
-                        nsIImageLoadingContent::ON_NONVISIBLE_NO_ACTION, this);
+  MarkFramesInSubtreeApproximatelyVisible(rootFrame, vis, visibleRegions, aRemoveOnly);
+
+  DecApproximateVisibleCount(oldApproximatelyVisibleFrames);
 
   NotifyCompositorOfVisibleRegionsChange(this, visibleRegions);
 }
 
 void
-PresShell::UpdateImageVisibility()
-{
-  DoUpdateImageVisibility(/* aRemoveOnly = */ false);
-}
-
-void
-PresShell::DoUpdateImageVisibility(bool aRemoveOnly)
+PresShell::UpdateApproximateFrameVisibility()
+{
+  DoUpdateApproximateFrameVisibility(/* aRemoveOnly = */ false);
+}
+
+void
+PresShell::DoUpdateApproximateFrameVisibility(bool aRemoveOnly)
 {
   MOZ_ASSERT(!mPresContext || mPresContext->IsRootContentDocument(),
-    "updating image visibility on a non-root content document?");
-
-  mUpdateImageVisibilityEvent.Revoke();
+             "Updating approximate frame visibility on a non-root content document?");
+
+  mUpdateApproximateFrameVisibilityEvent.Revoke();
 
   if (mHaveShutDown || mIsDestroying) {
     return;
   }
 
   // call update on that frame
   nsIFrame* rootFrame = GetRootFrame();
   if (!rootFrame) {
-    ClearVisibleImagesList(
-      nsIImageLoadingContent::ON_NONVISIBLE_REQUEST_DISCARD);
-    return;
-  }
-
-  RebuildImageVisibility(/* aRect = */ nullptr, aRemoveOnly);
-  ClearImageVisibilityVisited(rootFrame->GetView(), true);
-
-#ifdef DEBUG_IMAGE_VISIBILITY_DISPLAY_LIST
-  // This can be used to debug the frame walker by comparing beforeImageList and
-  // mVisibleImages in RebuildImageVisibilityDisplayList to see if they produce
-  // the same results (mVisibleImages holds the images the display list thinks
-  // are visible, beforeImageList holds the images the frame walker thinks are
-  // visible).
-  nsDisplayListBuilder builder(rootFrame, nsDisplayListBuilder::IMAGE_VISIBILITY, false);
+    ClearApproximatelyVisibleFramesList(Some(OnNonvisible::DISCARD_IMAGES));
+    return;
+  }
+
+  RebuildApproximateFrameVisibility(/* aRect = */ nullptr, aRemoveOnly);
+  ClearApproximateFrameVisibilityVisited(rootFrame->GetView(), true);
+
+#ifdef DEBUG_FRAME_VISIBILITY_DISPLAY_LIST
+  // This can be used to debug the frame walker by comparing beforeFrameList
+  // and mApproximatelyVisibleFrames in RebuildFrameVisibilityDisplayList to see if
+  // they produce the same results (mApproximatelyVisibleFrames holds the frames the
+  // display list thinks are visible, beforeFrameList holds the frames the
+  // frame walker thinks are visible).
+  nsDisplayListBuilder builder(rootFrame, nsDisplayListBuilder::FRAME_VISIBILITY, false);
   nsRect updateRect(nsPoint(0, 0), rootFrame->GetSize());
   nsIFrame* rootScroll = GetRootScrollFrame();
   if (rootScroll) {
     nsIContent* content = rootScroll->GetContent();
     if (content) {
       Unused << nsLayoutUtils::GetDisplayPortForVisibilityTesting(content, &updateRect,
         RelativeTo::ScrollFrame);
     }
@@ -5957,138 +5969,169 @@ PresShell::DoUpdateImageVisibility(bool 
     }
   }
   builder.IgnorePaintSuppression();
   builder.EnterPresShell(rootFrame, updateRect);
   nsDisplayList list;
   rootFrame->BuildDisplayListForStackingContext(&builder, updateRect, &list);
   builder.LeavePresShell(rootFrame, updateRect);
 
-  RebuildImageVisibilityDisplayList(list);
-
-  ClearImageVisibilityVisited(rootFrame->GetView(), true);
+  RebuildApproximateFrameVisibilityDisplayList(list);
+
+  ClearApproximateFrameVisibilityVisited(rootFrame->GetView(), true);
 
   list.DeleteAll();
 #endif
 }
 
 bool
-PresShell::AssumeAllImagesVisible()
-{
-  static bool sImageVisibilityEnabled = true;
-  static bool sImageVisibilityPrefCached = false;
-
-  if (!sImageVisibilityPrefCached) {
-    Preferences::AddBoolVarCache(&sImageVisibilityEnabled,
-      "layout.imagevisibility.enabled", true);
-    sImageVisibilityPrefCached = true;
-  }
-
-  if (!sImageVisibilityEnabled || !mPresContext || !mDocument) {
+PresShell::AssumeAllFramesVisible()
+{
+  static bool sFrameVisibilityEnabled = true;
+  static bool sFrameVisibilityPrefCached = false;
+
+  if (!sFrameVisibilityPrefCached) {
+    Preferences::AddBoolVarCache(&sFrameVisibilityEnabled,
+      "layout.framevisibility.enabled", true);
+    sFrameVisibilityPrefCached = true;
+  }
+
+  if (!sFrameVisibilityEnabled || !mPresContext || !mDocument) {
     return true;
   }
 
-  // We assume all images are visible in print, print preview, chrome, xul, and
+  // We assume all frames are visible in print, print preview, chrome, xul, and
   // resource docs and don't keep track of them.
   if (mPresContext->Type() == nsPresContext::eContext_PrintPreview ||
       mPresContext->Type() == nsPresContext::eContext_Print ||
       mPresContext->IsChrome() ||
       mDocument->IsResourceDoc() ||
       mDocument->IsXULDocument()) {
     return true;
   }
 
   return false;
 }
 
 void
-PresShell::ScheduleImageVisibilityUpdate()
-{
-  if (AssumeAllImagesVisible())
-    return;
+PresShell::ScheduleApproximateFrameVisibilityUpdateSoon()
+{
+  if (AssumeAllFramesVisible()) {
+    return;
+  }
+
+  if (!mPresContext) {
+    return;
+  }
+
+  nsRefreshDriver* refreshDriver = mPresContext->RefreshDriver();
+  if (!refreshDriver) {
+    return;
+  }
+
+  // Ask the refresh driver to update frame visibility soon.
+  refreshDriver->ScheduleFrameVisibilityUpdate();
+}
+
+void
+PresShell::ScheduleApproximateFrameVisibilityUpdateNow()
+{
+  if (AssumeAllFramesVisible()) {
+    return;
+  }
 
   if (!mPresContext->IsRootContentDocument()) {
     nsPresContext* presContext = mPresContext->GetToplevelContentDocumentPresContext();
     if (!presContext)
       return;
     MOZ_ASSERT(presContext->IsRootContentDocument(),
       "Didn't get a root prescontext from GetToplevelContentDocumentPresContext?");
-    presContext->PresShell()->ScheduleImageVisibilityUpdate();
-    return;
-  }
-
-  if (mHaveShutDown || mIsDestroying)
-    return;
-
-  if (mUpdateImageVisibilityEvent.IsPending())
-    return;
+    presContext->PresShell()->ScheduleApproximateFrameVisibilityUpdateNow();
+    return;
+  }
+
+  if (mHaveShutDown || mIsDestroying) {
+    return;
+  }
+
+  if (mUpdateApproximateFrameVisibilityEvent.IsPending()) {
+    return;
+  }
 
   RefPtr<nsRunnableMethod<PresShell> > ev =
-    NS_NewRunnableMethod(this, &PresShell::UpdateImageVisibility);
+    NS_NewRunnableMethod(this, &PresShell::UpdateApproximateFrameVisibility);
   if (NS_SUCCEEDED(NS_DispatchToCurrentThread(ev))) {
-    mUpdateImageVisibilityEvent = ev;
-  }
-}
-
-void
-PresShell::EnsureImageInVisibleList(nsIImageLoadingContent* aImage)
-{
-  if (AssumeAllImagesVisible()) {
-    aImage->IncrementVisibleCount();
+    mUpdateApproximateFrameVisibilityEvent = ev;
+  }
+}
+
+void
+PresShell::EnsureFrameInApproximatelyVisibleList(nsIFrame* aFrame)
+{
+  if (!aFrame->TrackingVisibility()) {
+    return;
+  }
+
+  if (AssumeAllFramesVisible()) {
+    aFrame->IncApproximateVisibleCount();
     return;
   }
 
 #ifdef DEBUG
-  // if it has a frame make sure its in this presshell
-  nsCOMPtr<nsIContent> content = do_QueryInterface(aImage);
+  // Make sure it's in this pres shell.
+  nsCOMPtr<nsIContent> content = aFrame->GetContent();
   if (content) {
     PresShell* shell = static_cast<PresShell*>(content->OwnerDoc()->GetShell());
     MOZ_ASSERT(!shell || shell == this, "wrong shell");
   }
 #endif
 
-  if (mInImageVisibility) {
-    gfxCriticalNoteOnce << "EnsureImageInVisibleList is re-entering on "
+  if (mInFrameVisibilityUpdate) {
+    gfxCriticalNoteOnce << "EnsureFrameInApproximatelyVisibleList is re-entering on "
                         << (NS_IsMainThread() ? "" : "non-") << "main thread";
   }
 
-  if (!mVisibleImages.Contains(aImage)) {
-    mVisibleImages.PutEntry(aImage);
-    aImage->IncrementVisibleCount();
-  }
-}
-
-void
-PresShell::RemoveImageFromVisibleList(nsIImageLoadingContent* aImage)
+  if (!mApproximatelyVisibleFrames.Contains(aFrame)) {
+    MOZ_ASSERT(!AssumeAllFramesVisible());
+    mApproximatelyVisibleFrames.PutEntry(aFrame);
+    aFrame->IncApproximateVisibleCount();
+  }
+}
+
+void
+PresShell::RemoveFrameFromApproximatelyVisibleList(nsIFrame* aFrame)
 {
 #ifdef DEBUG
-  // if it has a frame make sure its in this presshell
-  nsCOMPtr<nsIContent> content = do_QueryInterface(aImage);
+  // Make sure it's in this pres shell.
+  nsCOMPtr<nsIContent> content = aFrame->GetContent();
   if (content) {
     PresShell* shell = static_cast<PresShell*>(content->OwnerDoc()->GetShell());
     MOZ_ASSERT(!shell || shell == this, "wrong shell");
   }
 #endif
 
-  if (AssumeAllImagesVisible()) {
-    MOZ_ASSERT(mVisibleImages.Count() == 0, "shouldn't have any images in the table");
-    return;
-  }
-
-  if (mInImageVisibility) {
-    gfxCriticalNoteOnce << "RemoveImageFromVisibleList is re-entering on "
+  if (AssumeAllFramesVisible()) {
+    MOZ_ASSERT(mApproximatelyVisibleFrames.Count() == 0,
+               "Shouldn't have any frames in the table");
+    return;
+  }
+
+  if (mInFrameVisibilityUpdate) {
+    gfxCriticalNoteOnce << "RemoveFrameFromApproximatelyVisibleList is re-entering on "
                         << (NS_IsMainThread() ? "" : "non-") << "main thread";
   }
 
-  uint32_t count = mVisibleImages.Count();
-  mVisibleImages.RemoveEntry(aImage);
-  if (mVisibleImages.Count() < count) {
-    // aImage was in the hashtable, so we need to decrement its visible count
-    aImage->DecrementVisibleCount(
-      nsIImageLoadingContent::ON_NONVISIBLE_NO_ACTION);
+  uint32_t count = mApproximatelyVisibleFrames.Count();
+  mApproximatelyVisibleFrames.RemoveEntry(aFrame);
+
+  if (aFrame->TrackingVisibility() &&
+      mApproximatelyVisibleFrames.Count() < count) {
+    // aFrame was in the hashtable, and we're still tracking its visibility,
+    // so we need to decrement its visible count.
+    aFrame->DecApproximateVisibleCount();
   }
 }
 
 class nsAutoNotifyDidPaint
 {
 public:
   nsAutoNotifyDidPaint(PresShell* aShell, uint32_t aFlags)
     : mShell(aShell), mFlags(aFlags)
@@ -6157,17 +6200,17 @@ PresShell::Paint(nsView*        aViewToP
                  uint32_t        aFlags)
 {
   PROFILER_LABEL("PresShell", "Paint",
     js::ProfileEntry::Category::GRAPHICS);
 
   NS_ASSERTION(!mIsDestroying, "painting a destroyed PresShell");
   NS_ASSERTION(aViewToPaint, "null view");
 
-  MOZ_ASSERT(!mImageVisibilityVisited, "should have been cleared");
+  MOZ_ASSERT(!mApproximateFrameVisibilityVisited, "Should have been cleared");
 
   if (!mIsActive || mIsZombie) {
     return;
   }
 
   nsPresContext* presContext = GetPresContext();
   AUTO_LAYOUT_PHASE_ENTRY_POINT(presContext, Paint);
 
@@ -8824,17 +8867,17 @@ FreezeSubDocument(nsIDocument *aDocument
     shell->Freeze();
 
   return true;
 }
 
 void
 PresShell::Freeze()
 {
-  mUpdateImageVisibilityEvent.Revoke();
+  mUpdateApproximateFrameVisibilityEvent.Revoke();
 
   MaybeReleaseCapturingContent();
 
   mDocument->EnumerateActivityObservers(FreezeElement, nullptr);
 
   if (mCaret) {
     SetCaretEnabled(false);
   }
@@ -9571,18 +9614,18 @@ PresShell::Observe(nsISupports* aSubject
   if (!nsCRT::strcmp(aTopic, "author-sheet-removed")) {
     if (mStyleSet) {
       RemoveSheet(SheetType::Doc, aSubject);
     }
     return NS_OK;
   }
 
   if (!nsCRT::strcmp(aTopic, "memory-pressure")) {
-    if (!AssumeAllImagesVisible() && mPresContext->IsRootContentDocument()) {
-      DoUpdateImageVisibility(/* aRemoveOnly = */ true);
+    if (!AssumeAllFramesVisible() && mPresContext->IsRootContentDocument()) {
+      DoUpdateApproximateFrameVisibility(/* aRemoveOnly = */ true);
     }
     return NS_OK;
   }
 
   NS_WARNING("unrecognized topic in PresShell::Observe");
   return NS_ERROR_FAILURE;
 }
 
@@ -10847,26 +10890,25 @@ nsresult
 PresShell::UpdateImageLockingState()
 {
   // We're locked if we're both thawed and active.
   bool locked = !mFrozen && mIsActive;
 
   nsresult rv = mDocument->SetImageLockingState(locked);
 
   if (locked) {
-    if (mInImageVisibility) {
+    if (mInFrameVisibilityUpdate) {
       gfxCriticalNoteOnce << "UpdateImageLockingState is re-entering on "
                           << (NS_IsMainThread() ? "" : "non-") << "main thread";
     }
 
-    // Request decodes for visible images; we want to start decoding as
+    // Request decodes for visible image frames; we want to start decoding as
     // quickly as possible when we get foregrounded to minimize flashing.
-    for (auto iter = mVisibleImages.Iter(); !iter.Done(); iter.Next()) {
-      nsCOMPtr<nsIContent> content = do_QueryInterface(iter.Get()->GetKey());
-      nsImageFrame* imageFrame = do_QueryFrame(content->GetPrimaryFrame());
+    for (auto iter = mApproximatelyVisibleFrames.Iter(); !iter.Done(); iter.Next()) {
+      nsImageFrame* imageFrame = do_QueryFrame(iter.Get()->GetKey());
       if (imageFrame) {
         imageFrame->MaybeDecodeForPredictedSize();
       }
     }
   }
 
   return rv;
 }
@@ -10891,17 +10933,17 @@ PresShell::AddSizeOfIncludingThis(Malloc
                                   size_t *aTextRunsSize,
                                   size_t *aPresContextSize)
 {
   mFrameArena.AddSizeOfExcludingThis(aMallocSizeOf, aArenaObjectsSize);
   *aPresShellSize += aMallocSizeOf(this);
   if (mCaret) {
     *aPresShellSize += mCaret->SizeOfIncludingThis(aMallocSizeOf);
   }
-  *aPresShellSize += mVisibleImages.ShallowSizeOfExcludingThis(aMallocSizeOf);
+  *aPresShellSize += mApproximatelyVisibleFrames.ShallowSizeOfExcludingThis(aMallocSizeOf);
   *aPresShellSize += mFramesToDirty.ShallowSizeOfExcludingThis(aMallocSizeOf);
   *aPresShellSize += aArenaObjectsSize->mOther;
 
   if (nsStyleSet* styleSet = StyleSet()->GetAsGecko()) {
     *aStyleSetsSize += styleSet->SizeOfIncludingThis(aMallocSizeOf);
   } else {
     NS_WARNING("ServoStyleSets do not support memory measurements yet");
   }
--- a/layout/base/nsPresShell.h
+++ b/layout/base/nsPresShell.h
@@ -50,16 +50,20 @@ class ReflowCountMgr;
 
 class nsPresShellEventCB;
 class nsAutoCauseReflowNotifier;
 
 namespace mozilla {
 
 class EventDispatchingCallback;
 
+// A set type for tracking visible frames, for use by the visibility code in
+// PresShell. The set contains nsIFrame* pointers.
+typedef nsTHashtable<nsPtrHashKey<nsIFrame>> VisibleFrames;
+
 // A hash table type for tracking visible regions, for use by the visibility
 // code in PresShell. The mapping is from view IDs to regions in the
 // coordinate system of that view's scrolled frame.
 typedef nsClassHashtable<nsUint64HashKey, mozilla::CSSIntRegion> VisibleRegions;
 
 } // namespace mozilla
 
 // 250ms.  This is actually pref-controlled, but we use this value if we fail
@@ -68,16 +72,19 @@ typedef nsClassHashtable<nsUint64HashKey
 
 class PresShell final : public nsIPresShell,
                         public nsStubDocumentObserver,
                         public nsISelectionController,
                         public nsIObserver,
                         public nsSupportsWeakReference
 {
   template <typename T> using Maybe = mozilla::Maybe<T>;
+  using Nothing = mozilla::Nothing;
+  using OnNonvisible = mozilla::OnNonvisible;
+  using VisibleFrames = mozilla::VisibleFrames;
   using VisibleRegions = mozilla::VisibleRegions;
 
 public:
   PresShell();
 
   NS_DECL_AND_IMPL_ZEROING_OPERATOR_NEW
 
   // nsISupports
@@ -377,40 +384,42 @@ public:
   // This data is stored as a content property (nsGkAtoms::scrolling) on
   // mContentToScrollTo when we have a pending ScrollIntoView.
   struct ScrollIntoViewData {
     ScrollAxis mContentScrollVAxis;
     ScrollAxis mContentScrollHAxis;
     uint32_t   mContentToScrollToFlags;
   };
 
-  virtual void ScheduleImageVisibilityUpdate() override;
 
-  virtual void RebuildImageVisibilityDisplayList(const nsDisplayList& aList) override;
-  virtual void RebuildImageVisibility(nsRect* aRect = nullptr,
-                                      bool aRemoveOnly = false) override;
+  //////////////////////////////////////////////////////////////////////////////
+  // Approximate frame visibility tracking public API.
+  //////////////////////////////////////////////////////////////////////////////
+
+  void ScheduleApproximateFrameVisibilityUpdateSoon() override;
+  void ScheduleApproximateFrameVisibilityUpdateNow() override;
 
-  virtual void EnsureImageInVisibleList(nsIImageLoadingContent* aImage) override;
+  void RebuildApproximateFrameVisibilityDisplayList(const nsDisplayList& aList) override;
+  void RebuildApproximateFrameVisibility(nsRect* aRect = nullptr,
+                                         bool aRemoveOnly = false) override;
 
-  virtual void RemoveImageFromVisibleList(nsIImageLoadingContent* aImage) override;
+  void EnsureFrameInApproximatelyVisibleList(nsIFrame* aFrame) override;
+  void RemoveFrameFromApproximatelyVisibleList(nsIFrame* aFrame) override;
 
-  virtual bool AssumeAllImagesVisible() override;
+  bool AssumeAllFramesVisible() override;
+
 
   virtual void RecordShadowStyleChange(mozilla::dom::ShadowRoot* aShadowRoot) override;
 
   virtual void DispatchAfterKeyboardEvent(nsINode* aTarget,
                                           const mozilla::WidgetKeyboardEvent& aEvent,
                                           bool aEmbeddedCancelled) override;
 
   void SetNextPaintCompressed() { mNextPaintCompressed = true; }
 
-  void ReportAnyBadState();
-
-  void SetInImageVisibility(bool aState);
-
 protected:
   virtual ~PresShell();
 
   void HandlePostedReflowCallbacks(bool aInterruptible);
   void CancelPostedReflowCallbacks();
 
   void ScheduleBeforeFirstPaint();
   void UnsuppressAndInvalidate();
@@ -737,32 +746,52 @@ protected:
   virtual void BackingScaleFactorChanged() override { mPresContext->UIResolutionChanged(); }
 #ifdef ANDROID
   virtual nsIDocument* GetTouchEventTargetDocument();
 #endif
 
   virtual void PausePainting() override;
   virtual void ResumePainting() override;
 
-  void UpdateImageVisibility();
-  void DoUpdateImageVisibility(bool aRemoveOnly);
   void UpdateActivePointerState(mozilla::WidgetGUIEvent* aEvent);
 
-  nsRevocableEventPtr<nsRunnableMethod<PresShell> > mUpdateImageVisibilityEvent;
+
+  //////////////////////////////////////////////////////////////////////////////
+  // Approximate frame visibility tracking implementation.
+  //////////////////////////////////////////////////////////////////////////////
+
+  void UpdateApproximateFrameVisibility();
+  void DoUpdateApproximateFrameVisibility(bool aRemoveOnly);
 
-  void ClearVisibleImagesList(uint32_t aNonvisibleAction);
-  static void ClearImageVisibilityVisited(nsView* aView, bool aClear);
-  static void MarkImagesInListVisible(const nsDisplayList& aList,
-                                      Maybe<VisibleRegions>& aVisibleRegions);
-  void MarkImagesInSubtreeVisible(nsIFrame* aFrame,
-                                  const nsRect& aRect,
-                                  Maybe<VisibleRegions>& aVisibleRegions,
-                                  bool aRemoveOnly = false);
+  void ClearApproximatelyVisibleFramesList(Maybe<mozilla::OnNonvisible> aNonvisibleAction
+                                             = Nothing());
+  static void ClearApproximateFrameVisibilityVisited(nsView* aView, bool aClear);
+  static void MarkFramesInListApproximatelyVisible(const nsDisplayList& aList,
+                                                   Maybe<VisibleRegions>& aVisibleRegions);
+  void MarkFramesInSubtreeApproximatelyVisible(nsIFrame* aFrame,
+                                               const nsRect& aRect,
+                                               Maybe<VisibleRegions>& aVisibleRegions,
+                                               bool aRemoveOnly = false);
 
+  void DecApproximateVisibleCount(VisibleFrames& aFrames,
+                                  Maybe<OnNonvisible> aNonvisibleAction = Nothing());
+  void ReportBadStateDuringVisibilityUpdate();
+  void SetInFrameVisibilityUpdate(bool aState);
+
+  nsRevocableEventPtr<nsRunnableMethod<PresShell>> mUpdateApproximateFrameVisibilityEvent;
+
+  // A set of frames that were visible or could be visible soon at the time
+  // that we last did an approximate frame visibility update.
+  VisibleFrames mApproximatelyVisibleFrames;
+
+
+  //////////////////////////////////////////////////////////////////////////////
   // Methods for dispatching KeyboardEvent and BeforeAfterKeyboardEvent.
+  //////////////////////////////////////////////////////////////////////////////
+
   void HandleKeyboardEvent(nsINode* aTarget,
                            mozilla::WidgetKeyboardEvent& aEvent,
                            bool aEmbeddedCancelled,
                            nsEventStatus* aStatus,
                            mozilla::EventDispatchingCallback* aEventCB);
   void DispatchBeforeKeyboardEventInternal(
          const nsTArray<nsCOMPtr<mozilla::dom::Element> >& aChain,
          const mozilla::WidgetKeyboardEvent& aEvent,
@@ -770,19 +799,16 @@ protected:
          bool& aDefaultPrevented);
   void DispatchAfterKeyboardEventInternal(
          const nsTArray<nsCOMPtr<mozilla::dom::Element> >& aChain,
          const mozilla::WidgetKeyboardEvent& aEvent,
          bool aEmbeddedCancelled,
          size_t aChainIndex = 0);
   bool CanDispatchEvent(const mozilla::WidgetGUIEvent* aEvent = nullptr) const;
 
-  // A list of images that are visible or almost visible.
-  nsTHashtable< nsRefPtrHashKey<nsIImageLoadingContent> > mVisibleImages;
-
   nsresult SetResolutionImpl(float aResolution, bool aScaleToResolution);
 
 #ifdef DEBUG
   // The reflow root under which we're currently reflowing.  Null when
   // not in reflow.
   nsIFrame*                 mCurrentReflowRoot;
   uint32_t                  mUpdateCount;
 #endif
@@ -882,31 +908,31 @@ protected:
 
   // Indicates that it is safe to unlock painting once all pending reflows
   // have been processed.
   bool                      mShouldUnsuppressPainting : 1;
 
   bool                      mAsyncResizeTimerIsActive : 1;
   bool                      mInResize : 1;
 
-  bool                      mImageVisibilityVisited : 1;
+  bool                      mApproximateFrameVisibilityVisited : 1;
 
   bool                      mNextPaintCompressed : 1;
 
   bool                      mHasCSSBackgroundColor : 1;
 
   // Whether content should be scaled by the resolution amount. If this is
   // not set, a transform that scales by the inverse of the resolution is
   // applied to rendered layers.
   bool                      mScaleToResolution : 1;
 
   // Whether the last chrome-only escape key event is consumed.
   bool                      mIsLastChromeOnlyEscapeKeyConsumed : 1;
 
   // Whether the widget has received a paint message yet.
   bool                      mHasReceivedPaintMessage : 1;
 
-  bool                      mInImageVisibility : 1;
+  bool                      mInFrameVisibilityUpdate : 1;
 
   static bool               sDisableNonTestMouseEvents;
 };
 
 #endif /* !defined(nsPresShell_h_) */
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -1799,25 +1799,25 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
 
     // The pres context may be destroyed during we do the flushing.
     if (!mPresContext || !mPresContext->GetPresShell()) {
       StopTimer();
       return;
     }
   }
 
-  // Recompute image visibility if it's necessary and enough time has passed
-  // since the last time we did it.
+  // Recompute approximate frame visibility if it's necessary and enough time
+  // has passed since the last time we did it.
   if (mNeedToRecomputeVisibility && !mThrottled &&
       aNowTime >= mNextRecomputeVisibilityTick &&
       !presShell->IsPaintingSuppressed()) {
     mNextRecomputeVisibilityTick = aNowTime + mMinRecomputeVisibilityInterval;
     mNeedToRecomputeVisibility = false;
 
-    presShell->ScheduleImageVisibilityUpdate();
+    presShell->ScheduleApproximateFrameVisibilityUpdateNow();
   }
 
   /*
    * Perform notification to imgIRequests subscribed to listen
    * for refresh events.
    */
 
   for (auto iter = mStartTable.Iter(); !iter.Done(); iter.Next()) {
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -233,16 +233,22 @@ public:
 
   /**
    * Cancel all pending events scheduled by ScheduleEventDispatch which
    * targets any node in aDocument.
    */
   void CancelPendingEvents(nsIDocument* aDocument);
 
   /**
+   * Schedule a frame visibility update "soon", subject to the heuristics and
+   * throttling we apply to visibility updates.
+   */
+  void ScheduleFrameVisibilityUpdate() { mNeedToRecomputeVisibility = true; }
+
+  /**
    * Tell the refresh driver that it is done driving refreshes and
    * should stop its timer and forget about its pres context.  This may
    * be called from within a refresh.
    */
   void Disconnect() {
     StopTimer();
     mPresContext = nullptr;
   }
@@ -387,20 +393,20 @@ private:
   uint64_t mCompletedTransaction;
 
   uint32_t mFreezeCount;
 
   // How long we wait between ticks for throttled (which generally means
   // non-visible) documents registered with a non-throttled refresh driver.
   const mozilla::TimeDuration mThrottledFrameRequestInterval;
 
-  // How long we wait, at a minimum, before recomputing image visibility
-  // information. This is a minimum because, regardless of this interval, we
-  // only recompute visibility when we've seen a layout or style flush since the
-  // last time we did it.
+  // How long we wait, at a minimum, before recomputing approximate frame
+  // visibility information. This is a minimum because, regardless of this
+  // interval, we only recompute visibility when we've seen a layout or style
+  // flush since the last time we did it.
   const mozilla::TimeDuration mMinRecomputeVisibilityInterval;
 
   bool mThrottled;
   bool mNeedToRecomputeVisibility;
   bool mTestControllingRefreshes;
   bool mViewManagerFlushIsPending;
   bool mRequestedHighPrecision;
   bool mInRefresh;
--- a/layout/generic/TextOverflow.cpp
+++ b/layout/generic/TextOverflow.cpp
@@ -716,20 +716,21 @@ TextOverflow::HasClippedOverflow(nsIFram
          style->mTextOverflow.mRight.mType == NS_STYLE_TEXT_OVERFLOW_CLIP;
 }
 
 /* static */ bool
 TextOverflow::CanHaveTextOverflow(nsDisplayListBuilder* aBuilder,
                                   nsIFrame*             aBlockFrame)
 {
   // Nothing to do for text-overflow:clip or if 'overflow-x/y:visible' or if
-  // we're just building items for event processing or image visibility.
+  // we're just building items for event processing or frame visibility.
   if (HasClippedOverflow(aBlockFrame) ||
       IsInlineAxisOverflowVisible(aBlockFrame) ||
-      aBuilder->IsForEventDelivery() || aBuilder->IsForImageVisibility()) {
+      aBuilder->IsForEventDelivery() ||
+      aBuilder->IsForFrameVisibility()) {
     return false;
   }
 
   // Skip ComboboxControlFrame because it would clip the drop-down arrow.
   // Its anon block inherits 'text-overflow' and does what is expected.
   if (aBlockFrame->GetType() == nsGkAtoms::comboboxControlFrame) {
     return false;
   }
new file mode 100644
--- /dev/null
+++ b/layout/generic/Visibility.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Declares visibility-related types. @Visibility is an enumeration of the
+ * possible visibility states of a frame. @OnNonvisible is an enumeration that
+ * allows callers to request a specific action when a frame transitions from
+ * visible to nonvisible.
+ */
+
+#ifndef mozilla_layout_generic_Visibility_h
+#define mozilla_layout_generic_Visibility_h
+
+namespace mozilla {
+
+// Visibility states for frames.
+enum class Visibility : uint8_t
+{
+  // Indicates that we're not tracking visibility for this frame.
+  UNTRACKED,
+
+  // Indicates that the frame is probably nonvisible. Visible frames *may* be
+  // APPROXIMATELY_NONVISIBLE because approximate visibility is not updated
+  // synchronously. Some truly nonvisible frames may be marked
+  // APPROXIMATELY_VISIBLE instead if our heuristics lead us to think they may
+  // be visible soon.
+  APPROXIMATELY_NONVISIBLE,
+
+  // Indicates that the frame is either visible now or is likely to be visible
+  // soon according to our heuristics. As with APPROXIMATELY_NONVISIBLE, it's
+  // important to note that approximately visibility is not updated
+  // synchronously, so this information may be out of date.
+  APPROXIMATELY_VISIBLE
+};
+
+// Requested actions when frames transition to the nonvisible state.
+enum class OnNonvisible : uint8_t
+{
+  DISCARD_IMAGES  // Discard images associated with the frame.
+};
+
+} // namespace mozilla
+
+#endif // mozilla_layout_generic_Visibility_h
--- a/layout/generic/moz.build
+++ b/layout/generic/moz.build
@@ -91,16 +91,17 @@ EXPORTS += [
     'nsRubyFrame.h',
     'nsRubyTextContainerFrame.h',
     'nsRubyTextFrame.h',
     'nsSplittableFrame.h',
     'nsSubDocumentFrame.h',
     'nsTextRunTransformations.h',
     'RubyUtils.h',
     'ScrollbarActivity.h',
+    'Visibility.h',
 ]
 
 EXPORTS.mozilla += [
     'WritingModes.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'Selection.h',
--- a/layout/generic/nsContainerFrame.h
+++ b/layout/generic/nsContainerFrame.h
@@ -29,17 +29,20 @@ class FramePropertyTable;
 } // namespace mozilla
 
 // Some macros for container classes to do sanity checking on
 // width/height/x/y values computed during reflow.
 // NOTE: AppUnitsPerCSSPixel value hardwired here to remove the
 // dependency on nsDeviceContext.h.  It doesn't matter if it's a
 // little off.
 #ifdef DEBUG
-#define CRAZY_COORD (1000000*60)
+// 10 million pixels, converted to app units. Note that this a bit larger
+// than 1/4 of nscoord_MAX. So, if any content gets to be this large, we're
+// definitely in danger of grazing up against nscoord_MAX; hence, it's CRAZY.
+#define CRAZY_COORD (10000000*60)
 #define CRAZY_SIZE(_x) (((_x) < -CRAZY_COORD) || ((_x) > CRAZY_COORD))
 #endif
 
 /**
  * Implementation of a container frame.
  */
 class nsContainerFrame : public nsSplittableFrame
 {
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -406,16 +406,19 @@ nsFrame::nsFrame(nsStyleContext* aContex
   mStyleContext->FrameAddRef();
 #endif
 }
 
 nsFrame::~nsFrame()
 {
   MOZ_COUNT_DTOR(nsFrame);
 
+  MOZ_ASSERT(GetVisibility() != Visibility::APPROXIMATELY_VISIBLE,
+             "Visible nsFrame is being destroyed");
+
   NS_IF_RELEASE(mContent);
 #ifdef DEBUG
   mStyleContext->FrameRelease();
 #endif
   mStyleContext->Release();
 }
 
 NS_IMPL_FRAMEARENA_HELPERS(nsFrame)
@@ -539,16 +542,21 @@ nsFrame::Init(nsIContent*       aContent
     nsFrameState state = GetParent()->GetStateBits();
 
     // Make bits that are currently off (see constructor) the same:
     mState |= state & (NS_FRAME_INDEPENDENT_SELECTION |
                        NS_FRAME_GENERATED_CONTENT |
                        NS_FRAME_IS_SVG_TEXT |
                        NS_FRAME_IN_POPUP |
                        NS_FRAME_IS_NONDISPLAY);
+
+    if (HasAnyStateBits(NS_FRAME_IN_POPUP) && TrackingVisibility()) {
+      // Assume all frames in popups are visible.
+      IncApproximateVisibleCount();
+    }
   }
   const nsStyleDisplay *disp = StyleDisplay();
   if (disp->HasTransform(this)) {
     // The frame gets reconstructed if we toggle the -moz-transform
     // property, so we can set this bit here and then ignore it.
     mState |= NS_FRAME_MAY_BE_TRANSFORMED;
   }
   if (disp->mPosition == NS_STYLE_POSITION_STICKY &&
@@ -588,16 +596,21 @@ nsFrame::Init(nsIContent*       aContent
                  (GetStateBits() & NS_FRAME_FONT_INFLATION_CONTAINER),
                  "root frame should always be a container");
   }
 
   if (aContent && aContent->GetProperty(nsGkAtoms::vr_state) != nullptr) {
     AddStateBits(NS_FRAME_HAS_VR_CONTENT);
   }
 
+  if (PresContext()->PresShell()->AssumeAllFramesVisible() &&
+      TrackingVisibility()) {
+    IncApproximateVisibleCount();
+  }
+
   DidSetStyleContext(nullptr);
 
   if (::IsBoxWrapped(this))
     ::InitBoxMetrics(this, false);
 }
 
 void
 nsFrame::DestroyFrom(nsIFrame* aDestructRoot)
@@ -697,16 +710,27 @@ nsFrame::DestroyFrom(nsIFrame* aDestruct
       if (adf) {
         adf->Put(mContent, mStyleContext);
       }
     } else {
       NS_ERROR("stylo: ServoRestyleManager does not support animations yet");
     }
   }
 
+  // Disable visibility tracking. Note that we have to do this before calling
+  // NotifyDestroyingFrame(), which will clear frame properties and make us lose
+  // track of whether we were previously visible or not.
+  // XXX(seth): It'd be ideal to assert that we're already marked nonvisible
+  // here, but it's unfortunately tricky to guarantee in the face of things like
+  // frame reconstruction induced by style changes.
+  DisableVisibilityTracking();
+
+  // Ensure that we're not in the approximately visible list anymore.
+  PresContext()->GetPresShell()->RemoveFrameFromApproximatelyVisibleList(this);
+
   shell->NotifyDestroyingFrame(this);
 
   if (mState & NS_FRAME_EXTERNAL_REFERENCE) {
     shell->ClearFrameRefs(this);
   }
 
   if (view) {
     // Break association between view and frame
@@ -1437,16 +1461,200 @@ nsIFrame::GetCrossDocChildLists(nsTArray
         nsFrameList(root, nsLayoutUtils::GetLastSibling(root)),
         nsIFrame::kPrincipalList));
     }
   }
 
   GetChildLists(aLists);
 }
 
+Visibility
+nsIFrame::GetVisibility() const
+{
+  if (!(GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED)) {
+    return Visibility::UNTRACKED;
+  }
+
+  bool isSet = false;
+  FrameProperties props = Properties();
+  uint32_t visibleCount = props.Get(VisibilityStateProperty(), &isSet);
+
+  MOZ_ASSERT(isSet, "Should have a VisibilityStateProperty value "
+                    "if NS_FRAME_VISIBILITY_IS_TRACKED is set");
+
+  return visibleCount > 0
+       ? Visibility::APPROXIMATELY_VISIBLE
+       : Visibility::APPROXIMATELY_NONVISIBLE;
+}
+
+void
+nsIFrame::UpdateVisibilitySynchronously()
+{
+  nsIPresShell* presShell = PresContext()->PresShell();
+  if (!presShell) {
+    return;
+  }
+
+  if (presShell->AssumeAllFramesVisible()) {
+    presShell->EnsureFrameInApproximatelyVisibleList(this);
+    return;
+  }
+
+  bool visible = true;
+  nsIFrame* f = GetParent();
+  nsRect rect = GetRectRelativeToSelf();
+  nsIFrame* rectFrame = this;
+  while (f) {
+    nsIScrollableFrame* sf = do_QueryFrame(f);
+    if (sf) {
+      nsRect transformedRect =
+        nsLayoutUtils::TransformFrameRectToAncestor(rectFrame, rect, f);
+      if (!sf->IsRectNearlyVisible(transformedRect)) {
+        visible = false;
+        break;
+      }
+
+      // In this code we're trying to synchronously update *approximate*
+      // visibility. (In the future we may update precise visibility here as
+      // well, which is why the method name does not contain 'approximate'.) The
+      // IsRectNearlyVisible() check above tells us that the rect we're checking
+      // is approximately visible within the scrollframe, but we still need to
+      // ensure that, even if it was scrolled into view, it'd be visible when we
+      // consider the rest of the document. To do that, we move transformedRect
+      // to be contained in the scrollport as best we can (it might not fit) to
+      // pretend that it was scrolled into view.
+      rect = transformedRect.MoveInsideAndClamp(sf->GetScrollPortRect());
+      rectFrame = f;
+    }
+    nsIFrame* parent = f->GetParent();
+    if (!parent) {
+      parent = nsLayoutUtils::GetCrossDocParentFrame(f);
+      if (parent && parent->PresContext()->IsChrome()) {
+        break;
+      }
+    }
+    f = parent;
+  }
+
+  if (visible) {
+    presShell->EnsureFrameInApproximatelyVisibleList(this);
+  } else {
+    presShell->RemoveFrameFromApproximatelyVisibleList(this);
+  }
+}
+
+void
+nsIFrame::EnableVisibilityTracking()
+{
+  if (GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED) {
+    return;  // Nothing to do.
+  }
+
+  FrameProperties props = Properties();
+  MOZ_ASSERT(!props.Has(VisibilityStateProperty()),
+             "Shouldn't have a VisibilityStateProperty value "
+             "if NS_FRAME_VISIBILITY_IS_TRACKED is not set");
+
+  // Add the state bit so we know to track visibility for this frame, and
+  // initialize the frame property.
+  AddStateBits(NS_FRAME_VISIBILITY_IS_TRACKED);
+  props.Set(VisibilityStateProperty(), 0);
+
+  nsIPresShell* presShell = PresContext()->PresShell();
+  if (!presShell) {
+    return;
+  }
+
+  // Schedule a visibility update. This method will virtually always be called
+  // when layout has changed anyway, so it's very unlikely that any additional
+  // visibility updates will be triggered by this, but this way we guarantee
+  // that if this frame is currently visible we'll eventually find out.
+  presShell->ScheduleApproximateFrameVisibilityUpdateSoon();
+}
+
+void
+nsIFrame::DisableVisibilityTracking()
+{
+  if (!(GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED)) {
+    return;  // Nothing to do.
+  }
+
+  bool isSet = false;
+  FrameProperties props = Properties();
+  uint32_t visibleCount = props.Remove(VisibilityStateProperty(), &isSet);
+
+  MOZ_ASSERT(isSet, "Should have a VisibilityStateProperty value "
+                    "if NS_FRAME_VISIBILITY_IS_TRACKED is set");
+
+  RemoveStateBits(NS_FRAME_VISIBILITY_IS_TRACKED);
+
+  if (visibleCount == 0) {
+    return;  // We were nonvisible.
+  }
+
+  // We were visible, so send an OnVisibilityChange() notification.
+  OnVisibilityChange(Visibility::APPROXIMATELY_NONVISIBLE);
+}
+
+void
+nsIFrame::DecApproximateVisibleCount(Maybe<OnNonvisible> aNonvisibleAction
+                                       /* = Nothing() */)
+{
+  MOZ_ASSERT(GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED);
+
+  bool isSet = false;
+  FrameProperties props = Properties();
+  uint32_t visibleCount = props.Get(VisibilityStateProperty(), &isSet);
+
+  MOZ_ASSERT(isSet, "Should have a VisibilityStateProperty value "
+                    "if NS_FRAME_VISIBILITY_IS_TRACKED is set");
+  MOZ_ASSERT(visibleCount > 0, "Frame is already nonvisible and we're "
+                               "decrementing its visible count?");
+
+  visibleCount--;
+  props.Set(VisibilityStateProperty(), visibleCount);
+  if (visibleCount > 0) {
+    return;
+  }
+
+  // We just became nonvisible, so send an OnVisibilityChange() notification.
+  OnVisibilityChange(Visibility::APPROXIMATELY_NONVISIBLE, aNonvisibleAction);
+}
+
+void
+nsIFrame::IncApproximateVisibleCount()
+{
+  MOZ_ASSERT(GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED);
+
+  bool isSet = false;
+  FrameProperties props = Properties();
+  uint32_t visibleCount = props.Get(VisibilityStateProperty(), &isSet);
+
+  MOZ_ASSERT(isSet, "Should have a VisibilityStateProperty value "
+                    "if NS_FRAME_VISIBILITY_IS_TRACKED is set");
+
+  visibleCount++;
+  props.Set(VisibilityStateProperty(), visibleCount);
+  if (visibleCount > 1) {
+    return;
+  }
+
+  // We just became visible, so send an OnVisibilityChange() notification.
+  OnVisibilityChange(Visibility::APPROXIMATELY_VISIBLE);
+}
+
+void
+nsIFrame::OnVisibilityChange(Visibility aNewVisibility,
+                             Maybe<OnNonvisible> aNonvisibleAction
+                               /* = Nothing() */)
+{
+  // XXX(seth): In bug 1218990 we'll implement visibility tracking for CSS
+  // images here.
+}
+
 static nsIFrame*
 GetActiveSelectionFrame(nsPresContext* aPresContext, nsIFrame* aFrame)
 {
   nsIContent* capturingContent = nsIPresShell::GetCapturingContent();
   if (capturingContent) {
     nsIFrame* activeFrame = aPresContext->GetPrimaryFrameFor(capturingContent);
     return activeFrame ? activeFrame : aFrame;
   }
@@ -8934,16 +9142,22 @@ nsFrame::BoxMetrics() const
     static_cast<nsBoxLayoutMetrics*>(Properties().Get(BoxMetricsProperty()));
   NS_ASSERTION(metrics, "A box layout method was called but InitBoxMetrics was never called");
   return metrics;
 }
 
 /* static */ void
 nsIFrame::AddInPopupStateBitToDescendants(nsIFrame* aFrame)
 {
+  if (!aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP) &&
+      aFrame->TrackingVisibility()) {
+    // Assume all frames in popups are visible.
+    aFrame->IncApproximateVisibleCount();
+  }
+
   aFrame->AddStateBits(NS_FRAME_IN_POPUP);
 
   AutoTArray<nsIFrame::ChildList,4> childListArray;
   aFrame->GetCrossDocChildLists(&childListArray);
 
   nsIFrame::ChildListArrayIterator lists(childListArray);
   for (; !lists.IsDone(); lists.Next()) {
     nsFrameList::Enumerator childFrames(lists.CurrentList());
@@ -8958,16 +9172,22 @@ nsIFrame::RemoveInPopupStateBitFromDesce
 {
   if (!aFrame->HasAnyStateBits(NS_FRAME_IN_POPUP) ||
       nsLayoutUtils::IsPopup(aFrame)) {
     return;
   }
 
   aFrame->RemoveStateBits(NS_FRAME_IN_POPUP);
 
+  if (aFrame->TrackingVisibility()) {
+    // We assume all frames in popups are visible, so this decrement balances
+    // out the increment in AddInPopupStateBitToDescendants above.
+    aFrame->DecApproximateVisibleCount();
+  }
+
   AutoTArray<nsIFrame::ChildList,4> childListArray;
   aFrame->GetCrossDocChildLists(&childListArray);
 
   nsIFrame::ChildListArrayIterator lists(childListArray);
   for (; !lists.IsDone(); lists.Next()) {
     nsFrameList::Enumerator childFrames(lists.CurrentList());
     for (; !childFrames.AtEnd(); childFrames.Next()) {
       RemoveInPopupStateBitFromDescendants(childFrames.get());
--- a/layout/generic/nsFrameStateBits.h
+++ b/layout/generic/nsFrameStateBits.h
@@ -224,16 +224,20 @@ FRAME_STATE_BIT(Generic, 43, NS_FRAME_SV
 // Is this frame allowed to have generated (::before/::after) content?
 FRAME_STATE_BIT(Generic, 44, NS_FRAME_MAY_HAVE_GENERATED_CONTENT)
 
 // This bit is set on frames that create ContainerLayers with component
 // alpha children. With BasicLayers we avoid creating these, so we mark
 // the frames for future reference.
 FRAME_STATE_BIT(Generic, 45, NS_FRAME_NO_COMPONENT_ALPHA)
 
+// This bit indicates that we're tracking visibility for this frame, and that
+// the frame has a VisibilityStateProperty property.
+FRAME_STATE_BIT(Generic, 46, NS_FRAME_VISIBILITY_IS_TRACKED)
+
 // The frame is a descendant of SVGTextFrame and is thus used for SVG
 // text layout.
 FRAME_STATE_BIT(Generic, 47, NS_FRAME_IS_SVG_TEXT)
 
 // Frame is marked as needing painting
 FRAME_STATE_BIT(Generic, 48, NS_FRAME_NEEDS_PAINT)
 
 // Frame has a descendant frame that needs painting - This includes
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -1857,19 +1857,19 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
   , mLastScrollOrigin(nsGkAtoms::other)
   , mLastSmoothScrollOrigin(nullptr)
   , mScrollGeneration(++sScrollGenerationCounter)
   , mDestination(0, 0)
   , mScrollPosAtLastPaint(0, 0)
   , mRestorePos(-1, -1)
   , mLastPos(-1, -1)
   , mScrollPosForLayerPixelAlignment(-1, -1)
-  , mLastUpdateImagesPos(-1, -1)
-  , mHadDisplayPortAtLastImageUpdate(false)
-  , mDisplayPortAtLastImageUpdate()
+  , mLastUpdateFramesPos(-1, -1)
+  , mHadDisplayPortAtLastFrameUpdate(false)
+  , mDisplayPortAtLastFrameUpdate()
   , mNeverHasVerticalScrollbar(false)
   , mNeverHasHorizontalScrollbar(false)
   , mHasVerticalScrollbar(false)
   , mHasHorizontalScrollbar(false)
   , mFrameIsUpdatingScrollbar(false)
   , mDidHistoryRestore(false)
   , mIsRoot(aIsRoot)
   , mClipAllDescendants(aIsRoot)
@@ -1893,17 +1893,17 @@ ScrollFrameHelper::ScrollFrameHelper(nsC
   , mZoomableByAPZ(false)
   , mVelocityQueue(aOuter->PresContext())
   , mAsyncScrollEvent(END_DOM)
 {
   if (LookAndFeel::GetInt(LookAndFeel::eIntID_UseOverlayScrollbars) != 0) {
     mScrollbarActivity = new ScrollbarActivity(do_QueryFrame(aOuter));
   }
 
-  EnsureImageVisPrefsCached();
+  EnsureFrameVisPrefsCached();
 
   if (IsAlwaysActive() &&
       gfxPrefs::LayersTilesEnabled() &&
       !nsLayoutUtils::UsesAsyncScrolling(mOuter) &&
       mOuter->GetContent()) {
     // If we have tiling but no APZ, then set a 0-margin display port on
     // active scroll containers so that we paint by whole tile increments
     // when scrolling.
@@ -2628,30 +2628,31 @@ ScrollFrameHelper::ScheduleSyntheticMous
       return;
   }
 
   mScrollActivityTimer->InitWithFuncCallback(
         ScrollActivityCallback, this, 100, nsITimer::TYPE_ONE_SHOT);
 }
 
 void
-ScrollFrameHelper::NotifyImageVisibilityUpdate()
-{
-  mLastUpdateImagesPos = GetScrollPosition();
-  mHadDisplayPortAtLastImageUpdate =
-    nsLayoutUtils::GetDisplayPort(mOuter->GetContent(), &mDisplayPortAtLastImageUpdate);
+ScrollFrameHelper::NotifyApproximateFrameVisibilityUpdate()
+{
+  mLastUpdateFramesPos = GetScrollPosition();
+  mHadDisplayPortAtLastFrameUpdate =
+    nsLayoutUtils::GetDisplayPort(mOuter->GetContent(),
+                                  &mDisplayPortAtLastFrameUpdate);
 }
 
 bool
-ScrollFrameHelper::GetDisplayPortAtLastImageVisibilityUpdate(nsRect* aDisplayPort)
-{
-  if (mHadDisplayPortAtLastImageUpdate) {
-    *aDisplayPort = mDisplayPortAtLastImageUpdate;
-  }
-  return mHadDisplayPortAtLastImageUpdate;
+ScrollFrameHelper::GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort)
+{
+  if (mHadDisplayPortAtLastFrameUpdate) {
+    *aDisplayPort = mDisplayPortAtLastFrameUpdate;
+  }
+  return mHadDisplayPortAtLastFrameUpdate;
 }
 
 void
 ScrollFrameHelper::ScrollToImpl(nsPoint aPt, const nsRect& aRange, nsIAtom* aOrigin)
 {
   if (aOrigin == nullptr) {
     // If no origin was specified, we still want to set it to something that's
     // non-null, so that we can use nullness to distinguish if the frame was scrolled
@@ -2683,27 +2684,27 @@ ScrollFrameHelper::ScrollToImpl(nsPoint 
                                  aRange,
                                  alignWithPos,
                                  appUnitsPerDevPixel,
                                  scale);
   if (pt == curPos) {
     return;
   }
 
-  bool needImageVisibilityUpdate = (mLastUpdateImagesPos == nsPoint(-1,-1));
-
-  nsPoint dist(std::abs(pt.x - mLastUpdateImagesPos.x),
-               std::abs(pt.y - mLastUpdateImagesPos.y));
+  bool needFrameVisibilityUpdate = mLastUpdateFramesPos == nsPoint(-1,-1);
+
+  nsPoint dist(std::abs(pt.x - mLastUpdateFramesPos.x),
+               std::abs(pt.y - mLastUpdateFramesPos.y));
   nsSize scrollPortSize = GetScrollPositionClampingScrollPortSize();
   nscoord horzAllowance = std::max(scrollPortSize.width / std::max(sHorzScrollFraction, 1),
                                    nsPresContext::AppUnitsPerCSSPixel());
   nscoord vertAllowance = std::max(scrollPortSize.height / std::max(sVertScrollFraction, 1),
                                    nsPresContext::AppUnitsPerCSSPixel());
   if (dist.x >= horzAllowance || dist.y >= vertAllowance) {
-    needImageVisibilityUpdate = true;
+    needFrameVisibilityUpdate = true;
   }
 
   // notify the listeners.
   for (uint32_t i = 0; i < mListeners.Length(); i++) {
     mListeners[i]->ScrollPositionWillChange(pt.x, pt.y);
   }
 
   nsRect oldDisplayPort;
@@ -2762,18 +2763,18 @@ ScrollFrameHelper::ScrollToImpl(nsPoint 
         }
       }
     }
   }
 
   if (schedulePaint) {
     mOuter->SchedulePaint();
 
-    if (needImageVisibilityUpdate) {
-      presContext->PresShell()->ScheduleImageVisibilityUpdate();
+    if (needFrameVisibilityUpdate) {
+      presContext->PresShell()->ScheduleApproximateFrameVisibilityUpdateNow();
     }
   }
 
   if (mOuter->ChildrenHavePerspective()) {
     // The overflow areas of descendants may depend on the scroll position,
     // so ensure they get updated.
     mOuter->RecomputePerspectiveChildrenOverflow(mOuter, nullptr);
   }
@@ -2974,37 +2975,37 @@ ScrollFrameHelper::AppendScrollPartsTo(n
     // DISPLAY_CHILD_FORCE_STACKING_CONTEXT put everything into
     // partList.PositionedDescendants().
     ::AppendToTop(aBuilder, aLists,
                   partList.PositionedDescendants(), scrollParts[i],
                   appendToTopFlags);
   }
 }
 
-/* static */ bool ScrollFrameHelper::sImageVisPrefsCached = false;
+/* static */ bool ScrollFrameHelper::sFrameVisPrefsCached = false;
 /* static */ uint32_t ScrollFrameHelper::sHorzExpandScrollPort = 0;
 /* static */ uint32_t ScrollFrameHelper::sVertExpandScrollPort = 1;
 /* static */ int32_t ScrollFrameHelper::sHorzScrollFraction = 2;
 /* static */ int32_t ScrollFrameHelper::sVertScrollFraction = 2;
 
 /* static */ void
-ScrollFrameHelper::EnsureImageVisPrefsCached()
-{
-  if (!sImageVisPrefsCached) {
+ScrollFrameHelper::EnsureFrameVisPrefsCached()
+{
+  if (!sFrameVisPrefsCached) {
     Preferences::AddUintVarCache(&sHorzExpandScrollPort,
-      "layout.imagevisibility.numscrollportwidths", (uint32_t)0);
+      "layout.framevisibility.numscrollportwidths", (uint32_t)0);
     Preferences::AddUintVarCache(&sVertExpandScrollPort,
-      "layout.imagevisibility.numscrollportheights", 1);
+      "layout.framevisibility.numscrollportheights", 1);
 
     Preferences::AddIntVarCache(&sHorzScrollFraction,
-      "layout.imagevisibility.amountscrollbeforeupdatehorizontal", 2);
+      "layout.framevisibility.amountscrollbeforeupdatehorizontal", 2);
     Preferences::AddIntVarCache(&sVertScrollFraction,
-      "layout.imagevisibility.amountscrollbeforeupdatevertical", 2);
-
-    sImageVisPrefsCached = true;
+      "layout.framevisibility.amountscrollbeforeupdatevertical", 2);
+
+    sFrameVisPrefsCached = true;
   }
 }
 
 nsRect
 ScrollFrameHelper::ExpandRectToNearlyVisible(const nsRect& aRect) const
 {
   // We don't want to expand a rect in a direction that we can't scroll, so we
   // check the scroll range.
@@ -3098,18 +3099,18 @@ ClipListsExceptCaret(nsDisplayListCollec
   ClipItemsExceptCaret(aLists->Content(), aBuilder, aClipFrame, aNonCaretClip, aNonCaretScrollClip);
 }
 
 void
 ScrollFrameHelper::BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                     const nsRect&           aDirtyRect,
                                     const nsDisplayListSet& aLists)
 {
-  if (aBuilder->IsForImageVisibility()) {
-    NotifyImageVisibilityUpdate();
+  if (aBuilder->IsForFrameVisibility()) {
+    NotifyApproximateFrameVisibilityUpdate();
   }
 
   mOuter->DisplayBorderBackgroundOutline(aBuilder, aLists);
 
   if (aBuilder->IsPaintingToWindow()) {
     mScrollPosAtLastPaint = GetScrollPosition();
     if (IsMaybeScrollingActive() && NeedToInvalidateOnScroll(mOuter)) {
       MarkNotRecentlyScrolled();
@@ -3143,18 +3144,18 @@ ScrollFrameHelper::BuildDisplayList(nsDi
   }
 
   Unused << DecideScrollableLayer(aBuilder, &dirtyRect,
               /* aAllowCreateDisplayPort = */ !mIsRoot);
 
   bool usingDisplayPort = aBuilder->IsPaintingToWindow() &&
     nsLayoutUtils::HasDisplayPort(mOuter->GetContent());
 
-  if (aBuilder->IsForImageVisibility()) {
-    // We expand the dirty rect to catch images just outside of the scroll port.
+  if (aBuilder->IsForFrameVisibility()) {
+    // We expand the dirty rect to catch frames just outside of the scroll port.
     // We use the dirty rect instead of the whole scroll port to prevent
     // too much expansion in the presence of very large (bigger than the
     // viewport) scroll ports.
     dirtyRect = ExpandRectToNearlyVisible(dirtyRect);
   }
 
   // We put non-overlay scrollbars in their own layers when this is the root
   // scroll frame and we are a toplevel content document. In this situation,
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -388,18 +388,18 @@ public:
   void SetScrollableByAPZ(bool aScrollable);
   void SetZoomableByAPZ(bool aZoomable);
 
   bool UsesContainerScrolling() const;
 
   bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                              nsRect* aDirtyRect,
                              bool aAllowCreateDisplayPort);
-  void NotifyImageVisibilityUpdate();
-  bool GetDisplayPortAtLastImageVisibilityUpdate(nsRect* aDisplayPort);
+  void NotifyApproximateFrameVisibilityUpdate();
+  bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort);
 
   bool AllowDisplayPortExpiration();
   void TriggerDisplayPortExpiration();
   void ResetDisplayPortExpiryTimer();
 
   void ScheduleSyntheticMouseMove();
   static void ScrollActivityCallback(nsITimer *aTimer, void* anInstance);
 
@@ -487,20 +487,20 @@ public:
   // other than trying to restore mRestorePos.
   nsPoint mLastPos;
 
   nsExpirationState mActivityExpirationState;
 
   nsCOMPtr<nsITimer> mScrollActivityTimer;
   nsPoint mScrollPosForLayerPixelAlignment;
 
-  // The scroll position where we last updated image visibility.
-  nsPoint mLastUpdateImagesPos;
-  bool mHadDisplayPortAtLastImageUpdate;
-  nsRect mDisplayPortAtLastImageUpdate;
+  // The scroll position where we last updated frame visibility.
+  nsPoint mLastUpdateFramesPos;
+  bool mHadDisplayPortAtLastFrameUpdate;
+  nsRect mDisplayPortAtLastFrameUpdate;
 
   nsRect mPrevScrolledRect;
 
   FrameMetrics::ViewID mScrollParentID;
 
   // Timer to remove the displayport some time after scrolling has stopped
   nsCOMPtr<nsITimer> mDisplayPortExpiryTimer;
 
@@ -620,23 +620,23 @@ protected:
    * Helper that notifies plugins about async smooth scroll operations managed
    * by nsGfxScrollFrame.
    */
   enum AsyncScrollEventType { BEGIN_DOM, BEGIN_APZ, END_DOM, END_APZ };
   void NotifyPluginFrames(AsyncScrollEventType aEvent);
   AsyncScrollEventType mAsyncScrollEvent;
   bool HasPluginFrames();
 
-  static void EnsureImageVisPrefsCached();
-  static bool sImageVisPrefsCached;
-  // The number of scrollports wide/high to expand when looking for images.
+  static void EnsureFrameVisPrefsCached();
+  static bool sFrameVisPrefsCached;
+  // The number of scrollports wide/high to expand when tracking frame visibility.
   static uint32_t sHorzExpandScrollPort;
   static uint32_t sVertExpandScrollPort;
   // The fraction of the scrollport we allow to scroll by before we schedule
-  // an update of image visibility.
+  // an update of frame visibility.
   static int32_t sHorzScrollFraction;
   static int32_t sVertScrollFraction;
 };
 
 } // namespace mozilla
 
 /**
  * The scroll frame creates and manages the scrolling view
@@ -921,21 +921,21 @@ public:
   virtual bool UsesContainerScrolling() const override {
     return mHelper.UsesContainerScrolling();
   }
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aDirtyRect,
                                      bool aAllowCreateDisplayPort) override {
     return mHelper.DecideScrollableLayer(aBuilder, aDirtyRect, aAllowCreateDisplayPort);
   }
-  virtual void NotifyImageVisibilityUpdate() override {
-    mHelper.NotifyImageVisibilityUpdate();
+  virtual void NotifyApproximateFrameVisibilityUpdate() override {
+    mHelper.NotifyApproximateFrameVisibilityUpdate();
   }
-  virtual bool GetDisplayPortAtLastImageVisibilityUpdate(nsRect* aDisplayPort) override {
-    return mHelper.GetDisplayPortAtLastImageVisibilityUpdate(aDisplayPort);
+  virtual bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) override {
+    return mHelper.GetDisplayPortAtLastApproximateFrameVisibilityUpdate(aDisplayPort);
   }
   void TriggerDisplayPortExpiration() override {
     mHelper.TriggerDisplayPortExpiration();
   }
 
   // nsIStatefulFrame
   NS_IMETHOD SaveState(nsPresState** aState) override {
     NS_ENSURE_ARG_POINTER(aState);
@@ -1414,21 +1414,21 @@ public:
   void SetZoomableByAPZ(bool aZoomable) override {
     mHelper.SetZoomableByAPZ(aZoomable);
   }
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aDirtyRect,
                                      bool aAllowCreateDisplayPort) override {
     return mHelper.DecideScrollableLayer(aBuilder, aDirtyRect, aAllowCreateDisplayPort);
   }
-  virtual void NotifyImageVisibilityUpdate() override {
-    mHelper.NotifyImageVisibilityUpdate();
+  virtual void NotifyApproximateFrameVisibilityUpdate() override {
+    mHelper.NotifyApproximateFrameVisibilityUpdate();
   }
-  virtual bool GetDisplayPortAtLastImageVisibilityUpdate(nsRect* aDisplayPort) override {
-    return mHelper.GetDisplayPortAtLastImageVisibilityUpdate(aDisplayPort);
+  virtual bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) override {
+    return mHelper.GetDisplayPortAtLastApproximateFrameVisibilityUpdate(aDisplayPort);
   }
   void TriggerDisplayPortExpiration() override {
     mHelper.TriggerDisplayPortExpiration();
   }
 
 #ifdef DEBUG_FRAME_DUMP
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -21,27 +21,29 @@
    variables in this file.  -dwh */
 
 #include <algorithm>
 #include <stdio.h>
 
 #include "CaretAssociationHint.h"
 #include "FramePropertyTable.h"
 #include "mozilla/layout/FrameChildList.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/WritingModes.h"
 #include "nsDirection.h"
 #include "nsFrameList.h"
 #include "nsFrameState.h"
 #include "nsHTMLReflowMetrics.h"
 #include "nsITheme.h"
 #include "nsLayoutUtils.h"
 #include "nsQueryFrame.h"
 #include "nsStringGlue.h"
 #include "nsStyleContext.h"
 #include "nsStyleStruct.h"
+#include "Visibility.h"
 
 #ifdef ACCESSIBILITY
 #include "mozilla/a11y/AccTypes.h"
 #endif
 
 /**
  * New rules of reflow:
  * 1. you get a WillReflow() followed by a Reflow() followed by a DidReflow() in order
@@ -411,18 +413,22 @@ static void ReleaseValue(T* aPropertyVal
  * link to many of the functions defined here. Too bad.
  *
  * If you're not in layout but you must call functions in here, at least
  * restrict yourself to calling virtual methods, which won't hurt you as badly.
  */
 class nsIFrame : public nsQueryFrame
 {
 public:
+  template <typename T> using Maybe = mozilla::Maybe<T>;
+  using Nothing = mozilla::Nothing;
+  using OnNonvisible = mozilla::OnNonvisible;
   template<typename T=void>
   using PropertyDescriptor = const mozilla::FramePropertyDescriptor<T>*;
+  using Visibility = mozilla::Visibility;
 
   typedef mozilla::FrameProperties FrameProperties;
   typedef mozilla::layers::Layer Layer;
   typedef mozilla::layout::FrameChildList ChildList;
   typedef mozilla::layout::FrameChildListID ChildListID;
   typedef mozilla::layout::FrameChildListIDs ChildListIDs;
   typedef mozilla::layout::FrameChildListIterator ChildListIterator;
   typedef mozilla::layout::FrameChildListArrayIterator ChildListArrayIterator;
@@ -1076,16 +1082,104 @@ public:
    * relative to the top of the frame.  This is mostly needed for frames
    * which return a baseline from GetBaseline which is not useful for
    * caret positioning.
    */
   virtual nscoord GetCaretBaseline() const {
     return GetLogicalBaseline(GetWritingMode());
   }
 
+  ///////////////////////////////////////////////////////////////////////////////
+  // The public visibility API.
+  ///////////////////////////////////////////////////////////////////////////////
+
+  /// @return true if we're tracking visibility for this frame.
+  bool TrackingVisibility() const
+  {
+    return bool(GetStateBits() & NS_FRAME_VISIBILITY_IS_TRACKED);
+  }
+
+  /// @return the visibility state of this frame. See the Visibility enum
+  /// for the possible return values and their meanings.
+  Visibility GetVisibility() const;
+
+  /// Update the visibility state of this frame synchronously.
+  /// XXX(seth): Avoid using this method; we should be relying on the refresh
+  /// driver for visibility updates. This method, which replaces
+  /// nsLayoutUtils::UpdateApproximateFrameVisibility(), exists purely as a
+  /// temporary measure to avoid changing behavior during the transition from
+  /// the old image visibility code.
+  void UpdateVisibilitySynchronously();
+
+  // A frame property which stores the visibility state of this frame. Right
+  // now that consists of an approximate visibility counter represented as a
+  // uint32_t. When the visibility of this frame is not being tracked, this
+  // property is absent.
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(VisibilityStateProperty, uint32_t);
+
+protected:
+
+  /**
+   * Subclasses can call this method to enable visibility tracking for this frame.
+   *
+   * If visibility tracking was previously disabled, this will schedule an
+   * update an asynchronous update of visibility.
+   */
+  void EnableVisibilityTracking();
+
+  /**
+   * Subclasses can call this method to disable visibility tracking for this frame.
+   *
+   * Note that if visibility tracking was previously enabled, disabling visibility
+   * tracking will cause a synchronous call to OnVisibilityChange().
+   */
+  void DisableVisibilityTracking();
+
+  /**
+   * Called when a frame transitions between visibility states (for example,
+   * from nonvisible to visible, or from visible to nonvisible).
+   *
+   * @param aNewVisibility    The new visibility state.
+   * @param aNonvisibleAction A requested action if the frame has become
+   *                          nonvisible. If Nothing(), no action is
+   *                          requested. If DISCARD_IMAGES is specified, the
+   *                          frame is requested to ask any images it's
+   *                          associated with to discard their surfaces if
+   *                          possible.
+   *
+   * Subclasses which override this method should call their parent class's
+   * implementation.
+   */
+  virtual void OnVisibilityChange(Visibility aNewVisibility,
+                                  Maybe<OnNonvisible> aNonvisibleAction = Nothing());
+
+public:
+
+  ///////////////////////////////////////////////////////////////////////////////
+  // Internal implementation for the approximate frame visibility API.
+  ///////////////////////////////////////////////////////////////////////////////
+
+  /**
+   * We track the approximate visibility of frames using a counter; if it's
+   * non-zero, then the frame is considered visible. Using a counter allows us
+   * to account for situations where the frame may be visible in more than one
+   * place (for example, via -moz-element), and it simplifies the
+   * implementation of our approximate visibility tracking algorithms.
+   *
+   * @param aNonvisibleAction A requested action if the frame has become
+   *                          nonvisible. If Nothing(), no action is
+   *                          requested. If DISCARD_IMAGES is specified, the
+   *                          frame is requested to ask any images it's
+   *                          associated with to discard their surfaces if
+   *                          possible.
+   */
+  void DecApproximateVisibleCount(Maybe<OnNonvisible> aNonvisibleAction = Nothing());
+  void IncApproximateVisibleCount();
+
+
   /**
    * Get the specified child list.
    *
    * @param   aListID identifies the requested child list.
    * @return  the child list.  If the requested list is unsupported by this
    *          frame type, an empty list will be returned.
    */
   virtual const nsFrameList& GetChildList(ChildListID aListID) const = 0;
--- a/layout/generic/nsIScrollableFrame.h
+++ b/layout/generic/nsIScrollableFrame.h
@@ -342,23 +342,23 @@ public:
   virtual bool DidHistoryRestore() const = 0;
   /**
    * Clear the flag so that DidHistoryRestore() returns false until the next
    * RestoreState call.
    * @see nsIStatefulFrame::RestoreState
    */
   virtual void ClearDidHistoryRestore() = 0;
   /**
-   * Determine if the passed in rect is nearly visible according to the image
+   * Determine if the passed in rect is nearly visible according to the frame
    * visibility heuristics for how close it is to the visible scrollport.
    */
   virtual bool IsRectNearlyVisible(const nsRect& aRect) = 0;
  /**
   * Expand the given rect taking into account which directions we can scroll
-  * and how far we want to expand for image visibility purposes.
+  * and how far we want to expand for frame visibility purposes.
   */
   virtual nsRect ExpandRectToNearlyVisible(const nsRect& aRect) const = 0;
   /**
    * Returns the origin that triggered the last instant scroll. Will equal
    * nsGkAtoms::apz when the compositor's replica frame metrics includes the
    * latest instant scroll.
    */
   virtual nsIAtom* LastScrollOrigin() = 0;
@@ -448,26 +448,26 @@ public:
    * aAllowCreateDisplayPort is true. It is only allowed to be false if there
    * has been a call with it set to true before on the same paint.
    */
   virtual bool DecideScrollableLayer(nsDisplayListBuilder* aBuilder,
                                      nsRect* aDirtyRect,
                                      bool aAllowCreateDisplayPort) = 0;
 
   /**
-   * Notification that this scroll frame is getting its image visibility updated.
+   * Notification that this scroll frame is getting its frame visibility updated.
    */
-  virtual void NotifyImageVisibilityUpdate() = 0;
+  virtual void NotifyApproximateFrameVisibilityUpdate() = 0;
 
   /**
-   * Returns true if this scroll frame had a display port at the last image
+   * Returns true if this scroll frame had a display port at the last frame
    * visibility update and fills in aDisplayPort with that displayport. Returns
    * false otherwise, and doesn't touch aDisplayPort.
    */
-  virtual bool GetDisplayPortAtLastImageVisibilityUpdate(nsRect* aDisplayPort) = 0;
+  virtual bool GetDisplayPortAtLastApproximateFrameVisibilityUpdate(nsRect* aDisplayPort) = 0;
 
   /**
    * This is called when a descendant scrollframe's has its displayport expired.
    * This function will check to see if this scrollframe may safely expire its
    * own displayport and schedule a timer to do that if it is safe.
    */
   virtual void TriggerDisplayPortExpiration() = 0;
 };
--- a/layout/generic/nsImageFrame.cpp
+++ b/layout/generic/nsImageFrame.cpp
@@ -139,16 +139,18 @@ nsImageFrame::nsImageFrame(nsStyleContex
   ImageFrameSuper(aContext),
   mComputedSize(0, 0),
   mIntrinsicRatio(0, 0),
   mDisplayingIcon(false),
   mFirstFrameComplete(false),
   mReflowCallbackPosted(false),
   mForceSyncDecoding(false)
 {
+  EnableVisibilityTracking();
+
   // We assume our size is not constrained and we haven't gotten an
   // initial reflow yet, so don't touch those flags.
   mIntrinsicSize.width.SetCoordValue(0);
   mIntrinsicSize.height.SetCoordValue(0);
 }
 
 nsImageFrame::~nsImageFrame()
 {
@@ -707,19 +709,17 @@ nsImageFrame::MaybeDecodeForPredictedSiz
   if (!mImage) {
     return;  // Nothing to do yet.
   }
 
   if (mComputedSize.IsEmpty()) {
     return;  // We won't draw anything, so no point in decoding.
   }
 
-  nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
-  MOZ_ASSERT(imageLoader);
-  if (imageLoader->GetVisibleCount() == 0) {
+  if (GetVisibility() != Visibility::APPROXIMATELY_VISIBLE) {
     return;  // We're not visible, so don't decode.
   }
 
   // OK, we're ready to decode. Compute the scale to the screen...
   nsIPresShell* presShell = PresContext()->GetPresShell();
   LayoutDeviceToScreenScale2D resolutionToScreen(
       presShell->GetCumulativeResolution()
     * nsLayoutUtils::GetTransformToAncestorScaleExcludingAnimated(this));
@@ -1036,17 +1036,26 @@ nsImageFrame::Reflow(nsPresContext*     
   NS_FRAME_SET_TRUNCATION(aStatus, aReflowState, aMetrics);
 }
 
 bool
 nsImageFrame::ReflowFinished()
 {
   mReflowCallbackPosted = false;
 
-  nsLayoutUtils::UpdateImageVisibilityForFrame(this);
+  // XXX(seth): We don't need this. The purpose of updating visibility
+  // synchronously is to ensure that animated images start animating
+  // immediately. In the short term, however,
+  // nsImageLoadingContent::OnUnlockedDraw() is enough to ensure that
+  // animations start as soon as the image is painted for the first time, and in
+  // the long term we want to update visibility information from the display
+  // list whenever we paint, so we don't actually need to do this. However, to
+  // avoid behavior changes during the transition from the old image visibility
+  // code, we'll leave it in for now.
+  UpdateVisibilitySynchronously();
 
   return false;
 }
 
 void
 nsImageFrame::ReflowCallbackCanceled()
 {
   mReflowCallbackPosted = false;
@@ -2130,16 +2139,35 @@ nsImageFrame::AttributeChanged(int32_t a
     PresContext()->PresShell()->FrameNeedsReflow(this,
                                                  nsIPresShell::eStyleChange,
                                                  NS_FRAME_IS_DIRTY);
   }
 
   return NS_OK;
 }
 
+void
+nsImageFrame::OnVisibilityChange(Visibility aNewVisibility,
+                                 Maybe<OnNonvisible> aNonvisibleAction)
+{
+  nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mContent);
+  if (!imageLoader) {
+    MOZ_ASSERT_UNREACHABLE("Should have an nsIImageLoadingContent");
+    return;
+  }
+
+  imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+
+  if (aNewVisibility == Visibility::APPROXIMATELY_VISIBLE) {
+    MaybeDecodeForPredictedSize();
+  }
+
+  ImageFrameSuper::OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+}
+
 nsIAtom*
 nsImageFrame::GetType() const
 {
   return nsGkAtoms::imageFrame;
 }
 
 #ifdef DEBUG_FRAME_DUMP
 nsresult
--- a/layout/generic/nsImageFrame.h
+++ b/layout/generic/nsImageFrame.h
@@ -58,16 +58,20 @@ private:
   nsImageFrame *mFrame;
 };
 
 typedef nsAtomicContainerFrame ImageFrameSuper;
 
 class nsImageFrame : public ImageFrameSuper,
                      public nsIReflowCallback {
 public:
+  template <typename T> using Maybe = mozilla::Maybe<T>;
+  using Nothing = mozilla::Nothing;
+  using Visibility = mozilla::Visibility;
+
   typedef mozilla::image::DrawResult DrawResult;
   typedef mozilla::layers::ImageContainer ImageContainer;
   typedef mozilla::layers::ImageLayer ImageLayer;
   typedef mozilla::layers::LayerManager LayerManager;
 
   NS_DECL_FRAMEARENA_HELPERS
 
   explicit nsImageFrame(nsStyleContext* aContext);
@@ -99,16 +103,19 @@ public:
                                mozilla::WidgetGUIEvent* aEvent,
                                nsEventStatus* aEventStatus) override;
   virtual nsresult GetCursor(const nsPoint& aPoint,
                              nsIFrame::Cursor& aCursor) override;
   virtual nsresult AttributeChanged(int32_t aNameSpaceID,
                                     nsIAtom* aAttribute,
                                     int32_t aModType) override;
 
+  void OnVisibilityChange(Visibility aNewVisibility,
+                          Maybe<OnNonvisible> aNonvisibleAction = Nothing()) override;
+
 #ifdef ACCESSIBILITY
   virtual mozilla::a11y::AccType AccessibleType() override;
 #endif
 
   virtual nsIAtom* GetType() const override;
 
   virtual bool IsFrameOfType(uint32_t aFlags) const override
   {
--- a/layout/generic/nsSubDocumentFrame.cpp
+++ b/layout/generic/nsSubDocumentFrame.cpp
@@ -566,19 +566,19 @@ nsSubDocumentFrame::BuildDisplayList(nsD
   if (needsOwnLayer) {
     // We always want top level content documents to be in their own layer.
     nsDisplaySubDocument* layerItem = new (aBuilder) nsDisplaySubDocument(
       aBuilder, subdocRootFrame ? subdocRootFrame : this,
       &childItems, flags);
     childItems.AppendToTop(layerItem);
   }
 
-  if (aBuilder->IsForImageVisibility()) {
+  if (aBuilder->IsForFrameVisibility()) {
     // We don't add the childItems to the return list as we're dealing with them here.
-    presShell->RebuildImageVisibilityDisplayList(childItems);
+    presShell->RebuildApproximateFrameVisibilityDisplayList(childItems);
     childItems.DeleteAll();
   } else {
     aLists.Content()->AppendToTop(&childItems);
   }
 }
 
 nscoord
 nsSubDocumentFrame::GetIntrinsicISize()
--- a/layout/generic/nsTextFrame.cpp
+++ b/layout/generic/nsTextFrame.cpp
@@ -3594,17 +3594,21 @@ nsTextPaintStyle::GetTextColor()
         return NS_RGBA(0, 0, 0, 0);
       case eStyleSVGPaintType_Color:
         return nsLayoutUtils::GetColor(mFrame, eCSSProperty_fill);
       default:
         NS_ERROR("cannot resolve SVG paint to nscolor");
         return NS_RGBA(0, 0, 0, 255);
     }
   }
-  return nsLayoutUtils::GetColor(mFrame, eCSSProperty_color);
+
+  nsCSSProperty property =
+    mFrame->StyleText()->mWebkitTextFillColorForeground
+    ? eCSSProperty_color : eCSSProperty__webkit_text_fill_color;
+  return nsLayoutUtils::GetColor(mFrame, property);
 }
 
 bool
 nsTextPaintStyle::GetSelectionColors(nscolor* aForeColor,
                                      nscolor* aBackColor)
 {
   NS_ASSERTION(aForeColor, "aForeColor is null");
   NS_ASSERTION(aBackColor, "aBackColor is null");
--- a/layout/generic/nsVideoFrame.cpp
+++ b/layout/generic/nsVideoFrame.cpp
@@ -36,29 +36,30 @@ using namespace mozilla::gfx;
 nsIFrame*
 NS_NewHTMLVideoFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
 {
   return new (aPresShell) nsVideoFrame(aContext);
 }
 
 NS_IMPL_FRAMEARENA_HELPERS(nsVideoFrame)
 
-nsVideoFrame::nsVideoFrame(nsStyleContext* aContext) :
-  nsContainerFrame(aContext)
+nsVideoFrame::nsVideoFrame(nsStyleContext* aContext)
+  : nsVideoFrameBase(aContext)
 {
+  EnableVisibilityTracking();
 }
 
 nsVideoFrame::~nsVideoFrame()
 {
 }
 
 NS_QUERYFRAME_HEAD(nsVideoFrame)
   NS_QUERYFRAME_ENTRY(nsVideoFrame)
   NS_QUERYFRAME_ENTRY(nsIAnonymousContentCreator)
-NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsVideoFrameBase)
 
 nsresult
 nsVideoFrame::CreateAnonymousContent(nsTArray<ContentInfo>& aElements)
 {
   nsNodeInfoManager *nodeInfoManager = GetContent()->GetComposedDoc()->NodeInfoManager();
   RefPtr<NodeInfo> nodeInfo;
   Element *element;
 
@@ -139,17 +140,17 @@ nsVideoFrame::AppendAnonymousContentTo(n
 }
 
 void
 nsVideoFrame::DestroyFrom(nsIFrame* aDestructRoot)
 {
   nsContentUtils::DestroyAnonymousContent(&mCaptionDiv);
   nsContentUtils::DestroyAnonymousContent(&mVideoControls);
   nsContentUtils::DestroyAnonymousContent(&mPosterImage);
-  nsContainerFrame::DestroyFrom(aDestructRoot);
+  nsVideoFrameBase::DestroyFrom(aDestructRoot);
 }
 
 bool
 nsVideoFrame::IsLeaf() const
 {
   return true;
 }
 
@@ -606,21 +607,35 @@ nsVideoFrame::UpdatePosterSource(bool aN
 nsresult
 nsVideoFrame::AttributeChanged(int32_t aNameSpaceID,
                                nsIAtom* aAttribute,
                                int32_t aModType)
 {
   if (aAttribute == nsGkAtoms::poster && HasVideoElement()) {
     UpdatePosterSource(true);
   }
-  return nsContainerFrame::AttributeChanged(aNameSpaceID,
+  return nsVideoFrameBase::AttributeChanged(aNameSpaceID,
                                             aAttribute,
                                             aModType);
 }
 
+void
+nsVideoFrame::OnVisibilityChange(Visibility aNewVisibility,
+                                 Maybe<OnNonvisible> aNonvisibleAction)
+{
+  nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(mPosterImage);
+  if (!imageLoader) {
+    return;
+  }
+
+  imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+
+  nsVideoFrameBase::OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+}
+
 bool nsVideoFrame::HasVideoElement() {
   nsCOMPtr<nsIDOMHTMLMediaElement> mediaDomElement = do_QueryInterface(mContent);
   return mediaDomElement->IsVideo();
 }
 
 bool nsVideoFrame::HasVideoData()
 {
   if (!HasVideoElement())
--- a/layout/generic/nsVideoFrame.h
+++ b/layout/generic/nsVideoFrame.h
@@ -21,19 +21,25 @@ class Layer;
 class LayerManager;
 } // namespace layers
 } // namespace mozilla
 
 class nsAString;
 class nsPresContext;
 class nsDisplayItem;
 
-class nsVideoFrame : public nsContainerFrame, public nsIAnonymousContentCreator
+typedef nsContainerFrame nsVideoFrameBase;
+
+class nsVideoFrame : public nsVideoFrameBase, public nsIAnonymousContentCreator
 {
 public:
+  template <typename T> using Maybe = mozilla::Maybe<T>;
+  using Nothing = mozilla::Nothing;
+  using Visibility = mozilla::Visibility;
+
   typedef mozilla::layers::Layer Layer;
   typedef mozilla::layers::LayerManager LayerManager;
   typedef mozilla::ContainerLayerParameters ContainerLayerParameters;
 
   explicit nsVideoFrame(nsStyleContext* aContext);
 
   NS_DECL_QUERYFRAME
   NS_DECL_QUERYFRAME_TARGET(nsVideoFrame)
@@ -42,16 +48,19 @@ public:
   virtual void BuildDisplayList(nsDisplayListBuilder*   aBuilder,
                                 const nsRect&           aDirtyRect,
                                 const nsDisplayListSet& aLists) override;
 
   virtual nsresult AttributeChanged(int32_t aNameSpaceID,
                                     nsIAtom* aAttribute,
                                     int32_t aModType) override;
 
+  void OnVisibilityChange(Visibility aNewVisibility,
+                          Maybe<OnNonvisible> aNonvisibleAction = Nothing()) override;
+
   /* get the size of the video's display */
   nsSize GetVideoIntrinsicSize(nsRenderingContext *aRenderingContext);
   virtual nsSize GetIntrinsicRatio() override;
   virtual mozilla::LogicalSize
   ComputeSize(nsRenderingContext *aRenderingContext,
               mozilla::WritingMode aWritingMode,
               const mozilla::LogicalSize& aCBSize,
               nscoord aAvailableISize,
--- a/layout/style/StyleAnimationValue.cpp
+++ b/layout/style/StyleAnimationValue.cpp
@@ -3279,16 +3279,24 @@ StyleAnimationValue::ExtractComputedValu
         case eCSSProperty_text_emphasis_color: {
           auto styleText = static_cast<const nsStyleText*>(styleStruct);
           nscolor color = styleText->mTextEmphasisColorForeground ?
             aStyleContext->StyleColor()->mColor : styleText->mTextEmphasisColor;
           aComputedValue.SetColorValue(color);
           break;
         }
 
+        case eCSSProperty__webkit_text_fill_color: {
+          auto styleText = static_cast<const nsStyleText*>(styleStruct);
+          nscolor color = styleText->mWebkitTextFillColorForeground ?
+            aStyleContext->StyleColor()->mColor : styleText->mWebkitTextFillColor;
+          aComputedValue.SetColorValue(color);
+          break;
+        }
+
         case eCSSProperty_border_spacing: {
           const nsStyleTableBorder *styleTableBorder =
             static_cast<const nsStyleTableBorder*>(styleStruct);
           nsAutoPtr<nsCSSValuePair> pair(new nsCSSValuePair);
           nscoordToCSSValue(styleTableBorder->mBorderSpacingCol, pair->mXValue);
           nscoordToCSSValue(styleTableBorder->mBorderSpacingRow, pair->mYValue);
           aComputedValue.SetAndAdoptCSSValuePairValue(pair.forget(),
                                                       eUnit_CSSValuePair);
--- a/layout/style/nsCSSPropList.h
+++ b/layout/style/nsCSSPropList.h
@@ -3324,16 +3324,29 @@ CSS_PROP_TEXT(
         CSS_PROPERTY_VALUE_PARSER_FUNCTION |
         CSS_PROPERTY_ENABLED_IN_UA_SHEETS,
     "layout.css.text-emphasis.enabled",
     0,
     nullptr,
     CSS_PROP_NO_OFFSET,
     eStyleAnimType_None)
 CSS_PROP_TEXT(
+    -webkit-text-fill-color,
+    _webkit_text_fill_color,
+    WebkitTextFillColor,
+    CSS_PROPERTY_PARSE_VALUE |
+        CSS_PROPERTY_APPLIES_TO_FIRST_LETTER_AND_FIRST_LINE |
+        CSS_PROPERTY_APPLIES_TO_PLACEHOLDER |
+        CSS_PROPERTY_IGNORED_WHEN_COLORS_DISABLED,
+    "layout.css.prefixes.webkit",
+    VARIANT_HC,
+    nullptr,
+    offsetof(nsStyleText, mWebkitTextFillColor),
+    eStyleAnimType_Custom)
+CSS_PROP_TEXT(
     text-indent,
     text_indent,
     TextIndent,
     CSS_PROPERTY_PARSE_VALUE |
         CSS_PROPERTY_STORES_CALC |
         CSS_PROPERTY_UNITLESS_LENGTH_QUIRK |
         CSS_PROPERTY_GETCS_NEEDS_LAYOUT_FLUSH,
     "",
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -3910,16 +3910,27 @@ nsComputedDOMStyle::DoGetTextSizeAdjust(
     case NS_STYLE_TEXT_SIZE_ADJUST_NONE:
       val->SetIdent(eCSSKeyword_none);
       break;
   }
   return val.forget();
 }
 
 already_AddRefed<CSSValue>
+nsComputedDOMStyle::DoGetWebkitTextFillColor()
+{
+  RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
+  const nsStyleText* text = StyleText();
+  nscolor color = text->mWebkitTextFillColorForeground ?
+    StyleColor()->mColor : text->mWebkitTextFillColor;
+  SetToRGBAColor(val, color);
+  return val.forget();
+}
+
+already_AddRefed<CSSValue>
 nsComputedDOMStyle::DoGetPointerEvents()
 {
   RefPtr<nsROCSSPrimitiveValue> val = new nsROCSSPrimitiveValue;
   val->SetIdent(
     nsCSSProps::ValueToKeywordEnum(StyleVisibility()->mPointerEvents,
                                    nsCSSProps::kPointerEventsKTable));
   return val.forget();
 }
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -420,16 +420,17 @@ private:
   already_AddRefed<CSSValue> DoGetLetterSpacing();
   already_AddRefed<CSSValue> DoGetWordSpacing();
   already_AddRefed<CSSValue> DoGetWhiteSpace();
   already_AddRefed<CSSValue> DoGetWordBreak();
   already_AddRefed<CSSValue> DoGetWordWrap();
   already_AddRefed<CSSValue> DoGetHyphens();
   already_AddRefed<CSSValue> DoGetTabSize();
   already_AddRefed<CSSValue> DoGetTextSizeAdjust();
+  already_AddRefed<CSSValue> DoGetWebkitTextFillColor();
 
   /* Visibility properties */
   already_AddRefed<CSSValue> DoGetOpacity();
   already_AddRefed<CSSValue> DoGetPointerEvents();
   already_AddRefed<CSSValue> DoGetVisibility();
   already_AddRefed<CSSValue> DoGetWritingMode();
 
   /* Direction properties */
--- a/layout/style/nsComputedDOMStylePropertyList.h
+++ b/layout/style/nsComputedDOMStylePropertyList.h
@@ -292,16 +292,22 @@ COMPUTED_STYLE_PROP(text_align_last,    
 COMPUTED_STYLE_PROP(text_size_adjust,              TextSizeAdjust)
 COMPUTED_STYLE_PROP(user_focus,                    UserFocus)
 COMPUTED_STYLE_PROP(user_input,                    UserInput)
 COMPUTED_STYLE_PROP(user_modify,                   UserModify)
 COMPUTED_STYLE_PROP(user_select,                   UserSelect)
 COMPUTED_STYLE_PROP(_moz_window_dragging,          WindowDragging)
 COMPUTED_STYLE_PROP(_moz_window_shadow,            WindowShadow)
 
+/* ********************************** *\
+ * Implementations of -webkit- styles *
+\* ********************************** */
+
+COMPUTED_STYLE_PROP(_webkit_text_fill_color,       WebkitTextFillColor)
+
 /* ***************************** *\
  * Implementations of SVG styles *
 \* ***************************** */
 
 COMPUTED_STYLE_PROP(clip_path,                     ClipPath)
 COMPUTED_STYLE_PROP(clip_rule,                     ClipRule)
 COMPUTED_STYLE_PROP(color_interpolation,           ColorInterpolation)
 COMPUTED_STYLE_PROP(color_interpolation_filters,   ColorInterpolationFilters)
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -1651,19 +1651,20 @@ void
 nsRuleNode::ConvertChildrenToHash(int32_t aNumKids)
 {
   NS_ASSERTION(!ChildrenAreHashed() && HaveChildren(),
                "must have a non-empty list of children");
   PLDHashTable *hash = new PLDHashTable(&ChildrenHashOps,
                                         sizeof(ChildrenHashEntry),
                                         aNumKids);
   for (nsRuleNode* curr = ChildrenList(); curr; curr = curr->mNextSibling) {
+    Key key = curr->GetKey();
     // This will never fail because of the initial size we gave the table.
     auto entry =
-      static_cast<ChildrenHashEntry*>(hash->Add(curr->mRule));
+      static_cast<ChildrenHashEntry*>(hash->Add(&key));
     NS_ASSERTION(!entry->mRuleNode, "duplicate entries in list");
     entry->mRuleNode = curr;
   }
   SetChildrenHash(hash);
 }
 
 inline void
 nsRuleNode::PropagateNoneBit(uint32_t aBit, nsRuleNode* aHighestNode)
@@ -4706,16 +4707,37 @@ nsRuleNode::ComputeTextData(void* aStart
       TruncateStringToSingleGrapheme(strValue);
       text->mTextEmphasisStyleString = strValue;
       break;
     }
     default:
       MOZ_ASSERT_UNREACHABLE("Unknown value unit type");
   }
 
+  // -webkit-text-fill-color: color, string, inherit, initial
+  const nsCSSValue*
+    webkitTextFillColorValue = aRuleData->ValueForWebkitTextFillColor();
+  if (webkitTextFillColorValue->GetUnit() == eCSSUnit_Null) {
+    // We don't want to change anything in this case.
+  } else if (webkitTextFillColorValue->GetUnit() == eCSSUnit_Inherit ||
+             webkitTextFillColorValue->GetUnit() == eCSSUnit_Unset) {
+    conditions.SetUncacheable();
+    text->mWebkitTextFillColorForeground = parentText->mWebkitTextFillColorForeground;
+    text->mWebkitTextFillColor = parentText->mWebkitTextFillColor;
+  } else if ((webkitTextFillColorValue->GetUnit() == eCSSUnit_EnumColor &&
+              webkitTextFillColorValue->GetIntValue() == NS_COLOR_CURRENTCOLOR) ||
+             webkitTextFillColorValue->GetUnit() == eCSSUnit_Initial) {
+    text->mWebkitTextFillColorForeground = true;
+    text->mWebkitTextFillColor = mPresContext->DefaultColor();
+  } else {
+    text->mWebkitTextFillColorForeground = false;
+    SetColor(*webkitTextFillColorValue, 0, mPresContext, aContext,
+             text->mWebkitTextFillColor, conditions);
+  }
+
   // -moz-control-character-visibility: enum, inherit, initial
   SetDiscrete(*aRuleData->ValueForControlCharacterVisibility(),
               text->mControlCharacterVisibility,
               conditions,
               SETDSC_ENUMERATED | SETDSC_UNSET_INHERIT,
               parentText->mControlCharacterVisibility,
               nsCSSParser::ControlCharVisibilityDefault(), 0, 0, 0, 0);
 
--- a/layout/style/nsStyleContext.cpp
+++ b/layout/style/nsStyleContext.cpp
@@ -1086,17 +1086,20 @@ nsStyleContext::CalcStyleDifference(nsSt
     }
 
     // NB: Calling Peek on |this|, not |thisVis| (see above).
     if (!change && PeekStyleText()) {
       const nsStyleText* thisVisText = thisVis->StyleText();
       const nsStyleText* otherVisText = otherVis->StyleText();
       if (thisVisText->mTextEmphasisColorForeground !=
           otherVisText->mTextEmphasisColorForeground ||
-          thisVisText->mTextEmphasisColor != otherVisText->mTextEmphasisColor) {
+          thisVisText->mTextEmphasisColor != otherVisText->mTextEmphasisColor ||
+          thisVisText->mWebkitTextFillColorForeground !=
+          otherVisText->mWebkitTextFillColorForeground ||
+          thisVisText->mWebkitTextFillColor != otherVisText->mWebkitTextFillColor) {
         change = true;
       }
     }
 
     // NB: Calling Peek on |this|, not |thisVis| (see above).
     if (!change && PeekStyleTextReset()) {
       const nsStyleTextReset *thisVisTextReset = thisVis->StyleTextReset();
       const nsStyleTextReset *otherVisTextReset = otherVis->StyleTextReset();
@@ -1308,16 +1311,17 @@ nsStyleContext::GetVisitedDependentColor
                aProperty == eCSSProperty_border_top_color ||
                aProperty == eCSSProperty_border_right_color ||
                aProperty == eCSSProperty_border_bottom_color ||
                aProperty == eCSSProperty_border_left_color ||
                aProperty == eCSSProperty_outline_color ||
                aProperty == eCSSProperty__moz_column_rule_color ||
                aProperty == eCSSProperty_text_decoration_color ||
                aProperty == eCSSProperty_text_emphasis_color ||
+               aProperty == eCSSProperty__webkit_text_fill_color ||
                aProperty == eCSSProperty_fill ||
                aProperty == eCSSProperty_stroke,
                "we need to add to nsStyleContext::CalcStyleDifference");
 
   bool isPaintProperty = aProperty == eCSSProperty_fill ||
                          aProperty == eCSSProperty_stroke;
 
   nscolor colors[2];
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -3628,32 +3628,34 @@ AreShadowArraysEqual(nsCSSShadowArray* l
 nsStyleText::nsStyleText(nsPresContext* aPresContext)
 { 
   MOZ_COUNT_CTOR(nsStyleText);
   mTextAlign = NS_STYLE_TEXT_ALIGN_DEFAULT;
   mTextAlignLast = NS_STYLE_TEXT_ALIGN_AUTO;
   mTextAlignTrue = false;
   mTextAlignLastTrue = false;
   mTextEmphasisColorForeground = true;
+  mWebkitTextFillColorForeground = true;
   mTextTransform = NS_STYLE_TEXT_TRANSFORM_NONE;
   mWhiteSpace = NS_STYLE_WHITESPACE_NORMAL;
   mWordBreak = NS_STYLE_WORDBREAK_NORMAL;
   mWordWrap = NS_STYLE_WORDWRAP_NORMAL;
   mHyphens = NS_STYLE_HYPHENS_MANUAL;
   mRubyAlign = NS_STYLE_RUBY_ALIGN_SPACE_AROUND;
   mRubyPosition = NS_STYLE_RUBY_POSITION_OVER;
   mTextSizeAdjust = NS_STYLE_TEXT_SIZE_ADJUST_AUTO;
   mTextCombineUpright = NS_STYLE_TEXT_COMBINE_UPRIGHT_NONE;
   mTextEmphasisStyle = NS_STYLE_TEXT_EMPHASIS_STYLE_NONE;
   nsCOMPtr<nsIAtom> language = aPresContext->GetContentLanguage();
   mTextEmphasisPosition = language &&
     nsStyleUtil::MatchesLanguagePrefix(language, MOZ_UTF16("zh")) ?
     NS_STYLE_TEXT_EMPHASIS_POSITION_DEFAULT_ZH :
     NS_STYLE_TEXT_EMPHASIS_POSITION_DEFAULT;
   mTextEmphasisColor = aPresContext->DefaultColor();
+  mWebkitTextFillColor = aPresContext->DefaultColor();
   mControlCharacterVisibility = nsCSSParser::ControlCharVisibilityDefault();
 
   mWordSpacing.SetCoordValue(0);
   mLetterSpacing.SetNormalValue();
   mLineHeight.SetNormalValue();
   mTextIndent.SetCoordValue(0);
 
   mTextShadow = nullptr;
@@ -3661,30 +3663,32 @@ nsStyleText::nsStyleText(nsPresContext* 
 }
 
 nsStyleText::nsStyleText(const nsStyleText& aSource)
   : mTextAlign(aSource.mTextAlign),
     mTextAlignLast(aSource.mTextAlignLast),
     mTextAlignTrue(false),
     mTextAlignLastTrue(false),
     mTextEmphasisColorForeground(aSource.mTextEmphasisColorForeground),
+    mWebkitTextFillColorForeground(aSource.mWebkitTextFillColorForeground),
     mTextTransform(aSource.mTextTransform),
     mWhiteSpace(aSource.mWhiteSpace),
     mWordBreak(aSource.mWordBreak),
     mWordWrap(aSource.mWordWrap),
     mHyphens(aSource.mHyphens),
     mRubyAlign(aSource.mRubyAlign),
     mRubyPosition(aSource.mRubyPosition),
     mTextSizeAdjust(aSource.mTextSizeAdjust),
     mTextCombineUpright(aSource.mTextCombineUpright),
     mControlCharacterVisibility(aSource.mControlCharacterVisibility),
     mTextEmphasisPosition(aSource.mTextEmphasisPosition),
     mTextEmphasisStyle(aSource.mTextEmphasisStyle),
     mTabSize(aSource.mTabSize),
     mTextEmphasisColor(aSource.mTextEmphasisColor),
+    mWebkitTextFillColor(aSource.mWebkitTextFillColor),
     mWordSpacing(aSource.mWordSpacing),
     mLetterSpacing(aSource.mLetterSpacing),
     mLineHeight(aSource.mLineHeight),
     mTextIndent(aSource.mTextIndent),
     mTextShadow(aSource.mTextShadow),
     mTextEmphasisStyleString(aSource.mTextEmphasisStyleString)
 {
   MOZ_COUNT_CTOR(nsStyleText);
@@ -3744,17 +3748,19 @@ nsChangeHint nsStyleText::CalcDifference
   }
 
   MOZ_ASSERT(!mTextEmphasisColorForeground ||
              !aOther.mTextEmphasisColorForeground ||
              mTextEmphasisColor == aOther.mTextEmphasisColor,
              "If the text-emphasis-color are both foreground color, "
              "mTextEmphasisColor should also be identical");
   if (mTextEmphasisColorForeground != aOther.mTextEmphasisColorForeground ||
-      mTextEmphasisColor != aOther.mTextEmphasisColor) {
+      mTextEmphasisColor != aOther.mTextEmphasisColor ||
+      mWebkitTextFillColorForeground != aOther.mWebkitTextFillColorForeground ||
+      mWebkitTextFillColor != aOther.mWebkitTextFillColor) {
     return nsChangeHint_SchedulePaint |
            nsChangeHint_RepaintFrame;
   }
 
   if (mTextEmphasisPosition != aOther.mTextEmphasisPosition) {
     return nsChangeHint_NeutralChange;
   }
 
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -26,16 +26,17 @@
 #include "nsPresContext.h"
 #include "nsCOMPtr.h"
 #include "nsCOMArray.h"
 #include "nsTArray.h"
 #include "nsCSSValue.h"
 #include "imgRequestProxy.h"
 #include "Orientation.h"
 #include "CounterStyleManager.h"
+#include <cstddef> // offsetof()
 
 class nsIFrame;
 class nsIURI;
 class nsStyleContext;
 class nsTextFrame;
 class imgIContainer;
 struct nsStyleVisibility;
 
@@ -93,18 +94,61 @@ struct nsStyleVisibility;
 #define NS_RULE_NODE_HAS_ANIMATION_DATA     0x80000000
 
 static_assert(int(mozilla::SheetType::Count) - 1 <=
                 (NS_RULE_NODE_LEVEL_MASK >> NS_RULE_NODE_LEVEL_SHIFT),
               "NS_RULE_NODE_LEVEL_MASK cannot fit SheetType");
 
 static_assert(NS_RULE_NODE_IS_ANIMATION_RULE == (1 << nsStyleStructID_Length),
   "NS_RULE_NODE_IS_ANIMATION_RULE must not overlap the style struct bits.");
+
+/**
+ * These *_Simple types are used to map Gecko types to layout-equivalent but
+ * simpler Rust types, to aid Rust binding generation.
+ *
+ * If something in this types or the assertions below needs to change, ask
+ * bholley, heycam or emilio before!
+ *
+ * <div rustbindgen="true" replaces="nsPoint">
+ */
+struct nsPoint_Simple {
+  nscoord x, y;
+};
+
+static_assert(sizeof(nsPoint_Simple) == sizeof(nsPoint), "Wrong nsPoint_Simple size");
+static_assert(offsetof(nsPoint_Simple, x) == offsetof(nsPoint, x), "Wrong nsPoint_Simple member alignment");
+static_assert(offsetof(nsPoint_Simple, y) == offsetof(nsPoint, y), "Wrong nsPoint_Simple member alignment");
+
+/**
+ * <div rustbindgen="true" replaces="nsMargin">
+ */
+struct nsMargin_Simple {
+  nscoord top, right, bottom, left;
+};
+
+static_assert(sizeof(nsMargin_Simple) == sizeof(nsMargin), "Wrong nsMargin_Simple size");
+static_assert(offsetof(nsMargin_Simple, top) == offsetof(nsMargin, top), "Wrong nsMargin_Simple member alignment");
+static_assert(offsetof(nsMargin_Simple, right) == offsetof(nsMargin, right), "Wrong nsMargin_Simple member alignment");
+static_assert(offsetof(nsMargin_Simple, bottom) == offsetof(nsMargin, bottom), "Wrong nsMargin_Simple member alignment");
+static_assert(offsetof(nsMargin_Simple, left) == offsetof(nsMargin, left), "Wrong nsMargin_Simple member alignment");
+
+/**
+ * <div rustbindgen="true" replaces="nsRect">
+ */
+struct nsRect_Simple {
+  nscoord x, y, width, height;
+};
+
+static_assert(sizeof(nsRect_Simple) == sizeof(nsRect), "Wrong nsRect_Simple size");
+static_assert(offsetof(nsRect_Simple, x) == offsetof(nsRect, x), "Wrong nsRect_Simple member alignment");
+static_assert(offsetof(nsRect_Simple, y) == offsetof(nsRect, y), "Wrong nsRect_Simple member alignment");
+static_assert(offsetof(nsRect_Simple, width) == offsetof(nsRect, width), "Wrong nsRect_Simple member alignment");
+static_assert(offsetof(nsRect_Simple, height) == offsetof(nsRect, height), "Wrong nsRect_Simple member alignment");
+
 // The lifetime of these objects is managed by the presshell's arena.
-
 struct nsStyleFont
 {
   nsStyleFont(const nsFont& aFont, nsPresContext *aPresContext);
   nsStyleFont(const nsStyleFont& aStyleFont);
   explicit nsStyleFont(nsPresContext *aPresContext);
   ~nsStyleFont(void) {
     MOZ_COUNT_DTOR(nsStyleFont);
   }
@@ -1867,30 +1911,32 @@ struct nsStyleText
            nsChangeHint_ClearAncestorIntrinsics;
   }
 
   uint8_t mTextAlign;                   // [inherited] see nsStyleConsts.h
   uint8_t mTextAlignLast;               // [inherited] see nsStyleConsts.h
   bool mTextAlignTrue : 1;              // [inherited] see nsStyleConsts.h
   bool mTextAlignLastTrue : 1;          // [inherited] see nsStyleConsts.h
   bool mTextEmphasisColorForeground : 1;// [inherited] whether text-emphasis-color is currentColor
+  bool mWebkitTextFillColorForeground : 1;    // [inherited] whether -webkit-text-fill-color is currentColor
   uint8_t mTextTransform;               // [inherited] see nsStyleConsts.h
   uint8_t mWhiteSpace;                  // [inherited] see nsStyleConsts.h
   uint8_t mWordBreak;                   // [inherited] see nsStyleConsts.h
   uint8_t mWordWrap;                    // [inherited] see nsStyleConsts.h
   uint8_t mHyphens;                     // [inherited] see nsStyleConsts.h
   uint8_t mRubyAlign;                   // [inherited] see nsStyleConsts.h
   uint8_t mRubyPosition;                // [inherited] see nsStyleConsts.h
   uint8_t mTextSizeAdjust;              // [inherited] see nsStyleConsts.h
   uint8_t mTextCombineUpright;          // [inherited] see nsStyleConsts.h
   uint8_t mControlCharacterVisibility;  // [inherited] see nsStyleConsts.h
   uint8_t mTextEmphasisPosition;        // [inherited] see nsStyleConsts.h
   uint8_t mTextEmphasisStyle;           // [inherited] see nsStyleConsts.h
   int32_t mTabSize;                     // [inherited] see nsStyleConsts.h
   nscolor mTextEmphasisColor;           // [inherited]
+  nscolor mWebkitTextFillColor;         // [inherited]
 
   nsStyleCoord mWordSpacing;            // [inherited] coord, percent, calc
   nsStyleCoord mLetterSpacing;          // [inherited] coord, normal
   nsStyleCoord mLineHeight;             // [inherited] coord, factor, normal
   nsStyleCoord mTextIndent;             // [inherited] coord, percent, calc
 
   RefPtr<nsCSSShadowArray> mTextShadow; // [inherited] nullptr in case of a zero-length
 
--- a/layout/style/test/chrome/chrome.ini
+++ b/layout/style/test/chrome/chrome.ini
@@ -10,12 +10,13 @@ support-files =
   mismatch.png
 
 [test_author_specified_style.html]
 [test_bug418986-2.xul]
 [test_bug1157097.html]
 [test_bug1160724.xul]
 [test_bug535806.xul]
 [test_display_mode.html]
+[test_display_mode_reflow.html]
 tags = fullscreen
 [test_hover.html]
 skip-if = buildapp == 'mulet'
 [test_moz_document_rules.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/chrome/test_display_mode_reflow.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1256084
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Display Mode</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript">
+
+/** Test for Display Mode **/
+SimpleTest.waitForExplicitFinish();
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function waitOneEvent(element, name) {
+  return new Promise(function(resolve, reject) {
+    element.addEventListener(name, function listener() {
+      element.removeEventListener(name, listener);
+      resolve();
+    });
+  });
+}
+
+add_task(function* () {
+  yield waitOneEvent(window, "load");
+
+  var iframe = document.getElementById("subdoc");
+  var subdoc = iframe.contentDocument;
+  var style = subdoc.getElementById("style");
+  var bodyComputedStyled = subdoc.defaultView.getComputedStyle(subdoc.body, "");
+  var win = Services.wm.getMostRecentWindow("navigator:browser");
+
+  var secondDiv = subdoc.getElementById("b");
+  var offsetTop = secondDiv.offsetTop;
+
+  // Test entering the OS's fullscreen mode.
+  var fullScreenEntered = waitOneEvent(win, "sizemodechange");
+  synthesizeKey("VK_F11", {});
+  yield fullScreenEntered;
+  ok(offsetTop !== secondDiv.offsetTop, "offset top changes");
+  var fullScreenExited = waitOneEvent(win, "sizemodechange");
+  synthesizeKey("VK_F11", {});
+  yield fullScreenExited;
+  ok(offsetTop === secondDiv.offsetTop, "offset top returns to original value");
+
+  offsetTop = secondDiv.offsetTop;
+  // Test entering fullscreen through document requestFullScreen.
+  fullScreenEntered = waitOneEvent(document, "mozfullscreenchange");
+  document.body.mozRequestFullScreen();
+  yield fullScreenEntered
+  ok(offsetTop !== secondDiv.offsetTop, "offset top changes");
+  fullScreenExited = waitOneEvent(document, "mozfullscreenchange");
+  document.mozCancelFullScreen();
+  yield fullScreenExited;
+  ok(offsetTop === secondDiv.offsetTop, "offset top returns to original value");
+});
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1256084">Mozilla Bug 1256084</a>
+<iframe id="subdoc" src="http://mochi.test:8888/tests/layout/style/test/display_mode_reflow_iframe.html"></iframe>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/style/test/display_mode_reflow_iframe.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+  "http://www.w3.org/TR/html4/strict.dtd">
+<html lang="en-US">
+<head>
+  <title>Display Mode Reflow inner frame</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <meta http-equiv="Content-Style-Type" content="text/css">
+  <style type="text/css" id="style" media="all">
+  div {
+    border: 2px solid black;
+    width: 50px;
+    height: 50px;
+  }
+  @media (display-mode: fullscreen) {
+    #a { height: 100px; }
+  }
+  </style>
+</head>
+<body>
+  <div id="a"></div>
+  <div id="b"></div>
+</body>
+</html>
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -3,16 +3,17 @@ support-files =
   animation_utils.js
   ccd-quirks.html
   ccd.sjs
   ccd-standards.html
   chrome/bug418986-2.js
   chrome/match.png
   chrome/mismatch.png
   descriptor_database.js
+  display_mode_reflow_iframe.html
   empty.html
   media_queries_dynamic_xbl_binding.xml
   media_queries_dynamic_xbl_iframe.html
   media_queries_dynamic_xbl_style.css
   media_queries_iframe.html
   neverending_font_load.sjs
   neverending_stylesheet_load.sjs
   post-redirect-1.css
--- a/layout/style/test/property_database.js
+++ b/layout/style/test/property_database.js
@@ -7008,16 +7008,25 @@ if (IsCSSPropertyPrefEnabled("layout.css
   };
   gCSSProperties["-webkit-filter"] = {
     domProp: "webkitFilter",
     inherited: false,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
     alias_for: "filter",
     subproperties: [ "filter" ],
   };
+  gCSSProperties["-webkit-text-fill-color"] = {
+    domProp: "webkitTextFillColor",
+    inherited: true,
+    type: CSS_TYPE_LONGHAND,
+    prerequisites: { "color": "black" },
+    initial_values: [ "currentColor", "black", "#000", "#000000", "rgb(0,0,0)" ],
+    other_values: [ "red", "rgba(255,255,255,0.5)", "transparent" ],
+    invalid_values: [ "#0", "#00", "#0000", "#00000", "#0000000", "#00000000", "#000000000", "000000", "ff00ff", "rgb(255,xxx,255)" ]
+  };
   gCSSProperties["-webkit-text-size-adjust"] = {
     domProp: "webkitTextSizeAdjust",
     inherited: true,
     type: CSS_TYPE_SHORTHAND_AND_LONGHAND,
     alias_for: "-moz-text-size-adjust",
     subproperties: [ "-moz-text-size-adjust" ],
   };
   gCSSProperties["-webkit-transform"] = {
--- a/layout/style/test/test_bug657143.html
+++ b/layout/style/test/test_bug657143.html
@@ -1,107 +1,132 @@
-<!DOCTYPE HTML>
-<html>
-<!--
-https://bugzilla.mozilla.org/show_bug.cgi?id=657143
--->
-<head>
-  <title>Test for Bug 657143</title>
-  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
-  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
-</head>
-<body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657143">Mozilla Bug 657143</a>
-<p id="display"></p>
-<style>
-/* Ensure that there is at least one custom property on the root element's
-   computed style */
-:root { --test: some value; }
-</style>
-<div id="content" style="display: none">
-  
-</div>
-<pre id="test">
-<script class="testbody" type="text/javascript">
-
-/** Test for Bug 657143 **/
-
-// Checks ordering of CSS properties in nsComputedDOMStyle.cpp
-// by splitting the getComputedStyle() into four sublists
-// then cloning and sort()ing the lists and comparing with originals.
-
-function isPrefixed(aProp) {
-  return aProp.startsWith("-moz");
-}
-
-function isNotPrefixed(aProp) {
-  return !isPrefixed(aProp);
-}
-
-function isCustom(aProp) {
-  return aProp.startsWith("--");
-}
-
-function isNotCustom(aProp) {
-  return !isCustom(aProp);
-}
-
-var styleList = window.getComputedStyle(document.documentElement);
-    cssA = [], mozA = [], svgA = [], customA = [];
-
-// Partition the list of property names into four lists:
-//
-//   cssA: regular, non-SVG properties
-//   mozA: '-moz-' prefixed properties
-//   svgA: SVG properties
-//   customA: '--' prefixed custom properties
-
-var list = cssA;
-for (var i = 0, j = styleList.length; i < j; i++) {
-  var prop = styleList.item(i);
-
-  switch (list) {
-    case cssA:
-      if (isPrefixed(prop)) {
-        list = mozA;
-      }
-      // fall through
-    case mozA:
-      if (prop == "clip-path") {
-        // Assumes that the first SVG property is 'clip-path'.
-        list = svgA;
-      }
-      // fall through
-    case svgA:
-      if (isCustom(prop)) {
-        list = customA;
-      }
-      // fall through
-  }
-
-  list.push(prop);
-}
-
-var cssB = cssA.slice(0).sort(),
-    mozB = mozA.slice(0).sort(),
-    svgB = svgA.slice(0).sort();
-
-is(cssA.toString(), cssB.toString(), 'CSS property list should be alphabetical');
-is(mozA.toString(), mozB.toString(), 'Experimental -moz- CSS property list should be alphabetical');
-is(svgA.toString(), svgB.toString(), 'SVG property list should be alphabetical');
-
-// We don't test that the custom property list is sorted, as the CSSOM
-// specification does not yet require it, and we don't return them
-// in sorted order.
-
-ok(!cssA.find(isPrefixed), 'Experimental -moz- CSS properties should not be in the mature CSS property list');
-ok(!cssA.find(isCustom), 'Custom CSS properties should not be in the mature CSS property list');
-ok(!mozA.find(isNotPrefixed), 'Experimental -moz- CSS property list should not contain mature CSS properties');
-ok(!mozA.find(isCustom), 'Custom CSS properties should not be in the experimental -moz- CSS property list');
-ok(!svgA.find(isPrefixed), 'Experimental -moz- CSS properties should not be in the SVG property list'); 
-ok(!svgA.find(isCustom), 'Custom CSS properties should not be in the SVG property list');
-ok(!customA.find(isNotCustom), 'Non-custom CSS properties should not be in the custom property list');
-
-</script>
-</pre>
-</body>
-</html>
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657143
+-->
+<head>
+  <title>Test for Bug 657143</title>
+  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657143">Mozilla Bug 657143</a>
+<p id="display"></p>
+<style>
+/* Ensure that there is at least one custom property on the root element's
+   computed style */
+:root { --test: some value; }
+</style>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 657143 **/
+
+// Check ordering of CSS properties in nsComputedDOMStylePropertyList.h
+// by splitting the getComputedStyle() into five sublists
+// then cloning and sort()ing the lists and comparing with originals.
+
+function isMozPrefixed(aProp) {
+  return aProp.startsWith("-moz");
+}
+
+function isNotMozPrefixed(aProp) {
+  return !isMozPrefixed(aProp);
+}
+
+function isWebkitPrefixed(aProp) {
+  return aProp.startsWith("-webkit");
+}
+
+function isNotWebkitPrefixed(aProp) {
+  return !isWebkitPrefixed(aProp);
+}
+
+function isCustom(aProp) {
+  return aProp.startsWith("--");
+}
+
+function isNotCustom(aProp) {
+  return !isCustom(aProp);
+}
+
+var styleList = window.getComputedStyle(document.documentElement);
+    cssA = [], mozA = [], webkitA = [], svgA = [], customA = [];
+
+// Partition the list of property names into four lists:
+//
+//   cssA: regular, non-SVG properties
+//   mozA: '-moz-' prefixed properties
+//   svgA: SVG properties
+//   customA: '--' prefixed custom properties
+
+var list = cssA;
+for (var i = 0, j = styleList.length; i < j; i++) {
+  var prop = styleList.item(i);
+
+  switch (list) {
+    case cssA:
+      if (isMozPrefixed(prop)) {
+        list = mozA;
+      }
+      // fall through
+    case mozA:
+      if (isWebkitPrefixed(prop)) {
+        list = webkitA;
+      }
+      // fall through
+    case webkitA:
+      // Assume that the first SVG property is 'clip-path'.
+      if (prop == "clip-path") {
+        list = svgA;
+      }
+      // fall through
+    case svgA:
+      if (isCustom(prop)) {
+        list = customA;
+      }
+      // fall through
+  }
+
+  list.push(prop);
+}
+
+var cssB = cssA.slice(0).sort(),
+    mozB = mozA.slice(0).sort(),
+    webkitB = webkitA.slice(0).sort(),
+    svgB = svgA.slice(0).sort();
+
+is(cssA.toString(), cssB.toString(), 'CSS property list should be alphabetical');
+is(mozA.toString(), mozB.toString(), 'Experimental -moz- CSS property list should be alphabetical');
+is(webkitA.toString(), webkitB.toString(), 'Compatible -webkit- CSS property list should be alphabetical');
+is(svgA.toString(), svgB.toString(), 'SVG property list should be alphabetical');
+
+// We don't test that the custom property list is sorted, as the CSSOM
+// specification does not yet require it, and we don't return them
+// in sorted order.
+
+ok(!cssA.find(isWebkitPrefixed), 'Compatible -webkit- CSS properties should not be in the mature CSS property list');
+ok(!cssA.find(isMozPrefixed), 'Experimental -moz- CSS properties should not be in the mature CSS property list');
+ok(!cssA.find(isCustom), 'Custom CSS properties should not be in the mature CSS property list');
+ok(!mozA.find(isWebkitPrefixed), 'Compatible -webkit- CSS properties should not be in the experimental -moz- CSS '
+                                  + 'property list');
+ok(!mozA.find(isNotMozPrefixed), 'Experimental -moz- CSS property list should not contain non -moz- prefixed '
+                                  + 'CSS properties');
+ok(!mozA.find(isCustom), 'Custom CSS properties should not be in the experimental -moz- CSS property list');
+ok(!webkitA.find(isNotWebkitPrefixed), 'Compatible -webkit- CSS properties should not contain non -webkit- prefixed '
+                                        + 'CSS properties');
+ok(!webkitA.find(isMozPrefixed), 'Experimental -moz- CSS properties should not be in the compatible -webkit- CSS '
+                                  + 'property list');
+ok(!webkitA.find(isCustom), 'Custom CSS properties should not be in the compatible -webkit- CSS property list');
+ok(!svgA.find(isWebkitPrefixed), 'Compatible -webkit- CSS properties should not be in the SVG property list'); 
+ok(!svgA.find(isMozPrefixed), 'Experimental -moz- CSS properties should not be in the SVG property list');
+ok(!svgA.find(isCustom), 'Custom CSS properties should not be in the SVG property list');
+ok(!customA.find(isNotCustom), 'Non-custom CSS properties should not be in the custom property list');
+
+</script>
+</pre>
+</body>
+</html>
--- a/layout/style/test/test_transitions_events.html
+++ b/layout/style/test/test_transitions_events.html
@@ -18,16 +18,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 #four, #five, #six, #seven::before, #seven::after {
   transition: 500ms color;
   border-color: black; /* don't derive from color */
   -moz-column-rule-color: black; /* don't derive from color */
   text-decoration-color: black; /* don't derive from color */
   outline-color: black; /* don't derive from color */
   text-emphasis-color: black; /* don't derive from color */
+  -webkit-text-fill-color: black; /* don't derive from color */
 }
 
 #four {
   /* give the reversing transition a long duration; the reversing will
      still be quick */
   transition-duration: 30s;
   transition-timing-function: cubic-bezier(0, 1, 1, 0);
 }
@@ -70,16 +71,17 @@ var got_one_root = false;
 var got_one_target = false;
 var got_one_target_bordertop = false;
 var got_one_target_borderright = false;
 var got_one_target_borderbottom = false;
 var got_one_target_borderleft = false;
 var got_one_target_columnrule = false;
 var got_one_target_textdecorationcolor = false;
 var got_one_target_textemphasiscolor = false;
+var got_one_target_webkittextfillcolor = false;
 var got_one_target_outlinecolor = false;
 var got_two_target = false;
 var got_three_top = false;
 var got_three_right = false;
 var got_three_bottom = false;
 var got_three_left = false;
 var got_four_root = false;
 var got_body = false;
@@ -193,16 +195,22 @@ document.documentElement.addEventListene
         event.stopPropagation();
         break;
       case "text-emphasis-color":
         ok(!got_one_target_textemphasiscolor,
            "transitionend on one on target (text-emphasis-color)");
         got_one_target_textemphasiscolor = true;
         event.stopPropagation();
         break;
+      case "-webkit-text-fill-color":
+        ok(!got_one_target_webkittextfillcolor,
+           "transitionend on one on target (-webkit-text-fill-color)");
+        got_one_target_webkittextfillcolor = true;
+        event.stopPropagation();
+        break;
       case "outline-color":
         ok(!got_one_target_outlinecolor,
            "transitionend on one on target (outline-color)");
         got_one_target_outlinecolor = true;
         event.stopPropagation();
         break;
       default:
         ok(false, "unexpected property name " + event.propertyName +
@@ -221,16 +229,19 @@ started_test(); // border-right-color on
 started_test(); // border-right-color on #one (listener on root)
 started_test(); // border-bottom-color on #one
 started_test(); // border-left-color on #one
 started_test(); // -moz-column-rule-color on #one
 started_test(); // text-decoration-color on #one
 if (SpecialPowers.getBoolPref("layout.css.text-emphasis.enabled")) {
   started_test(); // text-emphasis-color on #one
 }
+if (SpecialPowers.getBoolPref("layout.css.prefixes.webkit")) {
+  started_test(); // -webkit-text-fill-color on #one
+}
 started_test(); // outline-color on #one
 $("one").style.color = "lime";
 
 
 $("two").addEventListener("transitionend",
   function(event) {
     event.stopPropagation();
 
--- a/layout/style/test/test_transitions_per_property.html
+++ b/layout/style/test/test_transitions_per_property.html
@@ -252,16 +252,18 @@ var supported_properties = {
                         test_length_percent_calc_transition,
                         test_length_unclamped, test_percent_unclamped ],
     "visibility": [ test_visibility_transition ],
     "width": [ test_length_transition, test_percent_transition,
                test_length_percent_calc_transition,
                test_length_clamped, test_percent_clamped ],
     "word-spacing": [ test_length_transition, test_length_unclamped ],
     "z-index": [ test_integer_transition, test_pos_integer_or_auto_transition ],
+    "-webkit-text-fill-color": [ test_color_transition,
+                                 test_border_color_transition ]
 };
 
 if (SupportsMaskShorthand()) {
   supported_properties["mask-position"] = [ test_background_position_transition,
                                      // FIXME: We don't currently test clamping,
                                      // since mask-position uses calc() as
                                      // an intermediate form.
                                      /* test_length_percent_pair_unclamped */ ];
--- a/layout/svg/SVGFEImageFrame.cpp
+++ b/layout/svg/SVGFEImageFrame.cpp
@@ -22,16 +22,22 @@ class SVGFEImageFrame : public SVGFEImag
 {
   friend nsIFrame*
   NS_NewSVGFEImageFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 protected:
   explicit SVGFEImageFrame(nsStyleContext* aContext)
     : SVGFEImageFrameBase(aContext)
   {
     AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY);
+
+    // This frame isn't actually displayed, but it contains an image and we want
+    // to use the nsImageLoadingContent machinery for managing images, which
+    // requires visibility tracking, so we enable visibility tracking and
+    // forcibly mark it visible below.
+    EnableVisibilityTracking();
   }
 
 public:
   NS_DECL_FRAMEARENA_HELPERS
 
   virtual void Init(nsIContent*       aContent,
                     nsContainerFrame* aParent,
                     nsIFrame*         aPrevInFlow) override;
@@ -55,16 +61,19 @@ public:
    * @see nsGkAtoms::svgFEImageFrame
    */
   virtual nsIAtom* GetType() const override;
 
   virtual nsresult AttributeChanged(int32_t  aNameSpaceID,
                                     nsIAtom* aAttribute,
                                     int32_t  aModType) override;
 
+  void OnVisibilityChange(Visibility aNewVisibility,
+                          Maybe<OnNonvisible> aNonvisibleAction = Nothing()) override;
+
   virtual bool UpdateOverflow() override {
     // We don't maintain a visual overflow rect
     return false;
   }
 };
 
 nsIFrame*
 NS_NewSVGFEImageFrame(nsIPresShell* aPresShell, nsStyleContext* aContext)
@@ -72,46 +81,44 @@ NS_NewSVGFEImageFrame(nsIPresShell* aPre
   return new (aPresShell) SVGFEImageFrame(aContext);
 }
 
 NS_IMPL_FRAMEARENA_HELPERS(SVGFEImageFrame)
 
 /* virtual */ void
 SVGFEImageFrame::DestroyFrom(nsIFrame* aDestructRoot)
 {
+  DecApproximateVisibleCount();
+
   nsCOMPtr<nsIImageLoadingContent> imageLoader =
     do_QueryInterface(SVGFEImageFrameBase::mContent);
-
   if (imageLoader) {
     imageLoader->FrameDestroyed(this);
-    imageLoader
-      ->DecrementVisibleCount(nsIImageLoadingContent::ON_NONVISIBLE_NO_ACTION);
   }
 
   SVGFEImageFrameBase::DestroyFrom(aDestructRoot);
 }
 
 void
 SVGFEImageFrame::Init(nsIContent*       aContent,
                       nsContainerFrame* aParent,
                       nsIFrame*         aPrevInFlow)
 {
   NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::feImage),
                "Trying to construct an SVGFEImageFrame for a "
                "content element that doesn't support the right interfaces");
 
   SVGFEImageFrameBase::Init(aContent, aParent, aPrevInFlow);
+
+  // We assume that feImage's are always visible.
+  IncApproximateVisibleCount();
+
   nsCOMPtr<nsIImageLoadingContent> imageLoader =
     do_QueryInterface(SVGFEImageFrameBase::mContent);
-
   if (imageLoader) {
-    // We assume that feImage's are always visible.
-    // Increment the visible count before calling FrameCreated so that
-    // FrameCreated will actually track the image correctly.
-    imageLoader->IncrementVisibleCount();
     imageLoader->FrameCreated(this);
   }
 }
 
 nsIAtom *
 SVGFEImageFrame::GetType() const
 {
   return nsGkAtoms::svgFEImageFrame;
@@ -135,8 +142,24 @@ SVGFEImageFrame::AttributeChanged(int32_
     } else {
       element->CancelImageRequests(true);
     }
   }
 
   return SVGFEImageFrameBase::AttributeChanged(aNameSpaceID,
                                                aAttribute, aModType);
 }
+
+void
+SVGFEImageFrame::OnVisibilityChange(Visibility aNewVisibility,
+                                    Maybe<OnNonvisible> aNonvisibleAction)
+{
+  nsCOMPtr<nsIImageLoadingContent> imageLoader =
+    do_QueryInterface(SVGFEImageFrameBase::mContent);
+  if (!imageLoader) {
+    MOZ_ASSERT_UNREACHABLE("Should have an nsIImageLoadingContent");
+    return;
+  }
+
+  imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+
+  SVGFEImageFrameBase::OnVisibilityChange(aNewVisibility, aNonvisibleAction);
+}
--- a/layout/svg/nsSVGImageFrame.cpp
+++ b/layout/svg/nsSVGImageFrame.cpp
@@ -49,18 +49,23 @@ typedef nsSVGPathGeometryFrame nsSVGImag
 
 class nsSVGImageFrame : public nsSVGImageFrameBase,
                         public nsIReflowCallback
 {
   friend nsIFrame*
   NS_NewSVGImageFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 
 protected:
-  explicit nsSVGImageFrame(nsStyleContext* aContext) : nsSVGImageFrameBase(aContext),
-                                                       mReflowCallbackPosted(false) {}
+  explicit nsSVGImageFrame(nsStyleContext* aContext)
+    : nsSVGImageFrameBase(aContext)
+    , mReflowCallbackPo