merge mozilla-inbound to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 13 Jan 2016 11:57:15 +0100
changeset 314861 531d1f6d1cde1182e9f7f9dff81a4fc5abc0a601
parent 314668 da1d2c63b0686a11be1b14c6e1f76a4875bb0e64 (current diff)
parent 314860 98756a36223c1a2b51cd0368736b728429038caf (diff)
child 314862 ad1f85f172b7302bef0fa9780df8e2b962780ac6
child 314869 182db13beca57456da8cfd10df3ef2c74ff0bd5b
child 314901 cae1c805bf9116cfb91d9e0f8b2496cfb37b1f56
child 314935 4c58dd262bbe68c219f5a349d7fa6353854b27ef
push id5703
push userraliiev@mozilla.com
push dateMon, 07 Mar 2016 14:18:41 +0000
treeherdermozilla-beta@31e373ad5b5f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone46.0a1
first release with
nightly linux32
531d1f6d1cde / 46.0a1 / 20160113030208 / files
nightly linux64
531d1f6d1cde / 46.0a1 / 20160113030208 / files
nightly mac
531d1f6d1cde / 46.0a1 / 20160113030208 / files
nightly win32
531d1f6d1cde / 46.0a1 / 20160113030208 / files
nightly win64
531d1f6d1cde / 46.0a1 / 20160113030208 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge mozilla-inbound to mozilla-central a=merge
browser/app/profile/firefox.js
browser/base/content/remote-newtab/newTab.css
browser/base/content/remote-newtab/newTab.js
browser/base/content/remote-newtab/newTab.xhtml
browser/components/newtab/RemoteAboutNewTab.jsm
browser/components/newtab/RemoteNewTabLocation.jsm
browser/components/newtab/RemoteNewTabUtils.jsm
browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js
browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js
browser/components/nsBrowserGlue.js
browser/components/nsIAboutNewTabService.idl
dom/base/test/test_bug827160.html
dom/base/test/test_object.html
js/src/jit-test/tests/collections/WeakMap-clear.js
js/src/jit-test/tests/collections/WeakSet-clear.js
media/libopus/tonality_init.patch
toolkit/components/telemetry/Histograms.json
--- a/.eslintignore
+++ b/.eslintignore
@@ -13,16 +13,17 @@ devtools/**/*.html
 # below.
 accessible/**
 addon-sdk/**
 build/**
 caps/**
 chrome/**
 config/**
 db/**
+devtools/**
 docshell/**
 dom/**
 editor/**
 embedding/**
 extensions/**
 gfx/**
 gradle/**
 hal/**
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1209344 - Remove debug button from about:addons. r=mossop
+Bug 1133073 - Use PR_DuplicateEnvironment from NSPR and remove interim mozglue wrappers.
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -72,17 +72,17 @@ this.EventManager.prototype = {
   // late). It is only called when the AccessFu is disabled explicitly.
   stop: function stop() {
     if (!this._started) {
       return;
     }
     Logger.debug('EventManager.stop');
     AccessibilityEventObserver.removeListener(this);
     try {
-      this._preDialogPosition.clear();
+      this._preDialogPosition = new WeakMap();
       this.webProgress.removeProgressListener(this);
       this.removeEventListener('wheel', this, true);
       this.removeEventListener('scroll', this, true);
       this.removeEventListener('resize', this, true);
     } catch (x) {
       // contentScope is dead.
     } finally {
       this._started = false;
@@ -613,17 +613,17 @@ const AccessibilityEventObserver = {
    * Stop an AccessibilityEventObserver.
    */
   stop: function stop() {
     if (!this.started) {
       return;
     }
     Services.obs.removeObserver(this, 'accessible-event');
     // Clean up all registered event managers.
-    this.eventManagers.clear();
+    this.eventManagers = new WeakMap();
     this.listenerCount = 0;
     this.started = false;
   },
 
   /**
    * Register an EventManager and start listening to the
    * 'accessible-event' messages.
    *
--- a/addon-sdk/source/lib/sdk/lang/weak-set.js
+++ b/addon-sdk/source/lib/sdk/lang/weak-set.js
@@ -8,26 +8,31 @@ module.metadata = {
 
 "use strict";
 
 const { Cu } = require("chrome");
 
 function makeGetterFor(Type) {
   let cache = new WeakMap();
 
-  return function getFor(target) {
-    if (!cache.has(target))
-      cache.set(target, new Type());
+  return {
+    getFor(target) {
+      if (!cache.has(target))
+        cache.set(target, new Type());
 
-    return cache.get(target);
+      return cache.get(target);
+    },
+    clearFor(target) {
+      return cache.delete(target)
+    }
   }
 }
 
-var getLookupFor = makeGetterFor(WeakMap);
-var getRefsFor = makeGetterFor(Set);
+var {getFor: getLookupFor, clearFor: clearLookupFor} = makeGetterFor(WeakMap);
+var {getFor: getRefsFor, clearFor: clearRefsFor} = makeGetterFor(Set);
 
 function add(target, value) {
   if (has(target, value))
     return;
 
   getLookupFor(target).set(value, true);
   getRefsFor(target).add(Cu.getWeakReference(value));
 }
@@ -39,18 +44,18 @@ function remove(target, value) {
 exports.remove = remove;
 
 function has(target, value) {
   return getLookupFor(target).has(value);
 }
 exports.has = has;
 
 function clear(target) {
-  getLookupFor(target).clear();
-  getRefsFor(target).clear();
+  clearLookupFor(target);
+  clearRefsFor(target);
 }
 exports.clear = clear;
 
 function iterator(target) {
   let refs = getRefsFor(target);
 
   for (let ref of refs) {
     let value = ref.get();
--- a/b2g/components/FilePicker.js
+++ b/b2g/components/FilePicker.js
@@ -66,21 +66,22 @@ FilePicker.prototype = {
       throw Cr.NS_ERROR_NOT_IMPLEMENTED;
     }
   },
 
   /* readonly attribute nsILocalFile file - not implemented; */
   /* readonly attribute nsISimpleEnumerator files - not implemented; */
   /* readonly attribute nsIURI fileURL - not implemented; */
 
-  get domfiles() {
+  get domFileOrDirectoryEnumerator() {
     return this.mFilesEnumerator;
   },
 
-  get domfile() {
+  // We don't support directory selection yet.
+  get domFileOrDirectory() {
     return this.mFilesEnumerator ? this.mFilesEnumerator.mFiles[0] : null;
   },
 
   get mode() {
     return this.mMode;
   },
 
   appendFilters: function(filterMask) {
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1344,20 +1344,18 @@ pref("browser.newtabpage.rows", 3);
 pref("browser.newtabpage.columns", 5);
 
 // directory tiles download URL
 pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%");
 
 // endpoint to send newtab click and view pings
 pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v3/links/");
 
-#ifndef RELEASE_BUILD
-// if true, it activates the remote-hosted newtab page
+// activates the remote-hosted newtab page
 pref("browser.newtabpage.remote", false);
-#endif
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // Startup Crash Tracking
 // number of startup crashes that can occur before starting into safe mode automatically
 // (this pref has no effect if more than 6 hours have passed since the last crash)
 pref("toolkit.startup.max_resumed_crashes", 3);
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -60,17 +60,16 @@ var FullZoom = {
     // before we were initialized we want to replay those upon initialization.
     for (let browser of gBrowser.browsers) {
       if (this._initialLocations.has(browser)) {
         this.onLocationChange(...this._initialLocations.get(browser), browser);
       }
     }
 
     // This should be nulled after initialization.
-    this._initialLocations.clear();
     this._initialLocations = null;
   },
 
   destroy: function FullZoom_destroy() {
     gPrefService.removeObserver("browser.zoom.", this);
     this._cps2.removeObserverForName(this.name, this);
     gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this);
   },
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6,16 +6,17 @@
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 var Cc = Components.classes;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NotificationDB.jsm");
 Cu.import("resource:///modules/RecentWindow.jsm");
 
+
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                   "resource:///modules/E10SUtils.jsm");
@@ -48,17 +49,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "mozIAsyncFavicons");
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
-
+XPCOMUtils.defineLazyServiceGetter(this, "gAboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
 XPCOMUtils.defineLazyGetter(this, "gBrowserBundle", function() {
   return Services.strings.createBundle('chrome://browser/locale/browser.properties');
 });
 
 const nsIWebNavigation = Ci.nsIWebNavigation;
 
 var gLastBrowserCharset = null;
 var gLastValidURLStr = "";
@@ -234,20 +237,16 @@ var gInitialPages = [
   "about:welcomeback",
   "about:sessionrestore"
 ];
 
 XPCOMUtils.defineLazyGetter(this, "Win7Features", function () {
   if (AppConstants.platform != "win")
     return null;
 
-  // Bug 666808 - AeroPeek support for e10s
-  if (gMultiProcessBrowser)
-    return null;
-
   const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
   if (WINTASKBAR_CONTRACTID in Cc &&
       Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available) {
     let AeroPeek = Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", {}).AeroPeek;
     return {
       onOpenWindow: function () {
         AeroPeek.onOpenWindow(window);
       },
@@ -2311,18 +2310,21 @@ function URLBarSetURI(aURI) {
   if (value == null) {
     let uri = aURI || gBrowser.currentURI;
     // Strip off "wyciwyg://" and passwords for the location bar
     try {
       uri = Services.uriFixup.createExposableURI(uri);
     } catch (e) {}
 
     // Replace initial page URIs with an empty string
-    // only if there's no opener (bug 370555).
-    if (gInitialPages.indexOf(uri.spec) != -1)
+    // 1. only if there's no opener (bug 370555).
+    // 2. if remote newtab is enabled and it's the default remote newtab page
+    let defaultRemoteURL = gAboutNewTabService.remoteEnabled &&
+                           uri.spec === gAboutNewTabService.newTabURL;
+    if (gInitialPages.includes(uri.spec) || defaultRemoteURL)
       value = gBrowser.selectedBrowser.hasContentOpener ? uri.spec : "";
     else
       value = losslessDecodeURI(uri);
 
     valid = !isBlankPageURL(uri.spec);
   }
 
   gURLBar.value = value;
@@ -3519,18 +3521,19 @@ const BrowserSearch = {
       }
       return;
     }
 
     let openSearchPageIfFieldIsNotActive = function(aSearchBar) {
       if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
         let url = gBrowser.currentURI.spec.toLowerCase();
         let mm = gBrowser.selectedBrowser.messageManager;
-        if (url === "about:home" ||
-            (url === "about:newtab" && NewTabUtils.allPages.enabled)) {
+        let newTabRemoted = Services.prefs.getBoolPref("browser.newtabpage.remote");
+        let localNewTabEnabled = url === "about:newtab" && !newTabRemoted && NewTabUtils.allPages.enabled;
+        if (url === "about:home" || localNewTabEnabled) {
           ContentSearch.focusInput(mm);
         } else {
           openUILinkIn("about:home", "current");
         }
       }
     };
 
     let searchBar = this.searchBar;
deleted file mode 100644
--- a/browser/base/content/remote-newtab/newTab.css
+++ /dev/null
@@ -1,23 +0,0 @@
-/* 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/. */
-
-html {
-  width: 100%;
-  height: 100%;
-}
-
-body {
-  width: 100%;
-  height: 100%;
-  padding: 0;
-  margin: 0;
-  position: relative;
-}
-
-#remotedoc {
-  width: 100%;
-  height: 100%;
-  border: none;
-  position: absolute;
-}
deleted file mode 100644
--- a/browser/base/content/remote-newtab/newTab.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/* 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/. */
-/*globals XPCOMUtils, Components, sendAsyncMessage, addMessageListener, removeMessageListener,
-          Services, PrivateBrowsingUtils*/
-"use strict";
-
-const {utils: Cu, interfaces: Ci} = Components;
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
-  "resource://gre/modules/PrivateBrowsingUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-(function() {
-  let remoteNewTabLocation;
-  let remoteIFrame;
-
-  /**
-   * Attempts to handle commands sent from the remote IFrame within this content frame.
-   * Expected commands below, with data types explained.
-   *
-   * @returns {Boolean} whether or not the command was handled
-   * @param {String} command
-   *        The command passed from the remote IFrame
-   * @param {Object} data
-   *        Parameters to the command
-   */
-  function handleCommand(command, data) {
-    let commandHandled = true;
-    switch (command) {
-    case "NewTab:UpdateTelemetryProbe":
-      /**
-       * Update a given Telemetry histogram
-       *
-       * @param {String} data.probe
-       *        Probe name to update
-       * @param {Number} data.value
-       *        Value to update histogram by
-       */
-      Services.telemetry.getHistogramById(data.probe).add(data.value);
-      break;
-    case "NewTab:Register":
-      registerEvent(data.type);
-      break;
-    case "NewTab:GetInitialState":
-      getInitialState();
-      break;
-    default:
-      commandHandled = false;
-    }
-    return commandHandled;
-  }
-
-  function initRemotePage(initData) {
-    // Messages that the iframe sends the browser will be passed onto
-    // the privileged parent process
-    remoteNewTabLocation = initData;
-    remoteIFrame = document.querySelector("#remotedoc");
-
-    let loadHandler = () => {
-      if (remoteIFrame.src !== remoteNewTabLocation.href) {
-        return;
-      }
-
-      remoteIFrame.removeEventListener("load", loadHandler);
-
-      remoteIFrame.contentDocument.addEventListener("NewTabCommand", (e) => {
-        // If the commands are not handled within this content frame, the command will be
-        // passed on to main process, in RemoteAboutNewTab.jsm
-        let handled = handleCommand(e.detail.command, e.detail.data);
-        if (!handled) {
-          sendAsyncMessage(e.detail.command, e.detail.data);
-        }
-      });
-      registerEvent("NewTab:Observe");
-      let ev = new CustomEvent("NewTabCommandReady");
-      remoteIFrame.contentDocument.dispatchEvent(ev);
-    };
-
-    remoteIFrame.src = remoteNewTabLocation.href;
-    remoteIFrame.addEventListener("load", loadHandler);
-  }
-
-  /**
-   * Allows the content IFrame to register a listener to an event sent by
-   * the privileged parent process, in RemoteAboutNewTab.jsm
-   *
-   * @param {String} eventName
-   *        Event name to listen to
-   */
-  function registerEvent(eventName) {
-    addMessageListener(eventName, (message) => {
-      remoteIFrame.contentWindow.postMessage(message, remoteNewTabLocation.origin);
-    });
-  }
-
-  /**
-   * Sends the initial data payload to a content IFrame so it can bootstrap
-   */
-  function getInitialState() {
-    let prefs = Services.prefs;
-    let isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(window);
-    let state = {
-      enabled: prefs.getBoolPref("browser.newtabpage.enabled"),
-      enhanced: prefs.getBoolPref("browser.newtabpage.enhanced"),
-      rows: prefs.getIntPref("browser.newtabpage.rows"),
-      columns: prefs.getIntPref("browser.newtabpage.columns"),
-      introShown: prefs.getBoolPref("browser.newtabpage.introShown"),
-      windowID: window.QueryInterface(Ci.nsIInterfaceRequestor)
-        .getInterface(Ci.nsIDOMWindowUtils).outerWindowID,
-      privateBrowsingMode: isPrivate
-    };
-    remoteIFrame.contentWindow.postMessage({
-      name: "NewTab:State",
-      data: state
-    }, remoteNewTabLocation.origin);
-  }
-
-  addMessageListener("NewTabFrame:Init", function loadHandler(message) {
-    // Everything is loaded. Initialize the New Tab Page.
-    removeMessageListener("NewTabFrame:Init", loadHandler);
-    initRemotePage(message.data);
-  });
-  sendAsyncMessage("NewTabFrame:GetInit");
-}());
deleted file mode 100644
--- a/browser/base/content/remote-newtab/newTab.xhtml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-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/. -->
-
-<!DOCTYPE html [
-  <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
-  %newTabDTD;
-]>
-
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-  <title>&newtab.pageTitle;</title>
-  <link rel="stylesheet" href="chrome://browser/content/remote-newtab/newTab.css"/>
-</head>
-<body>
-  <iframe id="remotedoc"/>
-  <script type="text/javascript;version=1.8"
-          src="chrome://browser/content/remote-newtab/newTab.js">
-  </script>
-</body>
-</html>
-
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -611,16 +611,19 @@ var DOMFullscreenHandler = {
     addEventListener("MozDOMFullscreen:Request", this);
     addEventListener("MozDOMFullscreen:Entered", this);
     addEventListener("MozDOMFullscreen:NewOrigin", this);
     addEventListener("MozDOMFullscreen:Exit", this);
     addEventListener("MozDOMFullscreen:Exited", this);
   },
 
   get _windowUtils() {
+    if (!content) {
+      return null;
+    }
     return content.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIDOMWindowUtils);
   },
 
   receiveMessage: function(aMessage) {
     switch(aMessage.name) {
       case "DOMFullscreen:Entered": {
         if (!this._windowUtils.handleFullscreenRequests() &&
@@ -628,17 +631,19 @@ var DOMFullscreenHandler = {
           // If we don't actually have any pending fullscreen request
           // to handle, neither we have been in fullscreen, tell the
           // parent to just exit.
           sendAsyncMessage("DOMFullscreen:Exit");
         }
         break;
       }
       case "DOMFullscreen:CleanUp": {
-        this._windowUtils.exitFullscreen();
+        if (this._windowUtils) {
+          this._windowUtils.exitFullscreen();
+        }
         this._fullscreenDoc = null;
         break;
       }
     }
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
--- a/browser/base/content/test/general/browser_bug763468_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug763468_perwindowpb.js
@@ -1,53 +1,57 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/* globals
+  waitForExplicitFinish, whenNewWindowLoaded, whenNewTabLoaded,
+  executeSoon, registerCleanupFunction, finish, is
+*/
+/* exported test */
 
 // This test makes sure that opening a new tab in private browsing mode opens about:privatebrowsing
 function test() {
   // initialization
   waitForExplicitFinish();
-  let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
-                                     .getService(Components.interfaces.nsIAboutNewTabService);
 
   let windowsToClose = [];
-  let newTab;
   let newTabURL;
   let mode;
 
   function doTest(aIsPrivateMode, aWindow, aCallback) {
-    whenNewTabLoaded(aWindow, function () {
+    whenNewTabLoaded(aWindow, function() {
       if (aIsPrivateMode) {
         mode = "per window private browsing";
         newTabURL = "about:privatebrowsing";
       } else {
         mode = "normal";
-        newTabURL = aboutNewTabService.newTabURL;
+        newTabURL = "about:newtab";
       }
 
       is(aWindow.gBrowser.currentURI.spec, newTabURL,
         "URL of NewTab should be " + newTabURL + " in " + mode +  " mode");
 
       aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
-      aCallback()
+      aCallback();
     });
-  };
+  }
 
   function testOnWindow(aOptions, aCallback) {
     whenNewWindowLoaded(aOptions, function(aWin) {
       windowsToClose.push(aWin);
       // execute should only be called when need, like when you are opening
       // web pages on the test. If calling executeSoon() is not necesary, then
       // call whenNewWindowLoaded() instead of testOnWindow() on your test.
       executeSoon(() => aCallback(aWin));
     });
-  };
+  }
 
-   // this function is called after calling finish() on the test.
+  // this function is called after calling finish() on the test.
   registerCleanupFunction(function() {
     windowsToClose.forEach(function(aWin) {
       aWin.close();
     });
   });
 
   // test first when not on private mode
   testOnWindow({}, function(aWin) {
--- a/browser/base/content/test/general/browser_bug767836_perwindowpb.js
+++ b/browser/base/content/test/general/browser_bug767836_perwindowpb.js
@@ -1,51 +1,57 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+/* globals waitForExplicitFinish, executeSoon, finish, whenNewWindowLoaded, ok */
+/* globals is */
+/* exported test */
 
 function test() {
   //initialization
   waitForExplicitFinish();
+
   let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
                                      .getService(Components.interfaces.nsIAboutNewTabService);
   let newTabURL;
   let testURL = "http://example.com/";
+  let defaultURL = aboutNewTabService.newTabURL;
   let mode;
 
   function doTest(aIsPrivateMode, aWindow, aCallback) {
-    openNewTab(aWindow, function () {
+    openNewTab(aWindow, function() {
       if (aIsPrivateMode) {
         mode = "per window private browsing";
         newTabURL = "about:privatebrowsing";
       } else {
         mode = "normal";
-        newTabURL = aboutNewTabService.newTabURL;
+        newTabURL = "about:newtab";
       }
 
       // Check the new tab opened while in normal/private mode
       is(aWindow.gBrowser.selectedBrowser.currentURI.spec, newTabURL,
         "URL of NewTab should be " + newTabURL + " in " + mode +  " mode");
       // Set the custom newtab url
       aboutNewTabService.newTabURL = testURL;
       is(aboutNewTabService.newTabURL, testURL, "Custom newtab url is set");
 
       // Open a newtab after setting the custom newtab url
-      openNewTab(aWindow, function () {
+      openNewTab(aWindow, function() {
         is(aWindow.gBrowser.selectedBrowser.currentURI.spec, testURL,
            "URL of NewTab should be the custom url");
 
         // Clear the custom url.
         aboutNewTabService.resetNewTabURL();
-        is(aboutNewTabService.newTabURL, "about:newtab", "No custom newtab url is set");
+        is(aboutNewTabService.newTabURL, defaultURL, "No custom newtab url is set");
 
         aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
         aWindow.gBrowser.removeTab(aWindow.gBrowser.selectedTab);
         aWindow.close();
-        aCallback()
+        aCallback();
       });
     });
   }
 
   function testOnWindow(aIsPrivate, aCallback) {
     whenNewWindowLoaded({private: aIsPrivate}, function(win) {
       executeSoon(() => aCallback(win));
     });
@@ -67,17 +73,17 @@ function test() {
   });
 }
 
 function openNewTab(aWindow, aCallback) {
   // Open a new tab
   aWindow.BrowserOpenTab();
 
   let browser = aWindow.gBrowser.selectedBrowser;
-  if (browser.contentDocument.readyState == "complete") {
+  if (browser.contentDocument.readyState === "complete") {
     executeSoon(aCallback);
     return;
   }
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     executeSoon(aCallback);
   }, true);
--- a/browser/base/content/test/newtab/browser_newtab_external_resource.js
+++ b/browser/base/content/test/newtab/browser_newtab_external_resource.js
@@ -4,23 +4,29 @@
  *
  * We perform two tests:
  * (1) We load a new tab (about:newtab) using the default url and make sure that URL
  *     of the doucment matches about:newtab and the principal is the systemPrincipal.
  * (2) We load a new tab (about:newtab) and make sure that document.location as well
  *     as the nodePrincipal match the URL in the URL bar.
  */
 
-const ABOUT_NEWTAB_URI = "about:newtab";
-const PREF_URI = "http://example.com/browser/browser/base/content/test/newtab/external_newtab.html";
+/* globals Cc, Ci, ok, is, content, TestRunner, addNewTabPageTab, gWindow, Services, info */
+/* exported runTests */
+
+"use strict";
 
 var browser = null;
 var aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
                            .getService(Ci.nsIAboutNewTabService);
 
+const ABOUT_NEWTAB_URI = "about:newtab";
+const PREF_URI = "http://example.com/browser/browser/base/content/test/newtab/external_newtab.html";
+const DEFAULT_URI = aboutNewTabService.newTabURL;
+
 function testPref() {
   // set the pref for about:newtab to point to an exteranl resource
   aboutNewTabService.newTabURL = PREF_URI;
   ok(aboutNewTabService.overridden,
      "sanity check: default URL for about:newtab should be overriden");
   is(aboutNewTabService.newTabURL, PREF_URI,
      "sanity check: default URL for about:newtab should return the new URL");
 
@@ -29,17 +35,17 @@ function testPref() {
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     is(content.document.location, PREF_URI, "document.location should match the external resource");
     is(content.document.documentURI, PREF_URI, "document.documentURI should match the external resource");
     is(content.document.nodePrincipal.URI.spec, PREF_URI, "nodePrincipal should match the external resource");
 
     // reset to about:newtab and perform sanity check
     aboutNewTabService.resetNewTabURL();
-    is(aboutNewTabService.newTabURL, ABOUT_NEWTAB_URI,
+    is(aboutNewTabService.newTabURL, DEFAULT_URI,
        "sanity check: resetting the URL to about:newtab should return about:newtab");
 
     // remove the tab and move on
     gBrowser.removeCurrentTab();
     TestRunner.next();
   }, true);
 }
 
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -15,17 +15,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "nsIAboutNewTabService");
 
 this.__defineGetter__("BROWSER_NEW_TAB_URL", () => {
   if (PrivateBrowsingUtils.isWindowPrivate(window) &&
       !PrivateBrowsingUtils.permanentPrivateBrowsing &&
       !aboutNewTabService.overridden) {
     return "about:privatebrowsing";
   }
-  return aboutNewTabService.newTabURL;
+  return "about:newtab";
 });
 
 var TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
 
 var gBidiUI = false;
 
 /**
  * Determines whether the given url is considered a special URL for new tabs.
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -131,19 +131,16 @@ browser.jar:
         content/browser/defaultthemes/devedition.icon.png     (content/defaultthemes/devedition.icon.png)
         content/browser/gcli_sec_bad.svg              (content/gcli_sec_bad.svg)
         content/browser/gcli_sec_good.svg             (content/gcli_sec_good.svg)
         content/browser/gcli_sec_moderate.svg         (content/gcli_sec_moderate.svg)
         content/browser/newtab/newTab.xhtml           (content/newtab/newTab.xhtml)
 *       content/browser/newtab/newTab.js              (content/newtab/newTab.js)
         content/browser/newtab/newTab.css             (content/newtab/newTab.css)
         content/browser/newtab/newTab.inadjacent.json         (content/newtab/newTab.inadjacent.json)
-        content/browser/remote-newtab/newTab.xhtml    (content/remote-newtab/newTab.xhtml)
-        content/browser/remote-newtab/newTab.js       (content/remote-newtab/newTab.js)
-        content/browser/remote-newtab/newTab.css      (content/remote-newtab/newTab.css)
 *       content/browser/pageinfo/pageInfo.xul         (content/pageinfo/pageInfo.xul)
         content/browser/pageinfo/pageInfo.js          (content/pageinfo/pageInfo.js)
         content/browser/pageinfo/pageInfo.css         (content/pageinfo/pageInfo.css)
         content/browser/pageinfo/pageInfo.xml         (content/pageinfo/pageInfo.xml)
         content/browser/pageinfo/feeds.js             (content/pageinfo/feeds.js)
         content/browser/pageinfo/feeds.xml            (content/pageinfo/feeds.xml)
         content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
         content/browser/pageinfo/security.js          (content/pageinfo/security.js)
--- a/browser/components/BrowserComponents.manifest
+++ b/browser/components/BrowserComponents.manifest
@@ -41,10 +41,8 @@ category command-line-validator b-browse
 
 component {eab9012e-5f74-4cbc-b2b5-a590235513cc} nsBrowserGlue.js
 contract @mozilla.org/browser/browserglue;1 {eab9012e-5f74-4cbc-b2b5-a590235513cc}
 category app-startup nsBrowserGlue service,@mozilla.org/browser/browserglue;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66} application={d1bfe7d9-c01e-4237-998b-7b5f960a4314}
 component {d8903bf6-68d5-4e97-bcd1-e4d3012f721a} nsBrowserGlue.js
 #ifndef MOZ_MULET
 contract @mozilla.org/content-permission/prompt;1 {d8903bf6-68d5-4e97-bcd1-e4d3012f721a}
 #endif
-component {97eea4bb-db50-4ae0-9147-1e5ed55b4ed5} nsBrowserGlue.js
-contract @mozilla.org/browser/aboutnewtab-service;1 {97eea4bb-db50-4ae0-9147-1e5ed55b4ed5}
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -81,23 +81,19 @@ static RedirEntry kRedirMap[] = {
     nsIAboutModule::ALLOW_SCRIPT },
   { "sync-tabs", "chrome://browser/content/sync/aboutSyncTabs.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "home", "chrome://browser/content/abouthome/aboutHome.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::ENABLE_INDEXED_DB },
-  { "newtab", "chrome://browser/content/newtab/newTab.xhtml",
+  // the newtab's actual URL will be determined when the channel is created
+  { "newtab", "about:blank",
     nsIAboutModule::ALLOW_SCRIPT },
-#ifndef RELEASE_BUILD
-  { "remote-newtab", "chrome://browser/content/remote-newtab/newTab.xhtml",
-    nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
-    nsIAboutModule::ALLOW_SCRIPT },
-#endif
   { "preferences", "chrome://browser/content/preferences/in-content/preferences.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "downloads", "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
@@ -157,28 +153,22 @@ AboutRedirector::NewChannel(nsIURI* aURI
   nsresult rv;
   nsCOMPtr<nsIIOService> ioService = do_GetIOService(&rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   for (int i = 0; i < kRedirTotal; i++) {
     if (!strcmp(path.get(), kRedirMap[i].id)) {
       nsAutoCString url;
 
-      // check if about:newtab got overridden
       if (path.EqualsLiteral("newtab")) {
+        // let the aboutNewTabService decide where to redirect
         nsCOMPtr<nsIAboutNewTabService> aboutNewTabService =
           do_GetService("@mozilla.org/browser/aboutnewtab-service;1", &rv);
-        NS_ENSURE_SUCCESS(rv, rv);
-        bool overridden = false;
-        rv = aboutNewTabService->GetOverridden(&overridden);
+        rv = aboutNewTabService->GetNewTabURL(url);
         NS_ENSURE_SUCCESS(rv, rv);
-        if (overridden) {
-          rv = aboutNewTabService->GetNewTabURL(url);
-          NS_ENSURE_SUCCESS(rv, rv);
-        }
       }
       // fall back to the specified url in the map
       if (url.IsEmpty()) {
         url.AssignASCII(kRedirMap[i].url);
       }
 
       nsCOMPtr<nsIChannel> tempChannel;
       nsCOMPtr<nsIURI> tempURI;
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1688,17 +1688,17 @@ CustomizeMode.prototype = {
     let targetArea = this._getCustomizableParent(aEvent.currentTarget);
     let document = aEvent.target.ownerDocument;
     let documentId = document.documentElement.id;
     let draggedItemId =
       aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
     let originArea = this._getCustomizableParent(draggedWrapper);
     if (this._dragSizeMap) {
-      this._dragSizeMap.clear();
+      this._dragSizeMap = new WeakMap();
     }
     // Do nothing if the target area or origin area are not customizable.
     if (!targetArea || !originArea) {
       return;
     }
     let targetNode = this._dragOverItem;
     let dropDir = targetNode.getAttribute("dragover");
     // Need to insert *after* this node if we promised the user that:
--- a/browser/components/customizableui/DragPositionManager.jsm
+++ b/browser/components/customizableui/DragPositionManager.jsm
@@ -405,17 +405,17 @@ var DragPositionManager = {
     if (CustomizableUI.getAreaType(aArea) != "toolbar") {
       return;
     }
 
     gManagers.delete(aContainer);
   },
 
   stop: function() {
-    gManagers.clear();
+    gManagers = new WeakMap();
   },
 
   getManagerForArea: function(aArea) {
     return gManagers.get(aArea);
   }
 };
 
 Object.freeze(DragPositionManager);
--- a/browser/components/extensions/test/browser/browser_ext_tabs_create.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_create.js
@@ -46,31 +46,31 @@ add_task(function* () {
         let activeWindow;
 
         function runTests() {
           const DEFAULTS = {
             index: 2,
             windowId: activeWindow,
             active: true,
             pinned: false,
-            url: "about:newtab",
+            url: "chrome://browser/content/newtab/newTab.xhtml",
           };
 
           let tests = [
             {
               create: { url: "http://example.com/" },
               result: { url: "http://example.com/" },
             },
             {
               create: { url: "blank.html" },
               result: { url: browser.runtime.getURL("bg/blank.html") },
             },
             {
               create: {},
-              result: { url: "about:newtab" },
+              result: { url: "chrome://browser/content/newtab/newTab.xhtml" },
             },
             {
               create: { active: false },
               result: { active: false },
             },
             {
               create: { active: true },
               result: { active: true },
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -23,17 +23,16 @@ DIRS += [
     'selfsupport',
     'uitour',
     'translation',
 ]
 
 DIRS += ['build']
 
 XPIDL_SOURCES += [
-    'nsIAboutNewTabService.idl',
     'nsIBrowserGlue.idl',
     'nsIBrowserHandler.idl',
 ]
 
 XPIDL_MODULE = 'browsercompsbase'
 
 EXTRA_PP_COMPONENTS += [
     'BrowserComponents.manifest',
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/NewTabComponents.manifest
@@ -0,0 +1,2 @@
+component {cef25b06-0ef6-4c50-a243-e69f943ef23d} aboutNewTabService.js
+contract @mozilla.org/browser/aboutnewtab-service;1 {cef25b06-0ef6-4c50-a243-e69f943ef23d}
--- a/browser/components/newtab/NewTabPrefsProvider.jsm
+++ b/browser/components/newtab/NewTabPrefsProvider.jsm
@@ -12,16 +12,17 @@ Cu.import("resource://gre/modules/XPCOMU
 
 XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return EventEmitter;
 });
 
 // Supported prefs and data type
 const gPrefsMap = new Map([
+  ["browser.newtabpage.remote", "bool"],
   ["browser.newtabpage.enabled", "bool"],
   ["browser.newtabpage.enhanced", "bool"],
   ["browser.newtabpage.pinned", "str"],
   ["intl.locale.matchOS", "bool"],
   ["general.useragent.locale", "localized"],
 ]);
 
 let PrefsProvider = function PrefsProvider() {
--- a/browser/components/newtab/NewTabURL.jsm
+++ b/browser/components/newtab/NewTabURL.jsm
@@ -1,19 +1,20 @@
 /* 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/. */
 
+/* globals XPCOMUtils, Deprecated, aboutNewTabService*/
+/* exported NewTabURL */
+
 "use strict";
 
-var Cc = Components.classes;
-var Ci = Components.interfaces;
-var Cu = Components.utils;
+const {utils: Cu} = Components;
 
-this.EXPORTED_SYMBOLS = [ "NewTabURL" ];
+this.EXPORTED_SYMBOLS = ["NewTabURL"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
                                    "@mozilla.org/browser/aboutnewtab-service;1",
                                    "nsIAboutNewTabService");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 
deleted file mode 100644
--- a/browser/components/newtab/RemoteAboutNewTab.jsm
+++ /dev/null
@@ -1,301 +0,0 @@
-/* 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/. */
-/* globals Services, XPCOMUtils, RemotePages, RemoteNewTabLocation, RemoteNewTabUtils, Task  */
-/* globals BackgroundPageThumbs, PageThumbs, DirectoryLinksProvider, PlacesProvider, NewTabPrefsProvider */
-/* exported RemoteAboutNewTab */
-
-"use strict";
-
-let Ci = Components.interfaces;
-let Cu = Components.utils;
-const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
-
-this.EXPORTED_SYMBOLS = ["RemoteAboutNewTab"];
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.importGlobalProperties(["URL"]);
-
-XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
-  "resource://gre/modules/RemotePageManager.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
-  "resource:///modules/RemoteNewTabUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "BackgroundPageThumbs",
-  "resource://gre/modules/BackgroundPageThumbs.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
-  "resource://gre/modules/PageThumbs.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
-  "resource:///modules/DirectoryLinksProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation",
-  "resource:///modules/RemoteNewTabLocation.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
-  "resource:///modules/PlacesProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-  "resource:///modules/NewTabPrefsProvider.jsm");
-
-let RemoteAboutNewTab = {
-
-  pageListener: null,
-
-  /**
-   * Initialize the RemotePageManager and add all message listeners for this page
-   */
-  init: function() {
-    RemoteNewTabLocation.init();
-    this.pageListener = new RemotePages("about:remote-newtab");
-    this.pageListener.addMessageListener("NewTab:InitializeGrid", this.initializeGrid.bind(this));
-    this.pageListener.addMessageListener("NewTab:UpdateGrid", this.updateGrid.bind(this));
-    this.pageListener.addMessageListener("NewTab:Customize", this.customize.bind(this));
-    this.pageListener.addMessageListener("NewTab:CaptureBackgroundPageThumbs",
-        this.captureBackgroundPageThumb.bind(this));
-    this.pageListener.addMessageListener("NewTab:PageThumbs", this.createPageThumb.bind(this));
-    this.pageListener.addMessageListener("NewTabFrame:GetInit", this.initContentFrame.bind(this));
-
-    this._addObservers();
-  },
-
-  customize: function(message) {
-    if (message.data.enabled !== undefined) {
-      Services.prefs.setBoolPref("browser.newtabpage.enabled", message.data.enabled);
-    }
-    if (message.data.enhanced !== undefined) {
-      Services.prefs.setBoolPref("browser.newtabpage.enhanced", message.data.enhanced);
-    }
-  },
-
-  /**
-   * Notifies when history is cleared
-   */
-  placesClearHistory: function() {
-    this.pageListener.sendAsyncMessage("NewTab:PlacesClearHistory");
-  },
-
-  /**
-   * Notifies when a link has changed
-   */
-  placesLinkChanged: function(name, data) { // jshint ignore:line
-    this.pageListener.sendAsyncMessage("NewTab:PlacesLinkChanged", data);
-  },
-
-  /**
-   * Notifies when many links have changed
-   */
-  placesManyLinksChanged: function() {
-    this.pageListener.sendAsyncMessage("NewTab:PlacesManyLinksChanged");
-  },
-
-  /**
-   * Notifies when one URL has been deleted
-   */
-  placesDeleteURI: function(name, data) { // jshint ignore:line
-    this.pageListener.sendAsyncMessage("NewTab:PlacesDeleteURI", data.url);
-  },
-
-  /**
-   * Initializes the grid for the first time when the page loads.
-   * Fetch all the links and send them down to the child to populate
-   * the grid with.
-   *
-   * @param {Object} message
-   *        A RemotePageManager message.
-   */
-  initializeGrid: function(message) {
-    RemoteNewTabUtils.links.populateCache(() => {
-      message.target.sendAsyncMessage("NewTab:InitializeLinks", {
-        links: RemoteNewTabUtils.links.getLinks(),
-        enhancedLinks: this.getEnhancedLinks(),
-      });
-    });
-  },
-
-  /**
-   * Inits the content iframe with the newtab location
-   */
-  initContentFrame: function(message) {
-    message.target.sendAsyncMessage("NewTabFrame:Init", {
-      href: RemoteNewTabLocation.href,
-      origin: RemoteNewTabLocation.origin
-    });
-  },
-
-  /**
-   * Updates the grid by getting a new set of links.
-   *
-   * @param {Object} message
-   *        A RemotePageManager message.
-   */
-  updateGrid: function(message) {
-    message.target.sendAsyncMessage("NewTab:UpdateLinks", {
-      links: RemoteNewTabUtils.links.getLinks(),
-      enhancedLinks: this.getEnhancedLinks(),
-    });
-  },
-
-  /**
-   * Captures the site's thumbnail in the background, then attemps to show the thumbnail.
-   *
-   * @param {Object} message
-   *        A RemotePageManager message with the following data:
-   *
-   *        link (Object):
-   *          A link object that contains:
-   *
-   *          baseDomain (String)
-   *          blockState (Boolean)
-   *          frecency (Integer)
-   *          lastVisiteDate (Integer)
-   *          pinState (Boolean)
-   *          title (String)
-   *          type (String)
-   *          url (String)
-   */
-  captureBackgroundPageThumb: Task.async(function* (message) {
-    try {
-      yield BackgroundPageThumbs.captureIfMissing(message.data.link.url);
-      this.createPageThumb(message);
-    } catch (err) {
-      Cu.reportError("error: " + err);
-    }
-  }),
-
-  /**
-   * Creates the thumbnail to display for each site based on the unique URL
-   * of the site and it's type (regular or enhanced). If the thumbnail is of
-   * type "regular", we create a blob and send that down to the child. If the
-   * thumbnail is of type "enhanced", get the file path for the URL and create
-   * and enhanced URI that will be sent down to the child.
-   *
-   * @param {Object} message
-   *        A RemotePageManager message with the following data:
-   *
-   *        link (Object):
-   *          A link object that contains:
-   *
-   *          baseDomain (String)
-   *          blockState (Boolean)
-   *          frecency (Integer)
-   *          lastVisiteDate (Integer)
-   *          pinState (Boolean)
-   *          title (String)
-   *          type (String)
-   *          url (String)
-   */
-  createPageThumb: function(message) {
-    let imgSrc = PageThumbs.getThumbnailURL(message.data.link.url);
-    let doc = Services.appShell.hiddenDOMWindow.document;
-    let img = doc.createElementNS(XHTML_NAMESPACE, "img");
-    let canvas = doc.createElementNS(XHTML_NAMESPACE, "canvas");
-    let enhanced = Services.prefs.getBoolPref("browser.newtabpage.enhanced");
-
-    img.onload = function(e) { // jshint ignore:line
-      canvas.width = img.naturalWidth;
-      canvas.height = img.naturalHeight;
-      var ctx = canvas.getContext("2d");
-      ctx.drawImage(this, 0, 0, this.naturalWidth, this.naturalHeight);
-      canvas.toBlob(function(blob) {
-        let host = new URL(message.data.link.url).host;
-        RemoteAboutNewTab.pageListener.sendAsyncMessage("NewTab:RegularThumbnailURI", {
-          thumbPath: "/pagethumbs/" + host,
-          enhanced,
-          url: message.data.link.url,
-          blob,
-        });
-      });
-    };
-    img.src = imgSrc;
-  },
-
-  /**
-   * Get the set of enhanced links (if any) from the Directory Links Provider.
-   */
-  getEnhancedLinks: function() {
-    let enhancedLinks = [];
-    for (let link of RemoteNewTabUtils.links.getLinks()) {
-      if (link) {
-        enhancedLinks.push(DirectoryLinksProvider.getEnhancedLink(link));
-      }
-    }
-    return enhancedLinks;
-  },
-
-  /**
-   * Listens for a preference change or session purge for all pages and sends
-   * a message to update the pages that are open. If a session purge occured,
-   * also clear the links cache and update the set of links to display, as they
-   * may have changed, then proceed with the page update.
-   */
-  observe: function(aSubject, aTopic, aData) { // jshint ignore:line
-    let extraData;
-    if (aTopic === "browser:purge-session-history") {
-      RemoteNewTabUtils.links.resetCache();
-      RemoteNewTabUtils.links.populateCache(() => {
-        this.pageListener.sendAsyncMessage("NewTab:UpdateLinks", {
-          links: RemoteNewTabUtils.links.getLinks(),
-          enhancedLinks: this.getEnhancedLinks(),
-        });
-      });
-    }
-
-    if (extraData !== undefined || aTopic === "page-thumbnail:create") {
-      if (aTopic !== "page-thumbnail:create") {
-        // Change the topic for enhanced and enabled observers.
-        aTopic = aData;
-      }
-      this.pageListener.sendAsyncMessage("NewTab:Observe", {topic: aTopic, data: extraData});
-    }
-  },
-
-  setEnabled: function(name, data) { // jshint ignore:line
-    this.pageListener.sendAsyncMessage("NewTab:setEnabled", data);
-  },
-
-  setEnhanced: function(name, data) { // jshint ignore:line
-    this.pageListener.sendAsyncMessage("NewTab:setEnhanced", data);
-  },
-
-  setPinned: function(name, data) { // jshint ignore:line
-    this.pageListener.sendAsyncMessage("NewTab:setPinnedLinks", data);
-  },
-
-  /**
-   * Add all observers that about:newtab page must listen for.
-   */
-  _addObservers: function() {
-    Services.obs.addObserver(this, "page-thumbnail:create", true);
-    Services.obs.addObserver(this, "browser:purge-session-history", true);
-    PlacesProvider.links.on("deleteURI", this.placesDeleteURI.bind(this));
-    PlacesProvider.links.on("clearHistory", this.placesClearHistory.bind(this));
-    PlacesProvider.links.on("linkChanged", this.placesLinkChanged.bind(this));
-    PlacesProvider.links.on("manyLinksChanged", this.placesManyLinksChanged.bind(this));
-    NewTabPrefsProvider.prefs.on("browser.newtabpage.enabled", this.setEnabled.bind(this));
-    NewTabPrefsProvider.prefs.on("browser.newtabpage.enhanced", this.setEnhanced.bind(this));
-    NewTabPrefsProvider.prefs.on("browser.newtabpage.pinned", this.setPinned.bind(this));
-  },
-
-  /**
-   * Remove all observers on the page.
-   */
-  _removeObservers: function() {
-    Services.obs.removeObserver(this, "page-thumbnail:create");
-    Services.obs.removeObserver(this, "browser:purge-session-history");
-    PlacesProvider.links.off("deleteURI", this.placesDeleteURI);
-    PlacesProvider.links.off("clearHistory", this.placesClearHistory);
-    PlacesProvider.links.off("linkChanged", this.placesLinkChanged);
-    PlacesProvider.links.off("manyLinksChanged", this.placesManyLinksChanged);
-    NewTabPrefsProvider.prefs.off("browser.newtabpage.enabled", this.setEnabled.bind(this));
-    NewTabPrefsProvider.prefs.off("browser.newtabpage.enhanced", this.setEnhanced.bind(this));
-    NewTabPrefsProvider.prefs.off("browser.newtabpage.pinned", this.setPinned.bind(this));
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference]),
-
-  uninit: function() {
-    RemoteNewTabLocation.uninit();
-    this._removeObservers();
-    this.pageListener.destroy();
-    this.pageListener = null;
-  },
-};
deleted file mode 100644
--- a/browser/components/newtab/RemoteNewTabLocation.jsm
+++ /dev/null
@@ -1,141 +0,0 @@
-/* globals Services, UpdateUtils, XPCOMUtils, URL, NewTabPrefsProvider, Locale */
-/* exported RemoteNewTabLocation */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["RemoteNewTabLocation"];
-
-const {utils: Cu} = Components;
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.importGlobalProperties(["URL"]);
-
-XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
-  "resource://gre/modules/UpdateUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-  "resource:///modules/NewTabPrefsProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Locale",
-  "resource://gre/modules/Locale.jsm");
-
-// The preference that tells whether to match the OS locale
-const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
-
-// The preference that tells what locale the user selected
-const PREF_SELECTED_LOCALE = "general.useragent.locale";
-
-const DEFAULT_PAGE_LOCATION = "https://newtab.cdn.mozilla.net/" +
-                              "v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
-
-const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);
-
-const NEWTAB_VERSION = "0";
-
-let RemoteNewTabLocation = {
-  /*
-   * Generate a default url based on locale and update channel
-   */
-  _generateDefaultURL() {
-    let releaseName = this._releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
-    let uri = DEFAULT_PAGE_LOCATION
-      .replace("%VERSION%", this.version)
-      .replace("%LOCALE%", Locale.getLocale())
-      .replace("%CHANNEL%", releaseName);
-    return new URL(uri);
-  },
-
-  _url: null,
-  _overridden: false,
-
-  get href() {
-    return this._url.href;
-  },
-
-  get origin() {
-    return this._url.origin;
-  },
-
-  get overridden() {
-    return this._overridden;
-  },
-
-  get version() {
-    return NEWTAB_VERSION;
-  },
-
-  get channels() {
-    return VALID_CHANNELS;
-  },
-
-  /**
-   * Returns the release name from an Update Channel name
-   *
-   * @return {String} a release name based on the update channel. Defaults to nightly
-   */
-  _releaseFromUpdateChannel(channel) {
-    let result = "nightly";
-    if (VALID_CHANNELS.has(channel)) {
-      result = channel;
-    }
-    return result;
-  },
-
-  /*
-   * Updates the location when the page is not overriden.
-   * Useful when there is a pref change
-   */
-  _updateMaybe() {
-    if (!this.overridden) {
-      let url = this._generateDefaultURL();
-      if (url.href !== this._url.href) {
-        this._url = url;
-        Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
-          this._url.href);
-      }
-    }
-  },
-
-  /*
-   * Override the Remote newtab page location.
-   */
-  override(newURL) {
-    let url = new URL(newURL);
-    if (url.href !== this._url.href) {
-      this._overridden = true;
-      this._url = url;
-      Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
-                                   this._url.href);
-    }
-  },
-
-  /*
-   * Reset the newtab page location to the default value
-   */
-  reset() {
-    let url = this._generateDefaultURL();
-    if (url.href !== this._url.href) {
-      this._url = url;
-      this._overridden = false;
-      Services.obs.notifyObservers(null, "remote-new-tab-location-changed",
-        this._url.href);
-    }
-  },
-
-  init() {
-    NewTabPrefsProvider.prefs.on(
-      PREF_SELECTED_LOCALE,
-      this._updateMaybe.bind(this));
-
-    NewTabPrefsProvider.prefs.on(
-      PREF_MATCH_OS_LOCALE,
-      this._updateMaybe.bind(this));
-
-    this._url = this._generateDefaultURL();
-  },
-
-  uninit() {
-    this._url = null;
-    this._overridden = false;
-    NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateMaybe);
-    NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateMaybe);
-  }
-};
deleted file mode 100644
--- a/browser/components/newtab/RemoteNewTabUtils.jsm
+++ /dev/null
@@ -1,766 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["RemoteNewTabUtils"];
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
-  "resource://gre/modules/PlacesUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
-  "resource://gre/modules/PageThumbs.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
-  "resource://gre/modules/BinarySearch.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "gPrincipal", function () {
-  let uri = Services.io.newURI("about:newtab", null, null);
-  return Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
-});
-
-// The maximum number of results PlacesProvider retrieves from history.
-const HISTORY_RESULTS_LIMIT = 100;
-
-// The maximum number of links Links.getLinks will return.
-const LINKS_GET_LINKS_LIMIT = 100;
-
-/**
- * Singleton that serves as the default link provider for the grid. It queries
- * the history to retrieve the most frequently visited sites.
- */
-let PlacesProvider = {
-  /**
-   * A count of how many batch updates are under way (batches may be nested, so
-   * we keep a counter instead of a simple bool).
-   **/
-  _batchProcessingDepth: 0,
-
-  /**
-   * A flag that tracks whether onFrecencyChanged was notified while a batch
-   * operation was in progress, to tell us whether to take special action after
-   * the batch operation completes.
-   **/
-  _batchCalledFrecencyChanged: false,
-
-  /**
-   * Set this to change the maximum number of links the provider will provide.
-   */
-  maxNumLinks: HISTORY_RESULTS_LIMIT,
-
-  /**
-   * Must be called before the provider is used.
-   */
-  init: function PlacesProvider_init() {
-    PlacesUtils.history.addObserver(this, true);
-  },
-
-  /**
-   * Gets the current set of links delivered by this provider.
-   * @param aCallback The function that the array of links is passed to.
-   */
-  getLinks: function PlacesProvider_getLinks(aCallback) {
-    let options = PlacesUtils.history.getNewQueryOptions();
-    options.maxResults = this.maxNumLinks;
-
-    // Sort by frecency, descending.
-    options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING
-
-    let links = [];
-
-    let callback = {
-      handleResult: function (aResultSet) {
-        let row;
-
-        while ((row = aResultSet.getNextRow())) {
-          let url = row.getResultByIndex(1);
-          if (LinkChecker.checkLoadURI(url)) {
-            let title = row.getResultByIndex(2);
-            let frecency = row.getResultByIndex(12);
-            let lastVisitDate = row.getResultByIndex(5);
-            links.push({
-              url: url,
-              title: title,
-              frecency: frecency,
-              lastVisitDate: lastVisitDate,
-              type: "history",
-            });
-          }
-        }
-      },
-
-      handleError: function (aError) {
-        // Should we somehow handle this error?
-        aCallback([]);
-      },
-
-      handleCompletion: function (aReason) {
-        // The Places query breaks ties in frecency by place ID descending, but
-        // that's different from how Links.compareLinks breaks ties, because
-        // compareLinks doesn't have access to place IDs.  It's very important
-        // that the initial list of links is sorted in the same order imposed by
-        // compareLinks, because Links uses compareLinks to perform binary
-        // searches on the list.  So, ensure the list is so ordered.
-        let i = 1;
-        let outOfOrder = [];
-        while (i < links.length) {
-          if (Links.compareLinks(links[i - 1], links[i]) > 0)
-            outOfOrder.push(links.splice(i, 1)[0]);
-          else
-            i++;
-        }
-        for (let link of outOfOrder) {
-          i = BinarySearch.insertionIndexOf(Links.compareLinks, links, link);
-          links.splice(i, 0, link);
-        }
-
-        aCallback(links);
-      }
-    };
-
-    // Execute the query.
-    let query = PlacesUtils.history.getNewQuery();
-    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase);
-    db.asyncExecuteLegacyQueries([query], 1, options, callback);
-  },
-
-  /**
-   * Registers an object that will be notified when the provider's links change.
-   * @param aObserver An object with the following optional properties:
-   *        * onLinkChanged: A function that's called when a single link
-   *          changes.  It's passed the provider and the link object.  Only the
-   *          link's `url` property is guaranteed to be present.  If its `title`
-   *          property is present, then its title has changed, and the
-   *          property's value is the new title.  If any sort properties are
-   *          present, then its position within the provider's list of links may
-   *          have changed, and the properties' values are the new sort-related
-   *          values.  Note that this link may not necessarily have been present
-   *          in the lists returned from any previous calls to getLinks.
-   *        * onManyLinksChanged: A function that's called when many links
-   *          change at once.  It's passed the provider.  You should call
-   *          getLinks to get the provider's new list of links.
-   */
-  addObserver: function PlacesProvider_addObserver(aObserver) {
-    this._observers.push(aObserver);
-  },
-
-  _observers: [],
-
-  /**
-   * Called by the history service.
-   */
-  onBeginUpdateBatch: function() {
-    this._batchProcessingDepth += 1;
-  },
-
-  onEndUpdateBatch: function() {
-    this._batchProcessingDepth -= 1;
-    if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
-      this.onManyFrecenciesChanged();
-      this._batchCalledFrecencyChanged = false;
-    }
-  },
-
-  onDeleteURI: function PlacesProvider_onDeleteURI(aURI, aGUID, aReason) {
-    // let observers remove sensetive data associated with deleted visit
-    this._callObservers("onDeleteURI", {
-      url: aURI.spec,
-    });
-  },
-
-  onClearHistory: function() {
-    this._callObservers("onClearHistory")
-  },
-
-  /**
-   * Called by the history service.
-   */
-  onFrecencyChanged: function PlacesProvider_onFrecencyChanged(aURI, aNewFrecency, aGUID, aHidden, aLastVisitDate) {
-    // If something is doing a batch update of history entries we don't want
-    // to do lots of work for each record. So we just track the fact we need
-    // to call onManyFrecenciesChanged() once the batch is complete.
-    if (this._batchProcessingDepth > 0) {
-      this._batchCalledFrecencyChanged = true;
-      return;
-    }
-    // The implementation of the query in getLinks excludes hidden and
-    // unvisited pages, so it's important to exclude them here, too.
-    if (!aHidden && aLastVisitDate) {
-      this._callObservers("onLinkChanged", {
-        url: aURI.spec,
-        frecency: aNewFrecency,
-        lastVisitDate: aLastVisitDate,
-        type: "history",
-      });
-    }
-  },
-
-  /**
-   * Called by the history service.
-   */
-  onManyFrecenciesChanged: function PlacesProvider_onManyFrecenciesChanged() {
-    this._callObservers("onManyLinksChanged");
-  },
-
-  /**
-   * Called by the history service.
-   */
-  onTitleChanged: function PlacesProvider_onTitleChanged(aURI, aNewTitle, aGUID) {
-    this._callObservers("onLinkChanged", {
-      url: aURI.spec,
-      title: aNewTitle
-    });
-  },
-
-  _callObservers: function PlacesProvider__callObservers(aMethodName, aArg) {
-    for (let obs of this._observers) {
-      if (obs[aMethodName]) {
-        try {
-          obs[aMethodName](this, aArg);
-        } catch (err) {
-          Cu.reportError(err);
-        }
-      }
-    }
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
-                                         Ci.nsISupportsWeakReference]),
-};
-
-/**
- * Singleton that provides access to all links contained in the grid (including
- * the ones that don't fit on the grid). A link is a plain object that looks
- * like this:
- *
- * {
- *   url: "http://www.mozilla.org/",
- *   title: "Mozilla",
- *   frecency: 1337,
- *   lastVisitDate: 1394678824766431,
- * }
- */
-let Links = {
-  /**
-   * The maximum number of links returned by getLinks.
-   */
-  maxNumLinks: LINKS_GET_LINKS_LIMIT,
-
-  /**
-   * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
-   * sortedLinks is the cached, sorted array of links for the provider.
-   * siteMap is a mapping from base domains to URL count associated with the domain.
-   *         siteMap is used to look up a user's top sites that can be targeted
-   *         with a suggested tile.
-   * linkMap is a Map from link URLs to link objects.
-   */
-  _providers: new Map(),
-
-  /**
-   * The properties of link objects used to sort them.
-   */
-  _sortProperties: [
-    "frecency",
-    "lastVisitDate",
-    "url",
-  ],
-
-  /**
-   * List of callbacks waiting for the cache to be populated.
-   */
-  _populateCallbacks: [],
-
-  /**
-   * A list of objects that are observing links updates.
-   */
-  _observers: [],
-
-  /**
-   * Registers an object that will be notified when links updates.
-   */
-  addObserver: function (aObserver) {
-    this._observers.push(aObserver);
-  },
-
-  /**
-   * Adds a link provider.
-   * @param aProvider The link provider.
-   */
-  addProvider: function Links_addProvider(aProvider) {
-    this._providers.set(aProvider, null);
-    aProvider.addObserver(this);
-  },
-
-  /**
-   * Removes a link provider.
-   * @param aProvider The link provider.
-   */
-  removeProvider: function Links_removeProvider(aProvider) {
-    if (!this._providers.delete(aProvider))
-      throw new Error("Unknown provider");
-  },
-
-  /**
-   * Populates the cache with fresh links from the providers.
-   * @param aCallback The callback to call when finished (optional).
-   * @param aForce When true, populates the cache even when it's already filled.
-   */
-  populateCache: function Links_populateCache(aCallback, aForce) {
-    let callbacks = this._populateCallbacks;
-
-    // Enqueue the current callback.
-    callbacks.push(aCallback);
-
-    // There was a callback waiting already, thus the cache has not yet been
-    // populated.
-    if (callbacks.length > 1)
-      return;
-
-    function executeCallbacks() {
-      while (callbacks.length) {
-        let callback = callbacks.shift();
-        if (callback) {
-          try {
-            callback();
-          } catch (e) {
-            // We want to proceed even if a callback fails.
-          }
-        }
-      }
-    }
-
-    let numProvidersRemaining = this._providers.size;
-    for (let [provider, links] of this._providers) {
-      this._populateProviderCache(provider, () => {
-        if (--numProvidersRemaining == 0)
-          executeCallbacks();
-      }, aForce);
-    }
-  },
-
-  /**
-   * Gets the current set of links contained in the grid.
-   * @return The links in the grid.
-   */
-  getLinks: function Links_getLinks() {
-    let links = this._getMergedProviderLinks();
-
-    let sites = new Set();
-
-    // Filter duplicate base domains.
-    links = links.filter(function (link) {
-      let site = RemoteNewTabUtils.extractSite(link.url);
-      link.baseDomain = site;
-      if (site == null || sites.has(site))
-        return false;
-      sites.add(site);
-
-      return true;
-    });
-
-    return links;
-  },
-
-  /**
-   * Resets the links cache.
-   */
-  resetCache: function Links_resetCache() {
-    for (let provider of this._providers.keys()) {
-      this._providers.set(provider, null);
-    }
-  },
-
-  /**
-   * Compares two links.
-   * @param aLink1 The first link.
-   * @param aLink2 The second link.
-   * @return A negative number if aLink1 is ordered before aLink2, zero if
-   *         aLink1 and aLink2 have the same ordering, or a positive number if
-   *         aLink1 is ordered after aLink2.
-   *
-   * @note compareLinks's this object is bound to Links below.
-   */
-  compareLinks: function Links_compareLinks(aLink1, aLink2) {
-    for (let prop of this._sortProperties) {
-      if (!(prop in aLink1) || !(prop in aLink2))
-        throw new Error("Comparable link missing required property: " + prop);
-    }
-    return aLink2.frecency - aLink1.frecency ||
-           aLink2.lastVisitDate - aLink1.lastVisitDate ||
-           aLink1.url.localeCompare(aLink2.url);
-  },
-
-  _incrementSiteMap: function(map, link) {
-    let site = RemoteNewTabUtils.extractSite(link.url);
-    map.set(site, (map.get(site) || 0) + 1);
-  },
-
-  _decrementSiteMap: function(map, link) {
-    let site = RemoteNewTabUtils.extractSite(link.url);
-    let previousURLCount = map.get(site);
-    if (previousURLCount === 1) {
-      map.delete(site);
-    } else {
-      map.set(site, previousURLCount - 1);
-    }
-  },
-
-  /**
-    * Update the siteMap cache based on the link given and whether we need
-    * to increment or decrement it. We do this by iterating over all stored providers
-    * to find which provider this link already exists in. For providers that
-    * have this link, we will adjust siteMap for them accordingly.
-    *
-    * @param aLink The link that will affect siteMap
-    * @param increment A boolean for whether to increment or decrement siteMap
-    */
-  _adjustSiteMapAndNotify: function(aLink, increment=true) {
-    for (let [provider, cache] of this._providers) {
-      // We only update siteMap if aLink is already stored in linkMap.
-      if (cache.linkMap.get(aLink.url)) {
-        if (increment) {
-          this._incrementSiteMap(cache.siteMap, aLink);
-          continue;
-        }
-        this._decrementSiteMap(cache.siteMap, aLink);
-      }
-    }
-    this._callObservers("onLinkChanged", aLink);
-  },
-
-  populateProviderCache: function(provider, callback) {
-    if (!this._providers.has(provider)) {
-      throw new Error("Can only populate provider cache for existing provider.");
-    }
-
-    return this._populateProviderCache(provider, callback, false);
-  },
-
-  /**
-   * Calls getLinks on the given provider and populates our cache for it.
-   * @param aProvider The provider whose cache will be populated.
-   * @param aCallback The callback to call when finished.
-   * @param aForce When true, populates the provider's cache even when it's
-   *               already filled.
-   */
-  _populateProviderCache: function (aProvider, aCallback, aForce) {
-    let cache = this._providers.get(aProvider);
-    let createCache = !cache;
-    if (createCache) {
-      cache = {
-        // Start with a resolved promise.
-        populatePromise: new Promise(resolve => resolve()),
-      };
-      this._providers.set(aProvider, cache);
-    }
-    // Chain the populatePromise so that calls are effectively queued.
-    cache.populatePromise = cache.populatePromise.then(() => {
-      return new Promise(resolve => {
-        if (!createCache && !aForce) {
-          aCallback();
-          resolve();
-          return;
-        }
-        aProvider.getLinks(links => {
-          // Filter out null and undefined links so we don't have to deal with
-          // them in getLinks when merging links from providers.
-          links = links.filter((link) => !!link);
-          cache.sortedLinks = links;
-          cache.siteMap = links.reduce((map, link) => {
-            this._incrementSiteMap(map, link);
-            return map;
-          }, new Map());
-          cache.linkMap = links.reduce((map, link) => {
-            map.set(link.url, link);
-            return map;
-          }, new Map());
-          aCallback();
-          resolve();
-        });
-      });
-    });
-  },
-
-  /**
-   * Merges the cached lists of links from all providers whose lists are cached.
-   * @return The merged list.
-   */
-  _getMergedProviderLinks: function Links__getMergedProviderLinks() {
-    // Build a list containing a copy of each provider's sortedLinks list.
-    let linkLists = [];
-    for (let provider of this._providers.keys()) {
-      let links = this._providers.get(provider);
-      if (links && links.sortedLinks) {
-        linkLists.push(links.sortedLinks.slice());
-      }
-    }
-
-    function getNextLink() {
-      let minLinks = null;
-      for (let links of linkLists) {
-        if (links.length &&
-            (!minLinks || Links.compareLinks(links[0], minLinks[0]) < 0))
-          minLinks = links;
-      }
-      return minLinks ? minLinks.shift() : null;
-    }
-
-    let finalLinks = [];
-    for (let nextLink = getNextLink();
-         nextLink && finalLinks.length < this.maxNumLinks;
-         nextLink = getNextLink()) {
-      finalLinks.push(nextLink);
-    }
-
-    return finalLinks;
-  },
-
-  /**
-   * Called by a provider to notify us when a single link changes.
-   * @param aProvider The provider whose link changed.
-   * @param aLink The link that changed.  If the link is new, it must have all
-   *              of the _sortProperties.  Otherwise, it may have as few or as
-   *              many as is convenient.
-   * @param aIndex The current index of the changed link in the sortedLinks
-                   cache in _providers. Defaults to -1 if the provider doesn't know the index
-   * @param aDeleted Boolean indicating if the provider has deleted the link.
-   */
-  onLinkChanged: function Links_onLinkChanged(aProvider, aLink, aIndex=-1, aDeleted=false) {
-    if (!("url" in aLink))
-      throw new Error("Changed links must have a url property");
-
-    let links = this._providers.get(aProvider);
-    if (!links)
-      // This is not an error, it just means that between the time the provider
-      // was added and the future time we call getLinks on it, it notified us of
-      // a change.
-      return;
-
-    let { sortedLinks, siteMap, linkMap } = links;
-    let existingLink = linkMap.get(aLink.url);
-    let insertionLink = null;
-
-    if (existingLink) {
-      // Update our copy's position in O(lg n) by first removing it from its
-      // list.  It's important to do this before modifying its properties.
-      if (this._sortProperties.some(prop => prop in aLink)) {
-        let idx = aIndex;
-        if (idx < 0) {
-          idx = this._indexOf(sortedLinks, existingLink);
-        } else if (this.compareLinks(aLink, sortedLinks[idx]) != 0) {
-          throw new Error("aLink should be the same as sortedLinks[idx]");
-        }
-
-        if (idx < 0) {
-          throw new Error("Link should be in _sortedLinks if in _linkMap");
-        }
-        sortedLinks.splice(idx, 1);
-
-        if (aDeleted) {
-          linkMap.delete(existingLink.url);
-          this._decrementSiteMap(siteMap, existingLink);
-        } else {
-          // Update our copy's properties.
-          Object.assign(existingLink, aLink);
-
-          // Finally, reinsert our copy below.
-          insertionLink = existingLink;
-        }
-      }
-      // Update our copy's title in O(1).
-      if ("title" in aLink && aLink.title != existingLink.title) {
-        existingLink.title = aLink.title;
-      }
-    }
-    else if (this._sortProperties.every(prop => prop in aLink)) {
-      // Before doing the O(lg n) insertion below, do an O(1) check for the
-      // common case where the new link is too low-ranked to be in the list.
-      if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
-        let lastLink = sortedLinks[sortedLinks.length - 1];
-        if (this.compareLinks(lastLink, aLink) < 0) {
-          return;
-        }
-      }
-      // Copy the link object so that changes later made to it by the caller
-      // don't affect our copy.
-      insertionLink = {};
-      for (let prop in aLink) {
-        insertionLink[prop] = aLink[prop];
-      }
-      linkMap.set(aLink.url, insertionLink);
-      this._incrementSiteMap(siteMap, aLink);
-    }
-
-    if (insertionLink) {
-      let idx = this._insertionIndexOf(sortedLinks, insertionLink);
-      sortedLinks.splice(idx, 0, insertionLink);
-      if (sortedLinks.length > aProvider.maxNumLinks) {
-        let lastLink = sortedLinks.pop();
-        linkMap.delete(lastLink.url);
-        this._decrementSiteMap(siteMap, lastLink);
-      }
-    }
-  },
-
-  /**
-   * Called by a provider to notify us when many links change.
-   */
-  onManyLinksChanged: function Links_onManyLinksChanged(aProvider) {
-    this._populateProviderCache(aProvider, () => {}, true);
-  },
-
-  _indexOf: function Links__indexOf(aArray, aLink) {
-    return this._binsearch(aArray, aLink, "indexOf");
-  },
-
-  _insertionIndexOf: function Links__insertionIndexOf(aArray, aLink) {
-    return this._binsearch(aArray, aLink, "insertionIndexOf");
-  },
-
-  _binsearch: function Links__binsearch(aArray, aLink, aMethod) {
-    return BinarySearch[aMethod](this.compareLinks, aArray, aLink);
-  },
-
-  _callObservers(methodName, ...args) {
-    for (let obs of this._observers) {
-      if (typeof(obs[methodName]) == "function") {
-        try {
-          obs[methodName](this, ...args);
-        } catch (err) {
-          Cu.reportError(err);
-        }
-      }
-    }
-  },
-};
-
-Links.compareLinks = Links.compareLinks.bind(Links);
-
-/**
- * Singleton that checks if a given link should be displayed on about:newtab
- * or if we should rather not do it for security reasons. URIs that inherit
- * their caller's principal will be filtered.
- */
-let LinkChecker = {
-  _cache: {},
-
-  get flags() {
-    return Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL |
-           Ci.nsIScriptSecurityManager.DONT_REPORT_ERRORS;
-  },
-
-  checkLoadURI: function LinkChecker_checkLoadURI(aURI) {
-    if (!(aURI in this._cache))
-      this._cache[aURI] = this._doCheckLoadURI(aURI);
-
-    return this._cache[aURI];
-  },
-
-  _doCheckLoadURI: function Links_doCheckLoadURI(aURI) {
-    try {
-      Services.scriptSecurityManager.
-        checkLoadURIStrWithPrincipal(gPrincipal, aURI, this.flags);
-      return true;
-    } catch (e) {
-      // We got a weird URI or one that would inherit the caller's principal.
-      return false;
-    }
-  }
-};
-
-let ExpirationFilter = {
-  init: function ExpirationFilter_init() {
-    PageThumbs.addExpirationFilter(this);
-  },
-
-  filterForThumbnailExpiration:
-  function ExpirationFilter_filterForThumbnailExpiration(aCallback) {
-    Links.populateCache(function () {
-      let urls = [];
-
-      // Add all URLs to the list that we want to keep thumbnails for.
-      for (let link of Links.getLinks().slice(0, 25)) {
-        if (link && link.url)
-          urls.push(link.url);
-      }
-
-      aCallback(urls);
-    });
-  }
-};
-
-/**
- * Singleton that provides the public API of this JSM.
- */
-this.RemoteNewTabUtils = {
-  _initialized: false,
-
-  /**
-   * Extract a "site" from a url in a way that multiple urls of a "site" returns
-   * the same "site."
-   * @param aUrl Url spec string
-   * @return The "site" string or null
-   */
-  extractSite: function Links_extractSite(url) {
-    let host;
-    try {
-      // Note that nsIURI.asciiHost throws NS_ERROR_FAILURE for some types of
-      // URIs, including jar and moz-icon URIs.
-      host = Services.io.newURI(url, null, null).asciiHost;
-    } catch (ex) {
-      return null;
-    }
-
-    // Strip off common subdomains of the same site (e.g., www, load balancer)
-    return host.replace(/^(m|mobile|www\d*)\./, "");
-  },
-
-  init: function RemoteNewTabUtils_init() {
-    if (this.initWithoutProviders()) {
-      PlacesProvider.init();
-      Links.addProvider(PlacesProvider);
-    }
-  },
-
-  initWithoutProviders: function RemoteNewTabUtils_initWithoutProviders() {
-    if (!this._initialized) {
-      this._initialized = true;
-      ExpirationFilter.init();
-      return true;
-    }
-    return false;
-  },
-
-  getProviderLinks: function(aProvider) {
-    let cache = Links._providers.get(aProvider);
-    if (cache && cache.sortedLinks) {
-      return cache.sortedLinks;
-    }
-    return [];
-  },
-
-  isTopSiteGivenProvider: function(aSite, aProvider) {
-    let cache = Links._providers.get(aProvider);
-    if (cache && cache.siteMap) {
-      return cache.siteMap.has(aSite);
-    }
-    return false;
-  },
-
-  isTopPlacesSite: function(aSite) {
-    return this.isTopSiteGivenProvider(aSite, PlacesProvider);
-  },
-
-  links: Links,
-  linkChecker: LinkChecker,
-  placesProvider: PlacesProvider
-};
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/aboutNewTabService.js
@@ -0,0 +1,190 @@
+/*
+ * 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/.
+*/
+
+/* globals XPCOMUtils, NewTabPrefsProvider, Services,
+  Locale, UpdateUtils
+*/
+"use strict";
+
+const {utils: Cu, interfaces: Ci} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+                                  "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                  "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Locale",
+                                  "resource://gre/modules/Locale.jsm");
+
+const LOCAL_NEWTAB_URL = "chrome://browser/content/newtab/newTab.xhtml";
+
+const REMOTE_NEWTAB_URL = "https://newtab.cdn.mozilla.net/" +
+                              "v%VERSION%/%CHANNEL%/%LOCALE%/index.html";
+
+// Pref that tells if remote newtab is enabled
+const PREF_REMOTE_ENABLED = "browser.newtabpage.remote";
+
+// The preference that tells whether to match the OS locale
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+
+// The preference that tells what locale the user selected
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const VALID_CHANNELS = new Set(["esr", "release", "beta", "aurora", "nightly"]);
+
+const REMOTE_NEWTAB_VERSION = "0";
+
+function AboutNewTabService() {
+  NewTabPrefsProvider.prefs.on(PREF_REMOTE_ENABLED, this._handleToggleEvent.bind(this));
+
+  // trigger remote change if needed, according to pref
+  this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED));
+}
+
+AboutNewTabService.prototype = {
+
+  _newTabURL: LOCAL_NEWTAB_URL,
+  _remoteEnabled: false,
+  _overridden: false,
+
+  classID: Components.ID("{cef25b06-0ef6-4c50-a243-e69f943ef23d}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
+  _xpcom_categories: [{
+    service: true
+  }],
+
+  _handleToggleEvent(prefName, stateEnabled, forceState) { //jshint unused:false
+    this.toggleRemote(stateEnabled, forceState);
+  },
+
+  /**
+   * React to changes to the remote newtab pref. Only act
+   * if there is a change of state and if not overridden.
+   *
+   * @returns {Boolean} Returns if there has been a state change
+   *
+   * @param {Boolean}   stateEnabled    remote state to set to
+   * @param {Boolean}   forceState      force state change
+   */
+  toggleRemote(stateEnabled, forceState) {
+
+    if (!forceState && (this._overriden || stateEnabled === this._remoteEnabled)) {
+      // exit there is no change of state
+      return false;
+    }
+
+    if (stateEnabled) {
+      this._newTabURL = this.generateRemoteURL();
+      NewTabPrefsProvider.prefs.on(
+        PREF_SELECTED_LOCALE,
+        this._updateRemoteMaybe.bind(this));
+      NewTabPrefsProvider.prefs.on(
+        PREF_MATCH_OS_LOCALE,
+        this._updateRemoteMaybe.bind(this));
+      this._remoteEnabled = true;
+    } else {
+      this._newTabURL = LOCAL_NEWTAB_URL;
+      NewTabPrefsProvider.prefs.off(PREF_SELECTED_LOCALE, this._updateRemoteMaybe);
+      NewTabPrefsProvider.prefs.off(PREF_MATCH_OS_LOCALE, this._updateRemoteMaybe);
+      this._remoteEnabled = false;
+    }
+    return true;
+  },
+
+  /*
+   * Generate a default url based on locale and update channel
+   */
+  generateRemoteURL() {
+    let releaseName = this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
+    let url = REMOTE_NEWTAB_URL
+      .replace("%VERSION%", REMOTE_NEWTAB_VERSION)
+      .replace("%LOCALE%", Locale.getLocale())
+      .replace("%CHANNEL%", releaseName);
+    return url;
+  },
+
+  /*
+   * Updates the remote location when the page is not overriden.
+   *
+   * Useful when there is a dependent pref change
+   */
+  _updateRemoteMaybe() {
+    if (!this._remoteEnabled || this._overridden) {
+      return;
+    }
+
+    let url = this.generateRemoteURL();
+    if (url !== this._newTabURL) {
+      this._newTabURL = url;
+      Services.obs.notifyObservers(null, "newtab-url-changed",
+        this._newTabURL);
+    }
+  },
+
+  /**
+   * Returns the release name from an Update Channel name
+   *
+   * @return {String} a release name based on the update channel. Defaults to nightly
+   */
+  releaseFromUpdateChannel(channelName) {
+    return VALID_CHANNELS.has(channelName) ? channelName : "nightly";
+  },
+
+  get newTabURL() {
+    return this._newTabURL;
+  },
+
+  get remoteVersion() {
+    return REMOTE_NEWTAB_VERSION;
+  },
+
+  get remoteReleaseName() {
+    return this.releaseFromUpdateChannel(UpdateUtils.UpdateChannel);
+  },
+
+  set newTabURL(aNewTabURL) {
+    if (aNewTabURL === "about:newtab") {
+      // avoid infinite redirects in case one sets the URL to about:newtab
+      this.resetNewTabURL();
+      return;
+    }
+    let remoteURL = this.generateRemoteURL();
+    let prefRemoteEnabled = Services.prefs.getBoolPref(PREF_REMOTE_ENABLED);
+    let isResetLocal = !prefRemoteEnabled && aNewTabURL === LOCAL_NEWTAB_URL;
+    let isResetRemote = prefRemoteEnabled && aNewTabURL === remoteURL;
+
+    if (isResetLocal || isResetRemote) {
+      if (this._overriden) {
+        // only trigger a reset if previously overridden
+        this.resetNewTabURL();
+      }
+      return;
+    }
+    // turn off remote state if needed
+    this.toggleRemote(false);
+    this._newTabURL = aNewTabURL;
+    this._overridden = true;
+    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+  },
+
+  get overridden() {
+    return this._overridden;
+  },
+
+  get remoteEnabled() {
+    return this._remoteEnabled;
+  },
+
+  resetNewTabURL() {
+    this._overridden = false;
+    this.toggleRemote(Services.prefs.getBoolPref(PREF_REMOTE_ENABLED), true);
+    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AboutNewTabService]);
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -1,21 +1,28 @@
 # -*- 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/.
 
-if not CONFIG['RELEASE_BUILD']:
-    BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
+BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
-    XPCSHELL_TESTS_MANIFESTS += [
-        'tests/xpcshell/xpcshell.ini',
-    ]
+XPCSHELL_TESTS_MANIFESTS += [
+    'tests/xpcshell/xpcshell.ini',
+]
 
-    EXTRA_JS_MODULES += [
-        'NewTabPrefsProvider.jsm',
-        'NewTabURL.jsm',
-        'PlacesProvider.jsm',
-        'RemoteAboutNewTab.jsm',
-        'RemoteNewTabLocation.jsm',
-        'RemoteNewTabUtils.jsm',
-    ]
+EXTRA_JS_MODULES += [
+    'NewTabPrefsProvider.jsm',
+    'NewTabURL.jsm',
+    'PlacesProvider.jsm'
+]
+
+XPIDL_SOURCES += [
+    'nsIAboutNewTabService.idl',
+]
+
+XPIDL_MODULE = 'browser-newtab'
+
+EXTRA_COMPONENTS += [
+    'aboutNewTabService.js',
+    'NewTabComponents.manifest',
+]
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/nsIAboutNewTabService.idl
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Allows to override about:newtab to point to a different location
+ * than the one specified within AboutRedirector.cpp
+ */
+
+[scriptable, uuid(cef25b06-0ef6-4c50-a243-e69f943ef23d)]
+interface nsIAboutNewTabService : nsISupports
+{
+  /**
+   * Returns the url of the resource for the newtab page if not overridden,
+   * otherwise a string represenation of the new URL.
+   */
+  attribute ACString newTabURL;
+
+  /**
+   * Returns true if the default resource got overridden.
+   */
+  readonly attribute bool overridden;
+
+  /**
+   * Returns true if the default resource is remotely hosted and isn't
+   * overridden
+   */
+  readonly attribute bool remoteEnabled;
+
+
+  /**
+  * Returns the version of the remote newtab page expected
+  */
+  readonly attribute ACString remoteVersion;
+
+  /**
+   * Returns the expected channel for the remote the newtab page
+   */
+  readonly attribute ACString remoteReleaseName;
+
+  /**
+   * Generates and returns the remote newtab page url
+   */
+  ACString generateRemoteURL();
+
+  /**
+   * Returns a remote new tab release name given an update channel name
+   */
+  ACString releaseFromUpdateChannel(in ACString channelName);
+
+  /**
+   * Resets to the default resource and also resets the
+   * overridden attribute to false.
+   */
+  void resetNewTabURL();
+};
--- a/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
+++ b/browser/components/newtab/tests/browser/browser_remotenewtab_pageloads.js
@@ -1,46 +1,57 @@
-/* globals XPCOMUtils, Task, RemoteAboutNewTab, RemoteNewTabLocation, ok */
+/* globals XPCOMUtils, aboutNewTabService, Services */
 "use strict";
 
 let Cu = Components.utils;
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabLocation",
-  "resource:///modules/RemoteNewTabLocation.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab",
-  "resource:///modules/RemoteAboutNewTab.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RemotePageManager",
+                                  "resource://gre/modules/RemotePageManager.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
 
 const TEST_URL = "https://example.com/browser/browser/components/newtab/tests/browser/dummy_page.html";
-const NEWTAB_URL = "about:remote-newtab";
-
-let tests = [];
-
-/*
- * Tests that:
- * 1. overriding the RemoteNewTabPageLocation url causes a remote newtab page
- *    to load with the new url.
- * 2. Messages pass between remote page <--> newTab.js <--> RemoteAboutNewTab.js
- */
-tests.push(Task.spawn(function* testMessage() {
-  yield new Promise(resolve => {
-    RemoteAboutNewTab.pageListener.addMessageListener("NewTab:testMessage", () => {
-      ok(true, "message received");
-      resolve();
-    });
-  });
-}));
 
 add_task(function* open_newtab() {
-  RemoteNewTabLocation.override(TEST_URL);
-  ok(RemoteNewTabLocation.href === TEST_URL, "RemoteNewTabLocation has been overridden");
+  let notificationPromise = nextChangeNotificationPromise(TEST_URL, "newtab page now points to test url");
+  aboutNewTabService.newTabURL = TEST_URL;
+
+  yield notificationPromise;
+  Assert.ok(aboutNewTabService.overridden, "url has been overridden");
+
   let tabOptions = {
     gBrowser,
-    url: NEWTAB_URL,
+    url: "about:newtab",
   };
 
-  for (let test of tests) {
-    yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) { // jshint ignore:line
-      yield test;
-    }); // jshint ignore:line
-  }
+  yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
+    Assert.equal(TEST_URL, browser.contentWindow.location, `New tab should open to ${TEST_URL}`);
+  });
 });
+
+add_task(function* emptyURL() {
+  let notificationPromise = nextChangeNotificationPromise("", "newtab service now points to empty url");
+  aboutNewTabService.newTabURL = "";
+  yield notificationPromise;
+
+  let tabOptions = {
+    gBrowser,
+    url: "about:newtab",
+  };
+
+  yield BrowserTestUtils.withNewTab(tabOptions, function* (browser) {
+    Assert.equal("about:blank", browser.contentWindow.location, `New tab should open to ${"about:blank"}`);
+  });
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {  // jshint unused:false
+      Services.obs.removeObserver(observer, aTopic);
+      Assert.equal(aData, aNewURL, testMessage);
+      resolve();
+    }, "newtab-url-changed", false);
+  });
+}
--- a/browser/components/newtab/tests/browser/dummy_page.html
+++ b/browser/components/newtab/tests/browser/dummy_page.html
@@ -1,22 +1,10 @@
 <!DOCTYPE html>
 
 <html>
 <head>
   <meta charset="utf-8">
 </head>
 <body>
 <p>Dummy Page</p>
-<script type="text/javascript;version=1.8">
-  document.addEventListener("NewTabCommandReady", function readyCmd() {
-    document.removeEventListener("NewTabCommandReady", readyCmd);
-
-    let event = new CustomEvent("NewTabCommand", {
-      detail: {
-        command: "NewTab:testMessage"
-      }
-    });
-    document.dispatchEvent(event);
-  });
-</script>
 </body>
 </html>
--- a/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
+++ b/browser/components/newtab/tests/xpcshell/test_AboutNewTabService.js
@@ -1,39 +1,151 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
+
+/* globals Services, XPCOMUtils, NewTabPrefsProvider, Preferences, aboutNewTabService */
+
 "use strict";
 
-Components.utils.import("resource://gre/modules/Services.jsm");
-let aboutNewTabService = Components.classes["@mozilla.org/browser/aboutnewtab-service;1"]
-                                   .getService(Components.interfaces.nsIAboutNewTabService);
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                  "resource:///modules/NewTabPrefsProvider.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
+
+const DEFAULT_HREF = aboutNewTabService.generateRemoteURL();
 
+/**
+ * Test the overriding of the default URL
+ */
 add_task(function* () {
-  Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Default newtab URL should be about:newtab");
+  NewTabPrefsProvider.prefs.init();
+  let notificationPromise;
+  Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+  let localChromeURL = "chrome://browser/content/newtab/newTab.xhtml";
+
+  // tests default is the local newtab resource
+  Assert.equal(aboutNewTabService.newTabURL, localChromeURL,
+               `Default newtab URL should be ${localChromeURL}`);
+
+  // test the newtab service does not go in a circular redirect
+  aboutNewTabService.newTabURL = "about:newtab";
+  Assert.equal(aboutNewTabService.newTabURL, localChromeURL,
+               "Newtab URL avoids a circular redirect by setting to the default URL");
+
   let url = "http://example.com/";
-  let notificationPromise = promiseNewtabURLNotification(url);
+  notificationPromise = nextChangeNotificationPromise(url);
   aboutNewTabService.newTabURL = url;
   yield notificationPromise;
   Assert.ok(aboutNewTabService.overridden, "Newtab URL should be overridden");
+  Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
   Assert.equal(aboutNewTabService.newTabURL, url, "Newtab URL should be the custom URL");
 
-  notificationPromise = promiseNewtabURLNotification("about:newtab");
+  notificationPromise = nextChangeNotificationPromise("chrome://browser/content/newtab/newTab.xhtml");
   aboutNewTabService.resetNewTabURL();
   yield notificationPromise;
   Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
-  Assert.equal(aboutNewTabService.newTabURL, "about:newtab", "Newtab URL should be the about:newtab");
+  Assert.equal(aboutNewTabService.newTabURL, "chrome://browser/content/newtab/newTab.xhtml",
+               "Newtab URL should be the default");
 
   // change newtab page to remote
   Services.prefs.setBoolPref("browser.newtabpage.remote", true);
-  Assert.equal(aboutNewTabService.newTabURL, "about:remote-newtab", "Newtab URL should be the about:remote-newtab");
+  let remoteHref = aboutNewTabService.generateRemoteURL();
+  Assert.equal(aboutNewTabService.newTabURL, remoteHref, "Newtab URL should be the default remote URL");
   Assert.ok(!aboutNewTabService.overridden, "Newtab URL should not be overridden");
+  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+  NewTabPrefsProvider.prefs.uninit();
 });
 
-function promiseNewtabURLNotification(aNewURL) {
+/**
+ * Tests reponse to updates to prefs
+ */
+add_task(function* test_updates() {
+  Preferences.set("browser.newtabpage.remote", true);
+  let notificationPromise;
+  let expectedHref = "https://newtab.cdn.mozilla.net" +
+                     `/v${aboutNewTabService.remoteVersion}` +
+                     `/${aboutNewTabService.remoteReleaseName}` +
+                     "/en-GB" +
+                     "/index.html";
+  Preferences.set("intl.locale.matchOS", true);
+  Preferences.set("general.useragent.locale", "en-GB");
+  NewTabPrefsProvider.prefs.init();
+
+  // test update checks for prefs
+  notificationPromise = nextChangeNotificationPromise(
+    expectedHref, "Remote href should be updated");
+  Preferences.set("intl.locale.matchOS", false);
+  yield notificationPromise;
+
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "Remote href changes back to default");
+  Preferences.set("general.useragent.locale", "en-US");
+
+  yield notificationPromise;
+
+  // test update fires on override and reset
+  let testURL = "https://example.com/";
+  notificationPromise = nextChangeNotificationPromise(
+    testURL, "a notification occurs on override");
+  aboutNewTabService.newTabURL = testURL;
+  yield notificationPromise;
+
+  // from overridden to default
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "a notification occurs on reset");
+  aboutNewTabService.resetNewTabURL();
+  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+  yield notificationPromise;
+
+  // override to default URL from default URL
+  notificationPromise = nextChangeNotificationPromise(
+    testURL, "a notification only occurs for a change in overridden urls");
+  aboutNewTabService.newTabURL = aboutNewTabService.generateRemoteURL();
+  Assert.ok(aboutNewTabService.remoteEnabled, "Newtab remote should be enabled");
+  aboutNewTabService.newTabURL = testURL;
+  Assert.ok(!aboutNewTabService.remoteEnabled, "Newtab remote should not be enabled");
+  yield notificationPromise;
+
+  // reset twice, only one notification for default URL
+  notificationPromise = nextChangeNotificationPromise(
+    DEFAULT_HREF, "reset occurs");
+  aboutNewTabService.resetNewTabURL();
+  yield notificationPromise;
+
+  NewTabPrefsProvider.prefs.uninit();
+});
+
+/**
+ * Verifies that releaseFromUpdateChannel
+ * Returns the correct release names
+ */
+add_task(function* test_release_names() {
+  let valid_channels = ["esr", "release", "beta", "aurora", "nightly"];
+  let invalid_channels = new Set(["default", "invalid"]);
+
+  for (let channel of valid_channels) {
+    Assert.equal(channel, aboutNewTabService.releaseFromUpdateChannel(channel),
+          "release == channel name when valid");
+  }
+
+  for (let channel of invalid_channels) {
+    Assert.equal("nightly", aboutNewTabService.releaseFromUpdateChannel(channel),
+          "release == nightly when invalid");
+  }
+});
+
+function nextChangeNotificationPromise(aNewURL, testMessage) {
   return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {  // jshint unused:false
       Services.obs.removeObserver(observer, aTopic);
-      Assert.equal(aData, aNewURL, "Data for newtab-url-changed notification should be new URL.");
+      Assert.equal(aData, aNewURL, testMessage);
       resolve();
     }, "newtab-url-changed", false);
   });
 }
--- a/browser/components/newtab/tests/xpcshell/test_NewTabURL.js
+++ b/browser/components/newtab/tests/xpcshell/test_NewTabURL.js
@@ -1,38 +1,53 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
+
+/* globals Services, NewTabURL, XPCOMUtils, aboutNewTabService */
 "use strict";
 
-Components.utils.import("resource:///modules/NewTabURL.jsm");
-Components.utils.import("resource://gre/modules/Services.jsm");
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/NewTabURL.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                  "resource:///modules/NewTabPrefsProvider.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
+                                   "@mozilla.org/browser/aboutnewtab-service;1",
+                                   "nsIAboutNewTabService");
 
 add_task(function* () {
-  Assert.equal(NewTabURL.get(), "about:newtab", "Default newtab URL should be about:newtab");
+  let defaultURL = aboutNewTabService.newTabURL;
+  Services.prefs.setBoolPref("browser.newtabpage.remote", false);
+
+  Assert.equal(NewTabURL.get(), defaultURL, `Default newtab URL should be ${defaultURL}`);
   let url = "http://example.com/";
   let notificationPromise = promiseNewtabURLNotification(url);
   NewTabURL.override(url);
   yield notificationPromise;
   Assert.ok(NewTabURL.overridden, "Newtab URL should be overridden");
   Assert.equal(NewTabURL.get(), url, "Newtab URL should be the custom URL");
 
-  notificationPromise = promiseNewtabURLNotification("about:newtab");
+  notificationPromise = promiseNewtabURLNotification(defaultURL);
   NewTabURL.reset();
   yield notificationPromise;
   Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
-  Assert.equal(NewTabURL.get(), "about:newtab", "Newtab URL should be the about:newtab");
+  Assert.equal(NewTabURL.get(), defaultURL, "Newtab URL should be the default");
 
   // change newtab page to remote
+  NewTabPrefsProvider.prefs.init();
   Services.prefs.setBoolPref("browser.newtabpage.remote", true);
-  Assert.equal(NewTabURL.get(), "about:remote-newtab", "Newtab URL should be the about:remote-newtab");
+  let remoteURL = aboutNewTabService.generateRemoteURL();
+  Assert.equal(NewTabURL.get(), remoteURL, `Newtab URL should be ${remoteURL}`);
   Assert.ok(!NewTabURL.overridden, "Newtab URL should not be overridden");
+  NewTabPrefsProvider.prefs.uninit();
 });
 
 function promiseNewtabURLNotification(aNewURL) {
   return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint ignore:line
       Services.obs.removeObserver(observer, aTopic);
       Assert.equal(aData, aNewURL, "Data for newtab-url-changed notification should be new URL.");
       resolve();
     }, "newtab-url-changed", false);
   });
 }
deleted file mode 100644
--- a/browser/components/newtab/tests/xpcshell/test_RemoteNewTabLocation.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/* globals ok, equal, RemoteNewTabLocation, NewTabPrefsProvider, Services, Preferences, XPCOMUtils, UpdateUtils */
-/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
-"use strict";
-
-const {utils: Cu} = Components;
-Cu.import("resource:///modules/RemoteNewTabLocation.jsm");
-Cu.import("resource:///modules/NewTabPrefsProvider.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.importGlobalProperties(["URL"]);
-
-XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
-  "resource://gre/modules/UpdateUtils.jsm");
-
-RemoteNewTabLocation.init();
-const DEFAULT_HREF = RemoteNewTabLocation.href;
-RemoteNewTabLocation.uninit();
-
-add_task(function* test_defaults() {
-  RemoteNewTabLocation.init();
-  ok(RemoteNewTabLocation.href, "Default location has an href");
-  ok(RemoteNewTabLocation.origin, "Default location has an origin");
-  ok(!RemoteNewTabLocation.overridden, "Default location is not overridden");
-  RemoteNewTabLocation.uninit();
-});
-
-/**
- * Tests the overriding of the default URL
- */
-add_task(function* test_overrides() {
-  RemoteNewTabLocation.init();
-  let testURL = new URL("https://example.com/");
-  let notificationPromise;
-
-  notificationPromise = nextChangeNotificationPromise(
-    testURL.href, "Remote Location should change");
-  RemoteNewTabLocation.override(testURL.href);
-  yield notificationPromise;
-  ok(RemoteNewTabLocation.overridden, "Remote location should be overridden");
-  equal(RemoteNewTabLocation.href, testURL.href,
-        "Remote href should be the custom URL");
-  equal(RemoteNewTabLocation.origin, testURL.origin,
-        "Remote origin should be the custom URL");
-
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "Remote href should be reset");
-  RemoteNewTabLocation.reset();
-  yield notificationPromise;
-  ok(!RemoteNewTabLocation.overridden, "Newtab URL should not be overridden");
-  RemoteNewTabLocation.uninit();
-});
-
-/**
- * Tests how RemoteNewTabLocation responds to updates to prefs
- */
-add_task(function* test_updates() {
-  RemoteNewTabLocation.init();
-  let notificationPromise;
-  let release = RemoteNewTabLocation._releaseFromUpdateChannel(
-    UpdateUtils.UpdateChannel);
-  let expectedHref = "https://newtab.cdn.mozilla.net" +
-                     `/v${RemoteNewTabLocation.version}` +
-                     `/${release}` +
-                     "/en-GB" +
-                     "/index.html";
-  Preferences.set("intl.locale.matchOS", true);
-  Preferences.set("general.useragent.locale", "en-GB");
-  NewTabPrefsProvider.prefs.init();
-
-  // test update checks for prefs
-  notificationPromise = nextChangeNotificationPromise(
-    expectedHref, "Remote href should be updated");
-  Preferences.set("intl.locale.matchOS", false);
-  yield notificationPromise;
-
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "Remote href changes back to default");
-  Preferences.set("general.useragent.locale", "en-US");
-
-  yield notificationPromise;
-
-  // test update fires on override and reset
-  let testURL = new URL("https://example.com/");
-  notificationPromise = nextChangeNotificationPromise(
-    testURL.href, "a notification occurs on override");
-  RemoteNewTabLocation.override(testURL.href);
-  yield notificationPromise;
-
-  // from overridden to default
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "a notification occurs on reset");
-  RemoteNewTabLocation.reset();
-  yield notificationPromise;
-
-  // override to default URL from default URL
-  notificationPromise = nextChangeNotificationPromise(
-    testURL.href, "a notification only occurs for a change in overridden urls");
-  RemoteNewTabLocation.override(DEFAULT_HREF);
-  RemoteNewTabLocation.override(testURL.href);
-  yield notificationPromise;
-
-  // reset twice, only one notification for default URL
-  notificationPromise = nextChangeNotificationPromise(
-    DEFAULT_HREF, "reset occurs");
-  RemoteNewTabLocation.reset();
-  yield notificationPromise;
-
-  notificationPromise = nextChangeNotificationPromise(
-    testURL.href, "a notification only occurs for a change in reset urls");
-  RemoteNewTabLocation.reset();
-  RemoteNewTabLocation.override(testURL.href);
-  yield notificationPromise;
-
-  NewTabPrefsProvider.prefs.uninit();
-  RemoteNewTabLocation.uninit();
-});
-
-/**
- * Verifies that RemoteNewTabLocation's _releaseFromUpdateChannel
- * Returns the correct release names
- */
-add_task(function* test_release_names() {
-  RemoteNewTabLocation.init();
-  let valid_channels = RemoteNewTabLocation.channels;
-  let invalid_channels = new Set(["default", "invalid"]);
-
-  for (let channel of valid_channels) {
-    equal(channel, RemoteNewTabLocation._releaseFromUpdateChannel(channel),
-          "release == channel name when valid");
-  }
-
-  for (let channel of invalid_channels) {
-    equal("nightly", RemoteNewTabLocation._releaseFromUpdateChannel(channel),
-          "release == nightly when invalid");
-  }
-  RemoteNewTabLocation.uninit();
-});
-
-function nextChangeNotificationPromise(aNewURL, testMessage) {
-  return new Promise(resolve => {
-    Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint ignore:line
-      Services.obs.removeObserver(observer, aTopic);
-      equal(aData, aNewURL, testMessage);
-      resolve();
-    }, "remote-new-tab-location-changed", false);
-  });
-}
deleted file mode 100644
--- a/browser/components/newtab/tests/xpcshell/test_RemoteNewTabUtils.js
+++ /dev/null
@@ -1,375 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-// See also browser/base/content/test/newtab/.
-
-const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-Cu.import("resource:///modules/RemoteNewTabUtils.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-function run_test() {
-  run_next_test();
-}
-
-add_task(function* validCacheMidPopulation() {
-  let expectedLinks = makeLinks(0, 3, 1);
-
-  let provider = new TestProvider(done => done(expectedLinks));
-  provider.maxNumLinks = expectedLinks.length;
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-  let promise = new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-
-  // isTopSiteGivenProvider() and getProviderLinks() should still return results
-  // even when cache is empty or being populated.
-  do_check_false(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
-  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), []);
-
-  yield promise;
-
-  // Once the cache is populated, we get the expected results
-  do_check_true(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider));
-  do_check_links(RemoteNewTabUtils.getProviderLinks(provider), expectedLinks);
-  RemoteNewTabUtils.links.removeProvider(provider);
-});
-
-add_task(function* notifyLinkDelete() {
-  let expectedLinks = makeLinks(0, 3, 1);
-
-  let provider = new TestProvider(done => done(expectedLinks));
-  provider.maxNumLinks = expectedLinks.length;
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Remove a link.
-  let removedLink = expectedLinks[2];
-  provider.notifyLinkChanged(removedLink, 2, true);
-  let links = RemoteNewTabUtils.links._providers.get(provider);
-
-  // Check that sortedLinks is correctly updated.
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks.slice(0, 2));
-
-  // Check that linkMap is accurately updated.
-  do_check_eq(links.linkMap.size, 2);
-  do_check_true(links.linkMap.get(expectedLinks[0].url));
-  do_check_true(links.linkMap.get(expectedLinks[1].url));
-  do_check_false(links.linkMap.get(removedLink.url));
-
-  // Check that siteMap is correctly updated.
-  do_check_eq(links.siteMap.size, 2);
-  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[0].url)));
-  do_check_true(links.siteMap.has(RemoteNewTabUtils.extractSite(expectedLinks[1].url)));
-  do_check_false(links.siteMap.has(RemoteNewTabUtils.extractSite(removedLink.url)));
-
-  RemoteNewTabUtils.links.removeProvider(provider);
-});
-
-add_task(function populatePromise() {
-  let count = 0;
-  let expectedLinks = makeLinks(0, 10, 2);
-
-  let getLinksFcn = Task.async(function* (callback) {
-    //Should not be calling getLinksFcn twice
-    count++;
-    do_check_eq(count, 1);
-    yield Promise.resolve();
-    callback(expectedLinks);
-  });
-
-  let provider = new TestProvider(getLinksFcn);
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-
-  RemoteNewTabUtils.links.populateProviderCache(provider, () => {});
-  RemoteNewTabUtils.links.populateProviderCache(provider, () => {
-    do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-    RemoteNewTabUtils.links.removeProvider(provider);
-  });
-});
-
-add_task(function* isTopSiteGivenProvider() {
-  let expectedLinks = makeLinks(0, 10, 2);
-
-  // The lowest 2 frecencies have the same base domain.
-  expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test";
-
-  let provider = new TestProvider(done => done(expectedLinks));
-  provider.maxNumLinks = expectedLinks.length;
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example1.com", provider), false);
-
-  // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3
-  let newLink = makeLink(3);
-  provider.notifyLinkChanged(newLink);
-
-  // There is still a frecent url with example2 domain, so it's still frecent.
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example3.com", provider), true);
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
-
-  // Push out frecency 3
-  newLink = makeLink(5);
-  provider.notifyLinkChanged(newLink);
-
-  // Push out frecency 4
-  newLink = makeLink(9);
-  provider.notifyLinkChanged(newLink);
-
-  // Our count reached 0 for the example2.com domain so it's no longer a frecent site.
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example5.com", provider), true);
-  do_check_eq(RemoteNewTabUtils.isTopSiteGivenProvider("example2.com", provider), false);
-
-  RemoteNewTabUtils.links.removeProvider(provider);
-});
-
-add_task(function* multipleProviders() {
-  // Make each provider generate RemoteNewTabUtils.links.maxNumLinks links to check
-  // that no more than maxNumLinks are actually returned in the merged list.
-  let evenLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks, 2);
-  let evenProvider = new TestProvider(done => done(evenLinks));
-  let oddLinks = makeLinks(0, 2 * RemoteNewTabUtils.links.maxNumLinks - 1, 2);
-  let oddProvider = new TestProvider(done => done(oddLinks));
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(evenProvider);
-  RemoteNewTabUtils.links.addProvider(oddProvider);
-
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-
-  let links = RemoteNewTabUtils.links.getLinks();
-  let expectedLinks = makeLinks(RemoteNewTabUtils.links.maxNumLinks,
-                                2 * RemoteNewTabUtils.links.maxNumLinks,
-                                1);
-  do_check_eq(links.length, RemoteNewTabUtils.links.maxNumLinks);
-  do_check_links(links, expectedLinks);
-
-  RemoteNewTabUtils.links.removeProvider(evenProvider);
-  RemoteNewTabUtils.links.removeProvider(oddProvider);
-});
-
-add_task(function* changeLinks() {
-  let expectedLinks = makeLinks(0, 20, 2);
-  let provider = new TestProvider(done => done(expectedLinks));
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Notify of a new link.
-  let newLink = makeLink(19);
-  expectedLinks.splice(1, 0, newLink);
-  provider.notifyLinkChanged(newLink);
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Notify of a link that's changed sort criteria.
-  newLink.frecency = 17;
-  expectedLinks.splice(1, 1);
-  expectedLinks.splice(2, 0, newLink);
-  provider.notifyLinkChanged({
-    url: newLink.url,
-    frecency: 17,
-  });
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Notify of a link that's changed title.
-  newLink.title = "My frecency is now 17";
-  provider.notifyLinkChanged({
-    url: newLink.url,
-    title: newLink.title,
-  });
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Notify of a new link again, but this time make it overflow maxNumLinks.
-  provider.maxNumLinks = expectedLinks.length;
-  newLink = makeLink(21);
-  expectedLinks.unshift(newLink);
-  expectedLinks.pop();
-  do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
-  provider.notifyLinkChanged(newLink);
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  // Notify of many links changed.
-  expectedLinks = makeLinks(0, 3, 1);
-  provider.notifyManyLinksChanged();
-
-  // Since _populateProviderCache() is async, we must wait until the provider's
-  // populate promise has been resolved.
-  yield RemoteNewTabUtils.links._providers.get(provider).populatePromise;
-
-  // RemoteNewTabUtils.links will now repopulate its cache
-  do_check_links(RemoteNewTabUtils.links.getLinks(), expectedLinks);
-
-  RemoteNewTabUtils.links.removeProvider(provider);
-});
-
-add_task(function* oneProviderAlreadyCached() {
-  let links1 = makeLinks(0, 10, 1);
-  let provider1 = new TestProvider(done => done(links1));
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider1);
-
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-  do_check_links(RemoteNewTabUtils.links.getLinks(), links1);
-
-  let links2 = makeLinks(10, 20, 1);
-  let provider2 = new TestProvider(done => done(links2));
-  RemoteNewTabUtils.links.addProvider(provider2);
-
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-  do_check_links(RemoteNewTabUtils.links.getLinks(), links2.concat(links1));
-
-  RemoteNewTabUtils.links.removeProvider(provider1);
-  RemoteNewTabUtils.links.removeProvider(provider2);
-});
-
-add_task(function* newLowRankedLink() {
-  // Init a provider with 10 links and make its maximum number also 10.
-  let links = makeLinks(0, 10, 1);
-  let provider = new TestProvider(done => done(links));
-  provider.maxNumLinks = links.length;
-
-  RemoteNewTabUtils.initWithoutProviders();
-  RemoteNewTabUtils.links.addProvider(provider);
-
-  yield new Promise(resolve => RemoteNewTabUtils.links.populateCache(resolve));
-  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
-
-  // Notify of a new link that's low-ranked enough not to make the list.
-  let newLink = makeLink(0);
-  provider.notifyLinkChanged(newLink);
-  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
-
-  // Notify about the new link's title change.
-  provider.notifyLinkChanged({
-    url: newLink.url,
-    title: "a new title",
-  });
-  do_check_links(RemoteNewTabUtils.links.getLinks(), links);
-
-  RemoteNewTabUtils.links.removeProvider(provider);
-});
-
-add_task(function extractSite() {
-  // All these should extract to the same site
-  [ "mozilla.org",
-    "m.mozilla.org",
-    "mobile.mozilla.org",
-    "www.mozilla.org",
-    "www3.mozilla.org",
-  ].forEach(host => {
-    let url = "http://" + host;
-    do_check_eq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted same " + host);
-  });
-
-  // All these should extract to the same subdomain
-  [ "bugzilla.mozilla.org",
-    "www.bugzilla.mozilla.org",
-  ].forEach(host => {
-    let url = "http://" + host;
-    do_check_eq(RemoteNewTabUtils.extractSite(url), "bugzilla.mozilla.org", "extracted eTLD+2 " + host);
-  });
-
-  // All these should not extract to the same site
-  [ "bugzilla.mozilla.org",
-    "bug123.bugzilla.mozilla.org",
-    "too.many.levels.bugzilla.mozilla.org",
-    "m2.mozilla.org",
-    "mobile30.mozilla.org",
-    "ww.mozilla.org",
-    "ww2.mozilla.org",
-    "wwwww.mozilla.org",
-    "wwwww50.mozilla.org",
-    "wwws.mozilla.org",
-    "secure.mozilla.org",
-    "secure10.mozilla.org",
-    "many.levels.deep.mozilla.org",
-    "just.check.in",
-    "192.168.0.1",
-    "localhost",
-  ].forEach(host => {
-    let url = "http://" + host;
-    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff " + host);
-  });
-
-  // All these should not extract to the same site
-  [ "about:blank",
-    "file:///Users/user/file",
-    "chrome://browser/something",
-    "ftp://ftp.mozilla.org/",
-  ].forEach(url => {
-    do_check_neq(RemoteNewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url);
-  });
-});
-
-function TestProvider(getLinksFn) {
-  this.getLinks = getLinksFn;
-  this._observers = new Set();
-}
-
-TestProvider.prototype = {
-  addObserver: function (observer) {
-    this._observers.add(observer);
-  },
-  notifyLinkChanged: function (link, index=-1, deleted=false) {
-    this._notifyObservers("onLinkChanged", link, index, deleted);
-  },
-  notifyManyLinksChanged: function () {
-    this._notifyObservers("onManyLinksChanged");
-  },
-  _notifyObservers: function () {
-    let observerMethodName = arguments[0];
-    let args = Array.prototype.slice.call(arguments, 1);
-    args.unshift(this);
-    for (let obs of this._observers) {
-      if (obs[observerMethodName])
-        obs[observerMethodName].apply(RemoteNewTabUtils.links, args);
-    }
-  },
-};
-
-function do_check_links(actualLinks, expectedLinks) {
-  do_check_true(Array.isArray(actualLinks));
-  do_check_eq(actualLinks.length, expectedLinks.length);
-  for (let i = 0; i < expectedLinks.length; i++) {
-    let expected = expectedLinks[i];
-    let actual = actualLinks[i];
-    do_check_eq(actual.url, expected.url);
-    do_check_eq(actual.title, expected.title);
-    do_check_eq(actual.frecency, expected.frecency);
-    do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
-  }
-}
-
-function makeLinks(frecRangeStart, frecRangeEnd, step) {
-  let links = [];
-  // Remember, links are ordered by frecency descending.
-  for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
-    links.push(makeLink(i));
-  }
-  return links;
-}
-
-function makeLink(frecency) {
-  return {
-    url: "http://example" + frecency + ".com/",
-    title: "My frecency is " + frecency,
-    frecency: frecency,
-    lastVisitDate: 0,
-  };
-}
--- a/browser/components/newtab/tests/xpcshell/xpcshell.ini
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -3,10 +3,8 @@ head =
 tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_AboutNewTabService.js]
 [test_NewTabPrefsProvider.js]
 [test_NewTabURL.js]
 [test_PlacesProvider.js]
-[test_RemoteNewTabLocation.js]
-[test_RemoteNewTabUtils.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -8,41 +8,30 @@ const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
-                                  "resource://gre/modules/AppConstants.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
                                   "resource:///modules/AboutHome.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
                                   "resource:///modules/AboutNewTab.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DirectoryLinksProvider",
                                   "resource:///modules/DirectoryLinksProvider.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
-if(!AppConstants.RELEASE_BUILD) {
-  XPCOMUtils.defineLazyModuleGetter(this, "RemoteAboutNewTab",
-                                    "resource:///modules/RemoteAboutNewTab.jsm");
-
-  XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
-                                    "resource:///modules/RemoteNewTabUtils.jsm");
-
-  XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-                                    "resource:///modules/NewTabPrefsProvider.jsm");
-}
+XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
+                                  "resource:///modules/NewTabPrefsProvider.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "UITour",
                                   "resource:///modules/UITour.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContentClick",
@@ -183,24 +172,25 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/AddonWatcher.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
                                   "resource://gre/modules/ExtensionManagement.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
+                                  "resource://gre/modules/AppConstants.jsm");
+
 XPCOMUtils.defineLazyServiceGetter(this, "WindowsUIUtils",
                                    "@mozilla.org/windows-ui-utils;1", "nsIWindowsUIUtils");
 
 XPCOMUtils.defineLazyServiceGetter(this, "AlertsService",
                                    "@mozilla.org/alerts-service;1", "nsIAlertsService");
 
-const ABOUT_NEWTAB = "about:newtab";
-
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 8 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -773,22 +763,17 @@ BrowserGlue.prototype = {
     webrtcUI.init();
     AboutHome.init();
 
     DirectoryLinksProvider.init();
     NewTabUtils.init();
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
     AboutNewTab.init();
 
-    if(!AppConstants.RELEASE_BUILD) {
-      RemoteNewTabUtils.init();
-      RemoteNewTabUtils.links.addProvider(DirectoryLinksProvider);
-      RemoteAboutNewTab.init();
-      NewTabPrefsProvider.prefs.init();
-    }
+    NewTabPrefsProvider.prefs.init();
 
     SessionStore.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
     FormValidationHandler.init();
 
     ContentClick.init();
     RemotePrompt.init();
@@ -1095,20 +1080,17 @@ BrowserGlue.prototype = {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     SelfSupportBackend.uninit();
 
     CustomizationTabPreloader.uninit();
     WebappManager.uninit();
 
-    if (!AppConstants.RELEASE_BUILD) {
-      RemoteAboutNewTab.uninit();
-      NewTabPrefsProvider.prefs.uninit();
-    }
+    NewTabPrefsProvider.prefs.uninit();
     AboutNewTab.uninit();
 #ifdef NIGHTLY_BUILD
     if (Services.prefs.getBoolPref("dom.identity.enabled")) {
       SignInToWebsiteUX.uninit();
     }
 #endif
     webrtcUI.uninit();
     FormValidationHandler.uninit();
@@ -2475,60 +2457,16 @@ BrowserGlue.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference,
                                          Ci.nsIBrowserGlue]),
 
   // redefine the default factory for XPCOMUtils
   _xpcom_factory: BrowserGlueServiceFactory,
 }
 
-// ------------------------------------
-// nsIAboutNewTabService implementation
-//-------------------------------------
-
-function AboutNewTabService()
-{
-  this._newTabURL = ABOUT_NEWTAB;
-  this._overridden = false;
-}
-
-AboutNewTabService.prototype = {
-  classID: Components.ID("{97eea4bb-db50-4ae0-9147-1e5ed55b4ed5}"),
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutNewTabService]),
-
-  get newTabURL() {
-
-    if (!AppConstants.RELEASE_BUILD && Services.prefs.getBoolPref("browser.newtabpage.remote")) {
-      return "about:remote-newtab";
-    }
-
-    return this._newTabURL;
-  },
-
-  set newTabURL(aNewTabURL) {
-    if (aNewTabURL === ABOUT_NEWTAB) {
-      this.resetNewTabURL();
-      return;
-    }
-    this._newTabURL = aNewTabURL;
-    this._overridden = true;
-    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
-  },
-
-  get overridden() {
-    return this._overridden;
-  },
-
-  resetNewTabURL: function() {
-    this._newTabURL = ABOUT_NEWTAB;
-    this._overridden = false;
-    Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
-   }
-};
-
 function ContentPermissionPrompt() {}
 
 ContentPermissionPrompt.prototype = {
   classID:          Components.ID("{d8903bf6-68d5-4e97-bcd1-e4d3012f721a}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
 
   _getBrowserForRequest: function (aRequest) {
@@ -3333,17 +3271,17 @@ var E10SAccessibilityCheck = {
 
     notification =
       win.PopupNotifications.show(browser, "a11y_enabled_with_e10s",
                                   promptMessage, null, mainAction,
                                   secondaryActions, options);
   },
 };
 
-var components = [BrowserGlue, ContentPermissionPrompt, AboutNewTabService];
+var components = [BrowserGlue, ContentPermissionPrompt];
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
 
 
 // Listen for UITour messages.
 // Do it here instead of the UITour module itself so that the UITour module is lazy loaded
 // when the first message is received.
 var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
 globalMM.addMessageListener("UITour:onPageEvent", function(aMessage) {
deleted file mode 100644
--- a/browser/components/nsIAboutNewTabService.idl
+++ /dev/null
@@ -1,32 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-#include "nsISupports.idl"
-
-/**
- * Allows to override about:newtab to point to a different location
- * than the one specified within AboutRedirector.cpp
- */
-
-[scriptable, uuid(6c66f022-beb1-46ea-8af6-c6c6dd937ea9)]
-interface nsIAboutNewTabService : nsISupports
-{
-  /**
-   * Returns "about:newtab" if not overridden, otherwise
-   * a string represenation of the new URL.
-   */
-  attribute ACString newTabURL;
-
-  /**
-   * Returns true if the default of "about:newtab" got
-   * overridden.
-   */
-  readonly attribute bool overridden;
-
-  /**
-   * Resets "about:newtab" to the default and also resets the
-   * overridden attribute to false.
-   */
-  void resetNewTabURL();
-};
--- a/browser/components/sessionstore/FrameTree.jsm
+++ b/browser/components/sessionstore/FrameTree.jsm
@@ -225,17 +225,17 @@ FrameTreeInternal.prototype = {
     // been restored: we don't want to accidentally send any updates to the
     // parent when the about:blank placeholder page has loaded.
     if (!this._chromeGlobal.docShell.hasLoadedNonBlankURI) {
       return;
     }
 
     if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
       // Clear the list of frames until we can recollect it.
-      this._frames.clear();
+      this._frames = new WeakMap();
 
       // Notify observers that the frame tree has been reset.
       this.notifyObservers("onFrameTreeReset");
     } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
       // The document and its resources have finished loading.
       this.collect(webProgress.DOMWindow);
 
       // Notify observers that the frame tree has been reset.
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -13,19 +13,24 @@ const Cr = Components.results;
 
 // Current version of the format used by Session Restore.
 const FORMAT_VERSION = 1;
 
 const TAB_STATE_NEEDS_RESTORE = 1;
 const TAB_STATE_RESTORING = 2;
 const TAB_STATE_WILL_RESTORE = 3;
 
+// A new window has just been restored. At this stage, tabs are generally
+// not restored.
+const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
+const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
+const NOTIFY_INITIATING_MANUAL_RESTORE = "sessionstore-initiating-manual-restore";
 
 const NOTIFY_TAB_RESTORED = "sessionstore-debug-tab-restored"; // WARNING: debug-only
 
 // Maximum number of tabs to restore simultaneously. Previously controlled by
 // the browser.sessionstore.max_concurrent_tabs pref.
 const MAX_CONCURRENT_TAB_RESTORES = 3;
 
 // global notifications observed
@@ -1174,16 +1179,19 @@ var SessionStoreInternal = {
       }
 
       if (this._sessionInitialized) {
         this.initializeWindow(aWindow);
       } else {
         let initialState = this.initSession();
         this._sessionInitialized = true;
 
+        if (initialState) {
+          Services.obs.notifyObservers(null, NOTIFY_RESTORING_ON_STARTUP, "");
+        }
         TelemetryStopwatch.start("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
         this.initializeWindow(aWindow, initialState);
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS");
 
         // Let everyone know we're done.
         this._deferredInitialized.resolve();
       }
     }, console.error);
@@ -1562,17 +1570,17 @@ var SessionStoreInternal = {
     var win = this._getMostRecentBrowserWindow();
     if (win) {
       win.setTimeout(() => SessionSaver.run(), 0);
     } else if (RunState.isRunning) {
       SessionSaver.run();
     }
 
     this._clearRestoringWindows();
-    this._saveableClosedWindowData.clear();
+    this._saveableClosedWindowData = new WeakSet();
   },
 
   /**
    * On purge of domain data
    * @param aData
    *        String domain data
    */
   onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
@@ -2311,16 +2319,18 @@ var SessionStoreInternal = {
    * be opened.
    */
   restoreLastSession: function ssi_restoreLastSession() {
     // Use the public getter since it also checks PB mode
     if (!this.canRestoreLastSession) {
       throw Components.Exception("Last session can not be restored");
     }
 
+    Services.obs.notifyObservers(null, NOTIFY_INITIATING_MANUAL_RESTORE, "");
+
     // First collect each window with its id...
     let windows = {};
     this._forEachBrowserWindow(function(aWindow) {
       if (aWindow.__SS_lastSessionWindowID)
         windows[aWindow.__SS_lastSessionWindowID] = aWindow;
     });
 
     let lastSessionState = LastSession.getState();
@@ -3012,16 +3022,19 @@ var SessionStoreInternal = {
     }
 
     // set smoothScroll back to the original value
     tabstrip.smoothScroll = smoothScroll;
 
     TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
 
     this._setWindowStateReady(aWindow);
+
+    Services.obs.notifyObservers(aWindow, NOTIFY_SINGLE_WINDOW_RESTORED, "");
+
     this._sendRestoreCompletedNotifications();
   },
 
   /**
    * Restore multiple windows using the provided state.
    * @param aWindow
    *        Window reference to the first window to use for restoration.
    *        Additionally required windows will be opened.
@@ -4312,17 +4325,17 @@ var DirtyWindows = {
     return this._data.set(window, true);
   },
 
   remove: function (window) {
     this._data.delete(window);
   },
 
   clear: function (window) {
-    this._data.clear();
+    this._data = new WeakMap();
   }
 };
 
 // The state from the previous session (after restoring pinned tabs). This
 // state is persisted and passed through to the next session during an app
 // restart to make the third party add-on warning not trash the deferred
 // session
 var LastSession = {
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/StartupPerformance.jsm
@@ -0,0 +1,200 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["StartupPerformance"];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+  "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "setTimeout",
+  "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "clearTimeout",
+  "resource://gre/modules/Timer.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+
+const COLLECT_RESULTS_AFTER_MS = 10000;
+
+const TOPICS = ["sessionstore-restoring-on-startup", "sessionstore-initiating-manual-restore"];
+
+this.StartupPerformance = {
+  // Instant at which we have started restoration (notification "sessionstore-restoring-on-startup")
+  _startTimeStamp: null,
+
+  // Latest instant at which we have finished restoring a tab (DOM event "SSTabRestored")
+  _latestRestoredTimeStamp: null,
+
+  // A promise resolved once we have finished restoring all the startup tabs.
+  _promiseFinished: null,
+
+  // Function `resolve()` for `_promiseFinished`.
+  _resolveFinished: null,
+
+  // A timer
+  _deadlineTimer: null,
+
+  // `true` once the timer has fired
+  _hasFired: false,
+
+  // Statistics on the session we need to restore.
+  _totalNumberOfEagerTabs: 0,
+  _totalNumberOfTabs: 0,
+  _totalNumberOfWindows: 0,
+
+  init: function() {
+    for (let topic of TOPICS) {
+      Services.obs.addObserver(this, topic, false);
+    }
+  },
+
+  // Called when restoration starts.
+  // Record the start timestamp, setup the timer and `this._promiseFinished`.
+  // Behavior is unspecified if there was already an ongoing measure.
+  _onRestorationStarts: function(isAutoRestore) {
+    this._startTimeStamp = Date.now();
+    this._totalNumberOfEagerTabs = 0;
+    this._totalNumberOfTabs = 0;
+    this._totalNumberOfWindows = 0;
+
+    // While we may restore several sessions in a single run of the browser,
+    // that's a very unusual case, and not really worth measuring, so let's
+    // stop listening for further restorations.
+
+    for (let topic of TOPICS) {
+      Services.obs.removeObserver(this, topic);
+    }
+
+    Services.obs.addObserver(this, "sessionstore-single-window-restored", false);
+    this._promiseFinished = new Promise(resolve => {
+      this._resolveFinished = resolve;
+    });
+    this._promiseFinished.then(() => {
+      try {
+        if (!this._latestRestoredTimeStamp) {
+          // Apparently, we haven't restored any tab.
+          return;
+        }
+
+        // Once we are done restoring tabs, update Telemetry.
+        let histogramName = isAutoRestore ?
+          "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS" :
+          "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS";
+        let histogram = Services.telemetry.getHistogramById(histogramName);
+        let delta = this._latestRestoredTimeStamp - this._startTimeStamp;
+        histogram.add(delta);
+
+        Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED").add(this._totalNumberOfEagerTabs);
+        Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED").add(this._totalNumberOfTabs);
+        Services.telemetry.getHistogramById("FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED").add(this._totalNumberOfWindows);
+
+
+        // Reset
+        this._startTimeStamp = null;
+     } catch (ex) {
+        console.error("StartupPerformance: error after resolving promise", ex);
+      }
+    });
+  },
+
+  _startTimer: function() {
+    if (this._hasFired) {
+      return;
+    }
+    if (this._deadlineTimer) {
+      clearTimeout(this._deadlineTimer);
+    }
+    this._deadlineTimer = setTimeout(() => {
+      try {
+        this._resolveFinished();
+      } catch (ex) {
+        console.error("StartupPerformance: Error in timeout handler", ex);
+      } finally {
+        // Clean up.
+        this._deadlineTimer = null;
+        this._hasFired = true;
+        this._resolveFinished = null;
+        Services.obs.removeObserver(this, "sessionstore-single-window-restored");
+      }
+    }, COLLECT_RESULTS_AFTER_MS);
+  },
+
+  observe: function(subject, topic, details) {
+    try {
+      switch (topic) {
+        case "sessionstore-restoring-on-startup":
+          this._onRestorationStarts(true);
+          break;
+        case "sessionstore-initiating-manual-restore":
+          this._onRestorationStarts(false);
+          break;
+        case "sessionstore-single-window-restored": {
+          // Session Restore has just opened a window with (initially empty) tabs.
+          // Some of these tabs will be restored eagerly, while others will be
+          // restored on demand. The process becomes usable only when all windows
+          // have finished restored their eager tabs.
+          //
+          // While it would be possible to track the restoration of each tab
+          // from within SessionRestore to determine exactly when the process
+          // becomes usable, experience shows that this is too invasive. Rather,
+          // we employ the following heuristic:
+          // - we maintain a timer of `COLLECT_RESULTS_AFTER_MS` that we expect
+          //   will be triggered only once all tabs have been restored;
+          // - whenever we restore a new window (hence a bunch of eager tabs),
+          //   we postpone the timer to ensure that the new eager tabs have
+          //   `COLLECT_RESULTS_AFTER_MS` to be restored;
+          // - whenever a tab is restored, we update
+          //   `this._latestRestoredTimeStamp`;
+          // - after `COLLECT_RESULTS_AFTER_MS`, we collect the final version
+          //   of `this._latestRestoredTimeStamp`, and use it to determine the
+          //   entire duration of the collection.
+          //
+          // Note that this heuristic may be inaccurate if a user clicks
+          // immediately on a restore-on-demand tab before the end of
+          // `COLLECT_RESULTS_AFTER_MS`. We assume that this will not
+          // affect too much the results.
+          //
+          // Reset the delay, to give the tabs a little (more) time to restore.
+          this._startTimer();
+
+          this._totalNumberOfWindows += 1;
+
+          // Observe the restoration of all tabs. We assume that all tabs of this
+          // window will have been restored before `COLLECT_RESULTS_AFTER_MS`.
+          // The last call to `observer` will let us determine how long it took
+          // to reach that point.
+          let win = subject;
+
+          let observer = () => {
+            this._latestRestoredTimeStamp = Date.now();
+            this._totalNumberOfEagerTabs += 1;
+          };
+          win.gBrowser.tabContainer.addEventListener("SSTabRestored", observer);
+          this._totalNumberOfTabs += win.gBrowser.tabContainer.itemCount;
+
+          // Once we have finished collecting the results, clean up the observers.
+          this._promiseFinished.then(() => {
+            if (!win.gBrowser.tabContainer) {
+              // May be undefined during shutdown and/or some tests.
+              return;
+            }
+            win.gBrowser.tabContainer.removeEventListener("SSTabRestored", observer);
+          });
+        }
+        break;
+        default:
+          throw new Error(`Unexpected topic ${topic}`);
+      }
+    } catch (ex) {
+      console.error("StartupPerformance error", ex, ex.stack);
+      throw ex;
+    }
+  }
+};
--- a/browser/components/sessionstore/content/aboutSessionRestore.js
+++ b/browser/components/sessionstore/content/aboutSessionRestore.js
@@ -107,16 +107,17 @@ function updateTabListVisibility() {
   } else {
     tabList.removeAttribute("available");
     container.classList.remove("restore-chosen");
   }
   initTreeView();
 }
 
 function restoreSession() {
+  Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore", "");
   document.getElementById("errorTryAgain").disabled = true;
 
   if (isTreeViewVisible()) {
     if (!gTreeData.some(aItem => aItem.checked)) {
       // This should only be possible when we have no "cancel" button, and thus
       // the "Restore session" button always remains enabled.  In that case and
       // when nothing is selected, we just want a new session.
       startNewSession();
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -36,16 +36,17 @@ EXTRA_JS_MODULES.sessionstore = [
     'SessionFile.jsm',
     'SessionHistory.jsm',
     'SessionMigration.jsm',
     'SessionSaver.jsm',
     'SessionStorage.jsm',
     'SessionStore.jsm',
     'SessionWorker.js',
     'SessionWorker.jsm',
+    'StartupPerformance.jsm',
     'TabAttributes.jsm',
     'TabState.jsm',
     'TabStateCache.jsm',
     'TabStateFlusher.jsm',
     'Utils.jsm',
 ]
 
 with Files('**'):
--- a/browser/components/sessionstore/nsSessionStartup.js
+++ b/browser/components/sessionstore/nsSessionStartup.js
@@ -40,16 +40,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
   "resource://gre/modules/Console.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
   "resource:///modules/sessionstore/SessionFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "StartupPerformance",
+  "resource:///modules/sessionstore/StartupPerformance.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CrashMonitor",
   "resource://gre/modules/CrashMonitor.jsm");
 
 const STATE_RUNNING_STR = "running";
 
 // 'browser.startup.page' preference value to resume the previous session.
 const BROWSER_STARTUP_RESUME_SESSION = 3;
 
@@ -91,16 +93,17 @@ SessionStartup.prototype = {
 
 /* ........ Global Event Handlers .............. */
 
   /**
    * Initialize the component
    */
   init: function sss_init() {
     Services.obs.notifyObservers(null, "sessionstore-init-started", null);
+    StartupPerformance.init();
 
     // do not need to initialize anything in auto-started private browsing sessions
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       this._initialized = true;
       gOnceInitializedDeferred.resolve();
       return;
     }
 
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -88,16 +88,19 @@ this.UITour = {
   // building the content source of a potential tour.
   pageIDsForSession: new Map(),
   pageIDSourceBrowsers: new WeakMap(),
   /* Map from browser chrome windows to a Set of <browser>s in which a tour is open (both visible and hidden) */
   tourBrowsersByWindow: new WeakMap(),
   urlbarCapture: new WeakMap(),
   appMenuOpenForAnnotation: new Set(),
   availableTargetsCache: new WeakMap(),
+  clearAvailableTargetsCache() {
+    this.availableTargetsCache = new WeakMap();
+  },
 
   _annotationPanelMutationObservers: new WeakMap(),
 
   highlightEffects: ["random", "wobble", "zoom", "color"],
   targets: new Map([
     ["accountStatus", {
       query: (aDocument) => {
         // If the user is logged in, use the avatar element.
@@ -280,17 +283,17 @@ this.UITour = {
     let listenerMethods = [
       "onWidgetAdded",
       "onWidgetMoved",
       "onWidgetRemoved",
       "onWidgetReset",
       "onAreaReset",
     ];
     CustomizableUI.addListener(listenerMethods.reduce((listener, method) => {
-      listener[method] = () => this.availableTargetsCache.clear();
+      listener[method] = () => this.clearAvailableTargetsCache();
       return listener;
     }, {}));
   },
 
   restoreSeenPageIDs: function() {
     delete this.seenPageIDs;
 
     if (UITelemetry.enabled) {
@@ -1577,17 +1580,17 @@ this.UITour = {
       let popup = aWindow.gIdentityHandler._identityPopup;
 
       // Add the listener even if the panel is already open since it will still
       // only get registered once even if it was UITour that opened it.
       popup.addEventListener("popuphiding", this.hideControlCenterAnnotations);
       popup.addEventListener("popuphidden", this.onPanelHidden);
 
       popup.setAttribute("noautohide", true);
-      this.availableTargetsCache.clear();
+      this.clearAvailableTargetsCache();
 
       if (popup.state == "open") {
         if (aOpenCallback) {
           aOpenCallback();
         }
         return;
       }
 
@@ -1606,17 +1609,17 @@ this.UITour = {
         log.debug("Can't show the Loop menu since the toolbarButton isn't placed");
         return;
       }
 
       let panel = aWindow.document.getElementById("loop-notification-panel");
       panel.setAttribute("noautohide", true);
       if (panel.state != "open") {
         this.recreatePopup(panel);
-        this.availableTargetsCache.clear();
+        this.clearAvailableTargetsCache();
       }
 
       // An event object is expected but we don't want to toggle the panel with a click if the panel
       // is already open.
       aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }, "rooms").then(() => {
         if (aOpenCallback) {
           aOpenCallback();
         }
@@ -1726,17 +1729,17 @@ this.UITour = {
     UITour.hideAnnotationsForPanel(aEvent, (aTarget) => {
       return aTarget.targetName.startsWith("controlCenter-");
     });
   },
 
   onPanelHidden: function(aEvent) {
     aEvent.target.removeAttribute("noautohide");
     UITour.recreatePopup(aEvent.target);
-    UITour.availableTargetsCache.clear();
+    UITour.clearAvailableTargetsCache();
   },
 
   recreatePopup: function(aPanel) {
     // After changing popup attributes that relate to how the native widget is created
     // (e.g. @noautohide) we need to re-create the frame/widget for it to take effect.
     if (aPanel.hidden) {
       // If the panel is already hidden, we don't need to recreate it but flush
       // in case someone just hid it.
--- a/browser/extensions/loop/content/modules/MozLoopService.jsm
+++ b/browser/extensions/loop/content/modules/MozLoopService.jsm
@@ -935,17 +935,17 @@ var MozLoopServiceInternal = {
           return;
         }
         chatbox.removeEventListener("DOMContentLoaded", loaded, true);
 
         let chatbar = chatbox.parentNode;
         let window = chatbox.contentWindow;
 
         function socialFrameChanged(eventName) {
-          UITour.availableTargetsCache.clear();
+          UITour.clearAvailableTargetsCache();
           UITour.notify(eventName);
 
           if (eventName == "Loop:ChatWindowDetached" || eventName == "Loop:ChatWindowAttached") {
             // After detach, re-attach of the chatbox, refresh its reference so
             // we can keep using it here.
             let ref = chatbar.chatboxForURL.get(chatbox.src);
             chatbox = ref && ref.get() || chatbox;
           } else if (eventName == "Loop:ChatWindowClosed") {
--- a/browser/extensions/shumway/content/shumway.gfx.js
+++ b/browser/extensions/shumway/content/shumway.gfx.js
@@ -914,17 +914,17 @@ var START_TIME = performance.now();
       jsGlobal.WeakMap = e;
     }
   })();
   b = function() {
     function e() {
       "undefined" !== typeof ShumwayCom && ShumwayCom.getWeakMapKeys ? (this._map = new WeakMap, this._id = 0, this._newAdditions = []) : this._list = [];
     }
     e.prototype.clear = function() {
-      this._map ? this._map.clear() : this._list.length = 0;
+      this._map ? this._map = new WeakMap() : this._list.length = 0;
     };
     e.prototype.push = function(g) {
       this._map ? (this._map.set(g, this._id++), this._newAdditions.forEach(function(d) {
         d.push(g);
       })) : this._list.push(g);
     };
     e.prototype.remove = function(g) {
       this._map ? this._map.delete(g) : this._list[this._list.indexOf(g)] = null;
--- a/browser/extensions/shumway/content/shumway.player.js
+++ b/browser/extensions/shumway/content/shumway.player.js
@@ -915,17 +915,17 @@ var START_TIME = performance.now();
       jsGlobal.WeakMap = a;
     }
   })();
   l = function() {
     function a() {
       "undefined" !== typeof ShumwayCom && ShumwayCom.getWeakMapKeys ? (this._map = new WeakMap, this._id = 0, this._newAdditions = []) : this._list = [];
     }
     a.prototype.clear = function() {
-      this._map ? this._map.clear() : this._list.length = 0;
+      this._map ? this._map = new WeakMap() : this._list.length = 0;
     };
     a.prototype.push = function(a) {
       this._map ? (this._map.set(a, this._id++), this._newAdditions.forEach(function(c) {
         c.push(a);
       })) : this._list.push(a);
     };
     a.prototype.remove = function(a) {
       this._map ? this._map.delete(a) : this._list[this._list.indexOf(a)] = null;
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -382,16 +382,19 @@
 @RESPATH@/browser/components/nsSetDefaultBrowser.manifest
 @RESPATH@/browser/components/nsSetDefaultBrowser.js
 @RESPATH@/browser/components/devtools-clhandler.manifest
 @RESPATH@/browser/components/devtools-clhandler.js
 @RESPATH@/browser/components/webideCli.js
 @RESPATH@/browser/components/webideComponents.manifest
 @RESPATH@/browser/components/Experiments.manifest
 @RESPATH@/browser/components/ExperimentsService.js
+@RESPATH@/browser/components/browser-newtab.xpt
+@RESPATH@/browser/components/aboutNewTabService.js
+@RESPATH@/browser/components/NewTabComponents.manifest
 @RESPATH@/components/Downloads.manifest
 @RESPATH@/components/DownloadLegacy.js
 @RESPATH@/components/BrowserPageThumbs.manifest
 @RESPATH@/components/crashmonitor.manifest
 @RESPATH@/components/nsCrashMonitor.js
 @RESPATH@/components/SiteSpecificUserAgent.js
 @RESPATH@/components/SiteSpecificUserAgent.manifest
 @RESPATH@/components/toolkitsearch.manifest
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -133,17 +133,18 @@ function handleGUMRequest(aSubject, aTop
       prompt(contentWindow, aSubject.windowID, aSubject.callID,
              constraints, devices, secure);
     },
     function (error) {
       // bug 827146 -- In the future, the UI should catch NotFoundError
       // and allow the user to plug in a device, instead of immediately failing.
       denyGUMRequest({callID: aSubject.callID}, error);
     },
-    aSubject.innerWindowID);
+    aSubject.innerWindowID,
+    aSubject.callID);
 }
 
 function prompt(aContentWindow, aWindowID, aCallID, aConstraints, aDevices, aSecure) {
   let audioDevices = [];
   let videoDevices = [];
   let devices = [];
 
   // MediaStreamConstraints defines video as 'boolean or MediaTrackConstraints'.
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -12,38 +12,30 @@ const Cu = Components.utils;
 const ParserUtils =  Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils);
 
 Cu.importGlobalProperties(["XMLHttpRequest"]);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/AppConstants.jsm")
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
   "resource://gre/modules/osfile.jsm")
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "eTLD",
   "@mozilla.org/network/effective-tld-service;1",
   "nsIEffectiveTLDService");
-
-// ensure remote new tab doesn't go beyond aurora
-if (!AppConstants.RELEASE_BUILD) {
-  XPCOMUtils.defineLazyModuleGetter(this, "RemoteNewTabUtils",
-    "resource:///modules/RemoteNewTabUtils.jsm");
-}
-
 XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => {
   return new TextDecoder();
 });
 XPCOMUtils.defineLazyGetter(this, "gCryptoHash", function () {
   return Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
 });
 XPCOMUtils.defineLazyGetter(this, "gUnicodeConverter", function () {
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
@@ -762,22 +754,16 @@ var DirectoryLinksProvider = {
     // setup frequency cap file path
     this._frequencyCapFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, FREQUENCY_CAP_FILE);
     // setup inadjacent sites URL
     this._inadjacentSitesUrl = INADJACENCY_SOURCE;
 
     NewTabUtils.placesProvider.addObserver(this);
     NewTabUtils.links.addObserver(this);
 
-    // ensure remote new tab doesn't go beyond aurora
-    if (!AppConstants.RELEASE_BUILD) {
-      RemoteNewTabUtils.placesProvider.addObserver(this);
-      RemoteNewTabUtils.links.addObserver(this);
-    }
-
     return Task.spawn(function*() {
       // get the last modified time of the links file if it exists
       let doesFileExists = yield OS.File.exists(this._directoryFilePath);
       if (doesFileExists) {
         let fileInfo = yield OS.File.stat(this._directoryFilePath);
         this._lastDownloadMS = Date.parse(fileInfo.lastModificationDate);
       }
       // read frequency cap file
--- a/browser/modules/WindowsPreviewPerTab.jsm
+++ b/browser/modules/WindowsPreviewPerTab.jsm
@@ -45,16 +45,18 @@ this.EXPORTED_SYMBOLS = ["AeroPeek"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/PageThumbs.jsm");
 
 // Pref to enable/disable preview-per-tab
 const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
 // Pref to determine the magic auto-disable threshold
 const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
 // Pref to control the time in seconds that tab contents live in the cache
 const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
 
@@ -70,19 +72,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "imgITools");
 XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
                                    "@mozilla.org/browser/favicon-service;1",
                                    "nsIFaviconService");
 
 // nsIURI -> imgIContainer
 function _imageFromURI(doc, uri, privateMode, callback) {
   let channel = ioSvc.newChannelFromURI2(uri,
-                                         doc,
-                                         null,  // aLoadingPrincipal
-                                         null,  // aTriggeringPrincipal
+                                         null,
+                                         Services.scriptSecurityManager.getSystemPrincipal(),
+                                         null,
                                          Ci.nsILoadInfo.SEC_NORMAL,
                                          Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE);
   try {
     channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
     channel.setPrivate(privateMode);
   } catch (e) {
     // Ignore channels which do not support nsIPrivateBrowsingChannel
   }
@@ -123,292 +125,232 @@ function snapRectAtScale(r, scale) {
   r.width = width / scale;
   r.height = height / scale;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PreviewController
 
 /*
- * This class manages the behavior of the preview.
- *
- * To give greater performance when drawing, the dirty areas of the content
- * window are tracked and drawn on demand into a canvas of the same size.
- * This provides a great increase in responsiveness when drawing a preview
- * for unchanged (or even only slightly changed) tabs.
+ * This class manages the behavior of thumbnails and previews. It has the following
+ * responsibilities:
+ * 1) responding to requests from Windows taskbar for a thumbnail or window
+ *    preview.
+ * 2) listens for dom events that result in a thumbnail or window preview needing
+ *    to be refresh, and communicates this to the taskbar.
+ * 3) Handles querying and returning to the taskbar new thumbnail or window
+ *    preview images through PageThumbs.
  *
  * @param win
  *        The TabWindow (see below) that owns the preview that this controls
  * @param tab
  *        The <tab> that this preview is associated with
  */
 function PreviewController(win, tab) {
   this.win = win;
   this.tab = tab;
   this.linkedBrowser = tab.linkedBrowser;
   this.preview = this.win.createTabPreview(this);
 
-  this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
-  this.linkedBrowser.addEventListener("resize", this, false);
   this.tab.addEventListener("TabAttrModified", this, false);
 
   XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
-    let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    let canvas = PageThumbs.createCanvas();
     canvas.mozOpaque = true;
     return canvas;
   });
-
-  XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
-    function () {
-      let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
-                       .createInstance(Ci.nsIScriptableRegion);
-      dirtyRegion.init();
-      return dirtyRegion;
-    });
-
-  XPCOMUtils.defineLazyGetter(this, "winutils",
-    function () {
-      let win = tab.linkedBrowser.contentWindow;
-      return win.QueryInterface(Ci.nsIInterfaceRequestor)
-                .getInterface(Ci.nsIDOMWindowUtils);
-  });
 }
 
 PreviewController.prototype = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
                                          Ci.nsIDOMEventListener]),
+
   destroy: function () {
     this.tab.removeEventListener("TabAttrModified", this, false);
-    this.linkedBrowser.removeEventListener("resize", this, false);
-    this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
 
     // Break cycles, otherwise we end up leaking the window with everything
     // attached to it.
     delete this.win;
     delete this.preview;
-    delete this.dirtyRegion;
   },
+
   get wrappedJSObject() {
     return this;
   },
 
-  get dirtyRects() {
-    let rectstream = this.dirtyRegion.getRects();
-    if (!rectstream)
-      return [];
-    let rects = [];
-    for (let i = 0; i < rectstream.length; i+= 4) {
-      let r = {x:      rectstream[i],
-               y:      rectstream[i+1],
-               width:  rectstream[i+2],
-               height: rectstream[i+3]};
-      rects.push(r);
-    }
-    return rects;
+  // Resizes the canvasPreview to 0x0, essentially freeing its memory.
+  resetCanvasPreview: function () {
+    this.canvasPreview.width = 0;
+    this.canvasPreview.height = 0;
   },
 
-  // Resizes the canvasPreview to 0x0, essentially freeing its memory.
-  // updateCanvasPreview() will detect the size mismatch as a resize event
-  // the next time it is called.
-  resetCanvasPreview: function () {
-    this.resizeCanvasPreview(0, 0);
+  /**
+   * Set the canvas dimensions.
+   */
+  resizeCanvasPreview: function (aRequestedWidth, aRequestedHeight) {
+    this.canvasPreview.width = aRequestedWidth;
+    this.canvasPreview.height = aRequestedHeight;
   },
 
-  resizeCanvasPreview: function (width, height) {
-    this.canvasPreview.width = width;
-    this.canvasPreview.height = height;
-  },
-
-  get wasResizedSinceLastPreview () {
-    let bx = this.linkedBrowser.boxObject;
-    return bx.width != this.canvasPreview.width ||
-           bx.height != this.canvasPreview.height;
-  },
 
   get zoom() {
     // Note that winutils.fullZoom accounts for "quantization" of the zoom factor
     // from nsIContentViewer due to conversion through appUnits.
     // We do -not- want screenPixelsPerCSSPixel here, because that would -also-
     // incorporate any scaling that is applied due to hi-dpi resolution options.
-    return this.winutils.fullZoom;
+    return this.tab.linkedBrowser.fullZoom;
+  },
+
+  get screenPixelsPerCSSPixel() {
+    let chromeWin = this.tab.ownerGlobal;
+    let windowUtils = chromeWin.getInterface(Ci.nsIDOMWindowUtils);
+    return windowUtils.screenPixelsPerCSSPixel;
+  },
+
+  get browserDims() {
+    return this.tab.linkedBrowser.getBoundingClientRect();
   },
 
-  // Updates the controller's canvas with the parts of the <browser> that need
-  // to be redrawn.
-  updateCanvasPreview: function () {
-    let win = this.linkedBrowser.contentWindow;
-    let bx = this.linkedBrowser.boxObject;
-    // If we resized then we need to flush layout so that the previews are up
-    // to date. Layout flushing for resizes is deferred for background tabs so
-    // we may need to force it. (bug 526620)
-    let flushLayout = this.wasResizedSinceLastPreview;
-    // Check for resize
-    if (flushLayout) {
-      // Invalidate the entire area and repaint
-      this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight});
-      this.resizeCanvasPreview(bx.width, bx.height);
-    }
-
-    // Draw dirty regions
-    let ctx = this.canvasPreview.getContext("2d");
-    let scale = this.zoom;
+  cacheBrowserDims: function () {
+    let dims = this.browserDims;
+    this._cachedWidth = dims.width;
+    this._cachedHeight = dims.height;
+  },
 
-    let flags = this.canvasPreviewFlags;
-    if (flushLayout)
-      flags &= ~Ci.nsIDOMCanvasRenderingContext2D.DRAWWINDOW_DO_NOT_FLUSH;
+  testCacheBrowserDims: function () {
+    let dims = this.browserDims;
+    return this._cachedWidth == dims.width &&
+      this._cachedHeight == dims.height;
+  },
 
-    // The dirty region may include parts that are offscreen so we clip to the
-    // canvas area.
-    this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight);
-    this.dirtyRects.forEach(function (r) {
-      // We need to snap the rectangle to be pixel aligned in the destination
-      // coordinate space. Otherwise natively themed widgets might not draw.
-      snapRectAtScale(r, scale);
-      let x = r.x;
-      let y = r.y;
-      let width = r.width;
-      let height = r.height;
-
-      ctx.save();
-      ctx.scale(scale, scale);
-      ctx.translate(x, y);
-      ctx.drawWindow(win, x, y, width, height, "white", flags);
-      ctx.restore();
-    });
-    this.dirtyRegion.setToRect(0,0,0,0);
-
+  /**
+   * Capture a new thumbnail image for this preview. Called by the controller
+   * in response to a request for a new thumbnail image.
+   */
+  updateCanvasPreview: function (aFullScale, aCallback) {
+    // Update our cached browser dims so that delayed resize
+    // events don't trigger another invalidation if this tab becomes active.
+    this.cacheBrowserDims();
+    PageThumbs.captureToCanvas(this.linkedBrowser, this.canvasPreview,
+                               aCallback, { fullScale: aFullScale });
     // If we're updating the canvas, then we're in the middle of a peek so
     // don't discard the cache of previews.
     AeroPeek.resetCacheTimer();
   },
 
-  onTabPaint: function (rect) {
-    let x = Math.floor(rect.left),
-        y = Math.floor(rect.top),
-        width = Math.ceil(rect.right) - x,
-        height = Math.ceil(rect.bottom) - y;
-    this.dirtyRegion.unionRect(x, y, width, height);
-  },
-
   updateTitleAndTooltip: function () {
     let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
     this.preview.title = title;
     this.preview.tooltip = title;
   },
 
   //////////////////////////////////////////////////////////////////////////////
-  //// nsITaskbarPreviewController 
+  //// nsITaskbarPreviewController
 
+  // window width and height, not browser
   get width() {
     return this.win.width;
   },
 
+  // window width and height, not browser
   get height() {
     return this.win.height;
   },
 
   get thumbnailAspectRatio() {
-    let boxObject = this.tab.linkedBrowser.boxObject;
+    let browserDims = this.browserDims;
     // Avoid returning 0
-    let tabWidth = boxObject.width || 1;
+    let tabWidth = browserDims.width || 1;
     // Avoid divide by 0
-    let tabHeight = boxObject.height || 1;
+    let tabHeight = browserDims.height || 1;
     return tabWidth / tabHeight;
   },
 
-  drawPreview: function (ctx) {
-    this.win.tabbrowser.previewTab(this.tab, () => this.previewTabCallback(ctx));
+  /**
+   * Responds to taskbar requests for window previews. Returns the results asynchronously
+   * through updateCanvasPreview.
+   *
+   * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
+   */
+  requestPreview: function (aTaskbarCallback) {
+    // Grab a high res content preview
+    this.resetCanvasPreview();
+    this.updateCanvasPreview(true, (aPreviewCanvas) => {
+      let winWidth = this.win.width;
+      let winHeight = this.win.height;
+
+      let composite = PageThumbs.createCanvas();
+
+      // Use transparency, Aero glass is drawn black without it.
+      composite.mozOpaque = false;
+
+      let ctx = composite.getContext('2d');
+      let scale = this.screenPixelsPerCSSPixel / this.zoom;
 
-    // We must avoid having the frame drawn around the window. See bug 520807
-    return false;
+      composite.width = winWidth * scale;
+      composite.height = winHeight * scale;
+
+      ctx.save();
+      ctx.scale(scale, scale);
+
+      // Draw chrome. Note we currently do not get scrollbars for remote frames
+      // in the image above.
+      ctx.drawWindow(this.win.win, 0, 0, winWidth, winHeight, "rgba(0,0,0,0)");
+
+      // Draw the content are into the composite canvas at the right location.
+      ctx.drawImage(aPreviewCanvas, this.browserDims.x, this.browserDims.y,
+                    aPreviewCanvas.width, aPreviewCanvas.height);
+      ctx.restore();
+
+      // Deliver the resulting composite canvas to Windows
+      this.win.tabbrowser.previewTab(this.tab, function () {
+        aTaskbarCallback.done(composite, false);
+      });
+    });
   },
 
-  previewTabCallback: function (ctx) {
-    // This will extract the resolution-scale component of the scaling we need,
-    // which should be applied to both chrome and content;
-    // the page zoom component is applied (to content only) within updateCanvasPreview.
-    let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom;
-    ctx.save();
-    ctx.scale(scale, scale);
-    let width = this.win.width;
-    let height = this.win.height;
-    // Draw our toplevel window
-    ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
-
-    // XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling
-    // unless we use this block of code; but doing this for "normal" (loaded) tabs
-    // results in blurry rendering on hidpi systems, so we avoid it if possible.
-    // I don't understand why pending and loaded tabs behave differently here...
-    // (see bug 857061).
-    if (this.tab.hasAttribute("pending")) {
-      // Compositor, where art thou?
-      // Draw the tab content on top of the toplevel window
-      this.updateCanvasPreview();
-
-      let boxObject = this.linkedBrowser.boxObject;
-      ctx.translate(boxObject.x, boxObject.y);
-      ctx.drawImage(this.canvasPreview, 0, 0);
-    }
-
-    ctx.restore();
+  /**
+   * Responds to taskbar requests for tab thumbnails. Returns the results asynchronously
+   * through updateCanvasPreview.
+   *
+   * Note Windows requests a specific width and height here, if the resulting thumbnail
+   * does not match these dimensions thumbnail display will fail.
+   *
+   * @param aTaskbarCallback nsITaskbarPreviewCallback results callback
+   * @param aRequestedWidth width of the requested thumbnail
+   * @param aRequestedHeight height of the requested thumbnail
+   */
+  requestThumbnail: function (aTaskbarCallback, aRequestedWidth, aRequestedHeight) {
+    this.resizeCanvasPreview(aRequestedWidth, aRequestedHeight);
+    this.updateCanvasPreview(false, (aThumbnailCanvas) => {
+      aTaskbarCallback.done(aThumbnailCanvas, false);
+    });
   },
 
-  drawThumbnail: function (ctx, width, height) {
-    this.updateCanvasPreview();
-
-    let scale = width/this.linkedBrowser.boxObject.width;
-    ctx.scale(scale, scale);
-    ctx.drawImage(this.canvasPreview, 0, 0);
-
-    // Don't draw a frame around the thumbnail
-    return false;
-  },
+  //////////////////////////////////////////////////////////////////////////////
+  //// Event handling
 
   onClose: function () {
     this.win.tabbrowser.removeTab(this.tab);
   },
 
   onActivate: function () {
     this.win.tabbrowser.selectedTab = this.tab;
 
     // Accept activation - this will restore the browser window
     // if it's minimized
     return true;
   },
 
   //// nsIDOMEventListener
   handleEvent: function (evt) {
     switch (evt.type) {
-      case "MozAfterPaint":
-        if (evt.originalTarget === this.linkedBrowser.contentWindow) {
-          let clientRects = evt.clientRects;
-          let length = clientRects.length;
-          for (let i = 0; i < length; i++) {
-            let r = clientRects.item(i);
-            this.onTabPaint(r);
-          }
-        }
-        this.preview.invalidate();
-        break;
       case "TabAttrModified":
         this.updateTitleAndTooltip();
         break;
-      case "resize":
-        // We need to invalidate our window's other tabs' previews since layout
-        // due to resizing is delayed for background tabs. Note that this
-        // resize may not be the first after the main window has been resized -
-        // the user may be switching to our tab which forces the resize.
-        this.win.previews.forEach(function (p) {
-          let controller = p.controller.wrappedJSObject;
-          if (controller.wasResizedSinceLastPreview) {
-            controller.resetCanvasPreview();
-            p.invalidate();
-          }
-        });
-        break;
     }
   }
 };
 
 XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
   function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
                 return canvasInterface.DRAWWINDOW_DRAW_VIEW
                      | canvasInterface.DRAWWINDOW_DRAW_CARET
@@ -418,47 +360,58 @@ XPCOMUtils.defineLazyGetter(PreviewContr
 
 ////////////////////////////////////////////////////////////////////////////////
 //// TabWindow
 
 /*
  * This class monitors a browser window for changes to its tabs
  *
  * @param win
- *        The nsIDOMWindow browser window 
+ *        The nsIDOMWindow browser window
  */
 function TabWindow(win) {
   this.win = win;
   this.tabbrowser = win.gBrowser;
 
+  this.cacheDims();
+
   this.previews = new Map();
 
   for (let i = 0; i < this.tabEvents.length; i++)
     this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
+
+  for (let i = 0; i < this.winEvents.length; i++)
+    this.win.addEventListener(this.winEvents[i], this, false);
+
   this.tabbrowser.addTabsProgressListener(this);
 
   AeroPeek.windows.push(this);
   let tabs = this.tabbrowser.tabs;
   for (let i = 0; i < tabs.length; i++)
     this.newTab(tabs[i]);
 
   this.updateTabOrdering();
   AeroPeek.checkPreviewCount();
 }
 
 TabWindow.prototype = {
   _enabled: false,
   tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
+  winEvents: ["resize"],
 
   destroy: function () {
     this._destroying = true;
 
     let tabs = this.tabbrowser.tabs;
 
     this.tabbrowser.removeTabsProgressListener(this);
+
+  for (let i = 0; i < this.winEvents.length; i++)
+    this.win.removeEventListener(this.winEvents[i], this, false);
+
     for (let i = 0; i < this.tabEvents.length; i++)
       this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
 
     for (let i = 0; i < tabs.length; i++)
       this.removeTab(tabs[i]);
 
     let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
     AeroPeek.windows.splice(idx, 1);
@@ -467,16 +420,25 @@ TabWindow.prototype = {
 
   get width () {
     return this.win.innerWidth;
   },
   get height () {
     return this.win.innerHeight;
   },
 
+  cacheDims: function () {
+    this._cachedWidth = this.width;
+    this._cachedHeight = this.height;
+  },
+
+  testCacheDims: function () {
+    return this._cachedWidth == this.width && this._cachedHeight == this.height;
+  },
+
   // Invoked when the given tab is added to this window
   newTab: function (tab) {
     let controller = new PreviewController(this, tab);
     // It's OK to add the preview now while the favicon still loads.
     this.previews.set(tab, controller.preview);
     AeroPeek.addPreview(controller.preview);
     // updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
     // Now that we've updated this.previews, it will resolve successfully.
@@ -488,17 +450,17 @@ TabWindow.prototype = {
                   .QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShell);
     let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
     preview.visible = AeroPeek.enabled;
     preview.active = this.tabbrowser.selectedTab == controller.tab;
     // Grab the default favicon
     getFaviconAsImage(
-      controller.linkedBrowser.contentWindow.document,
+      null,
       null,
       PrivateBrowsingUtils.isWindowPrivate(this.win),
       function (img) {
         // It is possible that we've already gotten the real favicon, so make sure
         // we have not set one before setting this default one.
         if (!preview.icon)
           preview.icon = img;
       });
@@ -572,25 +534,93 @@ TabWindow.prototype = {
         this.updateTabOrdering();
         break;
       case "TabSelect":
         this.previewFromTab(tab).active = true;
         break;
       case "TabMove":
         this.updateTabOrdering();
         break;
+      case "resize":
+        if (!AeroPeek._prefenabled)
+          return;
+        this.onResize();
+        break;
+    }
+  },
+
+  // Set or reset a timer that will invalidate visible thumbnails soon.
+  setInvalidationTimer: function () {
+    if (!this.invalidateTimer) {
+      this.invalidateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    }
+    this.invalidateTimer.cancel();
+
+    // delay 1 second before invalidating
+    this.invalidateTimer.initWithCallback(() => {
+      // invalidate every preview. note the internal implementation of
+      // invalidate ignores thumbnails that aren't visible.
+      this.previews.forEach(function (aPreview) {
+        let controller = aPreview.controller.wrappedJSObject;
+        if (!controller.testCacheBrowserDims()) {
+          controller.cacheBrowserDims();
+          aPreview.invalidate();
+        }
+      });
+    }, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
+  },
+
+  onResize: function () {
+    // Specific to a window.
+
+    // Call invalidate on each tab thumbnail so that Windows will request an
+    // updated image. However don't do this repeatedly across multiple resize
+    // events triggered during window border drags.
+
+    if (this.testCacheDims()) {
+      return;
+    }
+
+    // update the window dims on our TabWindow object.
+    this.cacheDims();
+
+    // invalidate soon
+    this.setInvalidationTimer();
+  },
+
+  invalidateTabPreview: function(aBrowser) {
+    for (let [tab, preview] of this.previews) {
+      if (aBrowser == tab.linkedBrowser) {
+        preview.invalidate();
+        break;
+      }
     }
   },
 
   //// Browser progress listener
+
+  onLocationChange: function (aBrowser) {
+    // I'm not sure we need this, onStateChange does a really good job
+    // of picking up page changes.
+    //this.invalidateTabPreview(aBrowser);
+  },
+
+  onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+    if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+      this.invalidateTabPreview(aBrowser);
+    }
+  },
+
   onLinkIconAvailable: function (aBrowser, aIconURL) {
     let self = this;
     getFaviconAsImage(
-      aBrowser.contentWindow.document,
-      aIconURL,PrivateBrowsingUtils.isWindowPrivate(this.win),
+      null,
+      aIconURL,
+      PrivateBrowsingUtils.isWindowPrivate(this.win),
       function (img) {
         let index = self.tabbrowser.browsers.indexOf(aBrowser);
         // Only add it if we've found the index.  The tab could have closed!
         if (index != -1) {
           let tab = self.tabbrowser.tabs[index];
           self.previews.get(tab).icon = img;
         }
       });
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -16,9 +16,9 @@ support-files =
 skip-if = e10s # Bug 666804 - Support NetworkPrioritizer in e10s
 [browser_SelfSupportBackend.js]
 support-files =
   ../../components/uitour/test/uitour.html
   ../../components/uitour/UITour-lib.js
 [browser_SignInToWebsite.js]
 skip-if = e10s # Bug 941426 - SignIntoWebsite.jsm not e10s friendly
 [browser_taskbar_preview.js]
-skip-if = os != win || e10s # Bug 666808 - AeroPeek support for e10s
+skip-if = os != "win"
--- a/browser/modules/test/browser_taskbar_preview.js
+++ b/browser/modules/test/browser_taskbar_preview.js
@@ -45,29 +45,16 @@ function test() {
   for (let preview of AeroPeek.previews)
     ok(preview.visible, "Preview is shown as expected after re-enabling");
 
   [1,2,3,4].forEach(function (idx) {
     gBrowser.selectedTab = gBrowser.tabs[idx];
     ok(checkSelectedTab(), "Current tab is correctly selected");
   });
 
-  let currentSelectedTab = gBrowser.selectedTab;
-  for (let idx of [1,2,3,4]) {
-    let preview = getPreviewForTab(gBrowser.tabs[0]);
-    let canvas = createThumbnailSurface(preview);
-    let ctx = canvas.getContext("2d");
-    preview.controller.drawThumbnail(ctx, canvas.width, canvas.height);
-    ok(currentSelectedTab.selected, "Drawing thumbnail does not change selection");
-    canvas = getCanvas(preview.controller.width, preview.controller.height);
-    ctx = canvas.getContext("2d");
-    preview.controller.drawPreview(ctx);
-    ok(currentSelectedTab.selected, "Drawing preview does not change selection");
-  }
-
   // Close #4
   getPreviewForTab(gBrowser.selectedTab).controller.onClose();
   checkPreviews(3, "Expected number of previews after closing selected tab via controller");
   ok(gBrowser.tabs.length == 3, "Successfully closed a tab");
 
   // Select #1
   ok(getPreviewForTab(gBrowser.tabs[0]).controller.onActivate(), "Activation was accepted");
   ok(gBrowser.tabs[0].selected, "Correct tab was selected");
@@ -112,31 +99,9 @@ function test() {
 
   function checkSelectedTab() {
     return getPreviewForTab(gBrowser.selectedTab).active;
   }
 
   function isTabSelected(idx) {
     return gBrowser.tabs[idx].selected;
   }
-
-  function createThumbnailSurface(p) {
-    let thumbnailWidth = 200,
-        thumbnailHeight = 120;
-    let ratio = p.controller.thumbnailAspectRatio;
-
-    if (thumbnailWidth/thumbnailHeight > ratio)
-      thumbnailWidth = thumbnailHeight * ratio;
-    else
-      thumbnailHeight = thumbnailWidth / ratio;
-
-    return getCanvas(thumbnailWidth, thumbnailHeight);
-  }
-
-  function getCanvas(width, height) {
-    let win = window.QueryInterface(Ci.nsIDOMWindow);
-    let doc = win.document;
-    let canvas = doc.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    canvas.width = width;
-    canvas.height = height;
-    return canvas;
-  }
 }
--- a/build/gen_test_packages_manifest.py
+++ b/build/gen_test_packages_manifest.py
@@ -14,43 +14,53 @@ ALL_HARNESSES = [
     'reftest',
     'webapprt',
     'xpcshell',
     'cppunittest',
     'jittest',
     'mozbase',
     'web-platform',
     'talos',
+    'gtest',
 ]
 
 PACKAGE_SPECIFIED_HARNESSES = [
     'cppunittest',
     'mochitest',
     'reftest',
     'xpcshell',
     'web-platform',
     'talos',
 ]
 
+# These packages are not present for every build configuration.
+OPTIONAL_PACKAGES = [
+    'gtest',
+]
+
 
 def parse_args():
     parser = ArgumentParser(description='Generate a test_packages.json file to tell automation which harnesses require which test packages.')
     parser.add_argument("--common", required=True,
                         action="store", dest="tests_common",
                         help="Name of the \"common\" archive, a package to be used by all harnesses.")
     parser.add_argument("--jsshell", required=True,
                         action="store", dest="jsshell",
                         help="Name of the jsshell zip.")
     parser.add_argument("--use-short-names", action="store_true",
                         help="Use short names for packages (target.$name.tests.zip "
                              "instead of $(PACKAGE_BASENAME).$name.tests.zip)")
     for harness in PACKAGE_SPECIFIED_HARNESSES:
         parser.add_argument("--%s" % harness, required=True,
                             action="store", dest=harness,
                             help="Name of the %s zip." % harness)
+    for harness in OPTIONAL_PACKAGES:
+        parser.add_argument("--%s" % harness, required=False,
+                            action="store", dest=harness,
+                            help="Name of the %s zip." % harness)
     parser.add_argument("--dest-file", required=True,
                         action="store", dest="destfile",
                         help="Path to the output file to be written.")
     return parser.parse_args()
 
 def generate_package_data(args):
     # Generate a dictionary mapping test harness names (exactly as they're known to
     # mozharness and testsuite-targets.mk, ideally) to the set of archive names that
@@ -61,18 +71,20 @@ def generate_package_data(args):
     tests_common = args.tests_common
     if args.use_short_names:
         tests_common = 'target.common.tests.zip'
 
     jsshell = args.jsshell
 
     harness_requirements = dict([(k, [tests_common]) for k in ALL_HARNESSES])
     harness_requirements['jittest'].append(jsshell)
-    for harness in PACKAGE_SPECIFIED_HARNESSES:
-        pkg_name = getattr(args, harness)
+    for harness in PACKAGE_SPECIFIED_HARNESSES + OPTIONAL_PACKAGES:
+        pkg_name = getattr(args, harness, None)
+        if pkg_name is None:
+            continue
         if args.use_short_names:
             pkg_name = 'target.%s.tests.zip' % harness
         harness_requirements[harness].append(pkg_name)
     return harness_requirements
 
 if __name__ == '__main__':
     args = parse_args()
     packages_data = generate_package_data(args)
--- a/config/external/nss/Makefile.in
+++ b/config/external/nss/Makefile.in
@@ -207,21 +207,24 @@ DEFAULT_GMAKE_FLAGS += \
 DEFAULT_GMAKE_FLAGS += ARCHFLAG='$(filter-out -W%,$(CFLAGS)) -DCHECK_FORK_GETPID $(addprefix -DANDROID_VERSION=,$(ANDROID_VERSION)) -include $(topsrcdir)/security/manager/android_stub.h'
 endif
 endif
 
 ifdef WRAP_LDFLAGS
 NSS_EXTRA_LDFLAGS += $(WRAP_LDFLAGS)
 endif
 
-ifdef MOZ_GLUE_WRAP_LDFLAGS
+# The SHARED_LIBS part is needed unconditionally on Android.  It's not
+# clear why this is the case, but see bug 1133073 (starting around
+# comment #8) for context.
+ifneq (,$(or $(MOZ_GLUE_WRAP_LDFLAGS), $(filter Android, $(OS_TARGET))))
 NSS_EXTRA_LDFLAGS += $(SHARED_LIBS:$(DEPTH)%=$(MOZ_BUILD_ROOT)%) $(MOZ_GLUE_WRAP_LDFLAGS)
 endif
 
-ifneq (,$(WRAP_LDFLAGS)$(MOZ_GLUE_WRAP_LDFLAGS))
+ifneq (,$(NSS_EXTRA_LDFLAGS))
 DEFAULT_GMAKE_FLAGS += \
 	LDFLAGS='$(LDFLAGS) $(NSS_EXTRA_LDFLAGS)' \
 	DSO_LDOPTS='$(DSO_LDOPTS) $(LDFLAGS) $(NSS_EXTRA_LDFLAGS)' \
 	$(NULL)
 endif
 
 DEFAULT_GMAKE_FLAGS += FREEBL_NO_DEPEND=0
 ifeq ($(OS_TARGET),Linux)
--- a/configure.in
+++ b/configure.in
@@ -7266,17 +7266,16 @@ if test -f "${srcdir}/${MOZ_BUILD_APP}/c
      "${srcdir}/${MOZ_BUILD_APP}/configure.in" > $tmpscript
   . $tmpscript
   rm -f $tmpscript
 fi
 
 dnl We need to wrap dlopen and related functions on Android because we use
 dnl our own linker.
 if test "$OS_TARGET" = Android; then
-    MOZ_GLUE_WRAP_LDFLAGS="${MOZ_GLUE_WRAP_LDFLAGS} -Wl,--wrap=PR_GetEnv,--wrap=PR_SetEnv"
     if test "$MOZ_WIDGET_TOOLKIT" = gonk -a -n "$MOZ_NUWA_PROCESS"; then
         MOZ_GLUE_WRAP_LDFLAGS="${MOZ_GLUE_WRAP_LDFLAGS} -Wl,--wrap=pthread_create,--wrap=epoll_wait,--wrap=poll,--wrap=pthread_cond_timedwait,--wrap=pthread_cond_wait,--wrap=epoll_create,--wrap=epoll_ctl,--wrap=close,--wrap=pthread_key_create,--wrap=pthread_key_delete,--wrap=socketpair,--wrap=pthread_self,--wrap=pthread_mutex_lock,--wrap=pthread_mutex_trylock,--wrap=pthread_join,--wrap=pipe,--wrap=pipe2"
     fi
     if test "$MOZ_WIDGET_TOOLKIT" = android -a "$MOZ_ANDROID_MIN_SDK_VERSION" -lt 11; then
         MOZ_GLUE_WRAP_LDFLAGS="${MOZ_GLUE_WRAP_LDFLAGS} -Wl,--wrap=getaddrinfo,--wrap=freeaddrinfo,--wrap=gai_strerror"
     fi
 fi
 
--- a/devtools/client/debugger/utils.js
+++ b/devtools/client/debugger/utils.js
@@ -109,17 +109,17 @@ var SourceUtils = {
   /**
    * Clears the labels, groups and minify cache, populated by methods like
    * SourceUtils.getSourceLabel or Source Utils.getSourceGroup.
    * This should be done every time the content location changes.
    */
   clearCache: function() {
     this._labelsCache.clear();
     this._groupsCache.clear();
-    this._minifiedCache.clear();
+    this._minifiedCache = new WeakMap();
   },
 
   /**
    * Gets a unique, simplified label from a source url.
    *
    * @param string aUrl
    *        The source url.
    * @return string
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -1504,17 +1504,16 @@ EventTooltip.prototype = {
 
       let boxes = container.querySelectorAll(".event-tooltip-content-box");
 
       for (let box of boxes) {
         let {editor} = this._tooltip.eventEditors.get(box);
         editor.destroy();
       }
 
-      this._tooltip.eventEditors.clear();
       this._tooltip.eventEditors = null;
     }
 
     let headerNodes = container.querySelectorAll(".event-header");
 
     for (let node of headerNodes) {
       node.removeEventListener("click", this._headerClicked);
     }
--- a/devtools/client/shared/widgets/VariablesView.jsm
+++ b/devtools/client/shared/widgets/VariablesView.jsm
@@ -150,17 +150,17 @@ VariablesView.prototype = {
    */
   empty: function(aTimeout = this.lazyEmptyDelay) {
     // If there are no items in this container, emptying is useless.
     if (!this._store.length) {
       return;
     }
 
     this._store.length = 0;
-    this._itemsByElement.clear();
+    this._itemsByElement = new WeakMap();
     this._prevHierarchy = this._currHierarchy;
     this._currHierarchy = new Map(); // Don't clear, this is just simple swapping.
 
     // Check if this empty operation may be executed lazily.
     if (this.lazyEmpty && aTimeout > 0) {
       this._emptySoon(aTimeout);
       return;
     }
--- a/docshell/base/SerializedLoadContext.h
+++ b/docshell/base/SerializedLoadContext.h
@@ -23,16 +23,21 @@ class nsIChannel;
 class nsIWebSocketChannel;
 
 namespace IPC {
 
 class SerializedLoadContext
 {
 public:
   SerializedLoadContext()
+    : mIsNotNull(false)
+    , mIsPrivateBitValid(false)
+    , mIsContent(false)
+    , mUsePrivateBrowsing(false)
+    , mUseRemoteTabs(false)
   {
     Init(nullptr);
   }
 
   explicit SerializedLoadContext(nsILoadContext* aLoadContext);
   explicit SerializedLoadContext(nsIChannel* aChannel);
   explicit SerializedLoadContext(nsIWebSocketChannel* aChannel);
 
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -228,16 +228,17 @@ EvictContentViewerForTransaction(nsISHTr
 }
 
 } // namespace
 
 nsSHistory::nsSHistory()
   : mIndex(-1)
   , mLength(0)
   , mRequestedIndex(-1)
+  , mRootDocShell(nullptr)
 {
   // Add this new SHistory object to the list
   PR_APPEND_LINK(this, &gSHistoryList);
 }
 
 nsSHistory::~nsSHistory()
 {
   // Remove this SHistory object from the list
@@ -999,30 +1000,30 @@ nsSHistory::EvictOutOfRangeWindowContent
 
 namespace {
 
 class TransactionAndDistance
 {
 public:
   TransactionAndDistance(nsISHTransaction* aTrans, uint32_t aDist)
     : mTransaction(aTrans)
+    , mLastTouched(0)
     , mDistance(aDist)
   {
     mViewer = GetContentViewerForTransaction(aTrans);
     NS_ASSERTION(mViewer, "Transaction should have a content viewer");
 
     nsCOMPtr<nsISHEntry> shentry;
     mTransaction->GetSHEntry(getter_AddRefs(shentry));
 
     nsCOMPtr<nsISHEntryInternal> shentryInternal = do_QueryInterface(shentry);
     if (shentryInternal) {
       shentryInternal->GetLastTouched(&mLastTouched);
     } else {
       NS_WARNING("Can't cast to nsISHEntryInternal?");
-      mLastTouched = 0;
     }
   }
 
   bool operator<(const TransactionAndDistance& aOther) const
   {
     // Compare distances first, and fall back to last-accessed times.
     if (aOther.mDistance != this->mDistance) {
       return this->mDistance < aOther.mDistance;
--- a/dom/alarm/AlarmHalService.h
+++ b/dom/alarm/AlarmHalService.h
@@ -28,16 +28,20 @@ class AlarmHalService : public nsIAlarmH
                         public AlarmObserver,
                         public SystemTimezoneChangeObserver,
                         public SystemClockChangeObserver
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIALARMHALSERVICE
 
+  AlarmHalService()
+    : mAlarmEnabled(false)
+  {}
+
   void Init();
 
   static already_AddRefed<AlarmHalService> GetInstance();
 
   // Implementing hal::AlarmObserver
   void Notify(const void_t& aVoid) override;
 
   // Implementing hal::SystemTimezoneChangeObserver
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -6,18 +6,16 @@
 
 #include "Animation.h"
 #include "AnimationUtils.h"
 #include "mozilla/dom/AnimationBinding.h"
 #include "mozilla/dom/AnimationPlaybackEvent.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/AsyncEventDispatcher.h" // For AsyncEventDispatcher
 #include "mozilla/Maybe.h" // For Maybe
-#include "AnimationCommon.h" // For AnimationCollection,
-                             // CommonAnimationManager
 #include "nsDOMMutationObserver.h" // For nsAutoAnimationMutationBatch
 #include "nsIDocument.h" // For nsIDocument
 #include "nsIPresShell.h" // For nsIPresShell
 #include "nsLayoutUtils.h" // For PostRestyleEvent (remove after bug 1073336)
 #include "nsThreadUtils.h" // For nsRunnableMethod and nsRevocableEventPtr
 #include "PendingAnimationTracker.h" // For PendingAnimationTracker
 
 namespace mozilla {
@@ -677,29 +675,22 @@ Animation::HasLowerCompositeOrderThan(co
   MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex || &aOther == this,
              "Animation indices should be unique");
 
   return mAnimationIndex < aOther.mAnimationIndex;
 }
 
 void
 Animation::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
-                        nsCSSPropertySet& aSetProperties,
-                        bool& aStyleChanging)
+                        nsCSSPropertySet& aSetProperties)
 {
   if (!mEffect) {
     return;
   }
 
-  AnimationPlayState playState = PlayState();
-  if (playState == AnimationPlayState::Running ||
-      playState == AnimationPlayState::Pending) {
-    aStyleChanging = true;
-  }
-
   if (!IsInEffect()) {
     return;
   }
 
   // In order to prevent flicker, there are a few cases where we want to use
   // a different time for rendering that would otherwise be returned by
   // GetCurrentTime. These are:
   //
@@ -729,16 +720,17 @@ Animation::ComposeStyle(RefPtr<AnimValue
   //     should use the current wallclock time to ensure the animation doesn't
   //     temporarily jump backwards.
   //
   // To address each of these cases we temporarily tweak the hold time
   // immediately before updating the style rule and then restore it immediately
   // afterwards. This is purely to prevent visual flicker. Other behavior
   // such as dispatching events continues to rely on the regular timeline time.
   bool updatedHoldTime = false;
+  AnimationPlayState playState = PlayState();
   {
     AutoRestore<Nullable<TimeDuration>> restoreHoldTime(mHoldTime);
 
     if (playState == AnimationPlayState::Pending &&
         mHoldTime.IsNull() &&
         !mStartTime.IsNull()) {
       Nullable<TimeDuration> timeToUse = mPendingReadyTime;
       if (timeToUse.IsNull() &&
@@ -1041,20 +1033,33 @@ Animation::FlushStyle() const
   if (doc) {
     doc->FlushPendingNotifications(Flush_Style);
   }
 }
 
 void
 Animation::PostUpdate()
 {
-  AnimationCollection* collection = GetCollection();
-  if (collection) {
-    collection->RequestRestyle(AnimationCollection::RestyleType::Layer);
+  nsPresContext* presContext = GetPresContext();
+  if (!presContext) {
+    return;
   }
+
+  Element* targetElement;
+  nsCSSPseudoElements::Type targetPseudoType;
+  mEffect->GetTarget(targetElement, targetPseudoType);
+  if (!targetElement) {
+    return;
+  }
+
+  presContext->EffectCompositor()
+             ->RequestRestyle(targetElement,
+                              targetPseudoType,
+                              EffectCompositor::RestyleType::Layer,
+                              CascadeLevel());
 }
 
 void
 Animation::CancelPendingTasks()
 {
   if (mPendingState == PendingState::NotPending) {
     return;
   }
@@ -1155,37 +1160,16 @@ Animation::GetPresContext() const
 {
   if (!mEffect) {
     return nullptr;
   }
 
   return mEffect->GetPresContext();
 }
 
-AnimationCollection*
-Animation::GetCollection() const
-{
-  CommonAnimationManager* manager = GetAnimationManager();
-  if (!manager) {
-    return nullptr;
-  }
-  MOZ_ASSERT(mEffect,
-             "An animation with an animation manager must have an effect");
-
-  Element* targetElement;
-  nsCSSPseudoElements::Type targetPseudoType;
-  mEffect->GetTarget(targetElement, targetPseudoType);
-  MOZ_ASSERT(targetElement,
-             "An animation with an animation manager must have a target");
-
-  return manager->GetAnimationCollection(targetElement,
-                                         targetPseudoType,
-                                         false /* aCreateIfNeeded */);
-}
-
 void
 Animation::DoFinishNotification(SyncNotifyFlag aSyncNotifyFlag)
 {
   if (aSyncNotifyFlag == SyncNotifyFlag::Sync) {
     DoFinishNotificationImmediately();
   } else if (!mFinishNotificationTask.IsPending()) {
     RefPtr<nsRunnableMethod<Animation>> runnable =
       NS_NewRunnableMethod(this, &Animation::DoFinishNotificationImmediately);
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -34,19 +34,17 @@
 
 struct JSContext;
 class nsCSSPropertySet;
 class nsIDocument;
 class nsPresContext;
 
 namespace mozilla {
 
-struct AnimationCollection;
 class AnimValuesStyleRule;
-class CommonAnimationManager;
 
 namespace dom {
 
 class CSSAnimation;
 class CSSTransition;
 
 class Animation
   : public DOMEventTargetHelper
@@ -302,28 +300,22 @@ public:
    * running on the compositor).
    */
   bool CanThrottle() const;
   /**
    * Updates |aStyleRule| with the animation values of this animation's effect,
    * if any.
    * Any properties already contained in |aSetProperties| are not changed. Any
    * properties that are changed are added to |aSetProperties|.
-   * |aStyleChanging| will be set to true if this animation expects to update
-   * the style rule on the next refresh driver tick as well (because it
-   * is running and has an effect to sample).
    */
   void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
-                    nsCSSPropertySet& aSetProperties,
-                    bool& aStyleChanging);
+                    nsCSSPropertySet& aSetProperties);
 
   void NotifyEffectTimingUpdated();
 
-  AnimationCollection* GetCollection() const;
-
 protected:
   void SilentlySetCurrentTime(const TimeDuration& aNewCurrentTime);
   void SilentlySetPlaybackRate(double aPlaybackRate);
   void DoCancel();
   void DoPlay(ErrorResult& aRv, LimitBehavior aLimitBehavior);
   void DoPause(ErrorResult& aRv);
   void ResumeAt(const TimeDuration& aReadyTime);
   void PauseAt(const TimeDuration& aReadyTime);
@@ -372,17 +364,16 @@ protected:
    */
   void CancelPendingTasks();
 
   bool IsPossiblyOrphanedPendingAnimation() const;
   StickyTimeDuration EffectEnd() const;
 
   nsIDocument* GetRenderedDocument() const;
   nsPresContext* GetPresContext() const;
-  virtual CommonAnimationManager* GetAnimationManager() const = 0;
 
   RefPtr<AnimationTimeline> mTimeline;
   RefPtr<KeyframeEffectReadOnly> mEffect;
   // The beginning of the delay period.
   Nullable<TimeDuration> mStartTime; // Timeline timescale
   Nullable<TimeDuration> mHoldTime;  // Animation timescale
   Nullable<TimeDuration> mPendingReadyTime; // Timeline timescale
   Nullable<TimeDuration> mPreviousCurrentTime; // Animation timescale
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -6,34 +6,54 @@
 
 #include "EffectCompositor.h"
 
 #include "mozilla/dom/Animation.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/KeyframeEffect.h" // For KeyframeEffectReadOnly
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/EffectSet.h"
+#include "mozilla/InitializerList.h"
 #include "mozilla/LayerAnimationInfo.h"
-#include "AnimationCommon.h" // For AnimationCollection
-#include "nsAnimationManager.h"
 #include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetPresShellForContent
 #include "nsCSSPropertySet.h"
 #include "nsCSSProps.h"
 #include "nsIPresShell.h"
 #include "nsLayoutUtils.h"
 #include "nsRuleNode.h" // For nsRuleNode::ComputePropertiesOverridingAnimation
 #include "nsTArray.h"
-#include "nsTransitionManager.h"
+#include "RestyleManager.h"
 
 using mozilla::dom::Animation;
 using mozilla::dom::Element;
 using mozilla::dom::KeyframeEffectReadOnly;
 
 namespace mozilla {
 
+NS_IMPL_CYCLE_COLLECTION_CLASS(EffectCompositor)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EffectCompositor)
+  for (auto& elementSet : tmp->mElementsToRestyle) {
+    elementSet.Clear();
+  }
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EffectCompositor)
+  for (auto& elementSet : tmp->mElementsToRestyle) {
+    for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+      CycleCollectionNoteChild(cb, iter.Key().mElement,
+                               "EffectCompositor::mElementsToRestyle[]",
+                               cb.Flags());
+    }
+  }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(EffectCompositor, AddRef)
+NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(EffectCompositor, Release)
+
 // Helper function to factor out the common logic from
 // GetAnimationsForCompositor and HasAnimationsForCompositor.
 //
 // Takes an optional array to fill with eligible animations.
 //
 // Returns true if there are eligible animations, false otherwise.
 bool
 FindAnimationsForCompositor(const nsIFrame* aFrame,
@@ -103,16 +123,242 @@ FindAnimationsForCompositor(const nsIFra
     foundSome = true;
   }
 
   MOZ_ASSERT(!foundSome || !aMatches || !aMatches->IsEmpty(),
              "If return value is true, matches array should be non-empty");
   return foundSome;
 }
 
+void
+EffectCompositor::RequestRestyle(dom::Element* aElement,
+                                 nsCSSPseudoElements::Type aPseudoType,
+                                 RestyleType aRestyleType,
+                                 CascadeLevel aCascadeLevel)
+{
+  if (!mPresContext) {
+    // Pres context will be null after the effect compositor is disconnected.
+    return;
+  }
+
+  auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+  PseudoElementHashKey key = { aElement, aPseudoType };
+
+  if (aRestyleType == RestyleType::Throttled &&
+      !elementsToRestyle.Contains(key)) {
+    elementsToRestyle.Put(key, false);
+    mPresContext->Document()->SetNeedStyleFlush();
+  } else {
+    // Get() returns 0 if the element is not found. It will also return
+    // false if the element is found but does not have a pending restyle.
+    bool hasPendingRestyle = elementsToRestyle.Get(key);
+    if (!hasPendingRestyle) {
+      PostRestyleForAnimation(aElement, aPseudoType, aCascadeLevel);
+    }
+    elementsToRestyle.Put(key, true);
+  }
+
+  if (aRestyleType == RestyleType::Layer) {
+    // Prompt layers to re-sync their animations.
+    mPresContext->RestyleManager()->IncrementAnimationGeneration();
+    EffectSet* effectSet =
+      EffectSet::GetEffectSet(aElement, aPseudoType);
+    if (effectSet) {
+      effectSet->UpdateAnimationGeneration(mPresContext);
+    }
+  }
+}
+
+void
+EffectCompositor::PostRestyleForAnimation(dom::Element* aElement,
+                                          nsCSSPseudoElements::Type aPseudoType,
+                                          CascadeLevel aCascadeLevel)
+{
+  if (!mPresContext) {
+    return;
+  }
+
+  dom::Element* element = GetElementToRestyle(aElement, aPseudoType);
+  if (!element) {
+    return;
+  }
+
+  nsRestyleHint hint = aCascadeLevel == CascadeLevel::Transitions ?
+                                        eRestyle_CSSTransitions :
+                                        eRestyle_CSSAnimations;
+  mPresContext->PresShell()->RestyleForAnimation(element, hint);
+}
+
+void
+EffectCompositor::PostRestyleForThrottledAnimations()
+{
+  for (size_t i = 0; i < kCascadeLevelCount; i++) {
+    CascadeLevel cascadeLevel = CascadeLevel(i);
+    auto& elementSet = mElementsToRestyle[cascadeLevel];
+
+    for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+      bool& postedRestyle = iter.Data();
+      if (postedRestyle) {
+        continue;
+      }
+
+      PostRestyleForAnimation(iter.Key().mElement,
+                              iter.Key().mPseudoType,
+                              cascadeLevel);
+      postedRestyle = true;
+    }
+  }
+}
+
+void
+EffectCompositor::MaybeUpdateAnimationRule(dom::Element* aElement,
+                                           nsCSSPseudoElements::Type
+                                             aPseudoType,
+                                           CascadeLevel aCascadeLevel)
+{
+  // First update cascade results since that may cause some elements to
+  // be marked as needing a restyle.
+  MaybeUpdateCascadeResults(aElement, aPseudoType);
+
+  auto& elementsToRestyle = mElementsToRestyle[aCascadeLevel];
+  PseudoElementHashKey key = { aElement, aPseudoType };
+
+  if (!mPresContext || !elementsToRestyle.Contains(key)) {
+    return;
+  }
+
+  ComposeAnimationRule(aElement, aPseudoType, aCascadeLevel,
+                       mPresContext->RefreshDriver()->MostRecentRefresh());
+
+  elementsToRestyle.Remove(key);
+}
+
+nsIStyleRule*
+EffectCompositor::GetAnimationRule(dom::Element* aElement,
+                                   nsCSSPseudoElements::Type aPseudoType,
+                                   CascadeLevel aCascadeLevel)
+{
+  if (!mPresContext || !mPresContext->IsDynamic()) {
+    // For print or print preview, ignore animations.
+    return nullptr;
+  }
+
+  EffectSet* effectSet = EffectSet::GetEffectSet(aElement, aPseudoType);
+  if (!effectSet) {
+    return nullptr;
+  }
+
+  if (mPresContext->RestyleManager()->SkipAnimationRules()) {
+    return nullptr;
+  }
+
+  MaybeUpdateAnimationRule(aElement, aPseudoType, aCascadeLevel);
+
+  return effectSet->AnimationRule(aCascadeLevel);
+}
+
+/* static */ dom::Element*
+EffectCompositor::GetElementToRestyle(dom::Element* aElement,
+                                      nsCSSPseudoElements::Type aPseudoType)
+{
+  if (aPseudoType == nsCSSPseudoElements::ePseudo_NotPseudoElement) {
+    return aElement;
+  }
+
+  nsIFrame* primaryFrame = aElement->GetPrimaryFrame();
+  if (!primaryFrame) {
+    return nullptr;
+  }
+  nsIFrame* pseudoFrame;
+  if (aPseudoType == nsCSSPseudoElements::ePseudo_before) {
+    pseudoFrame = nsLayoutUtils::GetBeforeFrame(primaryFrame);
+  } else if (aPseudoType == nsCSSPseudoElements::ePseudo_after) {
+    pseudoFrame = nsLayoutUtils::GetAfterFrame(primaryFrame);
+  } else {
+    NS_NOTREACHED("Should not try to get the element to restyle for a pseudo "
+                  "other that :before or :after");
+    return nullptr;
+  }
+  if (!pseudoFrame) {
+    return nullptr;
+  }
+  return pseudoFrame->GetContent()->AsElement();
+}
+
+bool
+EffectCompositor::HasPendingStyleUpdates() const
+{
+  for (auto& elementSet : mElementsToRestyle) {
+    if (elementSet.Count()) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+EffectCompositor::HasThrottledStyleUpdates() const
+{
+  for (auto& elementSet : mElementsToRestyle) {
+    for (auto iter = elementSet.ConstIter(); !iter.Done(); iter.Next()) {
+      if (!iter.Data()) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+void
+EffectCompositor::AddStyleUpdatesTo(RestyleTracker& aTracker)
+{
+  if (!mPresContext) {
+    return;
+  }
+
+  for (size_t i = 0; i < kCascadeLevelCount; i++) {
+    CascadeLevel cascadeLevel = CascadeLevel(i);
+    auto& elementSet = mElementsToRestyle[cascadeLevel];
+
+    // Copy the list of elements to restyle to a separate array that we can
+    // iterate over. This is because we need to call MaybeUpdateCascadeResults
+    // on each element, but doing that can mutate elementSet. In this case
+    // it will only mutate the bool value associated with each element in the
+    // set but even doing that will cause assertions in PLDHashTable to fail
+    // if we are iterating over the hashtable at the same time.
+    nsTArray<PseudoElementHashKey> elementsToRestyle(elementSet.Count());
+    for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
+      elementsToRestyle.AppendElement(iter.Key());
+    }
+
+    for (auto& pseudoElem : elementsToRestyle) {
+      MaybeUpdateCascadeResults(pseudoElem.mElement, pseudoElem.mPseudoType);
+
+      ComposeAnimationRule(pseudoElem.mElement,
+                           pseudoElem.mPseudoType,
+                           cascadeLevel,
+                           mPresContext->RefreshDriver()->MostRecentRefresh());
+
+      dom::Element* elementToRestyle =
+        GetElementToRestyle(pseudoElem.mElement, pseudoElem.mPseudoType);
+      if (elementToRestyle) {
+        nsRestyleHint rshint = cascadeLevel == CascadeLevel::Transitions ?
+                               eRestyle_CSSTransitions :
+                               eRestyle_CSSAnimations;
+        aTracker.AddPendingRestyle(elementToRestyle, rshint, nsChangeHint(0));
+      }
+    }
+
+    elementSet.Clear();
+    // Note: mElement pointers in elementsToRestyle might now dangle
+  }
+}
+
 /* static */ bool
 EffectCompositor::HasAnimationsForCompositor(const nsIFrame* aFrame,
                                              nsCSSProperty aProperty)
 {
   return FindAnimationsForCompositor(aFrame, aProperty, nullptr);
 }
 
 /* static */ nsTArray<RefPtr<dom::Animation>>
@@ -127,31 +373,64 @@ EffectCompositor::GetAnimationsForCompos
     FindAnimationsForCompositor(aFrame, aProperty, &result);
   MOZ_ASSERT(!foundSome || !result.IsEmpty(),
              "If return value is true, matches array should be non-empty");
 
   return result;
 }
 
 /* static */ void
+EffectCompositor::ClearIsRunningOnCompositor(const nsIFrame *aFrame,
+                                             nsCSSProperty aProperty)
+{
+  EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+  if (!effects) {
+    return;
+  }
+
+  for (KeyframeEffectReadOnly* effect : *effects) {
+    effect->SetIsRunningOnCompositor(aProperty, false);
+  }
+}
+
+/* static */ void
 EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
                                             nsCSSPseudoElements::Type
                                               aPseudoType,
                                             nsStyleContext* aStyleContext)
 {
   EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
   if (!effects || !effects->CascadeNeedsUpdate()) {
     return;
   }
 
   UpdateCascadeResults(*effects, aElement, aPseudoType, aStyleContext);
 
   MOZ_ASSERT(!effects->CascadeNeedsUpdate(), "Failed to update cascade state");
 }
 
+/* static */ void
+EffectCompositor::MaybeUpdateCascadeResults(Element* aElement,
+                                            nsCSSPseudoElements::Type
+                                              aPseudoType)
+{
+  nsStyleContext* styleContext = nullptr;
+  {
+    dom::Element* elementToRestyle = GetElementToRestyle(aElement, aPseudoType);
+    if (elementToRestyle) {
+      nsIFrame* frame = elementToRestyle->GetPrimaryFrame();
+      if (frame) {
+        styleContext = frame->StyleContext();
+      }
+    }
+  }
+
+  MaybeUpdateCascadeResults(aElement, aPseudoType, styleContext);
+}
+
 namespace {
   class EffectCompositeOrderComparator {
   public:
     bool Equals(const KeyframeEffectReadOnly* a,
                 const KeyframeEffectReadOnly* b) const
     {
       return a == b;
     }
@@ -223,17 +502,17 @@ EffectCompositor::GetAnimationElementAnd
 
   return result;
 }
 
 /* static */ void
 EffectCompositor::ComposeAnimationRule(dom::Element* aElement,
                                        nsCSSPseudoElements::Type aPseudoType,
                                        CascadeLevel aCascadeLevel,
-                                       bool& aStyleChanging)
+                                       TimeStamp aRefreshTime)
 {
   EffectSet* effects = EffectSet::GetEffectSet(aElement, aPseudoType);
   if (!effects) {
     return;
   }
 
   // The caller is responsible for calling MaybeUpdateCascadeResults first.
   MOZ_ASSERT(!effects->CascadeNeedsUpdate(),
@@ -248,29 +527,27 @@ EffectCompositor::ComposeAnimationRule(d
     }
   }
   sortedEffectList.Sort(EffectCompositeOrderComparator());
 
   RefPtr<AnimValuesStyleRule>& animationRule =
     effects->AnimationRule(aCascadeLevel);
   animationRule = nullptr;
 
-  // We'll set aStyleChanging to true below if necessary.
-  aStyleChanging = false;
-
   // If multiple animations specify behavior for the same property the
   // animation with the *highest* composite order wins.
   // As a result, we iterate from last animation to first and, if a
   // property has already been set, we don't change it.
   nsCSSPropertySet properties;
 
   for (KeyframeEffectReadOnly* effect : Reversed(sortedEffectList)) {
-    effect->GetAnimation()->ComposeStyle(animationRule, properties,
-                                         aStyleChanging);
+    effect->GetAnimation()->ComposeStyle(animationRule, properties);
   }
+
+  effects->UpdateAnimationRuleRefreshTime(aCascadeLevel, aRefreshTime);
 }
 
 /* static */ void
 EffectCompositor::GetOverriddenProperties(nsStyleContext* aStyleContext,
                                           EffectSet& aEffectSet,
                                           nsCSSPropertySet&
                                             aPropertiesOverridden)
 {
@@ -375,36 +652,24 @@ EffectCompositor::UpdateCascadeResults(E
   }
 
   aEffectSet.MarkCascadeUpdated();
 
   // If there is any change in the cascade result, update animations on
   // layers with the winning animations.
   nsPresContext* presContext = GetPresContext(aElement);
   if (changed && presContext) {
-    // We currently unconditionally update both animations and transitions
-    // even if we could, for example, get away with only updating animations.
-    // This is a temporary measure until we unify all animation style updating
-    // under EffectCompositor.
-    AnimationCollection* animations =
-      presContext->AnimationManager()->GetAnimationCollection(aElement,
-                                                              aPseudoType,
-                                                              false);
-                                                             /* don't create */
-    if (animations) {
-      animations->RequestRestyle(AnimationCollection::RestyleType::Layer);
-    }
-
-    AnimationCollection* transitions =
-      presContext->TransitionManager()->GetAnimationCollection(aElement,
-                                                               aPseudoType,
-                                                               false);
-                                                             /* don't create */
-    if (transitions) {
-      transitions->RequestRestyle(AnimationCollection::RestyleType::Layer);
+    // Update both transitions and animations. We could detect *which* levels
+    // actually changed and only update them, but that's probably unnecessary.
+    for (auto level : { CascadeLevel::Animations,
+                        CascadeLevel::Transitions }) {
+      presContext->EffectCompositor()->RequestRestyle(aElement,
+                                                      aPseudoType,
+                                                      RestyleType::Layer,
+                                                      level);
     }
   }
 }
 
 /* static */ nsPresContext*
 EffectCompositor::GetPresContext(Element* aElement)
 {
   MOZ_ASSERT(aElement);
--- a/dom/animation/EffectCompositor.h
+++ b/dom/animation/EffectCompositor.h
@@ -2,73 +2,160 @@
 /* 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_EffectCompositor_h
 #define mozilla_EffectCompositor_h
 
+#include "mozilla/EnumeratedArray.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/Pair.h"
+#include "mozilla/PseudoElementHashEntry.h"
 #include "mozilla/RefPtr.h"
 #include "nsCSSProperty.h"
 #include "nsCSSPseudoElements.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDataHashtable.h"
 #include "nsTArray.h"
 
 class nsCSSPropertySet;
 class nsIFrame;
+class nsIStyleRule;
 class nsPresContext;
 class nsStyleContext;
 
 namespace mozilla {
 
 class EffectSet;
+class RestyleTracker;
 
 namespace dom {
 class Animation;
 class Element;
 }
 
 class EffectCompositor
 {
 public:
+  explicit EffectCompositor(nsPresContext* aPresContext)
+    : mPresContext(aPresContext)
+  { }
+
+  NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor)
+  NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EffectCompositor)
+
+  void Disconnect() {
+    mPresContext = nullptr;
+  }
+
   // Animations can be applied at two different levels in the CSS cascade:
   enum class CascadeLevel {
     // The animations sheet (CSS animations, script-generated animations,
     // and CSS transitions that are no longer tied to CSS markup)
     Animations,
     // The transitions sheet (CSS transitions that are tied to CSS markup)
     Transitions
   };
   // We don't define this as part of CascadeLevel as then we'd have to add
   // explicit checks for the Count enum value everywhere CascadeLevel is used.
   static const size_t kCascadeLevelCount =
     static_cast<size_t>(CascadeLevel::Transitions) + 1;
 
+  // NOTE: This can return null after Disconnect().
+  nsPresContext* PresContext() const { return mPresContext; }
+
+  enum class RestyleType {
+    // Animation style has changed but the compositor is applying the same
+    // change so we might be able to defer updating the main thread until it
+    // becomes necessary.
+    Throttled,
+    // Animation style has changed and needs to be updated on the main thread.
+    Standard,
+    // Animation style has changed and needs to be updated on the main thread
+    // as well as forcing animations on layers to be updated.
+    // This is needed in cases such as when an animation becomes paused or has
+    // its playback rate changed. In such cases, although the computed style
+    // and refresh driver time might not change, we still need to ensure the
+    // corresponding animations on layers are updated to reflect the new
+    // configuration of the animation.
+    Layer
+  };
+
+  // Notifies the compositor that the animation rule for the specified
+  // (pseudo-)element at the specified cascade level needs to be updated.
+  // The specified steps taken to update the animation rule depend on
+  // |aRestyleType| whose values are described above.
+  void RequestRestyle(dom::Element* aElement,
+                      nsCSSPseudoElements::Type aPseudoType,
+                      RestyleType aRestyleType,
+                      CascadeLevel aCascadeLevel);
+
+  // Schedule an animation restyle. This is called automatically by
+  // RequestRestyle when necessary. However, it is exposed here since we also
+  // need to perform this step when triggering transitions *without* also
+  // invalidating the animation style rule (which RequestRestyle would do).
+  void PostRestyleForAnimation(dom::Element* aElement,
+                               nsCSSPseudoElements::Type aPseudoType,
+                               CascadeLevel aCascadeLevel);
+
+  // Posts an animation restyle for any elements whose animation style rule
+  // is out of date but for which an animation restyle has not yet been
+  // posted because updates on the main thread are throttled.
+  void PostRestyleForThrottledAnimations();
+
+  // Updates the animation rule stored on the EffectSet for the
+  // specified (pseudo-)element for cascade level |aLevel|.
+  // If the animation rule is not marked as needing an update,
+  // no work is done.
+  void MaybeUpdateAnimationRule(dom::Element* aElement,
+                                nsCSSPseudoElements::Type aPseudoType,
+                                CascadeLevel aCascadeLevel);
+
+  nsIStyleRule* GetAnimationRule(dom::Element* aElement,
+                                 nsCSSPseudoElements::Type aPseudoType,
+                                 CascadeLevel aCascadeLevel);
+
+  bool HasPendingStyleUpdates() const;
+  bool HasThrottledStyleUpdates() const;
+
+  // Tell the restyle tracker about all the animated styles that have
+  // pending updates so that it can update the animation rule for these
+  // elements.
+  void AddStyleUpdatesTo(RestyleTracker& aTracker);
+
   static bool HasAnimationsForCompositor(const nsIFrame* aFrame,
                                          nsCSSProperty aProperty);
 
   static nsTArray<RefPtr<dom::Animation>>
   GetAnimationsForCompositor(const nsIFrame* aFrame,
                              nsCSSProperty aProperty);
 
+  static void ClearIsRunningOnCompositor(const nsIFrame* aFrame,
+                                         nsCSSProperty aProperty);
 
   // Update animation cascade results for the specified (pseudo-)element
   // but only if we have marked the cascade as needing an update due a
   // the change in the set of effects or a change in one of the effects'
   // "in effect" state.
   //
   // This method does NOT detect if other styles that apply above the
   // animation level of the cascade have changed.
   static void
   MaybeUpdateCascadeResults(dom::Element* aElement,
                             nsCSSPseudoElements::Type aPseudoType,
                             nsStyleContext* aStyleContext);
 
+  // An overload of MaybeUpdateCascadeResults that uses the style context
+  // of the primary frame of the specified (pseudo-)element, when available.
+  static void
+  MaybeUpdateCascadeResults(dom::Element* aElement,
+                            nsCSSPseudoElements::Type aPseudoType);
+
   // Update the mWinsInCascade member for each property in effects targetting
   // the specified (pseudo-)element.
   //
   // This can be expensive so we should only call it if styles that apply
   // above the animation level of the cascade might have changed. For all
   // other cases we should call MaybeUpdateCascadeResults.
   static void
   UpdateCascadeResults(dom::Element* aElement,
@@ -82,36 +169,53 @@ public:
   // AnimationCollection), *not* the generated content.
   //
   // Returns an empty result when a suitable element cannot be found including
   // when the frame represents a pseudo-element on which we do not support
   // animations.
   static Maybe<Pair<dom::Element*, nsCSSPseudoElements::Type>>
   GetAnimationElementAndPseudoForFrame(const nsIFrame* aFrame);
 
+private:
+  ~EffectCompositor() = default;
+
   // Rebuilds the animation rule corresponding to |aCascadeLevel| on the
   // EffectSet associated with the specified (pseudo-)element.
   static void ComposeAnimationRule(dom::Element* aElement,
                                    nsCSSPseudoElements::Type aPseudoType,
                                    CascadeLevel aCascadeLevel,
-                                   bool& aStyleChanging);
+                                   TimeStamp aRefreshTime);
 
-private:
+  static dom::Element* GetElementToRestyle(dom::Element* aElement,
+                                           nsCSSPseudoElements::Type
+                                             aPseudoType);
+
   // Get the properties in |aEffectSet| that we are able to animate on the
   // compositor but which are also specified at a higher level in the cascade
   // than the animations level in |aStyleContext|.
   static void
   GetOverriddenProperties(nsStyleContext* aStyleContext,
                           EffectSet& aEffectSet,
                           nsCSSPropertySet& aPropertiesOverridden);
 
   static void
   UpdateCascadeResults(EffectSet& aEffectSet,
                        dom::Element* aElement,
                        nsCSSPseudoElements::Type aPseudoType,
                        nsStyleContext* aStyleContext);
 
   static nsPresContext* GetPresContext(dom::Element* aElement);
+
+  nsPresContext* mPresContext;
+
+  // Elements with a pending animation restyle. The associated bool value is
+  // true if a pending animation restyle has also been dispatched. For
+  // animations that can be throttled, we will add an entry to the hashtable to
+  // indicate that the style rule on the element is out of date but without
+  // posting a restyle to update it.
+  EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
+                  nsDataHashtable<PseudoElementHashEntry, bool>>
+                    mElementsToRestyle;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_EffectCompositor_h
--- a/dom/animation/EffectSet.h
+++ b/dom/animation/EffectSet.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_EffectSet_h
 #define mozilla_EffectSet_h
 
 #include "mozilla/AnimValuesStyleRule.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EnumeratedArray.h"
+#include "mozilla/TimeStamp.h"
 #include "nsCSSPseudoElements.h" // For nsCSSPseudoElements::Type
 #include "nsHashKeys.h" // For nsPtrHashKey
 #include "nsTHashtable.h" // For nsTHashtable
 
 class nsPresContext;
 
 namespace mozilla {
 
@@ -128,16 +129,28 @@ public:
   bool IsEmpty() const { return mEffects.IsEmpty(); }
 
   RefPtr<AnimValuesStyleRule>& AnimationRule(EffectCompositor::CascadeLevel
                                              aCascadeLevel)
   {
     return mAnimationRule[aCascadeLevel];
   }
 
+  const TimeStamp& AnimationRuleRefreshTime(EffectCompositor::CascadeLevel
+                                              aCascadeLevel) const
+  {
+    return mAnimationRuleRefreshTime[aCascadeLevel];
+  }
+  void UpdateAnimationRuleRefreshTime(EffectCompositor::CascadeLevel
+                                        aCascadeLevel,
+                                      const TimeStamp& aRefreshTime)
+  {
+    mAnimationRuleRefreshTime[aCascadeLevel] = aRefreshTime;
+  }
+
   bool CascadeNeedsUpdate() const { return mCascadeNeedsUpdate; }
   void MarkCascadeNeedsUpdate() { mCascadeNeedsUpdate = true; }
   void MarkCascadeUpdated() { mCascadeNeedsUpdate = false; }
 
   void UpdateAnimationGeneration(nsPresContext* aPresContext);
   uint64_t GetAnimationGeneration() const { return mAnimationGeneration; }
 
   static nsIAtom** GetEffectSetPropertyAtoms();
@@ -153,16 +166,25 @@ private:
   // style without animation, we need to not use them so that we can
   // detect any new changes; if necessary we restyle immediately
   // afterwards with animation.
   EnumeratedArray<EffectCompositor::CascadeLevel,
                   EffectCompositor::CascadeLevel(
                     EffectCompositor::kCascadeLevelCount),
                   RefPtr<AnimValuesStyleRule>> mAnimationRule;
 
+  // A parallel array to mAnimationRule that records the refresh driver
+  // timestamp when the rule was last updated. This is used for certain
+  // animations which are updated only periodically (e.g. transform animations
+  // running on the compositor that affect the scrollable overflow region).
+  EnumeratedArray<EffectCompositor::CascadeLevel,
+                  EffectCompositor::CascadeLevel(
+                    EffectCompositor::kCascadeLevelCount),
+                  TimeStamp> mAnimationRuleRefreshTime;
+
   // Dirty flag to represent when the mWinsInCascade flag on effects in
   // this set might need to be updated.
   //
   // Set to true any time the set of effects is changed or when
   // one the effects goes in or out of the "in effect" state.
   bool mCascadeNeedsUpdate;
 
   // RestyleManager keeps track of the number of animation restyles.
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -5,20 +5,20 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/KeyframeEffect.h"
 
 #include "mozilla/dom/AnimationEffectReadOnlyBinding.h"
 #include "mozilla/dom/KeyframeEffectBinding.h"
 #include "mozilla/dom/PropertyIndexedKeyframesBinding.h"
 #include "mozilla/AnimationUtils.h"
+#include "mozilla/EffectCompositor.h"
 #include "mozilla/FloatingPoint.h"
 #include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
 #include "mozilla/StyleAnimationValue.h"
-#include "AnimationCommon.h"
 #include "Layers.h" // For Layer
 #include "nsCSSParser.h"
 #include "nsCSSPropertySet.h"
 #include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
 #include "nsCSSValue.h"
 #include "nsStyleUtil.h"
 #include <algorithm> // For std::max
 
@@ -158,33 +158,39 @@ KeyframeEffectReadOnly::NotifyAnimationT
       if (effectSet) {
         effectSet->MarkCascadeNeedsUpdate();
       }
     }
     mInEffectOnLastAnimationTimingUpdate = inEffect;
   }
 
   // Request restyle if necessary.
-  ComputedTiming computedTiming = GetComputedTiming();
-  AnimationCollection* collection = GetCollection();
+  //
   // Bug 1235002: We should skip requesting a restyle when mProperties is empty.
   // However, currently we don't properly encapsulate mProperties so we can't
   // detect when it changes. As a result, if we skip requesting restyles when
   // mProperties is empty and we play an animation and *then* add properties to
   // it (as we currently do when building CSS animations), we will fail to
   // request a restyle at all. Since animations without properties are rare, we
   // currently just request the restyle regardless of whether mProperties is
   // empty or not.
-  if (collection &&
-      // Bug 1216843: When we implement iteration composite modes, we need to
-      // also detect if the current iteration has changed.
-      computedTiming.mProgress != mProgressOnLastCompose) {
-    collection->RequestRestyle(CanThrottle() ?
-                               AnimationCollection::RestyleType::Throttled :
-                               AnimationCollection::RestyleType::Standard);
+  //
+  // Bug 1216843: When we implement iteration composite modes, we need to
+  // also detect if the current iteration has changed.
+  if (mAnimation && GetComputedTiming().mProgress != mProgressOnLastCompose) {
+    EffectCompositor::RestyleType restyleType =
+      CanThrottle() ?
+      EffectCompositor::RestyleType::Throttled :
+      EffectCompositor::RestyleType::Standard;
+    nsPresContext* presContext = GetPresContext();
+    if (presContext) {
+      presContext->EffectCompositor()->
+        RequestRestyle(mTarget, mPseudoType, restyleType,
+                       mAnimation->CascadeLevel());
+    }
   }
 }
 
 Nullable<TimeDuration>
 KeyframeEffectReadOnly::GetLocalTime() const
 {
   // Since the *animation* start time is currently always zero, the local
   // time is equal to the parent time.
@@ -418,16 +424,41 @@ KeyframeEffectReadOnly::HasAnimationOfPr
     if (HasAnimationOfProperty(aProperties[i])) {
       return true;
     }
   }
   return false;
 }
 
 void
+KeyframeEffectReadOnly::CopyPropertiesFrom(const KeyframeEffectReadOnly& aOther)
+{
+  nsCSSPropertySet winningInCascadeProperties;
+  nsCSSPropertySet runningOnCompositorProperties;
+
+  for (const AnimationProperty& property : mProperties) {
+    if (property.mWinsInCascade) {
+      winningInCascadeProperties.AddProperty(property.mProperty);
+    }
+    if (property.mIsRunningOnCompositor) {
+      runningOnCompositorProperties.AddProperty(property.mProperty);
+    }
+  }
+
+  mProperties = aOther.mProperties;
+
+  for (AnimationProperty& property : mProperties) {
+    property.mWinsInCascade =
+      winningInCascadeProperties.HasProperty(property.mProperty);
+    property.mIsRunningOnCompositor =
+      runningOnCompositorProperties.HasProperty(property.mProperty);
+  }
+}
+
+void
 KeyframeEffectReadOnly::ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
                                      nsCSSPropertySet& aSetProperties)
 {
   ComputedTiming computedTiming = GetComputedTiming();
   mProgressOnLastCompose = computedTiming.mProgress;
 
   // If the progress is null, we don't have fill data for the current
   // time so we shouldn't animate.
@@ -445,17 +476,17 @@ KeyframeEffectReadOnly::ComposeStyle(Ref
   {
     const AnimationProperty& prop = mProperties[propIdx];
 
     MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
     MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
                "incorrect last to key");
 
     if (aSetProperties.HasProperty(prop.mProperty)) {
-      // Animations are composed by AnimationCollection by iterating
+      // Animations are composed by EffectCompositor by iterating
       // from the last animation to first. For animations targetting the
       // same property, the later one wins. So if this property is already set,
       // we should not override it.
       continue;
     }
 
     if (!prop.mWinsInCascade) {
       // This isn't the winning declaration, so don't add it to style.
@@ -1370,17 +1401,16 @@ BuildSegmentsFromValueEntries(nsTArray<K
     }
 
     // If we've moved on to a new property, create a new AnimationProperty
     // to insert segments into.
     if (aEntries[i].mProperty != lastProperty) {
       MOZ_ASSERT(aEntries[i].mOffset == 0.0f);
       animationProperty = aResult.AppendElement();
       animationProperty->mProperty = aEntries[i].mProperty;
-      animationProperty->mWinsInCascade = true;
       lastProperty = aEntries[i].mProperty;
     }
 
     MOZ_ASSERT(animationProperty, "animationProperty should be valid pointer.");
 
     // Now generate the segment.
     AnimationPropertySegment* segment =
       animationProperty->mSegments.AppendElement();
@@ -1572,17 +1602,16 @@ BuildAnimationPropertyListFromPropertyIn
         }
         MOZ_ASSERT(found, "properties is inconsistent with aResult");
       }
       if (!found) {
         // This is the first time we've encountered this property.
         animationPropertyIndexes[i] = aResult.Length();
         AnimationProperty* animationProperty = aResult.AppendElement();
         animationProperty->mProperty = p;
-        animationProperty->mWinsInCascade = true;
         properties.AddProperty(p);
       }
     }
 
     double portion = 1.0 / (count - 1);
     for (size_t i = 0; i < count - 1; ++i) {
       nsTArray<PropertyStyleAnimationValuePair> toValues;
       float toKey = (i + 1) * portion;
@@ -1902,23 +1931,26 @@ KeyframeEffectReadOnly::CanThrottleTrans
   nsPresContext* presContext = GetPresContext();
   // CanThrottleTransformChanges is only called as part of a refresh driver tick
   // in which case we expect to has a pres context.
   MOZ_ASSERT(presContext);
 
   TimeStamp now =
     presContext->RefreshDriver()->MostRecentRefresh();
 
-  AnimationCollection* collection = GetCollection();
-  MOZ_ASSERT(collection,
-    "CanThrottleTransformChanges should be involved with animation collection");
-  TimeStamp styleRuleRefreshTime = collection->mStyleRuleRefreshTime;
+  EffectSet* effectSet = EffectSet::GetEffectSet(mTarget, mPseudoType);
+  MOZ_ASSERT(effectSet, "CanThrottleTransformChanges is expected to be called"
+                        " on an effect in an effect set");
+  MOZ_ASSERT(mAnimation, "CanThrottleTransformChanges is expected to be called"
+                         " on an effect with a parent animation");
+  TimeStamp animationRuleRefreshTime =
+    effectSet->AnimationRuleRefreshTime(mAnimation->CascadeLevel());
   // If this animation can cause overflow, we can throttle some of the ticks.
-  if (!styleRuleRefreshTime.IsNull() &&
-      (now - styleRuleRefreshTime) < OverflowRegionRefreshInterval()) {
+  if (!animationRuleRefreshTime.IsNull() &&
+      (now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) {
     return true;
   }
 
   // If the nearest scrollable ancestor has overflow:hidden,
   // we don't care about overflow.
   nsIScrollableFrame* scrollable =
     nsLayoutUtils::GetNearestScrollableFrame(&aFrame);
   if (!scrollable) {
@@ -1980,22 +2012,16 @@ KeyframeEffectReadOnly::GetPresContext()
   }
   nsIPresShell* shell = doc->GetShell();
   if (!shell) {
     return nullptr;
   }
   return shell->GetPresContext();
 }
 
-AnimationCollection *
-KeyframeEffectReadOnly::GetCollection() const
-{
-  return mAnimation ? mAnimation->GetCollection() : nullptr;
-}
-
 /* static */ bool
 KeyframeEffectReadOnly::IsGeometricProperty(
   const nsCSSProperty aProperty)
 {
   switch (aProperty) {
     case eCSSProperty_bottom:
     case eCSSProperty_height:
     case eCSSProperty_left:
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -126,26 +126,30 @@ struct AnimationProperty
   // same property and element is not running, in which case we set this
   // to false so that the animation (lower in the cascade) can win.  We
   // then use this to decide whether to apply the style both in the CSS
   // cascade and for OMTA.
   //
   // For CSS Animations, which are overridden by !important rules in the
   // cascade, we actually determine this from the CSS cascade
   // computations, and then use it for OMTA.
+  //
   // **NOTE**: This member is not included when comparing AnimationProperty
   // objects for equality.
-  bool mWinsInCascade = true;
+  bool mWinsInCascade = false;
 
   // If true, the propery is currently being animated on the compositor.
   //
   // Note that when the owning Animation requests a non-throttled restyle, in
   // between calling RequestRestyle on its AnimationCollection and when the
   // restyle is performed, this member may temporarily become false even if
   // the animation remains on the layer after the restyle.
+  //
+  // **NOTE**: This member is not included when comparing AnimationProperty
+  // objects for equality.
   bool mIsRunningOnCompositor = false;
 
   InfallibleTArray<AnimationPropertySegment> mSegments;
 
   // NOTE: This operator does *not* compare the mWinsInCascade member *or* the
   // mIsRunningOnCompositor member.
   // This is because AnimationProperty objects are compared when recreating
   // CSS animations to determine if mutation observer change records need to
@@ -274,16 +278,20 @@ public:
   bool HasAnimationOfProperties(const nsCSSProperty* aProperties,
                                 size_t aPropertyCount) const;
   const InfallibleTArray<AnimationProperty>& Properties() const {
     return mProperties;
   }
   InfallibleTArray<AnimationProperty>& Properties() {
     return mProperties;
   }
+  // Copies the properties from another keyframe effect whilst preserving
+  // the mWinsInCascade and mIsRunningOnCompositor state of matching
+  // properties.
+  void CopyPropertiesFrom(const KeyframeEffectReadOnly& aOther);
 
   // Updates |aStyleRule| with the animation values produced by this
   // AnimationEffect for the current time except any properties already
   // contained in |aSetProperties|.
   // Any updated properties are added to |aSetProperties|.
   void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
                     nsCSSPropertySet& aSetProperties);
   // Returns true if at least one property is being animated on compositor.
new file mode 100644
--- /dev/null
+++ b/dom/animation/PseudoElementHashEntry.h
@@ -0,0 +1,60 @@
+/* -*- 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_PseudoElementHashEntry_h
+#define mozilla_PseudoElementHashEntry_h
+
+#include "mozilla/dom/Element.h"
+#include "mozilla/HashFunctions.h"
+#include "nsCSSPseudoElements.h"
+#include "PLDHashTable.h"
+
+namespace mozilla {
+
+struct PseudoElementHashKey
+{
+  dom::Element* mElement;
+  nsCSSPseudoElements::Type mPseudoType;
+};
+
+// A hash entry that uses a RefPtr<dom::Element>, nsCSSPseudoElements::Type pair
+class PseudoElementHashEntry : public PLDHashEntryHdr
+{
+public:
+  typedef PseudoElementHashKey KeyType;
+  typedef const PseudoElementHashKey* KeyTypePointer;
+
+  explicit PseudoElementHashEntry(KeyTypePointer aKey)
+    : mElement(aKey->mElement)
+    , mPseudoType(aKey->mPseudoType) { }
+  explicit PseudoElementHashEntry(const PseudoElementHashEntry& aCopy)=default;
+
+  ~PseudoElementHashEntry() = default;
+
+  KeyType GetKey() const { return {mElement, mPseudoType}; }
+  bool KeyEquals(KeyTypePointer aKey) const
+  {
+    return mElement == aKey->mElement &&
+           mPseudoType == aKey->mPseudoType;
+  }
+
+  static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+  static PLDHashNumber HashKey(KeyTypePointer aKey)
+  {
+    if (!aKey)
+      return 0;
+
+    return mozilla::HashGeneric(aKey->mElement, aKey->mPseudoType);
+  }
+  enum { ALLOW_MEMMOVE = true };
+
+  RefPtr<dom::Element> mElement;
+  nsCSSPseudoElements::Type mPseudoType;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PseudoElementHashEntry_h
--- a/dom/animation/moz.build
+++ b/dom/animation/moz.build
@@ -18,16 +18,17 @@ EXPORTS.mozilla.dom += [
 EXPORTS.mozilla += [
     'AnimationComparator.h',
     'AnimationUtils.h',
     'AnimValuesStyleRule.h',
     'ComputedTimingFunction.h',
     'EffectCompositor.h',
     'EffectSet.h',
     'PendingAnimationTracker.h',
+    'PseudoElementHashEntry.h',
 ]
 
 UNIFIED_SOURCES += [
     'Animation.cpp',
     'AnimationEffectReadOnly.cpp',
     'AnimationTimeline.cpp',
     'AnimationUtils.cpp',
     'AnimValuesStyleRule.cpp',
--- a/dom/apps/tests/test_packaged_app_update.html
+++ b/dom/apps/tests/test_packaged_app_update.html
@@ -146,17 +146,21 @@ var steps = [
     SpecialPowers.addPermission("webapps-manage", true, document);
     info("Set up");
     // Note that without useCurrentProfile the permissions just aren't added.
     SpecialPowers.pushPermissions(
       [{'type': 'permissions', 'allow': true, 'context': document}],
       function() {
         SpecialPowers.pushPrefEnv(
            {"set": [["dom.mozPermissionSettings.enabled", true],
-                    ["dom.webapps.useCurrentProfile", true]]},
+                    ["dom.webapps.useCurrentProfile", true],
+                    // This test resets a conection to simulate network error,
+                    // so we need to set the following pref to 0 that we do not
+                    // retry it in necko.
+                    ["network.http.request.max-attempts", 0]]},
            PackagedTestHelper.next);
       }
     );
   },
   function() {
     info("autoConfirmAppInstall");
     SpecialPowers.autoConfirmAppInstall(PackagedTestHelper.next);
   },
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1389,16 +1389,17 @@ Navigator::MozGetUserMedia(const MediaSt
   aRv = manager->GetUserMedia(mWindow, aConstraints, onsuccess, onerror);
 }
 
 void
 Navigator::MozGetUserMediaDevices(const MediaStreamConstraints& aConstraints,
                                   MozGetUserMediaDevicesSuccessCallback& aOnSuccess,
                                   NavigatorUserMediaErrorCallback& aOnError,
                                   uint64_t aInnerWindowID,
+                                  const nsAString& aCallID,
                                   ErrorResult& aRv)
 {
   CallbackObjectHolder<MozGetUserMediaDevicesSuccessCallback,
                        nsIGetUserMediaDevicesSuccessCallback> holder1(&aOnSuccess);
   nsCOMPtr<nsIGetUserMediaDevicesSuccessCallback> onsuccess =
     holder1.ToXPCOMCallback();
 
   CallbackObjectHolder<NavigatorUserMediaErrorCallback,
@@ -1408,17 +1409,17 @@ Navigator::MozGetUserMediaDevices(const 
   if (!mWindow || !mWindow->GetOuterWindow() ||
       mWindow->GetOuterWindow()->GetCurrentInnerWindow() != mWindow) {
     aRv.Throw(NS_ERROR_NOT_AVAILABLE);
     return;
   }
 
   MediaManager* manager = MediaManager::Get();
   aRv = manager->GetUserMediaDevices(mWindow, aConstraints, onsuccess, onerror,
-                                     aInnerWindowID);
+                                     aInnerWindowID, aCallID);
 }
 #endif
 
 DesktopNotificationCenter*
 Navigator::GetMozNotification(ErrorResult& aRv)
 {
   if (mNotification) {
     return mNotification;
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -292,16 +292,17 @@ public:
   void MozGetUserMedia(const MediaStreamConstraints& aConstraints,
                        NavigatorUserMediaSuccessCallback& aOnSuccess,
                        NavigatorUserMediaErrorCallback& aOnError,
                        ErrorResult& aRv);
   void MozGetUserMediaDevices(const MediaStreamConstraints& aConstraints,
                               MozGetUserMediaDevicesSuccessCallback& aOnSuccess,
                               NavigatorUserMediaErrorCallback& aOnError,
                               uint64_t aInnerWindowID,
+                              const nsAString& aCallID,
                               ErrorResult& aRv);
 #endif // MOZ_MEDIA_NAVIGATOR
 
   already_AddRefed<ServiceWorkerContainer> ServiceWorker();
 
   bool DoResolve(JSContext* aCx, JS::Handle<JSObject*> aObject,
                  JS::Handle<jsid> aId,
                  JS::MutableHandle<JSPropertyDescriptor> aDesc);
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1026,16 +1026,34 @@ nsDOMWindowUtils::SendNativeMouseEvent(i
     <LayoutDeviceIntPoint, int32_t, int32_t, nsIObserver*>
     (widget, &nsIWidget::SynthesizeNativeMouseEvent,
     LayoutDeviceIntPoint(aScreenX, aScreenY), aNativeMessage, aModifierFlags,
     aObserver));
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDOMWindowUtils::SendNativeMouseMove(int32_t aScreenX,
+                                      int32_t aScreenY,
+                                      nsIDOMElement* aElement,
+                                      nsIObserver* aObserver)
+{
+  // get the widget to send the event to
+  nsCOMPtr<nsIWidget> widget = GetWidgetForElement(aElement);
+  if (!widget)
+    return NS_ERROR_FAILURE;
+
+  NS_DispatchToMainThread(NS_NewRunnableMethodWithArgs
+    <LayoutDeviceIntPoint, nsIObserver*>
+    (widget, &nsIWidget::SynthesizeNativeMouseMove,
+    LayoutDeviceIntPoint(aScreenX, aScreenY), aObserver));
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDOMWindowUtils::SendNativeMouseScrollEvent(int32_t aScreenX,
                                              int32_t aScreenY,
                                              uint32_t aNativeMessage,
                                              double aDeltaX,
                                              double aDeltaY,
                                              double aDeltaZ,
                                              uint32_t aModifierFlags,
                                              uint32_t aAdditionalFlags,
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -685,18 +685,16 @@ skip-if = (buildapp == 'b2g' && toolkit 
 [test_bug809003.html]
 [test_bug810494.html]
 [test_bug811701.html]
 [test_bug811701.xhtml]
 [test_bug813919.html]
 [test_bug814576.html]
 [test_bug819051.html]
 [test_bug820909.html]
-[test_bug827160.html]
-skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' || e10s #needs plugin support # b2g(needs plugin support) b2g-debug(debug-only failure) b2g-desktop(needs plugin support)
 [test_bug840098.html]
 [test_bug864595.html]
 [test_bug868999.html]
 [test_bug869000.html]
 [test_bug869002.html]
 [test_bug869006.html]
 [test_bug876282.html]
 [test_bug890580.html]
@@ -772,18 +770,16 @@ skip-if = (os != 'b2g' && os != 'android
 [test_meta_viewport7.html]
 skip-if = (os != 'b2g' && os != 'android')    # meta-viewport tag support is mobile-only
 [test_mozfiledataurl.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' || e10s #TIMED_OUT
 [test_mozMatchesSelector.html]
 [test_mutationobservers.html]
 skip-if = buildapp == 'b2g' # b2g(bug 901385, showmodaldialog) b2g-debug(bug 901385, showmodaldialog) b2g-desktop(bug 901385, showmodaldialog)
 [test_nodelist_holes.html]
-[test_object.html]
-skip-if = buildapp == 'mulet' || buildapp == 'b2g' || toolkit == 'android' || e10s # b2g(needs plugin support) b2g-debug(needs plugin support) b2g-desktop(needs plugin support)
 [test_plugin_freezing.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #CLICK_TO_PLAY
 [test_processing_instruction_update_stylesheet.xhtml]
 [test_progress_events_for_gzip_data.html]
 [test_range_bounds.html]
 skip-if = toolkit == 'android'
 [test_reentrant_flush.html]
 skip-if = toolkit == 'android'
--- a/dom/bindings/BindingUtils.h
+++ b/dom/bindings/BindingUtils.h
@@ -306,17 +306,17 @@ class ProtoAndIfaceCache
     }
 
     JS::Heap<JSObject*>& EntrySlotMustExist(size_t i) {
       return (*this)[i];
     }
 
     void Trace(JSTracer* aTracer) {
       for (size_t i = 0; i < ArrayLength(*this); ++i) {
-        JS::TraceNullableEdge(aTracer, &(*this)[i], "protoAndIfaceCache[i]");
+        JS::TraceEdge(aTracer, &(*this)[i], "protoAndIfaceCache[i]");
       }
     }
 
     size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) {
       return aMallocSizeOf(this);
     }
   };
 
@@ -365,17 +365,17 @@ class ProtoAndIfaceCache
       return (*p)[leafIndex];
     }
 
     void Trace(JSTracer* trc) {
       for (size_t i = 0; i < ArrayLength(mPages); ++i) {
         Page* p = mPages[i];
         if (p) {
           for (size_t j = 0; j < ArrayLength(*p); ++j) {
-            JS::TraceNullableEdge(trc, &(*p)[j], "protoAndIfaceCache[i]");
+            JS::TraceEdge(trc, &(*p)[j], "protoAndIfaceCache[i]");
           }
         }
       }
     }
 
     size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) {
       size_t n = aMallocSizeOf(this);
       for (size_t i = 0; i < ArrayLength(mPages); ++i) {
--- a/dom/bindings/TypedArray.h
+++ b/dom/bindings/TypedArray.h
@@ -39,22 +39,18 @@ protected:
   {
     aOther.mTypedObj = nullptr;
     aOther.mWrappedObj = nullptr;
   }
 
 public:
   inline void TraceSelf(JSTracer* trc)
   {
-    if (mTypedObj) {
-      JS::UnsafeTraceRoot(trc, &mTypedObj, "TypedArray.mTypedObj");
-    }
-    if (mWrappedObj) {
-      JS::UnsafeTraceRoot(trc, &mTypedObj, "TypedArray.mWrappedObj");
-    }
+    JS::UnsafeTraceRoot(trc, &mTypedObj, "TypedArray.mTypedObj");
+    JS::UnsafeTraceRoot(trc, &mTypedObj, "TypedArray.mWrappedObj");
   }
 
 private:
   TypedArrayObjectStorage(const TypedArrayObjectStorage&) = delete;
 };
 
 /*
  * Various typed array classes for argument conversion.  We have a base class
--- a/dom/bluetooth/common/webapi/BluetoothPbapRequestHandle.h
+++ b/dom/bluetooth/common/webapi/BluetoothPbapRequestHandle.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_bluetooth_bluetoothpbaprequesthandle_h
 #define mozilla_dom_bluetooth_bluetoothpbaprequesthandle_h
 
 #include "nsCOMPtr.h"
+#include "mozilla/dom/bluetooth/BluetoothCommon.h"
 #include "mozilla/dom/DOMRequest.h"
 #include "mozilla/dom/BlobSet.h"
 
 namespace mozilla {
   class ErrorResult;
   namespace dom {
     class Blob;
     class DOMRequest;
--- a/dom/broadcastchannel/BroadcastChannelChild.cpp
+++ b/dom/broadcastchannel/BroadcastChannelChild.cpp
@@ -22,17 +22,18 @@ namespace mozilla {
 
 using namespace ipc;
 
 namespace dom {
 
 using namespace workers;
 
 BroadcastChannelChild::BroadcastChannelChild(const nsACString& aOrigin)
-  : mActorDestroyed(false)
+  : mBC(nullptr)
+  , mActorDestroyed(false)
 {
   CopyUTF8toUTF16(aOrigin, mOrigin);
 }
 
 BroadcastChannelChild::~BroadcastChannelChild()
 {
   MOZ_ASSERT(!mBC);
 }
--- a/dom/canvas/CanvasRenderingContext2D.h
+++ b/dom/canvas/CanvasRenderingContext2D.h
@@ -902,16 +902,17 @@ protected:
 
   bool CheckSizeForSkiaGL(mozilla::gfx::IntSize size);
 
   // state stack handling
   class ContextState {
   public:
     ContextState() : textAlign(TextAlign::START),
                      textBaseline(TextBaseline::ALPHABETIC),
+                     shadowColor(0),
                      lineWidth(1.0f),
                      miterLimit(10.0f),
                      globalAlpha(1.0f),
                      shadowBlur(0.0),
                      dashOffset(0.0f),
                      op(mozilla::gfx::CompositionOp::OP_OVER),
                      fillRule(mozilla::gfx::FillRule::FILL_WINDING),
                      lineCap(mozilla::gfx::CapStyle::BUTT),
--- a/dom/canvas/WebGLContext.cpp
+++ b/dom/canvas/WebGLContext.cpp
@@ -96,43 +96,64 @@ WebGLContextOptions::WebGLContextOptions
 }
 
 
 /*static*/ const uint32_t WebGLContext::kMinMaxColorAttachments = 4;
 /*static*/ const uint32_t WebGLContext::kMinMaxDrawBuffers = 4;
 
 WebGLContext::WebGLContext()
     : WebGLContextUnchecked(nullptr)
+    , mBufferFetchingIsVerified(false)
+    , mBufferFetchingHasPerVertex(false)
+    , mMaxFetchedVertices(0)
+    , mMaxFetchedInstances(0)
     , mBypassShaderValidation(false)
     , mGLMaxSamples(1)
     , mNeedsFakeNoAlpha(false)
     , mNeedsFakeNoDepth(false)
     , mNeedsFakeNoStencil(false)
 {
     mGeneration = 0;
     mInvalidated = false;
     mCapturedFrameInvalidated = false;
     mShouldPresent = true;
     mResetLayer = true;
     mOptionsFrozen = false;
+    mMinCapability = false;
+    mDisableExtensions = false;
+    mIsMesa = false;
+    mEmitContextLostErrorOnce = false;
+    mWebGLError = 0;
+    mUnderlyingGLError = 0;
 
     mActiveTexture = 0;
 
     mVertexAttrib0Vector[0] = 0;
     mVertexAttrib0Vector[1] = 0;
     mVertexAttrib0Vector[2] = 0;
     mVertexAttrib0Vector[3] = 1;
     mFakeVertexAttrib0BufferObjectVector[0] = 0;
     mFakeVertexAttrib0BufferObjectVector[1] = 0;
     mFakeVertexAttrib0BufferObjectVector[2] = 0;
     mFakeVertexAttrib0BufferObjectVector[3] = 1;
     mFakeVertexAttrib0BufferObjectSize = 0;
     mFakeVertexAttrib0BufferObject = 0;
     mFakeVertexAttrib0BufferStatus = WebGLVertexAttrib0Status::Default;
 
+    mStencilRefFront = 0;
+    mStencilRefBack = 0;
+    mStencilValueMaskFront = 0;
+    mStencilValueMaskBack = 0;
+    mStencilWriteMaskFront = 0;
+    mStencilWriteMaskBack = 0;
+    mDepthWriteMask = 0;
+    mStencilClearValue = 0;
+    mDepthClearValue = 0;
+    mContextLostErrorSet = false;
+
     mViewportX = 0;
     mViewportY = 0;
     mViewportWidth = 0;
     mViewportHeight = 0;
 
     mDitherEnabled = 1;
     mRasterizerDiscardEnabled = 0; // OpenGL ES 3.0 spec p244
     mScissorTestEnabled = 0;
--- a/dom/canvas/WebGLFramebuffer.cpp
+++ b/dom/canvas/WebGLFramebuffer.cpp
@@ -15,16 +15,18 @@
 #include "WebGLTexture.h"
 
 namespace mozilla {
 
 WebGLFBAttachPoint::WebGLFBAttachPoint(WebGLFramebuffer* fb, GLenum attachmentPoint)
     : mFB(fb)
     , mAttachmentPoint(attachmentPoint)
     , mTexImageTarget(LOCAL_GL_NONE)
+    , mTexImageLayer(0)
+    , mTexImageLevel(0)
 { }
 
 WebGLFBAttachPoint::~WebGLFBAttachPoint()
 {
     MOZ_ASSERT(!mRenderbufferPtr);
     MOZ_ASSERT(!mTexturePtr);
 }
 
--- a/dom/datastore/DataStoreService.cpp
+++ b/dom/datastore/DataStoreService.cpp
@@ -74,16 +74,18 @@ public:
     , mEnabled(false)
   {}
 
   DataStoreInfo(const nsAString& aName,
                 const nsAString& aOriginURL,
                 const nsAString& aManifestURL,
                 bool aReadOnly,
                 bool aEnabled)
+    : mReadOnly(true)
+    , mEnabled(false)
   {
     Init(aName, aOriginURL, aManifestURL, aReadOnly, aEnabled);
   }
 
   void Init(const nsAString& aName,
             const nsAString& aOriginURL,
             const nsAString& aManifestURL,
             bool aReadOnly,
--- a/dom/events/AsyncEventDispatcher.h
+++ b/dom/events/AsyncEventDispatcher.h
@@ -50,16 +50,17 @@ public:
     , mBubbles(aBubbles)
     , mOnlyChromeDispatch(false)
   {
   }
 
   AsyncEventDispatcher(dom::EventTarget* aTarget, nsIDOMEvent* aEvent)
     : mTarget(aTarget)
     , mEvent(aEvent)
+    , mBubbles(false)
     , mOnlyChromeDispatch(false)
   {
   }
 
   AsyncEventDispatcher(dom::EventTarget* aTarget, WidgetEvent& aEvent);
 
   NS_IMETHOD Run() override;
   nsresult PostDOMEvent();
--- a/dom/events/JSEventHandler.h
+++ b/dom/events/JSEventHandler.h
@@ -32,26 +32,29 @@ public:
   };
 
   TypedEventHandler()
     : mBits(0)
   {
   }
 
   explicit TypedEventHandler(dom::EventHandlerNonNull* aHandler)
+    : mBits(0)
   {
     Assign(aHandler, eNormal);
   }
 
   explicit TypedEventHandler(dom::OnErrorEventHandlerNonNull* aHandler)
+    : mBits(0)
   {
     Assign(aHandler, eOnError);
   }
 
   explicit TypedEventHandler(dom::OnBeforeUnloadEventHandlerNonNull* aHandler)
+    : mBits(0)
   {
     Assign(aHandler, eOnBeforeUnload);
   }
 
   TypedEventHandler(const TypedEventHandler& aOther)
   {
     if (aOther.HasEventHandler()) {
       // Have to make sure we take our own ref
--- a/dom/events/TextComposition.h
+++ b/dom/events/TextComposition.h
@@ -251,17 +251,30 @@ private:
   // Allow control characters appear in composition string.
   // When this is false, control characters except
   // CHARACTER TABULATION (horizontal tab) are removed from
   // both composition string and data attribute of compositionupdate
   // and compositionend events.
   bool mAllowControlCharacters;
 
   // Hide the default constructor and copy constructor.
-  TextComposition() {}
+  TextComposition()
+    : mPresContext(nullptr)
+    , mNativeContext(nullptr)
+    , mCompositionStartOffset(0)
+    , mCompositionTargetOffset(0)
+    , mIsSynthesizedForTests(false)
+    , mIsComposing(false)
+    , mIsEditorHandlingEvent(false)
+    , mIsRequestingCommit(false)
+    , mIsRequestingCancel(false)
+    , mRequestedToCommitOrCancel(false)
+    , mWasNativeCompositionEndEventDiscarded(false)
+    , mAllowControlCharacters(false)
+  {}
   TextComposition(const TextComposition& aOther);
 
   /**
    * GetEditor() returns nsIEditor pointer of mEditorWeak.
    */
   already_AddRefed<nsIEditor> GetEditor() const;
 
   /**
@@ -379,17 +392,17 @@ private:
 
   private:
     RefPtr<TextComposition> mTextComposition;
     nsCOMPtr<nsINode> mEventTarget;
     nsString mData;
     EventMessage mEventMessage;
     bool mIsSynthesizedEvent;
 
-    CompositionEventDispatcher() {};
+    CompositionEventDispatcher() : mIsSynthesizedEvent(false) {};
   };
 
   /**
    * DispatchCompositionEventRunnable() dispatches a composition event to the
    * content.  Be aware, if you use this method, nsPresShellEventCB isn't used.
    * That means that nsIFrame::HandleEvent() is never called.
    * WARNING: The instance which is managed by IMEStateManager may be
    *          destroyed by this method call.
--- a/dom/fetch/InternalRequest.cpp
+++ b/dom/fetch/InternalRequest.cpp
@@ -81,17 +81,16 @@ InternalRequest::InternalRequest(const I
   , mCredentialsMode(aOther.mCredentialsMode)
   , mResponseTainting(aOther.mResponseTainting)
   , mCacheMode(aOther.mCacheMode)
   , mRedirectMode(aOther.mRedirectMode)
   , mAuthenticationFlag(aOther.mAuthenticationFlag)
   , mForceOriginHeader(aOther.mForceOriginHeader)
   , mPreserveContentCodings(aOther.mPreserveContentCodings)
   , mSameOriginDataURL(aOther.mSameOriginDataURL)
-  , mSandboxedStorageAreaURLs(aOther.mSandboxedStorageAreaURLs)
   , mSkipServiceWorker(aOther.mSkipServiceWorker)
   , mSynchronous(aOther.mSynchronous)
   , mUnsafeRequest(aOther.mUnsafeRequest)
   , mUseURLCredentials(aOther.mUseURLCredentials)
   , mCreatedByFetchEvent(aOther.mCreatedByFetchEvent)
 {
   // NOTE: does not copy body stream... use the fallible Clone() for that
 }
--- a/dom/fetch/InternalRequest.h
+++ b/dom/fetch/InternalRequest.h
@@ -86,16 +86,17 @@ class InternalRequest final
   friend class Request;
 
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InternalRequest)
 
   explicit InternalRequest()
     : mMethod("GET")
     , mHeaders(new InternalHeaders(HeadersGuardEnum::None))
+    , mContentPolicyType(nsIContentPolicy::TYPE_FETCH)
     , mReferrer(NS_LITERAL_STRING(kFETCH_CLIENT_REFERRER_STR))
     , mMode(RequestMode::No_cors)
     , mCredentialsMode(RequestCredentials::Omit)
     , mResponseTainting(LoadTainting::Basic)
     , mCacheMode(RequestCache::Default)
     , mRedirectMode(RequestRedirect::Follow)
     , mAuthenticationFlag(false)
     , mForceOriginHeader(false)
@@ -408,17 +409,16 @@ private:
   LoadTainting mResponseTainting;
   RequestCache mCacheMode;
   RequestRedirect mRedirectMode;
 
   bool mAuthenticationFlag;
   bool mForceOriginHeader;
   bool mPreserveContentCodings;
   bool mSameOriginDataURL;
-  bool mSandboxedStorageAreaURLs;
   bool mSkipServiceWorker;
   bool mSynchronous;
   bool mUnsafeRequest;
   bool mUseURLCredentials;
   // This is only set when a Request object is created by a fetch event.  We
   // use it to check if Service Workers are simply fetching intercepted Request
   // objects without modifying them.
   bool mCreatedByFetchEvent = false;
--- a/dom/fetch/InternalResponse.cpp
+++ b/dom/fetch/InternalResponse.cpp
@@ -127,16 +127,26 @@ InternalResponse::GetTainting() const
     case ResponseType::Opaque:
       return LoadTainting::Opaque;
     default:
       return LoadTainting::Basic;
   }
 }
 
 already_AddRefed<InternalResponse>
+InternalResponse::Unfiltered()
+{
+  RefPtr<InternalResponse> ref = mWrappedResponse;
+  if (!ref) {
+    ref = this;
+  }
+  return ref.forget();
+}
+
+already_AddRefed<InternalResponse>
 InternalResponse::OpaqueResponse()
 {
   MOZ_ASSERT(!mWrappedResponse, "Can't OpaqueResponse a already wrapped response");
   RefPtr<InternalResponse> response = new InternalResponse(0, EmptyCString());
   response->mType = ResponseType::Opaque;
   response->mTerminationReason = mTerminationReason;
   response->mChannelInfo = mChannelInfo;
   if (mPrincipalInfo) {
--- a/dom/fetch/InternalResponse.h
+++ b/dom/fetch/InternalResponse.h
@@ -216,16 +216,19 @@ public:
   SetPrincipalInfo(UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo);
 
   nsresult
   StripFragmentAndSetUrl(const nsACString& aUrl);
 
   LoadTainting
   GetTainting() const;
 
+  already_AddRefed<InternalResponse>
+  Unfiltered();
+
 private:
   ~InternalResponse();
 
   explicit InternalResponse(const InternalResponse& aOther) = delete;
   InternalResponse& operator=(const InternalResponse&) = delete;
 
   // Returns an instance of InternalResponse which is a copy of this
   // InternalResponse, except headers, body and wrapped response (if any) which
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -233,16 +233,30 @@ Response::Clone(ErrorResult& aRv) const
     return nullptr;
   }
 
   RefPtr<InternalResponse> ir = mInternalResponse->Clone();
   RefPtr<Response> response = new Response(mOwner, ir);
   return response.forget();
 }
 
+already_AddRefed<Response>
+Response::CloneUnfiltered(ErrorResult& aRv) const
+{
+  if (BodyUsed()) {
+    aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>();
+    return nullptr;
+  }
+
+  RefPtr<InternalResponse> clone = mInternalResponse->Clone();
+  RefPtr<InternalResponse> ir = clone->Unfiltered();
+  RefPtr<Response> ref = new Response(mOwner, ir);
+  return ref.forget();
+}
+
 void
 Response::SetBody(nsIInputStream* aBody)
 {
   MOZ_ASSERT(!BodyUsed());
   mInternalResponse->SetBody(aBody);
 }
 
 already_AddRefed<InternalResponse>
--- a/dom/fetch/Response.h
+++ b/dom/fetch/Response.h
@@ -119,16 +119,19 @@ public:
   nsIGlobalObject* GetParentObject() const
   {
     return mOwner;
   }
 
   already_AddRefed<Response>
   Clone(ErrorResult& aRv) const;
 
+  already_AddRefed<Response>
+  CloneUnfiltered(ErrorResult& aRv) const;
+
   void
   SetBody(nsIInputStream* aBody);
 
   already_AddRefed<InternalResponse>
   GetInternalResponse() const;
 
 private:
   ~Response();
--- a/dom/gamepad/Gamepad.cpp
+++ b/dom/gamepad/Gamepad.cpp
@@ -41,17 +41,18 @@ Gamepad::Gamepad(nsISupports* aParent,
                  GamepadMappingType aMapping,
                  uint32_t aNumButtons, uint32_t aNumAxes)
   : mParent(aParent),
     mID(aID),
     mIndex(aIndex),
     mMapping(aMapping),
     mConnected(true),
     mButtons(aNumButtons),
-    mAxes(aNumAxes)
+    mAxes(aNumAxes),
+    mTimestamp(0)
 {
   for (unsigned i = 0; i < aNumButtons; i++) {
     mButtons.InsertElementAt(i, new GamepadButton(mParent));
   }
   mAxes.InsertElementsAt(0, aNumAxes, 0.0f);
   UpdateTimestamp();
 }
 
--- a/dom/geolocation/nsGeolocationSettings.h
+++ b/dom/geolocation/nsGeolocationSettings.h
@@ -97,17 +97,24 @@ public:
 #ifdef MOZ_APPROX_LOCATION
   inline int32_t GetApproxDistance() const { return mDistance; }
 #endif
   inline double GetFixedLatitude() const { return mLatitude; }
   inline double GetFixedLongitude() const { return mLongitude; }
   inline const nsString& GetOrigin() const { return mOrigin; }
 
 private:
-  GeolocationSetting() {} // can't default construct
+  GeolocationSetting() :
+#ifdef MOZ_APPROX_LOCATION
+    mDistance(0),
+#endif
+    mLatitude(0),
+    mLongitude(0)
+  {} // can't default construct
+
   GeolocationFuzzMethod mFuzzMethod;
 #ifdef MOZ_APPROX_LOCATION
   int32_t         mDistance;
 #endif
   double          mLatitude,
                   mLongitude;
   nsString        mOrigin;
 };
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -381,17 +381,18 @@ HTMLInputElement::nsFilePickerShownCallb
 
   int16_t mode;
   mFilePicker->GetMode(&mode);
 
   // Collect new selected filenames
   nsTArray<RefPtr<File>> newFiles;
   if (mode == static_cast<int16_t>(nsIFilePicker::modeOpenMultiple)) {
     nsCOMPtr<nsISimpleEnumerator> iter;
-    nsresult rv = mFilePicker->GetDomfiles(getter_AddRefs(iter));
+    nsresult rv =
+      mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter));
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (!iter) {
       return NS_OK;
     }
 
     nsCOMPtr<nsISupports> tmp;
     bool hasMore = true;
@@ -404,17 +405,17 @@ HTMLInputElement::nsFilePickerShownCallb
       if (domBlob) {
         newFiles.AppendElement(static_cast<File*>(domBlob.get()));
       }
     }
   } else {
     MOZ_ASSERT(mode == static_cast<int16_t>(nsIFilePicker::modeOpen) ||
                mode == static_cast<int16_t>(nsIFilePicker::modeGetFolder));
     nsCOMPtr<nsISupports> tmp;
-    nsresult rv = mFilePicker->GetDomfile(getter_AddRefs(tmp));
+    nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp));
     NS_ENSURE_SUCCESS(rv, rv);
 
     nsCOMPtr<nsIDOMBlob> blob = do_QueryInterface(tmp);
     if (blob) {
       RefPtr<File> file = static_cast<Blob*>(blob.get())->ToFile();
       newFiles.AppendElement(file);
     }
   }
--- a/dom/imptests/failures/html/js/builtins/test_WeakMap.prototype-properties.html.json
+++ b/dom/imptests/failures/html/js/builtins/test_WeakMap.prototype-properties.html.json
@@ -1,8 +1,7 @@
 {
-  "WeakMap.prototype.clear.length": true,
   "WeakMap.prototype.delete.length": true,
   "WeakMap.prototype.get.length": true,
   "WeakMap.prototype.has.length": true,
   "WeakMap.prototype.set.length": true,
   "WeakMap.prototype.@@toStringTag": true
 }
--- a/dom/imptests/html/js/builtins/test_WeakMap.prototype-properties.html
+++ b/dom/imptests/html/js/builtins/test_WeakMap.prototype-properties.html
@@ -50,72 +50,54 @@ test(function() {
 }, "The value of the [[Prototype]] internal property of the WeakMap prototype object is the standard built-in Object prototype object (15.2.4).")
 
 // 15.15.5.1 WeakMap.prototype.constructor
 test(function() {
   assert_equals(WeakMap.prototype.constructor, WeakMap);
   assert_propdesc(WeakMap.prototype, "constructor", true, false, true);
 }, "The initial value of WeakMap.prototype.constructor is the built-in WeakMap constructor.")
 
-// 15.15.5.2 WeakMap.prototype.clear ()
-test(function() {
-  assert_propdesc(WeakMap.prototype, "clear", true, false, true);
-  test_length("clear", 0);
-  // Step 1-3
-  test_thisval("clear", null);
-  // Step 4-5
-  test(function() {
-    var wm = new WeakMap();
-    var key = {};
-    wm.set(key, "fail");
-    assert_true(wm.has(key));
-    var res = wm.clear();
-    assert_equals(res, undefined);
-    assert_false(wm.has(key));
-  }, "WeakMap.prototype.clear: basic functionality");
-}, "WeakMap.prototype.clear")
-
-// 15.15.5.3 WeakMap.prototype.delete ( key )
+// 15.15.5.2 WeakMap.prototype.delete ( key )
 test(function() {
   assert_propdesc(WeakMap.prototype, "delete", true, false, true);
   test_length("delete", 1);
   // Step 1-3
   test_thisval("delete", [{}]);
 }, "WeakMap.prototype.delete")
 
-// 15.15.5.4 WeakMap.prototype.get ( key )
+// 15.15.5.3 WeakMap.prototype.get ( key )
 test(function() {
   assert_propdesc(WeakMap.prototype, "get", true, false, true);
   test_length("get", 1);
   // Step 1-3
   test_thisval("get", [{}]);
 
   // Step 8
   test(function() {
     var wm = new WeakMap();
     var key = {};
     var res = wm.get({}, {});
     assert_equals(res, undefined);
   }, "WeakMap.prototype.get: return undefined");
 }, "WeakMap.prototype.get")
 
-// 15.14.5.5 Map.prototype.has ( key )
+// 15.14.5.4 Map.prototype.has ( key )
 test(function() {
   assert_propdesc(WeakMap.prototype, "has", true, false, true);
   test_length("has", 1);
   // Step 1-3
   test_thisval("has", [{}]);
 }, "WeakMap.prototype.has")
 
-// 15.14.5.6 Map.prototype.set ( key , value )
+// 15.14.5.5 Map.prototype.set ( key , value )
 test(function() {
   assert_propdesc(WeakMap.prototype, "set", true, false, true);
   test_length("set", 2);
   // Step 1-3
   test_thisval("set", [{}, {}]);
 }, "WeakMap.prototype.set")
 
-// 15.15.5.7 Map.prototype.@@toStringTag
+// 15.15.5.6 Map.prototype.@@toStringTag
 test(function() {
   assert_class_string(new WeakMap(), "WeakMap");
   assert_class_string(WeakMap.prototype, "WeakMap");
 }, "WeakMap.prototype.@@toStringTag")
 </script>
--- a/dom/indexedDB/IDBRequest.cpp
+++ b/dom/indexedDB/IDBRequest.cpp
@@ -46,25 +46,39 @@ using namespace mozilla::ipc;
 namespace {
 
 NS_DEFINE_IID(kIDBRequestIID, PRIVATE_IDBREQUEST_IID);
 
 } // namespace
 
 IDBRequest::IDBRequest(IDBDatabase* aDatabase)
   : IDBWrapperCache(aDatabase)
+#ifdef DEBUG
+  , mOwningThread(nullptr)
+#endif
+  , mLoggingSerialNumber(0)
+  , mLineNo(0)
+  , mColumn(0)
+  , mHaveResultOrErrorCode(false)
 {
   MOZ_ASSERT(aDatabase);
   aDatabase->AssertIsOnOwningThread();
 
   InitMembers();
 }
 
 IDBRequest::IDBRequest(nsPIDOMWindow* aOwner)
   : IDBWrapperCache(aOwner)
+#ifdef DEBUG
+  , mOwningThread(nullptr)
+#endif
+  , mLoggingSerialNumber(0)
+  , mLineNo(0)
+  , mColumn(0)
+  , mHaveResultOrErrorCode(false)
 {
   InitMembers();
 }
 
 IDBRequest::~IDBRequest()
 {
   AssertIsOnOwningThread();
 }
--- a/dom/indexedDB/test/test_serviceworker.html
+++ b/dom/indexedDB/test/test_serviceworker.html
@@ -10,23 +10,29 @@
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <p id="display"></p>
 <div id="content" style="display: none"></div>
 <pre id="test"></pre>
 <script class="testbody" type="text/javascript">
 
+  var regisration;
   function simpleRegister() {
     return navigator.serviceWorker.register("service_worker.js", {
       scope: 'service_worker_client.html'
     });
   }
 
+  function unregister() {
+    return registration.unregister();
+  }
+
   function testIndexedDBAvailable(sw) {
+    registration = sw;
     var p = new Promise(function(resolve, reject) {
       window.onmessage = function(e) {
         if (e.data === "READY") {
           sw.active.postMessage("GO");
           return;
         }
 
         if (!("available" in e.data)) {
@@ -48,16 +54,17 @@
     content.appendChild(iframe);
 
     return p.then(() => content.removeChild(iframe));
   }
 
   function runTest() {
     simpleRegister()
       .then(testIndexedDBAvailable)
+      .then(unregister)
       .then(SimpleTest.finish)
       .catch(function(e) {
         ok(false, "Some test failed with error " + e);
         SimpleTest.finish();
       });
   }
 
   SimpleTest.waitForExplicitFinish();
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -44,17 +44,17 @@ interface nsIDOMClientRect;
 interface nsIURI;
 interface nsIDOMEventTarget;
 interface nsIRunnable;
 interface nsITranslationNodeList;
 interface nsIJSRAIIHelper;
 interface nsIContentPermissionRequest;
 interface nsIObserver;
 
-[scriptable, uuid(3f3f2bf4-d411-44b2-b2f7-dee5948c4763)]
+[scriptable, uuid(7846c43d-e131-40a6-8417-3be2c7e11df1)]
 interface nsIDOMWindowUtils : nsISupports {
 
   /**
    * Image animation mode of the window. When this attribute's value
    * is changed, the implementation should set all images in the window
    * to the given value. That is, when set to kDontAnimMode, all images
    * will stop animating. The attribute's value must be one of the
    * animationMode values from imgIContainer.
@@ -585,16 +585,23 @@ interface nsIDOMWindowUtils : nsISupport
    * topic.
    */
   void sendNativeMouseEvent(in long aScreenX,
                             in long aScreenY,
                             in long aNativeMessage,
                             in long aModifierFlags,
                             in nsIDOMElement aElement,
                             [optional] in nsIObserver aObserver);
+  /**
+   * See nsIWidget::SynthesizeNativeMouseMove and sendNativeMouseEvent
+   */
+  void sendNativeMouseMove(in long aScreenX,
+                           in long aScreenY,
+                           in nsIDOMElement aElement,
+                           [optional] in nsIObserver aObserver);
 
   /**
    * The values for sendNativeMouseScrollEvent's aAdditionalFlags.
    */
 
   /**
    * If MOUSESCROLL_PREFER_WIDGET_AT_POINT is set, widget will dispatch
    * the event to a widget which is under the cursor.  Otherwise, dispatch to
--- a/dom/media/AudioStream.cpp
+++ b/dom/media/AudioStream.cpp
@@ -113,29 +113,29 @@ public:
     }
   }
 private:
   nsAutoTArray<Chunk, 7> mChunks;
   int64_t mBaseOffset;
   double mBasePosition;
 };
 
-AudioStream::AudioStream()
+AudioStream::AudioStream(DataSource& aSource)
   : mMonitor("AudioStream")
   , mInRate(0)
   , mOutRate(0)
   , mChannels(0)
   , mOutChannels(0)
-  , mWritten(0)
   , mAudioClock(this)
   , mTimeStretcher(nullptr)
   , mDumpFile(nullptr)
   , mBytesPerFrame(0)
   , mState(INITIALIZED)
   , mIsMonoAudioEnabled(gfxPrefs::MonoAudio())
+  , mDataSource(aSource)
 {
 }
 
 AudioStream::~AudioStream()
 {
   LOG(("AudioStream: delete %p, state %d", this, mState));
   MOZ_ASSERT(mState == SHUTDOWN && !mCubebStream,
              "Should've called Shutdown() before deleting an AudioStream");
@@ -151,18 +151,16 @@ size_t
 AudioStream::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
 {
   size_t amount = aMallocSizeOf(this);
 
   // Possibly add in the future:
   // - mTimeStretcher
   // - mCubebStream
 
-  amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf);
-
   return amount;
 }
 
 nsresult AudioStream::EnsureTimeStretcherInitializedUnlocked()
 {
   mMonitor.AssertCurrentThreadOwns();
   if (!mTimeStretcher) {
     mTimeStretcher = soundtouch::createSoundTouchObj();
@@ -227,22 +225,16 @@ nsresult AudioStream::SetPreservesPitch(
     mTimeStretcher->setRate(mAudioClock.GetPlaybackRate());
   }
 
   mAudioClock.SetPreservesPitch(aPreservesPitch);
 
   return NS_OK;
 }
 
-int64_t AudioStream::GetWritten()
-{
-  MonitorAutoLock mon(mMonitor);
-  return mWritten;
-}
-
 static void SetUint16LE(uint8_t* aDest, uint16_t aValue)
 {
   aDest[0] = aValue & 0xFF;
   aDest[1] = aValue >> 8;
 }
 
 static void SetUint32LE(uint8_t* aDest, uint32_t aValue)
 {
@@ -346,23 +338,16 @@ AudioStream::Init(int32_t aNumChannels, 
     params.format = CUBEB_SAMPLE_S16NE;
   } else {
     params.format = CUBEB_SAMPLE_FLOAT32NE;
   }
   mBytesPerFrame = sizeof(AudioDataValue) * mOutChannels;
 
   mAudioClock.Init();
 
-  // Size mBuffer for one second of audio.  This value is arbitrary, and was
-  // selected based on the observed behaviour of the existing AudioStream
-  // implementations.
-  uint32_t bufferLimit = FramesToBytes(aRate);
-  MOZ_ASSERT(bufferLimit % mBytesPerFrame == 0, "Must buffer complete frames");
-  mBuffer.SetCapacity(bufferLimit);
-
   return OpenCubeb(params);
 }
 
 // This code used to live inside AudioStream::Init(), but on Mac (others?)
 // it has been known to take 300-800 (or even 8500) ms to execute(!)
 nsresult
 AudioStream::OpenCubeb(cubeb_stream_params &aParams)
 {
@@ -402,135 +387,54 @@ AudioStream::OpenCubeb(cubeb_stream_para
           (uint32_t) timeDelta.ToMilliseconds()));
     Telemetry::Accumulate(mIsFirst ? Telemetry::AUDIOSTREAM_FIRST_OPEN_MS :
         Telemetry::AUDIOSTREAM_LATER_OPEN_MS, timeDelta.ToMilliseconds());
   }
 
   return NS_OK;
 }
 
-// aTime is the time in ms the samples were inserted into MediaStreamGraph
-nsresult
-AudioStream::Write(const AudioDataValue* aBuf, uint32_t aFrames)
-{
-  MonitorAutoLock mon(mMonitor);
-
-  if (mState == ERRORED) {
-    return NS_ERROR_FAILURE;
-  }
-  NS_ASSERTION(mState == INITIALIZED || mState == STARTED || mState == RUNNING,
-    "Stream write in unexpected state.");
-
-  // Downmix to Stereo.
-  if (mChannels > 2 && mChannels <= 8) {
-    DownmixAudioToStereo(const_cast<AudioDataValue*> (aBuf), mChannels, aFrames);
-  } else if (mChannels > 8) {
-    return NS_ERROR_FAILURE;
-  }
-
-  if (mChannels >= 2 && mIsMonoAudioEnabled) {
-    DownmixStereoToMono(const_cast<AudioDataValue*> (aBuf), aFrames);
-  }
-
-  const uint8_t* src = reinterpret_cast<const uint8_t*>(aBuf);
-  uint32_t bytesToCopy = FramesToBytes(aFrames);
-
-  while (bytesToCopy > 0) {
-    uint32_t available = std::min(bytesToCopy, mBuffer.Available());
-    MOZ_ASSERT(available % mBytesPerFrame == 0,
-               "Must copy complete frames.");
-
-    mBuffer.AppendElements(src, available);
-    src += available;
-    bytesToCopy -= available;
-
-    if (bytesToCopy > 0) {
-     // If we are not playing, but our buffer is full, start playing to make
-     // room for soon-to-be-decoded data.
-     if (mState != STARTED && mState != RUNNING) {
-       MOZ_LOG(gAudioStreamLog, LogLevel::Warning, ("Starting stream %p in Write (%u waiting)",
-                                              this, bytesToCopy));
-       StartUnlocked();
-       if (mState == ERRORED) {
-         return NS_ERROR_FAILURE;
-       }
-     }
-     MOZ_LOG(gAudioStreamLog, LogLevel::Warning, ("Stream %p waiting in Write() (%u waiting)",
-                                              this, bytesToCopy));
-     mon.Wait();
-    }
-  }
-
-  mWritten += aFrames;
-  return NS_OK;
-}
-
-uint32_t
-AudioStream::Available()
-{
-  MonitorAutoLock mon(mMonitor);
-  MOZ_ASSERT(mBuffer.Length() % mBytesPerFrame == 0, "Buffer invariant violated.");
-  return BytesToFrames(mBuffer.Available());
-}
-
 void
 AudioStream::SetVolume(double aVolume)
 {
   MOZ_ASSERT(aVolume >= 0.0 && aVolume <= 1.0, "Invalid volume");
 
   if (cubeb_stream_set_volume(mCubebStream.get(), aVolume * CubebUtils::GetVolumeScale()) != CUBEB_OK) {
     NS_WARNING("Could not change volume on cubeb stream.");
   }
 }
 
 void
-AudioStream::Cancel()
-{
-  MonitorAutoLock mon(mMonitor);
-  mState = ERRORED;
-  mon.NotifyAll();
-}
-
-void
-AudioStream::Drain()
-{
-  MonitorAutoLock mon(mMonitor);
-  LOG(("AudioStream::Drain() for %p, state %d, avail %u", this, mState, mBuffer.Available()));
-  if (mState != STARTED && mState != RUNNING) {
-    NS_ASSERTION(mState == ERRORED || mBuffer.Available() == 0, "Draining without full buffer of unplayed audio");
-    return;
-  }
-  mState = DRAINING;
-  while (mState == DRAINING) {
-    mon.Wait();
-  }
-}
-
-void
 AudioStream::Start()
 {
   MonitorAutoLock mon(mMonitor);
   StartUnlocked();
 }
 
 void
 AudioStream::StartUnlocked()
 {
   mMonitor.AssertCurrentThreadOwns();
   if (!mCubebStream) {
     return;
   }
 
   if (mState == INITIALIZED) {
+    mState = STARTED;
     int r;
     {
       MonitorAutoUnlock mon(mMonitor);
       r = cubeb_stream_start(mCubebStream.get());
+      // DataCallback might be called before we exit this scope
+      // if cubeb_stream_start() succeeds. mState must be set to STARTED
+      // beforehand.
     }
-    mState = r == CUBEB_OK ? STARTED : ERRORED;
+    if (r != CUBEB_OK) {
+      mState = ERRORED;
+    }
     LOG(("AudioStream: started %p, state %s", this, mState == STARTED ? "STARTED" : "ERRORED"));
   }
 }
 
 void
 AudioStream::Pause()
 {
   MonitorAutoLock mon(mMonitor);
@@ -626,148 +530,177 @@ AudioStream::GetPositionInFramesUnlocked
 
 bool
 AudioStream::IsPaused()
 {
   MonitorAutoLock mon(mMonitor);
   return mState == STOPPED;
 }
 
+bool
+AudioStream::Downmix(AudioDataValue* aBuffer, uint32_t aFrames)
+{
+  if (mChannels > 8) {
+    return false;
+  }
+
+  if (mChannels > 2 && mChannels <= 8) {
+    DownmixAudioToStereo(aBuffer, mChannels, aFrames);
+  }
+
+  if (mChannels >= 2 && mIsMonoAudioEnabled) {
+    DownmixStereoToMono(aBuffer, aFrames);
+  }
+
+  return true;
+}
+
 long
 AudioStream::GetUnprocessed(void* aBuffer, long aFrames)
 {
   mMonitor.AssertCurrentThreadOwns();
   uint8_t* wpos = reinterpret_cast<uint8_t*>(aBuffer);
 
   // Flush the timestretcher pipeline, if we were playing using a playback rate
   // other than 1.0.
   uint32_t flushedFrames = 0;
   if (mTimeStretcher && mTimeStretcher->numSamples()) {
     flushedFrames = mTimeStretcher->receiveSamples(reinterpret_cast<AudioDataValue*>(wpos), aFrames);
     wpos += FramesToBytes(flushedFrames);
+
+    // TODO: There might be still unprocessed samples in the stretcher.
+    // We should either remove or flush them so they won't be in the output
+    // next time we switch a playback rate other than 1.0.
+    NS_WARN_IF(mTimeStretcher->numUnprocessedSamples() > 0);
   }
-  uint32_t toPopBytes = FramesToBytes(aFrames - flushedFrames);
-  uint32_t available = std::min(toPopBytes, mBuffer.Length());
 
-  void* input[2];
-  uint32_t input_size[2];
-  mBuffer.PopElements(available, &input[0], &input_size[0], &input[1], &input_size[1]);
-  memcpy(wpos, input[0], input_size[0]);
-  wpos += input_size[0];
-  memcpy(wpos, input[1], input_size[1]);
+  uint32_t toPopFrames = aFrames - flushedFrames;
+  while (toPopFrames > 0) {
+    UniquePtr<Chunk> c = mDataSource.PopFrames(toPopFrames);
+    if (c->Frames() == 0) {
+      break;
+    }
+    MOZ_ASSERT(c->Frames() <= toPopFrames);
+    if (Downmix(c->GetWritable(), c->Frames())) {
+      memcpy(wpos, c->Data(), FramesToBytes(c->Frames()));
+    } else {
+      // Write silence if downmixing fails.
+      memset(wpos, 0, FramesToBytes(c->Frames()));
+    }
+    wpos += FramesToBytes(c->Frames());
+    toPopFrames -= c->Frames();
+  }
 
-  return BytesToFrames(available) + flushedFrames;
+  return aFrames - toPopFrames;
 }
 
 long
 AudioStream::GetTimeStretched(void* aBuffer, long aFrames)
 {
   mMonitor.AssertCurrentThreadOwns();
-  long processedFrames = 0;
 
   // We need to call the non-locking version, because we already have the lock.
   if (EnsureTimeStretcherInitializedUnlocked() != NS_OK) {
     return 0;
   }
 
   uint8_t* wpos = reinterpret_cast<uint8_t*>(aBuffer);
   double playbackRate = static_cast<double>(mInRate) / mOutRate;
-  uint32_t toPopBytes = FramesToBytes(ceil(aFrames * playbackRate));
-  uint32_t available = 0;
-  bool lowOnBufferedData = false;
-  do {
-    // Check if we already have enough data in the time stretcher pipeline.
-    if (mTimeStretcher->numSamples() <= static_cast<uint32_t>(aFrames)) {
-      void* input[2];
-      uint32_t input_size[2];
-      available = std::min(mBuffer.Length(), toPopBytes);
-      if (available != toPopBytes) {
-        lowOnBufferedData = true;
-      }
-      mBuffer.PopElements(available, &input[0], &input_size[0],
-                                     &input[1], &input_size[1]);
-      for(uint32_t i = 0; i < 2; i++) {
-        mTimeStretcher->putSamples(reinterpret_cast<AudioDataValue*>(input[i]), BytesToFrames(input_size[i]));
-      }
+  uint32_t toPopFrames = ceil(aFrames * playbackRate);
+
+  while (mTimeStretcher->numSamples() < static_cast<uint32_t>(aFrames)) {
+    UniquePtr<Chunk> c = mDataSource.PopFrames(toPopFrames);
+    if (c->Frames() == 0) {
+      break;
     }
-    uint32_t receivedFrames = mTimeStretcher->receiveSamples(reinterpret_cast<AudioDataValue*>(wpos), aFrames - processedFrames);
-    wpos += FramesToBytes(receivedFrames);
-    processedFrames += receivedFrames;
-  } while (processedFrames < aFrames && !lowOnBufferedData);
+    MOZ_ASSERT(c->Frames() <= toPopFrames);
+    if (Downmix(c->GetWritable(), c->Frames())) {
+      mTimeStretcher->putSamples(c->Data(), c->Frames());
+    } else {
+      // Write silence if downmixing fails.
+      nsAutoTArray<AudioDataValue, 1000> buf;
+      buf.SetLength(mOutChannels * c->Frames());
+      memset(buf.Elements(), 0, buf.Length() * sizeof(AudioDataValue));
+      mTimeStretcher->putSamples(buf.Elements(), c->Frames());
+    }
+  }
 
-  return processedFrames;
+  uint32_t receivedFrames = mTimeStretcher->receiveSamples(reinterpret_cast<AudioDataValue*>(wpos), aFrames);
+  wpos += FramesToBytes(receivedFrames);
+  return receivedFrames;
 }
 
 long
 AudioStream::DataCallback(void* aBuffer, long aFrames)
 {
   MonitorAutoLock mon(mMonitor);
   MOZ_ASSERT(mState != SHUTDOWN, "No data callback after shutdown");
-  uint32_t available = std::min(static_cast<uint32_t>(FramesToBytes(aFrames)), mBuffer.Length());
-  MOZ_ASSERT(available % mBytesPerFrame == 0, "Must copy complete frames");
-  AudioDataValue* output = reinterpret_cast<AudioDataValue*>(aBuffer);
   uint32_t underrunFrames = 0;
   uint32_t servicedFrames = 0;
 
+  // FIXME: cubeb_pulse sometimes calls us before cubeb_stream_start() is called.
+  // We don't want to consume audio data until Start() is called by the client.
+  if (mState == INITIALIZED) {
+    NS_WARNING("data callback fires before cubeb_stream_start() is called");
+    mAudioClock.UpdateFrameHistory(0, aFrames);
+    memset(aBuffer, 0, FramesToBytes(aFrames));
+    return aFrames;
+  }
+
   // NOTE: wasapi (others?) can call us back *after* stop()/Shutdown() (mState == SHUTDOWN)
   // Bug 996162
 
   // callback tells us cubeb succeeded initializing
   if (mState == STARTED) {
     mState = RUNNING;
   }
 
-  if (available) {
-    if (mInRate == mOutRate) {
-      servicedFrames = GetUnprocessed(output, aFrames);
-    } else {
-      servicedFrames = GetTimeStretched(output, aFrames);
-    }
-
-    MOZ_ASSERT(mBuffer.Length() % mBytesPerFrame == 0, "Must copy complete frames");
-
-    // Notify any blocked Write() call that more space is available in mBuffer.
-    mon.NotifyAll();
+  if (mInRate == mOutRate) {
+    servicedFrames = GetUnprocessed(aBuffer, aFrames);
+  } else {
+    servicedFrames = GetTimeStretched(aBuffer, aFrames);
   }
 
   underrunFrames = aFrames - servicedFrames;
 
   // Always send audible frames first, and silent frames later.
   // Otherwise it will break the assumption of FrameHistory.
-  if (mState != DRAINING) {
+  if (!mDataSource.Ended()) {
     mAudioClock.UpdateFrameHistory(servicedFrames, underrunFrames);
     uint8_t* rpos = static_cast<uint8_t*>(aBuffer) + FramesToBytes(aFrames - underrunFrames);
     memset(rpos, 0, FramesToBytes(underrunFrames));
     if (underrunFrames) {
       MOZ_LOG(gAudioStreamLog, LogLevel::Warning,
              ("AudioStream %p lost %d frames", this, underrunFrames));
     }
     servicedFrames += underrunFrames;
   } else {
+    // No more new data in the data source. Don't send silent frames so the
+    // cubeb stream can start draining.
     mAudioClock.UpdateFrameHistory(servicedFrames, 0);
   }
 
   WriteDumpFile(mDumpFile, this, aFrames, aBuffer);
 
   return servicedFrames;
 }
 
 void
 AudioStream::StateCallback(cubeb_state aState)
 {
   MonitorAutoLock mon(mMonitor);
   MOZ_ASSERT(mState != SHUTDOWN, "No state callback after shutdown");
   LOG(("AudioStream: StateCallback %p, mState=%d cubeb_state=%d", this, mState, aState));
   if (aState == CUBEB_STATE_DRAINED) {
     mState = DRAINED;
+    mDataSource.Drained();
   } else if (aState == CUBEB_STATE_ERROR) {
     LOG(("AudioStream::StateCallback() state %d cubeb error", mState));
     mState = ERRORED;
   }
-  mon.NotifyAll();
 }
 
 AudioClock::AudioClock(AudioStream* aStream)
  :mAudioStream(aStream),
   mOutRate(0),
   mInRate(0),
   mPreservesPitch(true),
   mFrameHistory(new FrameHistory())
--- a/dom/media/AudioStream.h
+++ b/dom/media/AudioStream.h
@@ -153,55 +153,61 @@ private:
 // GetPosition, GetPositionInFrames, SetVolume, and Get{Rate,Channels},
 // SetMicrophoneActive is thread-safe without external synchronization.
 class AudioStream final
 {
   virtual ~AudioStream();
 
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioStream)
-  AudioStream();
+
+  class Chunk {
+  public:
+    // Return a pointer to the audio data.
+    virtual const AudioDataValue* Data() const = 0;
+    // Return the number of frames in this chunk.
+    virtual uint32_t Frames() const = 0;
+    // Return a writable pointer for downmixing.
+    virtual AudioDataValue* GetWritable() const = 0;
+    virtual ~Chunk() {}
+  };
+
+  class DataSource {
+  public:
+    // Return a chunk which contains at most aFrames frames or zero if no
+    // frames in the source at all.
+    virtual UniquePtr<Chunk> PopFrames(uint32_t aFrames) = 0;
+    // Return true if no more data will be added to the source.
+    virtual bool Ended() const = 0;
+    // Notify that all data is drained by the AudioStream.
+    virtual void Drained() = 0;
+  protected:
+    virtual ~DataSource() {}
+  };
+
+  explicit AudioStream(DataSource& aSource);
 
   // Initialize the audio stream. aNumChannels is the number of audio
   // channels (1 for mono, 2 for stereo, etc) and aRate is the sample rate
   // (22050Hz, 44100Hz, etc).
   nsresult Init(int32_t aNumChannels, int32_t aRate,
                 const dom::AudioChannel aAudioStreamChannel);
 
   // Closes the stream. All future use of the stream is an error.
   void Shutdown();
 
   void Reset();
 
-  // Write audio data to the audio hardware.  aBuf is an array of AudioDataValues
-  // AudioDataValue of length aFrames*mChannels.  If aFrames is larger
-  // than the result of Available(), the write will block until sufficient
-  // buffer space is available.
-  nsresult Write(const AudioDataValue* aBuf, uint32_t aFrames);
-
-  // Return the number of audio frames that can be written without blocking.
-  uint32_t Available();
-
   // Set the current volume of the audio playback. This is a value from
   // 0 (meaning muted) to 1 (meaning full volume).  Thread-safe.
   void SetVolume(double aVolume);
 
-  // Block until buffered audio data has been consumed.
-  void Drain();
-
-  // Break any blocking operation and set the stream to shutdown.
-  void Cancel();
-
   // Start the stream.
   void Start();
 
-  // Return the number of frames written so far in the stream. This allow the
-  // caller to check if it is safe to start the stream, if needed.
-  int64_t GetWritten();
-
   // Pause audio playback.
   void Pause();
 
   // Resume audio playback.
   void Resume();
 
   // Return the position in microseconds of the audio frame being played by
   // the audio hardware, compensated for playback rate change. Thread-safe.
@@ -249,53 +255,45 @@ private:
   }
 
 
   long DataCallback(void* aBuffer, long aFrames);
   void StateCallback(cubeb_state aState);
 
   nsresult EnsureTimeStretcherInitializedUnlocked();
 
+  // Return true if downmixing succeeds otherwise false.
+  bool Downmix(AudioDataValue* aBuffer, uint32_t aFrames);
+
   long GetUnprocessed(void* aBuffer, long aFrames);
   long GetTimeStretched(void* aBuffer, long aFrames);
 
   void StartUnlocked();
 
-  // The monitor is held to protect all access to member variables.  Write()
-  // waits while mBuffer is full; DataCallback() notifies as it consumes
-  // data from mBuffer.  Drain() waits while mState is DRAINING;
-  // StateCallback() notifies when mState is DRAINED.
+  // The monitor is held to protect all access to member variables.
   Monitor mMonitor;
 
   // Input rate in Hz (characteristic of the media being played)
   int mInRate;
   // Output rate in Hz (characteristic of the playback rate)
   int mOutRate;
   int mChannels;
   int mOutChannels;
 #if defined(__ANDROID__)
   dom::AudioChannel mAudioChannel;
 #endif
-  // Number of frames written to the buffers.
-  int64_t mWritten;
   AudioClock mAudioClock;
   soundtouch::SoundTouch* mTimeStretcher;
 
   // Stream start time for stream open delay telemetry.
   TimeStamp mStartTime;
 
   // Output file for dumping audio
   FILE* mDumpFile;
 
-  // Temporary audio buffer.  Filled by Write() and consumed by
-  // DataCallback().  Once mBuffer is full, Write() blocks until sufficient
-  // space becomes available in mBuffer.  mBuffer is sized in bytes, not
-  // frames.
-  CircularByteBuffer mBuffer;
-
   // Owning reference to a cubeb_stream.
   UniquePtr<cubeb_stream, CubebDestroyPolicy> mCubebStream;
 
   uint32_t mBytesPerFrame;
 
   uint32_t BytesToFrames(uint32_t aBytes) {
     NS_ASSERTION(aBytes % mBytesPerFrame == 0,
                  "Byte count not aligned on frames size.");
@@ -306,26 +304,24 @@ private:
     return aFrames * mBytesPerFrame;
   }
 
   enum StreamState {
     INITIALIZED, // Initialized, playback has not begun.
     STARTED,     // cubeb started, but callbacks haven't started
     RUNNING,     // DataCallbacks have started after STARTED, or after Resume().
     STOPPED,     // Stopped by a call to Pause().
-    DRAINING,    // Drain requested.  DataCallback will indicate end of stream
-                 // once the remaining contents of mBuffer are requested by
-                 // cubeb, after which StateCallback will indicate drain
-                 // completion.
     DRAINED,     // StateCallback has indicated that the drain is complete.
     ERRORED,     // Stream disabled due to an internal error.
     SHUTDOWN     // Shutdown has been called
   };
 
   StreamState mState;
   bool mIsFirst;
   // Get this value from the preferece, if true, we would downmix the stereo.
   bool mIsMonoAudioEnabled;
+
+  DataSource& mDataSource;
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/media/MediaData.h
+++ b/dom/media/MediaData.h
@@ -313,17 +313,17 @@ public:
 
 protected:
   ~VideoData();
 };
 
 class CryptoTrack
 {
 public:
-  CryptoTrack() : mValid(false) {}
+  CryptoTrack() : mValid(false), mMode(0), mIVSize(0) {}
   bool mValid;
   int32_t mMode;
   int32_t mIVSize;
   nsTArray<uint8_t> mKeyId;
 };
 
 class CryptoSample : public CryptoTrack
 {
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -867,21 +867,26 @@ MediaDecoder::MetadataLoaded(nsAutoPtr<M
               aInfo->HasAudio(), aInfo->HasVideo());
 
   SetMediaSeekable(aInfo->mMediaSeekable);
   mInfo = aInfo.forget();
   ConstructMediaTracks();
 
   // Make sure the element and the frame (if any) are told about
   // our new size.
-  Invalidate();
   if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
     mFiredMetadataLoaded = true;
     mOwner->MetadataLoaded(mInfo, nsAutoPtr<const MetadataTags>(aTags.forget()));
   }
+  // Invalidate() will end up calling mOwner->UpdateMediaSize with the last
+  // dimensions retrieved from the video frame container. The video frame
+  // container contains more up to date dimensions than aInfo.
+  // So we call Invalidate() after calling mOwner->MetadataLoaded to ensure
+  // the media element has the latest dimensions.
+  Invalidate();
 
   EnsureTelemetryReported();
 }
 
 void
 MediaDecoder::EnsureTelemetryReported()
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -91,23 +91,16 @@ static const uint32_t LOW_AUDIO_USECS = 
 // If more than this many usecs of decoded audio is queued, we'll hold off
 // decoding more audio. If we increase the low audio threshold (see
 // LOW_AUDIO_USECS above) we'll also increase this value to ensure it's not
 // less than the low audio threshold.
 const int64_t AMPLE_AUDIO_USECS = 1000000;
 
 } // namespace detail
 
-// When we're only playing audio and we don't have a video stream, we divide
-// AMPLE_AUDIO_USECS and LOW_AUDIO_USECS by the following value. This reduces
-// the amount of decoded audio we buffer, reducing our memory usage. We only
-// need to decode far ahead when we're decoding video using software decoding,
-// as otherwise a long video decode could cause an audio underrun.
-const int64_t NO_VIDEO_AMPLE_AUDIO_DIVISOR = 8;
-
 // If we have fewer than LOW_VIDEO_FRAMES decoded frames, and
 // we're not "prerolling video", we'll skip the video up to the next keyframe
 // which is at or after the current playback position.
 static const uint32_t LOW_VIDEO_FRAMES = 2;
 
 // Threshold in usecs that used to check if we are low on decoded video.
 // If the last video frame's end time |mDecodedVideoEndTime| is more than
 // |LOW_VIDEO_THRESHOLD_USECS*mPlaybackRate| after the current clock in
@@ -218,32 +211,32 @@ MediaDecoderStateMachine::MediaDecoderSt
   mDecodedAudioEndTime(-1),
   mDecodedVideoEndTime(-1),
   mPlaybackRate(1.0),
   mLowAudioThresholdUsecs(detail::LOW_AUDIO_USECS),
   mAmpleAudioThresholdUsecs(detail::AMPLE_AUDIO_USECS),
   mQuickBufferingLowDataThresholdUsecs(detail::QUICK_BUFFERING_LOW_DATA_USECS),
   mIsAudioPrerolling(false),
   mIsVideoPrerolling(false),
-  mAudioCaptured(false, "MediaDecoderStateMachine::mAudioCaptured"),
+  mAudioCaptured(false),
   mAudioCompleted(false, "MediaDecoderStateMachine::mAudioCompleted"),
   mVideoCompleted(false, "MediaDecoderStateMachine::mVideoCompleted"),
   mNotifyMetadataBeforeFirstFrame(false),
   mDispatchedEventToDecode(false),
   mQuickBuffering(false),
   mMinimizePreroll(false),
   mDecodeThreadWaiting(false),
   mDropAudioUntilNextDiscontinuity(false),
   mDropVideoUntilNextDiscontinuity(false),
   mDecodeToSeekTarget(false),
   mCurrentTimeBeforeSeek(0),
   mCorruptFrames(60),
   mDecodingFirstFrame(true),
   mSentLoadedMetadataEvent(false),
-  mSentFirstFrameLoadedEvent(false, "MediaDecoderStateMachine::mSentFirstFrameLoadedEvent"),
+  mSentFirstFrameLoadedEvent(false),
   mSentPlaybackEndedEvent(false),
   mOutputStreamManager(new OutputStreamManager()),
   mResource(aDecoder->GetResource()),
   mAudioOffloading(false),
   mBuffered(mTaskQueue, TimeIntervals(),
             "MediaDecoderStateMachine::mBuffered (Mirror)"),
   mEstimatedDuration(mTaskQueue, NullableTimeUnit(),
                     "MediaDecoderStateMachine::mEstimatedDuration (Mirror)"),
@@ -361,18 +354,16 @@ MediaDecoderStateMachine::Initialization
   mWatchManager.Watch(mVolume, &MediaDecoderStateMachine::VolumeChanged);
   mWatchManager.Watch(mLogicalPlaybackRate, &MediaDecoderStateMachine::LogicalPlaybackRateChanged);
   mWatchManager.Watch(mPreservesPitch, &MediaDecoderStateMachine::PreservesPitchChanged);
   mWatchManager.Watch(mEstimatedDuration, &MediaDecoderStateMachine::RecomputeDuration);
   mWatchManager.Watch(mExplicitDuration, &MediaDecoderStateMachine::RecomputeDuration);
   mWatchManager.Watch(mObservedDuration, &MediaDecoderStateMachine::RecomputeDuration);
   mWatchManager.Watch(mPlayState, &MediaDecoderStateMachine::PlayStateChanged);
   mWatchManager.Watch(mLogicallySeeking, &MediaDecoderStateMachine::LogicallySeekingChanged);
-  mWatchManager.Watch(mSentFirstFrameLoadedEvent, &MediaDecoderStateMachine::AdjustAudioThresholds);
-  mWatchManager.Watch(mAudioCaptured, &MediaDecoderStateMachine::AdjustAudioThresholds);
 }
 
 media::MediaSink*
 MediaDecoderStateMachine::CreateAudioSink()
 {
   RefPtr<MediaDecoderStateMachine> self = this;
   auto audioSinkCreator = [self] () {
     MOZ_ASSERT(self->OnTaskQueue());
@@ -2013,42 +2004,16 @@ MediaDecoderStateMachine::EnqueueFirstFr
 
 bool
 MediaDecoderStateMachine::IsDecodingFirstFrame()
 {
   return mState == DECODER_STATE_DECODING && mDecodingFirstFrame;
 }
 
 void
-MediaDecoderStateMachine::AdjustAudioThresholds()
-{
-  MOZ_ASSERT(OnTaskQueue());
-
-  // Experiments show that we need to buffer more if audio is captured to avoid
-  // audio glitch. See bug 1188643 comment 16 for the details.
-  int64_t divisor = mAudioCaptured ? NO_VIDEO_AMPLE_AUDIO_DIVISOR / 2
-                                   : NO_VIDEO_AMPLE_AUDIO_DIVISOR;
-
-  // We're playing audio only. We don't need to worry about slow video
-  // decodes causing audio underruns, so don't buffer so much audio in
-  // order to reduce memory usage.
-  if (HasAudio() && !HasVideo() && mSentFirstFrameLoadedEvent) {
-    mAmpleAudioThresholdUsecs = detail::AMPLE_AUDIO_USECS / divisor;
-    mLowAudioThresholdUsecs = detail::LOW_AUDIO_USECS / divisor;
-    mQuickBufferingLowDataThresholdUsecs =
-      detail::QUICK_BUFFERING_LOW_DATA_USECS / divisor;
-
-    // Check if we need to stop audio prerolling for thresholds changed.
-    if (mIsAudioPrerolling && DonePrerollingAudio()) {
-      StopPrerollingAudio();
-    }
-  }
-}
-
-void
 MediaDecoderStateMachine::FinishDecodeFirstFrame()
 {
   MOZ_ASSERT(OnTaskQueue());
   DECODER_LOG("FinishDecodeFirstFrame");
 
   if (!IsRealTime() && !mSentFirstFrameLoadedEvent) {
     mMediaSink->Redraw();
   }
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -643,18 +643,16 @@ private:
   // Rejected by the MediaSink to signal errors for audio/video.
   void OnMediaSinkAudioError();
   void OnMediaSinkVideoError();
 
   // Return true if the video decoder's decode speed can not catch up the
   // play time.
   bool NeedToSkipToNextKeyframe();
 
-  void AdjustAudioThresholds();
-
   void* const mDecoderID;
   const RefPtr<FrameStatistics> mFrameStats;
   const RefPtr<VideoFrameContainer> mVideoFrameContainer;
   const dom::AudioChannel mAudioChannel;
 
   // Task queue for running the state machine.
   RefPtr<TaskQueue> mTaskQueue;
 
@@ -965,23 +963,17 @@ private:
   // got a few frames decoded before we consider whether decode is falling
   // behind. Otherwise our "we're falling behind" logic will trigger
   // unneccessarily if we start playing as soon as the first sample is
   // decoded. These two fields store how many video frames and audio
   // samples we must consume before are considered to be finished prerolling.
   uint32_t AudioPrerollUsecs() const
   {
     MOZ_ASSERT(OnTaskQueue());
-    if (IsRealTime()) {
-      return 0;
-    }
-
-    uint32_t result = mLowAudioThresholdUsecs * 2;
-    MOZ_ASSERT(result <= mAmpleAudioThresholdUsecs, "Prerolling will never finish");
-    return result;
+    return IsRealTime() ? 0 : mAmpleAudioThresholdUsecs;
   }
 
   uint32_t VideoPrerollFrames() const
   {
     MOZ_ASSERT(OnTaskQueue());
     return IsRealTime() ? 0 : GetAmpleVideoFrames() / 2;
   }
 
@@ -1070,17 +1062,17 @@ private:
   {
     MOZ_ASSERT(OnTaskQueue());
     return aType == MediaData::AUDIO_DATA ? mAudioWaitRequest : mVideoWaitRequest;
   }
 
   // True if we shouldn't play our audio (but still write it to any capturing
   // streams). When this is true, the audio thread will never start again after
   // it has stopped.
-  Watchable<bool> mAudioCaptured;
+  bool mAudioCaptured;
 
   // True if the audio playback thread has finished. It is finished
   // when either all the audio frames have completed playing, or we've moved
   // into shutdown state, and the threads are to be
   // destroyed. Written by the audio playback thread and read and written by
   // the state machine thread. Synchronised via decoder monitor.
   // When data is being sent to a MediaStream, this is true when all data has
   // been written to the MediaStream.
@@ -1168,17 +1160,17 @@ private:
   // True if we are back from DECODER_STATE_DORMANT state and
   // LoadedMetadataEvent was already sent.
   bool mSentLoadedMetadataEvent;
   // True if we are back from DECODER_STATE_DORMANT state and
   // FirstFrameLoadedEvent was already sent, then we can skip
   // SetStartTime because the mStartTime already set before. Also we don't need
   // to decode any audio/video since the MediaDecoder will trigger a seek
   // operation soon.
-  Watchable<bool> mSentFirstFrameLoadedEvent;
+  bool mSentFirstFrameLoadedEvent;
 
   bool mSentPlaybackEndedEvent;
 
   // Data about MediaStreams that are being fed by the decoder.
   const RefPtr<OutputStreamManager> mOutputStreamManager;
 
   // Media data resource from the decoder.
   RefPtr<MediaResource> mResource;
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -1176,16 +1176,26 @@ MediaFormatReader::ReturnOutput(MediaDat
             "This is an unsupported configuration",
             mInfo.mAudio.mRate, audioData->mRate);
         mInfo.mAudio.mRate = audioData->mRate;
         mInfo.mAudio.mChannels = audioData->mChannels;
       }
     }
     mAudio.mPromise.Resolve(aData, __func__);
   } else if (aTrack == TrackInfo::kVideoTrack) {
+    if (aData->mType != MediaData::RAW_DATA) {
+      VideoData* videoData = static_cast<VideoData*>(aData);
+
+      if (videoData->mDisplay != mInfo.mVideo.mDisplay) {
+        LOG("change of video display size (%dx%d->%dx%d)",
+            mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height,
+            videoData->mDisplay.width, videoData->mDisplay.height);
+        mInfo.mVideo.mDisplay = videoData->mDisplay;
+      }
+    }
     mVideo.mPromise.Resolve(aData, __func__);
   }
   LOG("Resolved data promise for %s", TrackTypeToStr(aTrack));
 }
 
 size_t
 MediaFormatReader::SizeOfVideoQueueInFrames()
 {
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -2415,17 +2415,18 @@ MediaManager::EnumerateDevices(nsPIDOMWi
  * GetUserMediaDevices - called by the UI-part of getUserMedia from chrome JS.
  */
 
 nsresult
 MediaManager::GetUserMediaDevices(nsPIDOMWindow* aWindow,
                                   const MediaStreamConstraints& aConstraints,
                                   nsIGetUserMediaDevicesSuccessCallback* aOnSuccess,
                                   nsIDOMGetUserMediaErrorCallback* aOnFailure,
-                                  uint64_t aWindowId)
+                                  uint64_t aWindowId,
+                                  const nsAString& aCallID)
 {
   MOZ_ASSERT(NS_IsMainThread());
   nsCOMPtr<nsIGetUserMediaDevicesSuccessCallback> onSuccess(aOnSuccess);
   nsCOMPtr<nsIDOMGetUserMediaErrorCallback> onFailure(aOnFailure);
   if (!aWindowId) {
     aWindowId = aWindow->WindowID();
   }
 
@@ -2433,20 +2434,22 @@ MediaManager::GetUserMediaDevices(nsPIDO
 
   nsTArray<nsString>* callIDs;
   if (!mCallIds.Get(aWindowId, &callIDs)) {
     return NS_ERROR_UNEXPECTED;
   }
 
   for (auto& callID : *callIDs) {
     GetUserMediaTask* task;
-    if (mActiveCallbacks.Get(callID, &task)) {
-      nsCOMPtr<nsIWritableVariant> array = MediaManager_ToJSArray(*task->mSourceSet);
-      onSuccess->OnSuccess(array);
-      return NS_OK;
+    if (!aCallID.Length() || aCallID == callID) {
+      if (mActiveCallbacks.Get(callID, &task)) {
+        nsCOMPtr<nsIWritableVariant> array = MediaManager_ToJSArray(*task->mSourceSet);
+        onSuccess->OnSuccess(array);
+        return NS_OK;
+      }
     }
   }
   return NS_ERROR_UNEXPECTED;
 }
 
 MediaEngine*
 MediaManager::GetBackend(uint64_t aWindowId)
 {
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -454,17 +454,18 @@ public:
     const dom::MediaStreamConstraints& aConstraints,
     nsIDOMGetUserMediaSuccessCallback* onSuccess,
     nsIDOMGetUserMediaErrorCallback* onError);
 
   nsresult GetUserMediaDevices(nsPIDOMWindow* aWindow,
                                const dom::MediaStreamConstraints& aConstraints,
                                nsIGetUserMediaDevicesSuccessCallback* onSuccess,
                                nsIDOMGetUserMediaErrorCallback* onError,
-                               uint64_t aInnerWindowID = 0);
+                               uint64_t aInnerWindowID = 0,
+                               const nsAString& aCallID = nsString());
 
   nsresult EnumerateDevices(nsPIDOMWindow* aWindow,
                             nsIGetUserMediaDevicesSuccessCallback* aOnSuccess,
                             nsIDOMGetUserMediaErrorCallback* aOnFailure);
 
   nsresult EnumerateDevices(nsPIDOMWindow* aWindow, dom::Promise& aPromise);
   void OnNavigation(uint64_t aWindowID);
   bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);
--- a/dom/media/MediaPermissionGonk.cpp
+++ b/dom/media/MediaPermissionGonk.cpp
@@ -487,32 +487,35 @@ MediaPermissionManager::Observe(nsISuppo
 }
 
 // Handle GetUserMediaRequest, query available media device first.
 nsresult
 MediaPermissionManager::HandleRequest(RefPtr<dom::GetUserMediaRequest> &req)
 {
   nsString callID;
   req->GetCallID(callID);
+  uint64_t innerWindowID = req->InnerWindowID();
 
   nsCOMPtr<nsPIDOMWindow> innerWindow = static_cast<nsPIDOMWindow*>
-      (nsGlobalWindow::GetInnerWindowWithId(req->InnerWindowID()));
+      (nsGlobalWindow::GetInnerWindowWithId(innerWindowID));
   if (!innerWindow) {
     MOZ_ASSERT(false, "No inner window");
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsIGetUserMediaDevicesSuccessCallback> onSuccess =
       new MediaDeviceSuccessCallback(req);
   nsCOMPtr<nsIDOMGetUserMediaErrorCallback> onError =
       new MediaDeviceErrorCallback(callID);
 
   dom::MediaStreamConstraints constraints;
   req->GetConstraints(constraints);
 
   RefPtr<MediaManager> MediaMgr = MediaManager::GetInstance();
-  nsresult rv = MediaMgr->GetUserMediaDevices(innerWindow, constraints, onSuccess, onError);
+  nsresult rv = MediaMgr->GetUserMediaDevices(innerWindow, constraints,
+                                              onSuccess, onError,
+                                              innerWindowID, callID);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 } // namespace mozilla
--- a/dom/media/MediaResource.h
+++ b/dom/media/MediaResource.h
@@ -57,17 +57,22 @@ class MediaChannelStatistics;
  * kind of average of the data passing through over the time the
  * channel is active.
  *
  * All methods take "now" as a parameter so the user of this class can
  * control the timeline used.
  */
 class MediaChannelStatistics {
 public:
-  MediaChannelStatistics() { Reset(); }
+  MediaChannelStatistics()
+    : mAccumulatedBytes(0)
+    , mIsStarted(false)
+  {
+    Reset();
+  }
 
   explicit MediaChannelStatistics(MediaChannelStatistics * aCopyFrom)
   {
     MOZ_ASSERT(aCopyFrom);
     mAccumulatedBytes = aCopyFrom->mAccumulatedBytes;
     mAccumulatedTime = aCopyFrom->mAccumulatedTime;
     mLastStartTime = aCopyFrom->mLastStartTime;
     mIsStarted = aCopyFrom->mIsStarted;
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -1039,17 +1039,17 @@ private:
  * This stream processes zero or more input streams in parallel to produce
  * its output. The details of how the output is produced are handled by
  * subclasses overriding the ProcessInput method.
  */
 class ProcessedMediaStream : public MediaStream
 {
 public:
   explicit ProcessedMediaStream(DOMMediaStream* aWrapper)
-    : MediaStream(aWrapper), mAutofinish(false)
+    : MediaStream(aWrapper), mAutofinish(false), mCycleMarker(0)
   {}
 
   // Control API.
   /**
    * Allocates a new input port attached to source aStream.
    * This stream can be removed by calling MediaInputPort::Remove().
    * The input port is tied to aTrackID in the source stream.
    * aTrackID can be set to TRACK_ANY to automatically forward all tracks from
--- a/dom/media/StreamBuffer.h
+++ b/dom/media/StreamBuffer.h
@@ -155,17 +155,18 @@ public:
       return aA->GetID() == aB->GetID();
     }
     bool LessThan(Track* aA, Track* aB) const {
       return aA->GetID() < aB->GetID();
     }
   };
 
   StreamBuffer()
-    : mTracksKnownTime(0)
+    : mGraphRate(0)
+    , mTracksKnownTime(0)
     , mForgottenTime(0)
     , mTracksDirty(false)
 #ifdef DEBUG
     , mGraphRateIsSet(false)
 #endif
   {
     MOZ_COUNT_CTOR(StreamBuffer);
   }
--- a/dom/media/mediasink/AudioSink.h
+++ b/dom/media/mediasink/AudioSink.h
@@ -5,16 +5,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #if !defined(AudioSink_h__)
 #define AudioSink_h__
 
 #include "mozilla/MozPromise.h"
 #include "mozilla/RefPtr.h"
 #include "nsISupportsImpl.h"
 
+#include "MediaSink.h"
+
 namespace mozilla {
 
 class MediaData;
 template <class T> class MediaQueue;
 
 namespace media {
 
 /*
@@ -23,19 +25,21 @@ namespace media {
  */
 class AudioSink {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioSink)
   AudioSink(MediaQueue<MediaData>& aAudioQueue)
     : mAudioQueue(aAudioQueue)
   {}
 
+  typedef MediaSink::PlaybackParams PlaybackParams;
+
   // Return a promise which will be resolved when AudioSink finishes playing,
   // or rejected if any error.
-  virtual RefPtr<GenericPromise> Init() = 0;
+  virtual RefPtr<GenericPromise> Init(const PlaybackParams& aParams) = 0;
 
   virtual int64_t GetEndTime() const = 0;
   virtual int64_t GetPosition() = 0;
 
   // Check whether we've pushed more frames to the audio
   // hardware than it has played.
   virtual bool HasUnplayedFrames() = 0;
 
--- a/dom/media/mediasink/AudioSinkWrapper.cpp
+++ b/dom/media/mediasink/AudioSinkWrapper.cpp
@@ -183,18 +183,17 @@ AudioSinkWrapper::Start(int64_t aStartTi
   mPlayDuration = aStartTime;
   mPlayStartTime = TimeStamp::Now();
 
   // no audio is equivalent to audio ended before video starts.
   mAudioEnded = !aInfo.HasAudio();
 
   if (aInfo.HasAudio()) {
     mAudioSink = mCreator->Create();
-    mEndPromise = mAudioSink->Init();
-    SetPlaybackParams(mParams);
+    mEndPromise = mAudioSink->Init(mParams);
 
     mAudioSinkPromise.Begin(mEndPromise->Then(
       mOwnerThread.get(), __func__, this,
       &AudioSinkWrapper::OnAudioEnded,
       &AudioSinkWrapper::OnAudioEnded));
   }
 }
 
--- a/dom/media/mediasink/DecodedAudioDataSink.cpp
+++ b/dom/media/mediasink/DecodedAudioDataSink.cpp
@@ -1,15 +1,14 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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 "AudioStream.h"
 #include "MediaQueue.h"
 #include "DecodedAudioDataSink.h"
 #include "VideoUtils.h"
 
 #include "mozilla/CheckedInt.h"
 #include "mozilla/DebugOnly.h"
 
 namespace mozilla {
@@ -27,124 +26,43 @@ namespace media {
 // The amount of audio frames that is used to fuzz rounding errors.
 static const int64_t AUDIO_FUZZ_FRAMES = 1;
 
 DecodedAudioDataSink::DecodedAudioDataSink(MediaQueue<MediaData>& aAudioQueue,
                                            int64_t aStartTime,
                                            const AudioInfo& aInfo,
                                            dom::AudioChannel aChannel)
   : AudioSink(aAudioQueue)
-  , mMonitor("DecodedAudioDataSink::mMonitor")
-  , mState(AUDIOSINK_STATE_INIT)
-  , mAudioLoopScheduled(false)
   , mStartTime(aStartTime)
   , mWritten(0)
   , mLastGoodPosition(0)
   , mInfo(aInfo)
   , mChannel(aChannel)
-  , mStopAudioThread(false)
   , mPlaying(true)
 {
 }
 
 DecodedAudioDataSink::~DecodedAudioDataSink()
 {
 }
 
-void
-DecodedAudioDataSink::SetState(State aState)
-{
-  AssertOnAudioThread();
-  mPendingState = Some(aState);
-}
-
-void
-DecodedAudioDataSink::DispatchTask(already_AddRefed<nsIRunnable>&& event)
-{
-  DebugOnly<nsresult> rv = mThread->Dispatch(Move(event), NS_DISPATCH_NORMAL);
-  // There isn't much we can do if Dispatch() fails.
-  // Just assert it to keep things simple.
-  MOZ_ASSERT(NS_SUCCEEDED(rv));
-}
-
-void
-DecodedAudioDataSink::OnAudioQueueEvent()
-{
-  AssertOnAudioThread();
-  if (!mAudioLoopScheduled) {
-    AudioLoop();
-  }
-}
-
-void
-DecodedAudioDataSink::ConnectListener()
-{
-  AssertOnAudioThread();
-  mPushListener = AudioQueue().PushEvent().Connect(
-    mThread, this, &DecodedAudioDataSink::OnAudioQueueEvent);
-  mFinishListener = AudioQueue().FinishEvent().Connect(
-    mThread, this, &DecodedAudioDataSink::OnAudioQueueEvent);
-}
-
-void
-DecodedAudioDataSink::DisconnectListener()
-{
-  AssertOnAudioThread();
-  mPushListener.Disconnect();
-  mFinishListener.Disconnect();
-}
-
-void
-DecodedAudioDataSink::ScheduleNextLoop()
-{
-  AssertOnAudioThread();
-  if (mAudioLoopScheduled) {
-    return;
-  }
-  mAudioLoopScheduled = true;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableMethod(this, &DecodedAudioDataSink::AudioLoop);
-  DispatchTask(r.forget());
-}
-
-void
-DecodedAudioDataSink::ScheduleNextLoopCrossThread()
-{
-  AssertNotOnAudioThread();
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([self] () {
-    // Do nothing if there is already a pending task waiting for its turn.
-    if (!self->mAudioLoopScheduled) {
-      self->AudioLoop();
-    }
-  });
-  DispatchTask(r.forget());
-}
-
 RefPtr<GenericPromise>
-DecodedAudioDataSink::Init()
+DecodedAudioDataSink::Init(const PlaybackParams& aParams)
 {
   RefPtr<GenericPromise> p = mEndPromise.Ensure(__func__);
-  nsresult rv = NS_NewNamedThread("Media Audio",
-                                  getter_AddRefs(mThread),
-                                  nullptr,
-                                  SharedThreadPool::kStackSize);
+  nsresult rv = InitializeAudioStream(aParams);
   if (NS_FAILED(rv)) {
     mEndPromise.Reject(rv, __func__);
-    return p;
   }
-
-  ScheduleNextLoopCrossThread();
   return p;
 }
 
 int64_t
 DecodedAudioDataSink::GetPosition()
 {
-  ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
-
   int64_t pos;
   if (mAudioStream &&
       (pos = mAudioStream->GetPosition()) >= 0) {
     NS_ASSERTION(pos >= mLastGoodPosition,
                  "AudioStream position shouldn't go backward");
     // Update the last good position when we got a good one.
     if (pos >= mLastGoodPosition) {
       mLastGoodPosition = pos;
@@ -152,386 +70,213 @@ DecodedAudioDataSink::GetPosition()
   }
 
   return mStartTime + mLastGoodPosition;
 }
 
 bool
 DecodedAudioDataSink::HasUnplayedFrames()
 {
-  ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
   // Experimentation suggests that GetPositionInFrames() is zero-indexed,
   // so we need to add 1 here before comparing it to mWritten.
   return mAudioStream && mAudioStream->GetPositionInFrames() + 1 < mWritten;
 }
 
 void
 DecodedAudioDataSink::Shutdown()
 {
-  {
-    ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
-    if (mAudioStream) {
-      mAudioStream->Cancel();
-    }
-  }
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-    self->mStopAudioThread = true;
-    if (!self->mAudioLoopScheduled) {
-      self->AudioLoop();
-    }
-  });
-  DispatchTask(r.forget());
-
-  mThread->Shutdown();
-  mThread = nullptr;
   if (mAudioStream) {
     mAudioStream->Shutdown();
     mAudioStream = nullptr;
   }
-
-  // Should've reached the final state after shutdown.
-  MOZ_ASSERT(mState == AUDIOSINK_STATE_SHUTDOWN ||
-             mState == AUDIOSINK_STATE_ERROR);
-  // Should have no pending state change.
-  MOZ_ASSERT(mPendingState.isNothing());
+  mEndPromise.ResolveIfExists(true, __func__);
 }
 
 void
 DecodedAudioDataSink::SetVolume(double aVolume)
 {
-  AssertNotOnAudioThread();
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-    if (self->mState == AUDIOSINK_STATE_PLAYING) {
-      self->mAudioStream->SetVolume(aVolume);
-    }
-  });
-  DispatchTask(r.forget());
+  if (mAudioStream) {
+    mAudioStream->SetVolume(aVolume);
+  }
 }
 
 void
 DecodedAudioDataSink::SetPlaybackRate(double aPlaybackRate)
 {
-  AssertNotOnAudioThread();
   MOZ_ASSERT(aPlaybackRate != 0, "Don't set the playbackRate to 0 on AudioStream");
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-    if (self->mState == AUDIOSINK_STATE_PLAYING) {
-      self->mAudioStream->SetPlaybackRate(aPlaybackRate);
-    }
-  });
-  DispatchTask(r.forget());
+  if (mAudioStream) {
+    mAudioStream->SetPlaybackRate(aPlaybackRate);
+  }
 }
 
 void
 DecodedAudioDataSink::SetPreservesPitch(bool aPreservesPitch)
 {
-  AssertNotOnAudioThread();
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-    if (self->mState == AUDIOSINK_STATE_PLAYING) {
-      self->mAudioStream->SetPreservesPitch(aPreservesPitch);
-    }
-  });
-  DispatchTask(r.forget());
+  if (mAudioStream) {
+    mAudioStream->SetPreservesPitch(aPreservesPitch);
+  }
 }
 
 void
 DecodedAudioDataSink::SetPlaying(bool aPlaying)
 {
-  AssertNotOnAudioThread();
-  RefPtr<DecodedAudioDataSink> self = this;
-  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction([=] () {
-    if (self->mState != AUDIOSINK_STATE_PLAYING ||
-        self->mPlaying == aPlaying) {
-      return;
-    }
-    self->mPlaying = aPlaying;
-    // pause/resume AudioStream as necessary.
-    if (!aPlaying && !self->mAudioStream->IsPaused()) {
-      self->mAudioStream->Pause();
-    } else if (aPlaying && self->mAudioStream->IsPaused()) {
-      self->mAudioStream->Resume();
-    }
-    // Wake up the audio loop to play next sample.
-    if (aPlaying && !self->mAudioLoopScheduled) {
-      self->AudioLoop();
-    }
-  });
-  DispatchTask(r.forget());
+  if (!mAudioStream || mPlaying == aPlaying) {
+    return;
+  }
+  // pause/resume AudioStream as necessary.
+  if (!aPlaying && !mAudioStream->IsPaused()) {
+    mAudioStream->Pause();
+  } else if (aPlaying && mAudioStream->IsPaused()) {
+    mAudioStream->Resume();
+  }
+  mPlaying = aPlaying;
 }
 
 nsresult
-DecodedAudioDataSink::InitializeAudioStream()
+DecodedAudioDataSink::InitializeAudioStream(const PlaybackParams& aParams)
 {
-  // AudioStream initialization can block for extended periods in unusual
-  // circumstances, so we take care to drop the decoder monitor while
-  // initializing.
-  RefPtr<AudioStream> audioStream(new AudioStream());
-  nsresult rv = audioStream->Init(mInfo.mChannels, mInfo.mRate, mChannel);
+  mAudioStream = new AudioStream(*this);
+  nsresult rv = mAudioStream->Init(mInfo.mChannels, mInfo.mRate, mChannel);
   if (NS_FAILED(rv)) {
-    audioStream->Shutdown();
+    mAudioStream->Shutdown();
+    mAudioStream = nullptr;
     return rv;
   }
 
-  ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
-  mAudioStream = audioStream;
+  // Set playback params before calling Start() so they can take effect
+  // as soon as the 1st DataCallback of the AudioStream fires.
+  mAudioStream->SetVolume(aParams.mVolume);
+  mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
+  mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
+  mAudioStream->Start();
 
   return NS_OK;
 }
 
-void
-DecodedAudioDataSink::Drain()
-{
-  AssertOnAudioThread();
-  MOZ_ASSERT(mPlaying && !mAudioStream->IsPaused());
-  // If the media was too short to trigger the start of the audio stream,
-  // start it now.
-  mAudioStream->Start();
-  mAudioStream->Drain();
-}
-
-void
-DecodedAudioDataSink::Cleanup()
-{
-  AssertOnAudioThread();
-  mEndPromise.Resolve(true, __func__);
-  // Since the promise if resolved asynchronously, we don't shutdown
-  // AudioStream here so MDSM::ResyncAudioClock can get the correct
-  // audio position.
-}
-
-bool
-DecodedAudioDataSink::ExpectMoreAudioData()
-{
-  return AudioQueue().GetSize() == 0 && !AudioQueue().IsFinished();
-}
-
-bool
-DecodedAudioDataSink::WaitingForAudioToPlay()
-{
-  AssertOnAudioThread();
-  // Return true if we're not playing, and we're not shutting down, or we're
-  // playing and we've got no audio to play.
-  if (!mStopAudioThread && (!mPlaying || ExpectMoreAudioData())) {
-    return true;
-  }
-  return false;
-}
-
-bool
-DecodedAudioDataSink::IsPlaybackContinuing()
-{
-  AssertOnAudioThread();
-  // If we're shutting down, captured, or at EOS, break out and exit the audio
-  // thread.
-  if (mStopAudioThread || AudioQueue().AtEndOfStream()) {
-    return false;
-  }
-
-  return true;
-}
-
-void
-DecodedAudioDataSink::AudioLoop()
-{
-  AssertOnAudioThread();
-  mAudioLoopScheduled = false;
-
-  switch (mState) {
-    case AUDIOSINK_STATE_INIT: {
-      SINK_LOG("AudioLoop started");
-      nsresult rv = InitializeAudioStream();
-      if (NS_FAILED(rv)) {
-        NS_WARNING("Initializing AudioStream failed.");
-        mEndPromise.Reject(rv, __func__);
-        SetState(AUDIOSINK_STATE_ERROR);
-        break;
-      }
-      SetState(AUDIOSINK_STATE_PLAYING);
-      ConnectListener();
-      break;
-    }
-
-    case AUDIOSINK_STATE_PLAYING: {
-      if (WaitingForAudioToPlay()) {
-        // OnAudioQueueEvent() will schedule next loop.
-        break;
-      }
-      if (!IsPlaybackContinuing()) {
-        SetState(AUDIOSINK_STATE_COMPLETE);
-        break;
-      }
-      if (!PlayAudio()) {
-        SetState(AUDIOSINK_STATE_COMPLETE);
-        break;
-      }
-      // Schedule next loop to play next sample.
-      ScheduleNextLoop();
-      break;
-    }
-
-    case AUDIOSINK_STATE_COMPLETE: {
-      DisconnectListener();
-      FinishAudioLoop();
-      SetState(AUDIOSINK_STATE_SHUTDOWN);
-      break;
-    }
-
-    case AUDIOSINK_STATE_SHUTDOWN:
-      break;
-
-    case AUDIOSINK_STATE_ERROR:
-      break;
-  } // end of switch
-
-  // We want mState to stay stable during AudioLoop to keep things simple.
-  // Therefore, we only do state transition at the end of AudioLoop.
-  if (mPendingState.isSome()) {
-    MOZ_ASSERT(mState != mPendingState.ref());
-    SINK_LOG("change mState, %d -> %d", mState, mPendingState.ref());
-    mState = mPendingState.ref();
-    mPendingState.reset();
-    // Schedule next loop when state changes.
-    ScheduleNextLoop();
-  }
-}
-
-bool
-DecodedAudioDataSink::PlayAudio()
-{
-  // See if there's a gap in the audio. If there is, push silence into the
-  // audio hardware, so we can play across the gap.
-  // Calculate the timestamp of the next chunk of audio in numbers of
-  // samples.
-  NS_ASSERTION(AudioQueue().GetSize() > 0, "Should have data to play");
-  CheckedInt64 sampleTime = UsecsToFrames(AudioQueue().PeekFront()->mTime, mInfo.mRate);
-
-  // Calculate the number of frames that have been pushed onto the audio hardware.
-  CheckedInt64 playedFrames = UsecsToFrames(mStartTime, mInfo.mRate) +
-                              static_cast<int64_t>(mWritten);
-
-  CheckedInt64 missingFrames = sampleTime - playedFrames;
-  if (!missingFrames.isValid() || !sampleTime.isValid()) {
-    NS_WARNING("Int overflow adding in AudioLoop");
-    return false;
-  }
-
-  if (missingFrames.value() > AUDIO_FUZZ_FRAMES) {
-    // The next audio chunk begins some time after the end of the last chunk
-    // we pushed to the audio hardware. We must push silence into the audio
-    // hardware so that the next audio chunk begins playback at the correct
-    // time.
-    missingFrames = std::min<int64_t>(UINT32_MAX, missingFrames.value());
-    mWritten += PlaySilence(static_cast<uint32_t>(missingFrames.value()));
-  } else {
-    mWritten += PlayFromAudioQueue();
-  }
-
-  return true;
-}
-
-void
-DecodedAudioDataSink::FinishAudioLoop()
-{
-  AssertOnAudioThread();
-  MOZ_ASSERT(mStopAudioThread || AudioQueue().AtEndOfStream());
-  if (!mStopAudioThread && mPlaying) {
-    Drain();
-  }
-  SINK_LOG("AudioLoop complete");
-  Cleanup();
-  SINK_LOG("AudioLoop exit");
-}
-
-uint32_t
-DecodedAudioDataSink::PlaySilence(uint32_t aFrames)
-{
-  // Maximum number of bytes we'll allocate and write at once to the audio
-  // hardware when the audio stream contains missing frames and we're
-  // writing silence in order to fill the gap. We limit our silence-writes
-  // to 32KB in order to avoid allocating an impossibly large chunk of
-  // memory if we encounter a large chunk of silence.
-  const uint32_t SILENCE_BYTES_CHUNK = 32 * 1024;
-
-  AssertOnAudioThread();
-  NS_ASSERTION(!mAudioStream->IsPaused(), "Don't play when paused");
-  uint32_t maxFrames = SILENCE_BYTES_CHUNK / mInfo.mChannels / sizeof(AudioDataValue);
-  uint32_t frames = std::min(aFrames, maxFrames);
-  SINK_LOG_V("playing %u frames of silence", aFrames);
-  WriteSilence(frames);
-  return frames;
-}
-
-uint32_t
-DecodedAudioDataSink::PlayFromAudioQueue()
-{
-  AssertOnAudioThread();
-  NS_ASSERTION(!mAudioStream->IsPaused(), "Don't play when paused");
-  RefPtr<AudioData> audio =
-    dont_AddRef(AudioQueue().PopFront().take()->As<AudioData>());
-
-  SINK_LOG_V("playing %u frames of audio at time %lld",
-             audio->mFrames, audio->mTime);
-  if (audio->mRate == mInfo.mRate && audio->mChannels == mInfo.mChannels) {
-    mAudioStream->Write(audio->mAudioData.get(), audio->mFrames);
-  } else {
-    SINK_LOG_V("mismatched sample format mInfo=[%uHz/%u channels] audio=[%uHz/%u channels]",
-               mInfo.mRate, mInfo.mChannels, audio->mRate, audio->mChannels);
-    PlaySilence(audio->mFrames);
-  }
-
-  StartAudioStreamPlaybackIfNeeded();
-
-  return audio->mFrames;
-}
-
-void
-DecodedAudioDataSink::StartAudioStreamPlaybackIfNeeded()
-{
-  // This value has been chosen empirically.
-  const uint32_t MIN_WRITE_BEFORE_START_USECS = 200000;
-
-  // We want to have enough data in the buffer to start the stream.
-  if (static_cast<double>(mAudioStream->GetWritten()) / mAudioStream->GetRate() >=
-      static_cast<double>(MIN_WRITE_BEFORE_START_USECS) / USECS_PER_S) {
-    mAudioStream->Start();
-  }
-}
-
-void
-DecodedAudioDataSink::WriteSilence(uint32_t aFrames)
-{
-  uint32_t numSamples = aFrames * mInfo.mChannels;
-  nsAutoTArray<AudioDataValue, 1000> buf;
-  buf.SetLength(numSamples);
-  memset(buf.Elements(), 0, numSamples * sizeof(AudioDataValue));
-  mAudioStream->Write(buf.Elements(), aFrames);
-
-  StartAudioStreamPlaybackIfNeeded();
-}
-
 int64_t
 DecodedAudioDataSink::GetEndTime() const
 {
   CheckedInt64 playedUsecs = FramesToUsecs(mWritten, mInfo.mRate) + mStartTime;
   if (!playedUsecs.isValid()) {
     NS_WARNING("Int overflow calculating audio end time");
     return -1;
   }
   return playedUsecs.value();
 }
 
-void
-DecodedAudioDataSink::AssertOnAudioThread()
+UniquePtr<AudioStream::Chunk>
+DecodedAudioDataSink::PopFrames(uint32_t aFrames)
 {
-  MOZ_ASSERT(NS_GetCurrentThread() == mThread);
+  class Chunk : public AudioStream::Chunk {
+  public:
+    Chunk(AudioData* aBuffer, uint32_t aFrames, uint32_t aOffset)
+      : mBuffer(aBuffer)
+      , mFrames(aFrames)
+      , mData(aBuffer->mAudioData.get() + aBuffer->mChannels * aOffset) {
+      MOZ_ASSERT(aOffset + aFrames <= aBuffer->mFrames);
+    }
+    Chunk() : mFrames(0), mData(nullptr) {}
+    const AudioDataValue* Data() const { return mData; }
+    uint32_t Frames() const { return mFrames; }
+    AudioDataValue* GetWritable() const { return mData; }
+
+  private:
+    const RefPtr<AudioData> mBuffer;
+    const uint32_t mFrames;
+    AudioDataValue* const mData;
+  };
+
+  class SilentChunk : public AudioStream::Chunk {
+  public:
+    SilentChunk(uint32_t aFrames, uint32_t aChannels)
+      : mFrames(aFrames)
+      , mData(MakeUnique<AudioDataValue[]>(aChannels * aFrames)) {
+      memset(mData.get(), 0, aChannels * aFrames * sizeof(AudioDataValue));
+    }
+    const AudioDataValue* Data() const { return mData.get(); }
+    uint32_t Frames() const { return mFrames; }
+    AudioDataValue* GetWritable() const { return mData.get(); }
+  private:
+    const uint32_t mFrames;
+    UniquePtr<AudioDataValue[]> mData;
+  };
+
+  if (!mCurrentData) {
+    // No data in the queue. Return an empty chunk.
+    if (AudioQueue().GetSize() == 0) {
+      return MakeUnique<Chunk>();
+    }
+
+    // See if there's a gap in the audio. If there is, push silence into the
+    // audio hardware, so we can play across the gap.
+    // Calculate the timestamp of the next chunk of audio in numbers of
+    // samples.
+    CheckedInt64 sampleTime = UsecsToFrames(AudioQueue().PeekFront()->mTime, mInfo.mRate);
+    // Calculate the number of frames that have been pushed onto the audio hardware.
+    CheckedInt64 playedFrames = UsecsToFrames(mStartTime, mInfo.mRate) +
+                                static_cast<int64_t>(mWritten);
+    CheckedInt64 missingFrames = sampleTime - playedFrames;
+
+    if (!missingFrames.isValid() || !sampleTime.isValid()) {
+      NS_WARNING("Int overflow in DecodedAudioDataSink");
+      mErrored = true;
+      return MakeUnique<Chunk>();
+    }
+
+    if (missingFrames.value() > AUDIO_FUZZ_FRAMES) {
+      // The next audio chunk begins some time after the end of the last chunk
+      // we pushed to the audio hardware. We must push silence into the audio
+      // hardware so that the next audio chunk begins playback at the correct
+      // time.
+      missingFrames = std::min<int64_t>(UINT32_MAX, missingFrames.value());
+      auto framesToPop = std::min<uint32_t>(missingFrames.value(), aFrames);
+      mWritten += framesToPop;
+      return MakeUnique<SilentChunk>(framesToPop, mInfo.mChannels);
+    }
+
+    mFramesPopped = 0;
+    mCurrentData = dont_AddRef(AudioQueue().PopFront().take()->As<AudioData>());
+  }
+
+  auto framesToPop = std::min(aFrames, mCurrentData->mFrames - mFramesPopped);
+
+  SINK_LOG_V("playing audio at time=%lld offset=%u length=%u",
+             mCurrentData->mTime, mFramesPopped, framesToPop);
+
+  UniquePtr<AudioStream::Chunk> chunk;
+
+  if (mCurrentData->mRate == mInfo.mRate &&
+      mCurrentData->mChannels == mInfo.mChannels) {
+    chunk = MakeUnique<Chunk>(mCurrentData, framesToPop, mFramesPopped);
+  } else {
+    SINK_LOG_V("mismatched sample format mInfo=[%uHz/%u channels] audio=[%uHz/%u channels]",
+               mInfo.mRate, mInfo.mChannels, mCurrentData->mRate, mCurrentData->mChannels);
+    chunk = MakeUnique<SilentChunk>(framesToPop, mInfo.mChannels);
+  }
+
+  mWritten += framesToPop;
+  mFramesPopped += framesToPop;
+
+  // All frames are popped. Reset mCurrentData so we can pop new elements from
+  // the audio queue in next calls to PopFrames().
+  if (mFramesPopped == mCurrentData->mFrames) {
+    mCurrentData = nullptr;
+  }
+
+  return chunk;
+}
+
+bool
+DecodedAudioDataSink::Ended() const
+{
+  // Return true when error encountered so AudioStream can start draining.
+  return AudioQueue().IsFinished() || mErrored;
 }
 
 void
-DecodedAudioDataSink::AssertNotOnAudioThread()
+DecodedAudioDataSink::Drained()
 {
-  MOZ_ASSERT(NS_GetCurrentThread() != mThread);
+  SINK_LOG("Drained");
+  mEndPromise.Resolve(true, __func__);
 }
 
 } // namespace media
 } // namespace mozilla
--- a/dom/media/mediasink/DecodedAudioDataSink.h
+++ b/dom/media/mediasink/DecodedAudioDataSink.h
@@ -2,47 +2,47 @@
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* 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/. */
 #if !defined(DecodedAudioDataSink_h__)
 #define DecodedAudioDataSink_h__
 
 #include "AudioSink.h"
+#include "AudioStream.h"
 #include "MediaEventSource.h"
 #include "MediaInfo.h"
 #include "mozilla/RefPtr.h"
 #include "nsISupportsImpl.h"
 
 #include "mozilla/dom/AudioChannelBinding.h"
 #include "mozilla/Atomics.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/ReentrantMonitor.h"
 
 namespace mozilla {
 
-class AudioStream;
-
 namespace media {
 
-class DecodedAudioDataSink : public AudioSink {
+class DecodedAudioDataSink : public AudioSink,
+                             private AudioStream::DataSource {
 public:
-
   DecodedAudioDataSink(MediaQueue<MediaData>& aAudioQueue,
                        int64_t aStartTime,
                        const AudioInfo& aInfo,
                        dom::AudioChannel aChannel);
 
   // Return a promise which will be resolved when DecodedAudioDataSink
   // finishes playing, or rejected if any error.
-  RefPtr<GenericPromise> Init() override;
+  RefPtr<GenericPromise> Init(const PlaybackParams& aParams) override;
 
   /*
-   * All public functions below are thread-safe.
+   * All public functions are not thread-safe.
+   * Called on the task queue of MDSM only.
    */
   int64_t GetPosition() override;
   int64_t GetEndTime() const override;
 
   // Check whether we've pushed more frames to the audio hardware than it has
   // played.
   bool HasUnplayedFrames() override;
 
@@ -50,133 +50,60 @@ public:
   void Shutdown() override;
 
   void SetVolume(double aVolume) override;
   void SetPlaybackRate(double aPlaybackRate) override;
   void SetPreservesPitch(bool aPreservesPitch) override;
   void SetPlaying(bool aPlaying) override;
 
 private:
-  enum State {
-    AUDIOSINK_STATE_INIT,
-    AUDIOSINK_STATE_PLAYING,
-    AUDIOSINK_STATE_COMPLETE,
-    AUDIOSINK_STATE_SHUTDOWN,
-    AUDIOSINK_STATE_ERROR
-  };
-
   virtual ~DecodedAudioDataSink();
 
-  void DispatchTask(already_AddRefed<nsIRunnable>&& event);
-  void SetState(State aState);
-  void ScheduleNextLoop();
-  void ScheduleNextLoopCrossThread();
-
-  void OnAudioQueueEvent();
-  void ConnectListener();
-  void DisconnectListener();
-
-  // The main loop for the audio thread. Sent to the thread as
-  // an nsRunnableMethod. This continually does blocking writes to
-  // to audio stream to play audio data.
-  void AudioLoop();
-
-  // Allocate and initialize mAudioStream.  Returns NS_OK on success.
-  nsresult InitializeAudioStream();
-
-  void Drain();
-
-  void Cleanup();
-
-  bool ExpectMoreAudioData();
-
-  // Return true if playback is not ready and the sink is not told to shut down.
-  bool WaitingForAudioToPlay();
-
-  // Check if the sink has been told to shut down, resuming mAudioStream if
-  // not.  Returns true if processing should continue, false if AudioLoop
-  // should shutdown.
-  bool IsPlaybackContinuing();
-
-  // Write audio samples or silence to the audio hardware.
-  // Return false if any error. Called on the audio thread.
-  bool PlayAudio();
-
-  void FinishAudioLoop();
+  // Allocate and initialize mAudioStream. Returns NS_OK on success.
+  nsresult InitializeAudioStream(const PlaybackParams& aParams);
 
-  // Write aFrames of audio frames of silence to the audio hardware. Returns
-  // the number of frames actually written. The write size is capped at
-  // SILENCE_BYTES_CHUNK (32kB), so must be called in a loop to write the
-  // desired number of frames. This ensures that the playback position
-  // advances smoothly, and guarantees that we don't try to allocate an
-  // impossibly large chunk of memory in order to play back silence. Called
-  // on the audio thread.
-  uint32_t PlaySilence(uint32_t aFrames);
-
-  // Pops an audio chunk from the front of the audio queue, and pushes its
-  // audio data to the audio hardware.  Called on the audio thread.
-  uint32_t PlayFromAudioQueue();
-
-  // If we have already written enough frames to the AudioStream, start the
-  // playback.
-  void StartAudioStreamPlaybackIfNeeded();
-  void WriteSilence(uint32_t aFrames);
-
-  ReentrantMonitor& GetReentrantMonitor() const {
-    return mMonitor;
-  }
+  // Interface of AudioStream::DataSource.
+  // Called on the callback thread of cubeb.
+  UniquePtr<AudioStream::Chunk> PopFrames(uint32_t aFrames) override;
+  bool Ended() const override;
+  void Drained() override;
 
-  void AssertCurrentThreadInMonitor() const {
-    GetReentrantMonitor().AssertCurrentThreadIn();
-  }
-
-  void AssertOnAudioThread();
-  void AssertNotOnAudioThread();
-
-  mutable ReentrantMonitor mMonitor;
-
-  // There members are accessed on the audio thread only.
-  State mState;
-  Maybe<State> mPendingState;
-  bool mAudioLoopScheduled;
-
-  // Thread for pushing audio onto the audio hardware.
-  // The "audio push thread".
-  nsCOMPtr<nsIThread> mThread;
-
-  // The audio stream resource. Used on the state machine, and audio threads.
-  // This is created and destroyed on the audio thread, while holding the
-  // decoder monitor, so if this is used off the audio thread, you must
-  // first acquire the decoder monitor and check that it is non-null.
+  // The audio stream resource. Used on the task queue of MDSM only.
   RefPtr<AudioStream> mAudioStream;
 
   // The presentation time of the first audio frame that was played in
   // microseconds. We can add this to the audio stream position to determine
-  // the current audio time. Accessed on audio and state machine thread.
-  // Synchronized by decoder monitor.
+  // the current audio time.
   const int64_t mStartTime;
 
   // PCM frames written to the stream so far.
   Atomic<int64_t> mWritten;
 
   // Keep the last good position returned from the audio stream. Used to ensure
   // position returned by GetPosition() is mono-increasing in spite of audio
-  // stream error.
+  // stream error. Used on the task queue of MDSM only.
   int64_t mLastGoodPosition;
 
   const AudioInfo mInfo;
 
   const dom::AudioChannel mChannel;
 
-  bool mStopAudioThread;
-
+  // Used on the task queue of MDSM only.
   bool mPlaying;
 
   MozPromiseHolder<GenericPromise> mEndPromise;
 
-  MediaEventListener mPushListener;
-  MediaEventListener mFinishListener;
+  /*
+   * Members to implement AudioStream::DataSource.
+   * Used on the callback thread of cubeb.
+   */
+  // The AudioData at which AudioStream::DataSource is reading.
+  RefPtr<AudioData> mCurrentData;
+  // The number of frames that have been popped from mCurrentData.
+  uint32_t mFramesPopped = 0;
+  // True if there is any error in processing audio data like overflow.
+  bool mErrored = false;
 };
 
 } // namespace media
 } // namespace mozilla
 
 #endif
--- a/dom/media/omx/moz.build
+++ b/dom/media/omx/moz.build
@@ -66,20 +66,21 @@ if CONFIG['ANDROID_VERSION'] >= '18':
     ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 # Suppress some GCC/clang warnings being treated as errors:
 #  - about attributes on forward declarations for types that are already
 #    defined, which complains about an important MOZ_EXPORT for android::AString
 #  - about multi-character constants which are used in codec-related code
+#    and are part of Android's libstagefright API style.
 if CONFIG['GNU_CC'] or CONFIG['CLANG_CL']:
   CXXFLAGS += [
     '-Wno-error=attributes',
-    '-Wno-error=multichar'
+    '-Wno-multichar'
   ]
 
 FINAL_LIBRARY = 'xul'
 LOCAL_INCLUDES += [
     '/dom/base',
     '/dom/html',
     '/ipc/chromium/src',
 ]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/MANIFEST.in
@@ -0,0 +1,7 @@
+exclude MANIFEST.in
+include requirements.txt
+include setup.py
+recursive-include firefox_media_tests *
+recursive-include media_utils *
+
+
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/README.md
@@ -0,0 +1,157 @@
+firefox-media-tests
+===================
+
+[Marionette Python tests][marionette-python-tests] for media playback in Mozilla Firefox. MediaTestCase uses [Firefox Puppeteer][ff-puppeteer-docs] library.
+
+Setup
+-----
+
+The instructions below assume you have a copy of the project in `some/path/firefox-media-tests` and they refer to this path as `$PROJECT_HOME`.
+
+* Create a virtualenv called `foo`.
+
+   ```sh
+   $ virtualenv foo
+   $ source foo/bin/activate #or `foo\Scripts\activate` on Windows
+   ```
+
+* Install `firefox-media-tests` in development mode. (To get an environment that is closer to what is actually used in Mozilla's automation jobs, run `pip install -r requirements.txt` first.)
+
+   ```sh
+   $ python setup.py develop
+   ```
+
+Now `firefox-media-tests` should be a recognized command. Try `firefox-media-tests --help` to see if it works.
+
+
+Running the Tests
+-----------------
+
+In the examples below, `$FF_PATH` is a path to a recent Firefox binary.
+
+This runs all the tests listed in `$PROJECT_HOME/firefox_media_tests/manifest.ini`:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH
+   ```
+
+You can also run all the tests at a particular path:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/path/foo
+   ```
+
+Or you can run the tests that are listed in a manifest file of your choice.
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/other/path/manifest.ini
+   ```
+
+By default, the urls listed in `firefox_media_tests/urls/default.ini` are used for the tests, but you can also supply your own ini file of urls:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH --urls some/other/path/my_urls.ini
+   ```
+
+### Running EME tests
+
+In order to run EME tests, you must use a Firefox profile that has a signed plugin-container.exe and voucher.bin. With Netflix, this will be created when you log in and save the credentials. You must also use a custom .ini file for urls to the provider's content and indicate which test to run, like above. Ex:
+
+   ```sh
+   $ firefox-media-tests --binary $FF_PATH some/path/tests.ini --profile custom_profile --urls some/path/provider-urls.ini
+   ```
+
+
+### Running tests in a way that provides information about a crash
+
+What if Firefox crashes during a test run? You want to know why! To report useful crash data, the test runner needs access to a "minidump_stackwalk" binary and a "symbols.zip" file.
+
+1. Download a `minidump_stackwalk` binary for your platform (save it whereever). Get it from http://hg.mozilla.org/build/tools/file/tip/breakpad/.
+2. Make `minidump_stackwalk` executable
+
+   ```sh
+   $ chmod +x path/to/minidump_stackwalk
+   ```
+
+3. Create an environment variable called `MINIDUMP_STACKWALK` that points to that local path
+
+   ```sh
+   $ export MINIDUMP_STACKWALK=path/to/minidump_stackwalk
+   ```
+
+4. Download the `crashreporter-symbols.zip` file for the Firefox build you are testing and extract it. Example: ftp://ftp.mozilla.org/pub/firefox/tinderbox-builds/mozilla-aurora-win32/1427442016/firefox-38.0a2.en-US.win32.crashreporter-symbols.zip
+
+5. Run the tests with a `--symbols-path` flag
+
+  ```sh
+   $ firefox-media-tests --binary $FF_PATH --symbols-path path/to/example/firefox-38.0a2.en-US.win32.crashreporter-symbols
+  ```
+
+To check whether the above setup is working for you, trigger a (silly) Firefox crash while the tests are running. One way to do this is with the [crashme add-on](https://github.com/luser/crashme) -- you can add it to Firefox even while the tests are running. Another way on Linux and Mac OS systems:
+
+1. Find the process id (PID) of the Firefox process being used by the tests.
+
+  ```sh
+   $ ps x | grep 'Firefox'
+  ```
+
+2. Kill the Firefox process with SIGABRT.
+  ```sh
+  # 1234 is an example of a PID
+   $ kill -6 1234
+  ```
+
+Somewhere in the output produced by `firefox-media-tests`, you should see something like:
+
+```
+0:12.68 CRASH: MainThread pid:1234. Test:test_basic_playback.py TestVideoPlayback.test_playback_starts.
+Minidump anaylsed:False.
+Signature:[@ XUL + 0x2a65900]
+Crash dump filename:
+/var/folders/5k/xmn_fndx0qs2jcpcwhzl86wm0000gn/T/tmpB4Bolj.mozrunner/minidumps/DA3BB025-8302-4F96-8DF3-A97E424C877A.dmp
+Operating system: Mac OS X
+                  10.10.2 14C1514
+CPU: amd64
+     family 6 model 69 stepping 1
+     4 CPUs
+
+Crash reason:  EXC_SOFTWARE / SIGABRT
+Crash address: 0x104616900
+...
+```
+
+### Setting up for network shaping tests (browsermobproxy)
+
+1. Download the browsermob proxy zip file from http://bmp.lightbody.net/. The most current version as of this writing is browsermob-proxy-2.1.0-beta-2-bin.zip.
+2. Unpack the .zip file.
+3. Verify that you can launch browsermobproxy on your machine by running \<browsermob\>/bin/browsermob-proxy on your machine. I had to do a lot of work to install and use a java that browsermobproxy would like.
+4. Import the certificate into your Firefox profile. Select Preferences->Advanced->Certificates->View Certificates->Import... Navigate to <browsermob>/ssl-support and select cybervilliansCA.cer. Select all of the checkboxes.
+5. Tell marionette where browsermobproxy is and what port to start it on. Add the following command-line parameters to your firefox-media-tests command line:
+
+<pre><code>
+--browsermob-script <browsermob>/bin/browsermob-proxy --browsermob-port 999 --profile <your saved profile>
+</code></pre>
+
+On Windows, use browsermob-proxy.bat.
+
+You can then call browsermob to shape the network. You can find an example in firefox_media_tests/playback/test_playback_limiting_bandwidth.py. Another example can be found at https://dxr.mozilla.org/mozilla-central/source/testing/marionette/client/marionette/tests/unit/test_browsermobproxy.py.
+
+### A warning about video URLs
+The ini files in `firefox_media_tests/urls` may contain URLs pulled from Firefox crash or bug data. Automated tests don't care about video content, but you might: visit these at your own risk and be aware that they may be NSFW. We do not intend to ever moderate or filter these URLs.
+
+Writing a test
+--------------
+Write your test in a new or existing `test_*.py` file under `$PROJECT_HOME/firefox_media_tests`. Add it to the appropriate `manifest.ini` file(s) as well. Look in `media_utils` for useful video-playback functions.
+
+* [Marionette docs][marionette-docs]
+  - [Marionette Command Line Options](https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options)
+* [Firefox Puppeteer docs][ff-puppeteer-docs]
+
+License
+-------
+This software is licensed under the [Mozilla Public License v. 2.0](http://mozilla.org/MPL/2.0/).
+
+[marionette-python-tests]: https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/Marionette_Python_Tests
+[ff-puppeteer-docs]: http://firefox-puppeteer.readthedocs.org/en/latest/
+[marionette-docs]: http://marionette-client.readthedocs.org/en/latest/reference.html
+[ff-nightly]:https://nightly.mozilla.org/
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/__init__.py
@@ -0,0 +1,5 @@
+# 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/.
+
+from runtests import cli
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/runtests.py
@@ -0,0 +1,108 @@
+# 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/.
+
+from manifestparser import read_ini
+import os
+import sys
+
+from marionette import BaseMarionetteTestRunner, BaseMarionetteArguments
+from marionette.runner import BrowserMobProxyArguments
+from marionette.runtests import MarionetteHarness, cli as mn_cli
+import mozlog
+
+import media_tests
+from testcase import MediaTestCase
+from media_utils.video_puppeteer import debug_script
+
+
+class MediaTestArgumentsBase(object):
+    name = 'Firefox Media Tests'
+    args = [
+        [['--urls'], {
+            'help': 'ini file of urls to make available to all tests',
+            'default': os.path.join(media_tests.urls, 'default.ini'),
+        }],
+    ]
+
+    def verify_usage_handler(self, args):
+        if args.urls:
+           if not os.path.isfile(args.urls):
+               raise ValueError('--urls must provide a path to an ini file')
+           else:
+               path = os.path.abspath(args.urls)
+               args.video_urls = MediaTestArgumentsBase.get_urls(path)
+
+    def parse_args_handler(self, args):
+        if not args.tests:
+           args.tests = [media_tests.manifest]
+
+
+    @staticmethod
+    def get_urls(manifest):
+        with open(manifest, 'r'):
+            return [line[0] for line in read_ini(manifest)]
+
+
+class MediaTestArguments(BaseMarionetteArguments):
+    def __init__(self, **kwargs):
+        BaseMarionetteArguments.__init__(self, **kwargs)
+        self.register_argument_container(MediaTestArgumentsBase())
+        self.register_argument_container(BrowserMobProxyArguments())
+
+
+class MediaTestRunner(BaseMarionetteTestRunner):
+    def __init__(self, **kwargs):
+        BaseMarionetteTestRunner.__init__(self, **kwargs)
+        if not self.server_root:
+            self.server_root = media_tests.resources
+        # pick up prefs from marionette_driver.geckoinstance.DesktopInstance
+        self.app = 'fxdesktop'
+        self.test_handlers = [MediaTestCase]
+
+        # Used in HTML report (--log-html)
+        def gather_media_debug(test, status):
+            rv = {}
+            marionette = test._marionette_weakref()
+
+            if marionette.session is not None:
+                try:
+                    with marionette.using_context(marionette.CONTEXT_CHROME):
+                        debug_lines = marionette.execute_script(debug_script)
+                        if debug_lines:
+                            name = 'mozMediaSourceObject.mozDebugReaderData'
+                            rv[name] = '\n'.join(debug_lines)
+                        else:
+                            logger = mozlog.get_default_logger()
+                            logger.info('No data available about '
+                                        'mozMediaSourceObject')
+                except:
+                    logger = mozlog.get_default_logger()
+                    logger.warning('Failed to gather test failure media debug',
+                                   exc_info=True)
+            return rv
+
+        self.result_callbacks.append(gather_media_debug)
+
+
+class FirefoxMediaHarness(MarionetteHarness):
+    def __init__(self,
+                 runner_class=MediaTestRunner,
+                 parser_class=MediaTestArguments):
+        # workaround until next marionette-client release - Bug 1227918
+        try:
+            MarionetteHarness.__init__(self, runner_class, parser_class)
+        except Exception:
+            logger = mozlog.commandline.setup_logging('Media-test harness', {})
+            logger.error('Failure setting up harness', exc_info=True)
+            raise
+
+    def parse_args(self, *args, **kwargs):
+        return MarionetteHarness.parse_args(self, {'mach': sys.stdout})
+
+
+def cli():
+    mn_cli(MediaTestRunner, MediaTestArguments, FirefoxMediaHarness)
+
+if __name__ == '__main__':
+    cli()
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/harness/testcase.py
@@ -0,0 +1,138 @@
+# 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/.
+
+import os
+
+from marionette import BrowserMobProxyTestCaseMixin
+from marionette_driver import Wait
+from marionette_driver.errors import TimeoutException
+from marionette.marionette_test import SkipTest
+
+from firefox_puppeteer.testcases import FirefoxTestCase
+from media_tests.utils import (timestamp_now, verbose_until)
+from media_utils.video_puppeteer import (playback_done, playback_started,
+                                         VideoException, VideoPuppeteer as VP)
+
+
+class MediaTestCase(FirefoxTestCase):
+
+    def __init__(self, *args, **kwargs):
+        self.video_urls = kwargs.pop('video_urls', False)
+        FirefoxTestCase.__init__(self, *args, **kwargs)
+
+    def save_screenshot(self):
+        screenshot_dir = os.path.join(self.marionette.instance.workspace or '',
+                                      'screenshots')
+        filename = ''.join([self.id().replace(' ', '-'),
+                            '_',
+                            str(timestamp_now()),
+                            '.png'])
+        path = os.path.join(screenshot_dir, filename)
+        if not os.path.exists(screenshot_dir):
+            os.makedirs(screenshot_dir)
+        with self.marionette.using_context('content'):
+            img_data = self.marionette.screenshot()
+        with open(path, 'wb') as f:
+            f.write(img_data.decode('base64'))
+        self.marionette.log('Screenshot saved in %s' % os.path.abspath(path))
+
+    def log_video_debug_lines(self):
+        with self.marionette.using_context('chrome'):
+            debug_lines = self.marionette.execute_script(VP._debug_script)
+            if debug_lines:
+                self.marionette.log('\n'.join(debug_lines))
+
+    def run_playback(self, video):
+        with self.marionette.using_context('content'):
+            self.logger.info(video.test_url)
+            try:
+                verbose_until(Wait(video, interval=video.interval,
+                                   timeout=video.expected_duration * 1.3 +
+                                   video.stall_wait_time),
+                              video, playback_done)
+            except VideoException as e:
+                raise self.failureException(e)
+
+    def check_playback_starts(self, video):
+        with self.marionette.using_context('content'):
+            self.logger.info(video.test_url)
+            try:
+                verbose_until(Wait(video, timeout=video.timeout),
+                              video, playback_started)
+            except TimeoutException as e:
+                raise self.failureException(e)
+
+    def skipTest(self, reason):
+        """
+        Skip this test.
+
+        Skip with marionette.marionette_test import SkipTest so that it
+        gets recognized a skip in marionette.marionette_test.CommonTestCase.run
+        """
+        raise SkipTest(reason)
+
+
+class NetworkBandwidthTestCase(MediaTestCase):
+
+    def __init__(self, *args, **kwargs):
+        MediaTestCase.__init__(self, *args, **kwargs)
+        BrowserMobProxyTestCaseMixin.__init__(self, *args, **kwargs)
+        self.proxy = None
+
+    def setUp(self):
+        MediaTestCase.setUp(self)
+        BrowserMobProxyTestCaseMixin.setUp(self)
+        self.proxy = self.create_browsermob_proxy()
+
+    def tearDown(self):
+        MediaTestCase.tearDown(self)
+        BrowserMobProxyTestCaseMixin.tearDown(self)
+        self.proxy = None
+
+
+    def run_videos(self):
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                video = VP(self.marionette, url,
+                                       stall_wait_time=60,
+                                       set_duration=60)
+                self.run_playback(video)
+
+
+class VideoPlaybackTestsMixin(object):
+
+    """ Test MSE playback in HTML5 video element.
+
+    These tests should pass on any site where a single video element plays
+    upon loading and is uninterrupted (by ads, for example).
+
+    This test both starting videos and performing partial playback at one
+    minute each, and is the test that should be run frequently in automation.
+    """
+
+    def test_playback_starts(self):
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                try:
+                    video = VP(self.marionette, url, timeout=60)
+                    # Second playback_started check in case video._start_time
+                    # is not 0
+                    self.check_playback_starts(video)
+                    video.pause()
+                    src = video.video_src
+                    if not src.startswith('mediasource'):
+                        self.marionette.log('video is not '
+                                            'mediasource: %s' % src,
+                                            level='WARNING')
+                except TimeoutException as e:
+                    raise self.failureException(e)
+
+    def test_video_playback_partial(self):
+        """ First 60 seconds of video play well. """
+        with self.marionette.using_context('content'):
+            for url in self.video_urls:
+                video = VP(self.marionette, url,
+                           stall_wait_time=10,
+                           set_duration=60)
+                self.run_playback(video)
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/__init__.py
@@ -0,0 +1,10 @@
+# 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/.
+
+import os
+
+root = os.path.abspath(os.path.dirname(__file__))
+manifest = os.path.join(root, 'manifest.ini')
+resources = os.path.join(root, 'resources')
+urls = os.path.join(root, 'urls')
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/manifest.ini
@@ -0,0 +1,1 @@
+[include:playback/manifest.ini]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/eme.ini
@@ -0,0 +1,1 @@
+[test_eme_playback.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/limiting_bandwidth.ini
@@ -0,0 +1,2 @@
+[test_playback_limiting_bandwidth.py]
+[test_ultra_low_bandwidth.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/manifest.ini
@@ -0,0 +1,1 @@
+[test_video_playback.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/netflix_limiting_bandwidth.ini
@@ -0,0 +1,1 @@
+[test_playback_limiting_bandwidth.py]
new file mode 100644
--- /dev/null
+++ b/dom/media/test/external-media-tests/media_tests/playback/test_eme_playback.py
@@ -0,0 +1,71 @@
+# 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/.
+
+import re
+
+from harness.testcase import MediaTestCase, VideoPlaybackTestsMixin
+
+
+class TestEMEPlayback(MediaTestCase, VideoPlaybackTestsMixin):
+
+    def setUp(self):
+        super(TestEMEPlayback, self).setUp()
+        self.set_eme_prefs()
+        assert(self.check_eme_prefs())
+
+    def set_eme_prefs(self):
+        with self.marionette.using_context('chrome'):
+
+            # https://bugzilla.mozilla.org/show_bug.cgi?id=1187471#c28
+            # 2015-09-28 cpearce says this is no longer necessary, but in case
+            # we are working with older firefoxes...
+            self.prefs.set_pref('media.gmp.trial-create.enabled', False)
+
+    def check_and_log_boolean_pref(self, pref_name, expected_value):
+        with self.marionette.using_context('chrome'):
+            pref_value = self.prefs.get_pref(pref_name)
+
+            if pref_value is None:
+                self.logger.info('Pref %s has no value.' % pref_name)
+                return False
+            else:
+                self.logger.info('Pref %s = %s' % (pref_name, pref_value))
+                if pref_value != expected_value:
+                    self.logger.info('Pref %s has unexpected value.'
+                                     % pref_name)
+                    return False
+
+        return True
+
+    def check_and_log_integer_pref(self, pref_name, minimum_value=0):
+        with self.marionette.using_context('chrome'):
+            pref_value = self.prefs.get_pref(pref_name)
+
+            if pref_value is None:
+                self.logger.info('Pref %s has no value.' % pref_name)
+                return False
+            else:
+                self.logger.info('Pref %s = %s' % (pref_name, pref_value))
+
+                match = re.search('^\d+$', pref_value)
+                if not match:
+                    self.logger.info('Pref %s is not an integer' % pref_name)
+                    return False
+
+            return pref_value >= minimum_value
+
+    def check_eme_prefs(self):
+        with self.marionette.using_context('chrome'):
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.mediasource.enabled', True)
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.eme.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.mediasource.mp4.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_boolean_pref(
+                'media.gmp-eme-adobe.enabled', True) and prefs_ok
+            prefs_ok = self.check_and_log_integer_pref(
+                'media.gmp-eme-adobe.version', 1) and prefs_ok
+