Merge m-c to b2g-inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 24 Mar 2015 12:00:00 -0400
changeset 265640 1097d0faed8b7cc204248b9f165ad0d79c414d14
parent 265639 70e03dfe1e44204d7db9d87c2c4f7628f66170b7 (current diff)
parent 265625 2e22440310321d890737f32af5ff31549ec70c6d (diff)
child 265641 8a9318e736bace03b5c62bcf7ce78b495487ef59
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b2g-inbound. a=merge
browser/devtools/webide/modules/remote-resources.js
toolkit/content/aboutwebrtc/README.txt
toolkit/content/aboutwebrtc/aboutWebrtc.jsx
--- a/accessible/xpcom/xpcAccessibleTable.cpp
+++ b/accessible/xpcom/xpcAccessibleTable.cpp
@@ -263,17 +263,17 @@ NS_IMETHODIMP
 xpcAccessibleTable::GetSelectedCells(nsIArray** aSelectedCells)
 {
   NS_ENSURE_ARG_POINTER(aSelectedCells);
   *aSelectedCells = nullptr;
 
   if (!Intl())
     return NS_ERROR_FAILURE;
 
-  NS_IMETHODIMP rv = NS_OK;
+  nsresult rv = NS_OK;
   nsCOMPtr<nsIMutableArray> selCells =
     do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsAutoTArray<Accessible*, XPC_TABLE_DEFAULT_SIZE> cellsArray;
   Intl()->SelectedCells(&cellsArray);
 
   uint32_t totalCount = cellsArray.Length();
new file mode 100644
--- /dev/null
+++ b/b2g/config/desktop/config.json
@@ -0,0 +1,8 @@
+{
+    "gaia": {
+        "l10n": {
+            "vcs": "hgtool",
+            "root": "https://hg.mozilla.org/gaia-l10n"
+        }
+    }
+}
--- a/b2g/locales/jar.mn
+++ b/b2g/locales/jar.mn
@@ -31,16 +31,18 @@ relativesrcdir toolkit/locales:
 #about:crashes
   locale/@AB_CD@/b2g-l10n/overrides/crashreporter/crashes.dtd         (%crashreporter/crashes.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/crashreporter/crashes.properties  (%crashreporter/crashes.properties)
 #about:mozilla
   locale/@AB_CD@/b2g-l10n/overrides/global/mozilla.dtd                (%chrome/global/mozilla.dtd)
 #about:telemetry
   locale/@AB_CD@/b2g-l10n/overrides/global/aboutTelemetry.dtd         (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/global/aboutTelemetry.properties  (%chrome/global/aboutTelemetry.properties)
+#about:webrtc
+  locale/@AB_CD@/b2g-l10n/overrides/global/aboutWebrtc.properties  (%chrome/global/aboutWebrtc.properties)
 
 % override chrome://global/locale/about.dtd chrome://b2g-l10n/locale/overrides/about.dtd
 % override chrome://global/locale/aboutAbout.dtd chrome://b2g-l10n/locale/overrides/aboutAbout.dtd
 % override chrome://global/locale/aboutRights.dtd chrome://b2g-l10n/locale/overrides/aboutRights.dtd
 % override chrome://global/locale/commonDialogs.properties chrome://b2g-l10n/locale/overrides/commonDialogs.properties
 % override chrome://mozapps/locale/handling/handling.properties chrome://b2g-l10n/locale/overrides/handling/handling.properties
 % override chrome://global/locale/intl.properties chrome://b2g-l10n/locale/overrides/intl.properties
 % override chrome://global/locale/intl.css chrome://b2g-l10n/locale/overrides/intl.css
@@ -49,16 +51,17 @@ relativesrcdir toolkit/locales:
 % override chrome://mozapps/locale/update/updates.properties chrome://b2g-l10n/locale/overrides/update/updates.properties
 % override chrome://global/locale/aboutSupport.dtd chrome://b2g-l10n/locale/overrides/global/aboutSupport.dtd
 % override chrome://global/locale/aboutSupport.properties chrome://b2g-l10n/locale/overrides/global/aboutSupport.properties
 % override chrome://global/locale/crashes.dtd chrome://b2g-l10n/locale/overrides/crashreporter/crashes.dtd
 % override chrome://global/locale/crashes.properties chrome://b2g-l10n/locale/overrides/crashreporter/crashes.properties
 % override chrome://global/locale/mozilla.dtd chrome://b2g-l10n/locale/overrides/global/mozilla.dtd
 % override chrome://global/locale/aboutTelemetry.dtd chrome://b2g-l10n/locale/overrides/global/aboutTelemetry.dtd
 % override chrome://global/locale/aboutTelemetry.properties chrome://b2g-l10n/locale/overrides/global/aboutTelemetry.properties
+% override chrome://global/locale/aboutWebrtc.properties chrome://b2g-l10n/locale/overrides/global/aboutWebrtc.properties
 
 # overrides for dom l10n, also for en-US
 relativesrcdir dom/locales:
   locale/@AB_CD@/b2g-l10n/overrides/global.dtd                  (%chrome/global.dtd)
   locale/@AB_CD@/b2g-l10n/overrides/AccessFu.properties         (%chrome/accessibility/AccessFu.properties)
   locale/@AB_CD@/b2g-l10n/overrides/dom/dom.properties          (%chrome/dom/dom.properties)
 #about:plugins
   locale/@AB_CD@/b2g-l10n/overrides/plugins.properties          (%chrome/plugins.properties)
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1293,17 +1293,17 @@ pref("services.sync.prefs.sync.dom.disab
 pref("services.sync.prefs.sync.dom.disable_window_flip", true);
 pref("services.sync.prefs.sync.dom.disable_window_move_resize", true);
 pref("services.sync.prefs.sync.dom.event.contextmenu.enabled", true);
 pref("services.sync.prefs.sync.extensions.personas.current", true);
 pref("services.sync.prefs.sync.extensions.update.enabled", true);
 pref("services.sync.prefs.sync.intl.accept_languages", true);
 pref("services.sync.prefs.sync.javascript.enabled", true);
 pref("services.sync.prefs.sync.layout.spellcheckDefault", true);
-pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
+pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
 pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
 pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true);
 pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true);
 pref("services.sync.prefs.sync.permissions.default.image", true);
 pref("services.sync.prefs.sync.pref.advanced.images.disable_button.view_image", true);
 pref("services.sync.prefs.sync.pref.advanced.javascript.disable_button.advanced", true);
 pref("services.sync.prefs.sync.pref.downloads.disable_button.edit_actions", true);
 pref("services.sync.prefs.sync.pref.privacy.disable_button.cookie_exceptions", true);
@@ -1879,8 +1879,9 @@ pref("dom.ipc.reportProcessHangs", true)
 pref("reader.parse-on-load.enabled", false);
 #endif
 
 // Enable ReadingList browser UI by default.
 pref("browser.readinglist.enabled", true);
 pref("browser.readinglist.sidebarEverOpened", false);
 // Enable the readinglist engine by default.
 pref("readinglist.scheduler.enabled", true);
+pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
--- a/browser/base/content/browser-devedition.js
+++ b/browser/base/content/browser-devedition.js
@@ -4,17 +4,17 @@
 
 /**
  * Listeners for the DevEdition theme.  This adds an extra stylesheet
  * to browser.xul if a pref is set and no other themes are applied.
  */
 let DevEdition = {
   _prefName: "browser.devedition.theme.enabled",
   _themePrefName: "general.skins.selectedSkin",
-  _lwThemePrefName: "lightweightThemes.isThemeSelected",
+  _lwThemePrefName: "lightweightThemes.selectedThemeID",
   _devtoolsThemePrefName: "devtools.theme",
 
   styleSheetLocation: "chrome://browser/skin/devedition.css",
   styleSheet: null,
 
   init: function () {
     this._updateDevtoolsThemeAttribute();
     this._updateStyleSheetFromPrefs();
@@ -71,17 +71,17 @@ let DevEdition = {
     document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
     this._inferBrightness();
     this._updateStyleSheetFromPrefs();
   },
 
   _updateStyleSheetFromPrefs: function() {
     let lightweightThemeSelected = false;
     try {
-      lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
+      lightweightThemeSelected = !!Services.prefs.getCharPref(this._lwThemePrefName);
     } catch(e) {}
 
     let defaultThemeSelected = false;
     try {
        defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
     } catch(e) {}
 
     let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -452,17 +452,17 @@ let gSyncUI = {
 
   // Return true if the reading-list is in a "prolonged" error state. That
   // engine doesn't impose what that means, so calculate it here. For
   // consistency, we just use the sync prefs.
   isProlongedReadingListError() {
     let lastSync, threshold, prolonged;
     try {
       lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
-      threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout"));
+      threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
       prolonged = lastSync <= threshold;
     } catch (ex) {
       // no pref, assume not prolonged.
       prolonged = false;
     }
     this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
                    {lastSync, threshold, prolonged});
     return prolonged;
--- a/browser/base/content/test/general/browser_devedition.js
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -1,44 +1,48 @@
 /*
  * Testing changes for Developer Edition theme.
  * A special stylesheet should be added to the browser.xul document
  * when browser.devedition.theme.enabled is set to true and no themes
  * are applied.
  */
 
 const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
-const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
+const PREF_LWTHEME = "lightweightThemes.selectedThemeID";
+const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";
 const PREF_DEVTOOLS_THEME = "devtools.theme";
+const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
 
 registerCleanupFunction(() => {
   // Set preferences back to their original values
+  LightweightThemeManager.currentTheme = null;
   Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
   Services.prefs.clearUserPref(PREF_LWTHEME);
   Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+  Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES);
 });
 
 add_task(function* startTests() {
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
   ok (!DevEdition.styleSheet, "There is no devedition style sheet when the pref is false.");
 
   info ("Setting browser.devedition.theme.enabled to true.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
 
   info ("Adding a lightweight theme.");
-  Services.prefs.setBoolPref(PREF_LWTHEME, true);
+  LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0");
   ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
 
   info ("Removing a lightweight theme.");
   let onAttributeAdded = waitForBrightTitlebarAttribute();
-  Services.prefs.setBoolPref(PREF_LWTHEME, false);
+  LightweightThemeManager.currentTheme = null;
   ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
   yield onAttributeAdded;
 
   is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
      "The brighttitlebarforeground attribute is set on the window.");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
@@ -80,26 +84,24 @@ add_task(function* testDevtoolsTheme() {
   ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
      "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
 });
 
 function dummyLightweightTheme(id) {
   return {
     id: id,
     name: id,
-    headerURL: "http://lwttest.invalid/a.png",
-    footerURL: "http://lwttest.invalid/b.png",
+    headerURL: "resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg",
+    iconURL: "resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg",
     textcolor: "red",
     accentcolor: "blue"
   };
 }
 
 add_task(function* testLightweightThemePreview() {
-  let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
-
   info ("Turning the pref on, then previewing lightweight themes");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
   LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0"));
   ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview.");
   LightweightThemeManager.resetPreview();
   LightweightThemeManager.previewTheme(dummyLightweightTheme("preview1"));
   ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a second lightweight theme preview.");
--- a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
+++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js
@@ -44,17 +44,17 @@ add_task(function () {
   ok(installedThemeId.startsWith(firstLWThemeId),
      "The second theme in the 'My Themes' section should be the newly installed theme: " +
      "Installed theme id: " + installedThemeId + "; First theme ID: " + firstLWThemeId);
   is(header.nextSibling.nextSibling.nextSibling, recommendedHeader,
      "There should be two themes in the 'My Themes' section");
 
   let defaultTheme = header.nextSibling;
   defaultTheme.doCommand();
-  is(Services.prefs.getBoolPref("lightweightThemes.isThemeSelected"), false, "No lwtheme should be selected");
+  is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
 
   Services.prefs.clearUserPref("lightweightThemes.usedThemes");
   Services.prefs.clearUserPref("lightweightThemes.recommendedThemes");
 })
\ No newline at end of file
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -634,16 +634,23 @@ function injectLoopAPI(targetWindow) {
 
     TWO_WAY_MEDIA_CONN_LENGTH: {
       enumerable: true,
       get: function() {
         return Cu.cloneInto(TWO_WAY_MEDIA_CONN_LENGTH, targetWindow);
       }
     },
 
+    SHARING_STATE_CHANGE: {
+      enumerable: true,
+      get: function() {
+        return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow);
+      }
+    },
+
     fxAEnabled: {
       enumerable: true,
       get: function() {
         return MozLoopService.fxAEnabled;
       },
     },
 
     logInToFxA: {
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -24,30 +24,44 @@ const LOOP_SESSION_TYPE = {
  */
 const TWO_WAY_MEDIA_CONN_LENGTH = {
   SHORTER_THAN_10S: "SHORTER_THAN_10S",
   BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
   BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
   MORE_THAN_5M: "MORE_THAN_5M",
 };
 
+/**
+ * Buckets that we segment sharing state change telemetry probes into.
+ *
+ * @type {{WINDOW_ENABLED: String, WINDOW_DISABLED: String,
+ *   BROWSER_ENABLED: String, BROWSER_DISABLED: String}}
+ */
+const SHARING_STATE_CHANGE = {
+  WINDOW_ENABLED: "WINDOW_ENABLED",
+  WINDOW_DISABLED: "WINDOW_DISABLED",
+  BROWSER_ENABLED: "BROWSER_ENABLED",
+  BROWSER_DISABLED: "BROWSER_DISABLED"
+};
+
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL = "loop.debug.loglevel";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
-this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "TWO_WAY_MEDIA_CONN_LENGTH"];
+this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
+  "TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
   "resource:///modules/loop/MozLoopAPI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "convertToRTCStatsReport",
   "resource://gre/modules/media/RTCStatsReport.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -356,17 +356,19 @@ loop.shared.actions = (function() {
     EmailRoomUrl: Action.define("emailRoomUrl", {
       roomUrl: String
     }),
 
     /**
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     RoomFailure: Action.define("roomFailure", {
-      error: Object
+      error: Object,
+      // True when the failures occurs in the join room request to the loop-server.
+      failedJoinRequest: Boolean
     }),
 
     /**
      * Sets up the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -100,17 +100,17 @@ loop.store.ActiveRoomStore = (function()
         actionData.error);
 
       this.setStoreState({
         error: actionData.error,
         failureReason: getReason(actionData.error.errno)
       });
 
       this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
-          ROOM_STATES.FULL : ROOM_STATES.FAILED);
+          ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest);
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in after the initial setup has been performed.
      */
     _registerPostSetupActions: function() {
       this.dispatcher.register(this, [
@@ -156,17 +156,20 @@ loop.store.ActiveRoomStore = (function()
         roomState: ROOM_STATES.GATHER,
         windowId: actionData.windowId
       });
 
       // Get the window data from the mozLoop api.
       this._mozLoop.rooms.get(actionData.roomToken,
         function(error, roomData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              failedJoinRequest: false
+            }));
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
             roomName: roomData.roomName,
             roomOwner: roomData.roomOwner,
             roomUrl: roomData.roomUrl
@@ -288,17 +291,25 @@ loop.store.ActiveRoomStore = (function()
      * granted and starts joining the room.
      */
     gotMediaPermission: function() {
       this.setStoreState({roomState: ROOM_STATES.JOINING});
 
       this._mozLoop.rooms.join(this._storeState.roomToken,
         function(error, responseData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              // This is an explicit flag to avoid the leave happening if join
+              // fails. We can't track it on ROOM_STATES.JOINING as the user
+              // might choose to leave the room whilst the XHR is in progress
+              // which would then mean we'd run the race condition of not
+              // notifying the server of a leave.
+              failedJoinRequest: true
+            }));
             return;
           }
 
           this.dispatchAction(new sharedActions.JoinedRoom({
             apiKey: responseData.apiKey,
             sessionToken: responseData.sessionToken,
             sessionId: responseData.sessionId,
             expires: responseData.expires
@@ -550,31 +561,37 @@ loop.store.ActiveRoomStore = (function()
      * Refreshes the membership of the room with the server, and then
      * sets up the refresh for the next cycle.
      */
     _refreshMembership: function() {
       this._mozLoop.rooms.refreshMembership(this._storeState.roomToken,
         this._storeState.sessionToken,
         function(error, responseData) {
           if (error) {
-            this.dispatchAction(new sharedActions.RoomFailure({error: error}));
+            this.dispatchAction(new sharedActions.RoomFailure({
+              error: error,
+              failedJoinRequest: false
+            }));
             return;
           }
 
           this._setRefreshTimeout(responseData.expires);
         }.bind(this));
     },
 
     /**
      * Handles leaving a room. Clears any membership timeouts, then
      * signals to the server the leave of the room.
      *
-     * @param {ROOM_STATES} nextState The next state to switch to.
+     * @param {ROOM_STATES} nextState         The next state to switch to.
+     * @param {Boolean}     failedJoinRequest Optional. Set to true if the join
+     *                                        request to loop-server failed. It
+     *                                        will skip the leave message.
      */
-    _leaveRoom: function(nextState) {
+    _leaveRoom: function(nextState, failedJoinRequest) {
       if (loop.standaloneMedia) {
         loop.standaloneMedia.multiplexGum.reset();
       }
 
       this._mozLoop.setScreenShareState(
         this.getStoreState().windowId,
         false);
 
@@ -587,20 +604,21 @@ loop.store.ActiveRoomStore = (function()
       // We probably don't need to end screen share separately, but lets be safe.
       this._sdkDriver.disconnectSession();
 
       if (this._timeout) {
         clearTimeout(this._timeout);
         delete this._timeout;
       }
 
-      if (this._storeState.roomState === ROOM_STATES.JOINING ||
-          this._storeState.roomState === ROOM_STATES.JOINED ||
-          this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
-          this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
+      if (!failedJoinRequest &&
+          (this._storeState.roomState === ROOM_STATES.JOINING ||
+           this._storeState.roomState === ROOM_STATES.JOINED ||
+           this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
+           this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
         this._mozLoop.rooms.leave(this._storeState.roomToken,
           this._storeState.sessionToken);
       }
 
       this.setStoreState({roomState: nextState});
     },
 
     /**
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -160,16 +160,18 @@ loop.OTSdkDriver = (function() {
       }
 
       var config = _.extend(this._getCopyPublisherConfig(), options);
 
       this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
         config);
       this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
       this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
+
+      this._noteSharingState(options.videoSource, true);
     },
 
     /**
      * Initiates switching the browser window that is being shared.
      *
      * @param {Integer} windowId  The windowId of the browser.
      */
     switchAcquiredWindow: function(windowId) {
@@ -191,16 +193,17 @@ loop.OTSdkDriver = (function() {
       if (!this.screenshare) {
         return false;
       }
 
       this.session.unpublish(this.screenshare);
       this.screenshare.off("accessAllowed accessDenied");
       this.screenshare.destroy();
       delete this.screenshare;
+      this._noteSharingState(this._windowId ? "browser" : "window", false);
       delete this._windowId;
       return true;
     },
 
     /**
      * Connects a session for the SDK, listening to the required events.
      *
      * sessionData items:
@@ -643,25 +646,25 @@ loop.OTSdkDriver = (function() {
      * Wrapper for adding a keyed value that also updates
      * connectionLengthNoted calls and sets the twoWayMediaStartTime to
      * this.CONNECTION_START_TIME_ALREADY_NOTED.
      *
      * @param {number} callLengthSeconds  the call length in seconds
      * @private
      */
     _noteConnectionLength: function(callLengthSeconds) {
+      var buckets = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH;
 
-      var bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S;
-
+      var bucket = buckets.SHORTER_THAN_10S;
       if (callLengthSeconds >= 10 && callLengthSeconds <= 30) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S;
+        bucket = buckets.BETWEEN_10S_AND_30S;
       } else if (callLengthSeconds > 30 && callLengthSeconds <= 300) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M;
+        bucket = buckets.BETWEEN_30S_AND_5M;
       } else if (callLengthSeconds > 300) {
-        bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M;
+        bucket = buckets.MORE_THAN_5M;
       }
 
       this.mozLoop.telemetryAddKeyedValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
         bucket);
       this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_ALREADY_NOTED);
 
       this._connectionLengthNotedCalls++;
       if (this._debugTwoWayMediaTelemetry) {
@@ -700,14 +703,39 @@ loop.OTSdkDriver = (function() {
       var callLengthSeconds = (endTime - startTime) / 1000;
       this._noteConnectionLength(callLengthSeconds);
     },
 
     /**
      * If set to true, make it easy to test/verify 2-way media connection
      * telemetry code operation by viewing the logs.
      */
-    _debugTwoWayMediaTelemetry: false
+    _debugTwoWayMediaTelemetry: false,
+
+    /**
+     * Note the sharing state. If this.mozLoop is not defined, we're assumed to
+     * be running in the standalone client and return immediately.
+     *
+     * @param  {String}  type    Type of sharing that was flipped. May be 'window'
+     *                           or 'tab'.
+     * @param  {Boolean} enabled Flag that tells us if the feature was flipped on
+     *                           or off.
+     * @private
+     */
+    _noteSharingState: function(type, enabled) {
+      if (!this.mozLoop) {
+        return;
+      }
+
+      var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
+        (enabled ? "ENABLED" : "DISABLED")];
+      if (!bucket) {
+        console.error("No sharing state bucket found for '" + type + "'");
+        return;
+      }
+
+      this.mozLoop.telemetryAddKeyedValue("LOOP_SHARING_STATE_CHANGE", bucket);
+    }
   };
 
   return OTSdkDriver;
 
 })();
--- a/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
@@ -42,8 +42,34 @@ add_task(function* test_mozLoop_telemetr
   }
 
   let snapshot = histogram.snapshot();
   is(snapshot["SHORTER_THAN_10S"].sum, 1, "TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S");
   is(snapshot["BETWEEN_10S_AND_30S"].sum, 2, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S");
   is(snapshot["BETWEEN_30S_AND_5M"].sum, 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M");
   is(snapshot["MORE_THAN_5M"].sum, 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M");
 });
+
+add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() {
+  let histogramId = "LOOP_SHARING_STATE_CHANGE";
+  let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
+  const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE;
+
+  histogram.clear();
+  for (let value of [SHARING_STATES.WINDOW_ENABLED,
+                     SHARING_STATES.WINDOW_DISABLED,
+                     SHARING_STATES.WINDOW_DISABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_ENABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED,
+                     SHARING_STATES.BROWSER_DISABLED]) {
+    gMozLoopAPI.telemetryAddKeyedValue(histogramId, value);
+  }
+
+  let snapshot = histogram.snapshot();
+  Assert.strictEqual(snapshot["WINDOW_ENABLED"].sum, 1, "SHARING_STATE_CHANGE.WINDOW_ENABLED");
+  Assert.strictEqual(snapshot["WINDOW_DISABLED"].sum, 2, "SHARING_STATE_CHANGE.WINDOW_DISABLED");
+  Assert.strictEqual(snapshot["BROWSER_ENABLED"].sum, 3, "SHARING_STATE_CHANGE.BROWSER_ENABLED");
+  Assert.strictEqual(snapshot["BROWSER_DISABLED"].sum, 4, "SHARING_STATE_CHANGE.BROWSER_DISABLED");
+});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -90,107 +90,149 @@ describe("loop.store.ActiveRoomStore", f
       store.setStoreState({
         roomState: ROOM_STATES.JOINED,
         roomToken: "fakeToken",
         sessionToken: "1627384950"
       });
     });
 
     it("should log the error", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(console.error);
       sinon.assert.calledWith(console.error,
         sinon.match(ROOM_STATES.JOINED), fakeError);
     });
 
     it("should set the state to `FULL` on server error room full", function() {
       fakeError.errno = REST_ERRNOS.ROOM_FULL;
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
     });
 
     it("should set the state to `FAILED` on generic error", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
       expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
     });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "invalid token", function() {
         fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
 
-        store.roomFailure({error: fakeError});
+        store.roomFailure(new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: false
+        }));
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
         expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "expired", function() {
         fakeError.errno = REST_ERRNOS.EXPIRED;
 
-        store.roomFailure({error: fakeError});
+        store.roomFailure(new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: false
+        }));
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
         expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should reset the multiplexGum", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMultiplexGum.reset);
     });
 
     it("should set screen sharing inactive", function() {
       store.setStoreState({windowId: "1234"});
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
       sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
     });
 
     it("should disconnect from the servers via the sdk", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
     });
 
     it("should clear any existing timeout", function() {
       sandbox.stub(window, "clearTimeout");
       store._timeout = {};
 
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(clearTimeout);
     });
 
     it("should remove the sharing listener", function() {
       // Setup the listener.
       store.startScreenShare(new sharedActions.StartScreenShare({
         type: "browser"
       }));
 
       // Now simulate room failure.
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener);
     });
 
     it("should call mozLoop.rooms.leave", function() {
-      store.roomFailure({error: fakeError});
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: false
+      }));
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
       sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
         "fakeToken", "1627384950");
     });
+
+    it("should not call mozLoop.rooms.leave if failedJoinRequest is true", function() {
+      store.roomFailure(new sharedActions.RoomFailure({
+        error: fakeError,
+        failedJoinRequest: true
+      }));
+
+      sinon.assert.notCalled(fakeMozLoop.rooms.leave);
+    });
   });
 
   describe("#setupWindowData", function() {
     var fakeToken, fakeRoomData;
 
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
@@ -266,17 +308,18 @@ describe("loop.store.ActiveRoomStore", f
           windowId: "42",
           type: "room",
           roomToken: fakeToken
         }));
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.RoomFailure({
-            error: fakeError
+            error: fakeError,
+            failedJoinRequest: false
           }));
       });
   });
 
   describe("#fetchServerData", function() {
     it("should save the token", function() {
       store.fetchServerData(new sharedActions.FetchServerData({
         windowType: "room",
@@ -444,17 +487,20 @@ describe("loop.store.ActiveRoomStore", f
       var fakeError = new Error("fake");
 
       fakeMozLoop.rooms.join.callsArgWith(1, fakeError);
 
       store.gotMediaPermission();
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWith(dispatcher.dispatch,
-        new sharedActions.RoomFailure({error: fakeError}));
+        new sharedActions.RoomFailure({
+          error: fakeError,
+          failedJoinRequest: true
+        }));
     });
   });
 
   describe("#joinedRoom", function() {
     var fakeJoinedData;
 
     beforeEach(function() {
       fakeJoinedData = {
@@ -577,17 +623,18 @@ describe("loop.store.ActiveRoomStore", f
 
         // Clock tick for the first expiry time (which
         // sets up the refreshMembership).
         sandbox.clock.tick(fakeJoinedData.expires * 1000);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWith(dispatcher.dispatch,
           new sharedActions.RoomFailure({
-            error: fakeError
+            error: fakeError,
+            failedJoinRequest: false
           }));
     });
   });
 
   describe("#connectedToSdkServers", function() {
     it("should set the state to `SESSION_CONNECTED`", function() {
       store.connectedToSdkServers(new sharedActions.ConnectedToSdkServers());
 
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -66,16 +66,22 @@ describe("loop.OTSdkDriver", function ()
 
     mozLoop = {
       telemetryAddKeyedValue: sinon.stub(),
       TWO_WAY_MEDIA_CONN_LENGTH: {
         SHORTER_THAN_10S: "SHORTER_THAN_10S",
         BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
         BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
         MORE_THAN_5M: "MORE_THAN_5M"
+      },
+      SHARING_STATE_CHANGE: {
+        WINDOW_ENABLED: "WINDOW_ENABLED",
+        WINDOW_DISABLED: "WINDOW_DISABLED",
+        BROWSER_ENABLED: "BROWSER_ENABLED",
+        BROWSER_DISABLED: "BROWSER_DISABLED"
       }
     };
 
     driver = new loop.OTSdkDriver({
       dispatcher: dispatcher,
       sdk: sdk,
       mozLoop: mozLoop,
       isDesktop: true
@@ -184,16 +190,17 @@ describe("loop.OTSdkDriver", function ()
     });
   });
 
   describe("#startScreenShare", function() {
     var fakeElement;
 
     beforeEach(function() {
       sandbox.stub(dispatcher, "dispatch");
+      sandbox.stub(driver, "_noteSharingState");
 
       fakeElement = {
         className: "fakeVideo"
       };
 
       driver.getScreenShareElementFunc = function() {
         return fakeElement;
       };
@@ -209,16 +216,29 @@ describe("loop.OTSdkDriver", function ()
           scrollWithPage: true
         }
       };
       driver.startScreenShare(options);
 
       sinon.assert.calledOnce(sdk.initPublisher);
       sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
     });
+
+    it("should log a telemetry action", function() {
+      var options = {
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42,
+          scrollWithPage: true
+        }
+      };
+      driver.startScreenShare(options);
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
+    });
   });
 
   describe("#switchAcquiredWindow", function() {
     beforeEach(function() {
       var options = {
         videoSource: "browser",
         constraints: {
           browserWindow: 42,
@@ -246,36 +266,80 @@ describe("loop.OTSdkDriver", function ()
       sinon.assert.notCalled(publisher._.switchAcquiredWindow);
     });
   });
 
   describe("#endScreenShare", function() {
     beforeEach(function() {
       driver.getScreenShareElementFunc = function() {};
 
+      sandbox.stub(dispatcher, "dispatch");
+      sandbox.stub(driver, "_noteSharingState");
+    });
+
+    it("should unpublish the share", function() {
       driver.startScreenShare({
         videoSource: "window"
       });
-
-      sandbox.stub(dispatcher, "dispatch");
+      driver.session = session;
 
-      driver.session = session;
-    });
-
-    it("should unpublish the share", function() {
       driver.endScreenShare(new sharedActions.EndScreenShare());
 
       sinon.assert.calledOnce(session.unpublish);
     });
 
+    it("should log a telemetry action", function() {
+      driver.startScreenShare({
+        videoSource: "window"
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "window", false);
+    });
+
     it("should destroy the share", function() {
+      driver.startScreenShare({
+        videoSource: "window"
+      });
+      driver.session = session;
+
       expect(driver.endScreenShare()).to.equal(true);
 
       sinon.assert.calledOnce(publisher.destroy);
     });
+
+    it("should unpublish the share too when type is 'browser'", function() {
+      driver.startScreenShare({
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42
+        }
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledOnce(session.unpublish);
+    });
+
+    it("should log a telemetry action too when type is 'browser'", function() {
+      driver.startScreenShare({
+        videoSource: "browser",
+        constraints: {
+          browserWindow: 42
+        }
+      });
+      driver.session = session;
+
+      driver.endScreenShare(new sharedActions.EndScreenShare());
+
+      sinon.assert.calledWithExactly(driver._noteSharingState, "browser", false);
+    });
   });
 
   describe("#connectSession", function() {
     it("should initialise a new session", function() {
       driver.connectSession(sessionData);
 
       sinon.assert.calledOnce(sdk.initSession);
       sinon.assert.calledWithExactly(sdk.initSession, "3216549870");
@@ -426,16 +490,54 @@ describe("loop.OTSdkDriver", function ()
         driver._isDesktop = false;
 
         driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
         sinon.assert.notCalled(mozLoop.telemetryAddKeyedValue);
       });
   });
 
+  describe("#_noteSharingState", function() {
+    it("should record enabled sharing states for window", function() {
+      driver._noteSharingState("window", true);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED);
+    });
+
+    it("should record enabled sharing states for browser", function() {
+      driver._noteSharingState("browser", true);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED);
+    });
+
+    it("should record disabled sharing states for window", function() {
+      driver._noteSharingState("window", false);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED);
+    });
+
+    it("should record disabled sharing states for browser", function() {
+      driver._noteSharingState("browser", false);
+
+      sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
+      sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
+        "LOOP_SHARING_STATE_CHANGE",
+        mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED);
+    });
+  });
+
   describe("#forceDisconnectAll", function() {
     it("should not disconnect anything when not connected", function() {
       driver.session = session;
       driver.forceDisconnectAll(function() {});
 
       sinon.assert.notCalled(session.forceDisconnect);
     });
 
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -337,16 +337,27 @@ nsBrowserContentHandler.prototype = {
     if (cmdLine.handleFlag("browser", false)) {
       // Passing defaultArgs, so use NO_EXTERNAL_URIS
       openWindow(null, this.chromeURL, "_blank",
                  "chrome,dialog=no,all" + this.getFeatures(cmdLine),
                  this.defaultArgs, NO_EXTERNAL_URIS);
       cmdLine.preventDefault = true;
     }
 
+    // In the past, when an instance was not already running, the -remote
+    // option returned an error code. Any script or application invoking the
+    // -remote option is expected to be handling this case, otherwise they
+    // wouldn't be doing anything when there is no Firefox already running.
+    // Making the -remote option always return an error code makes those
+    // scripts or applications handle the situation as if Firefox was not
+    // already running.
+    if (cmdLine.handleFlag("remote", true)) {
+      throw NS_ERROR_ABORT;
+    }
+
     var uriparam;
     try {
       while ((uriparam = cmdLine.handleFlagWithParam("new-window", false))) {
         var uri = resolveURIInternal(cmdLine, uriparam);
         if (!shouldLoadURI(uri))
           continue;
         openWindow(null, this.chromeURL, "_blank",
                    "chrome,dialog=no,all" + this.getFeatures(cmdLine),
--- a/browser/components/readinglist/ReadingList.jsm
+++ b/browser/components/readinglist/ReadingList.jsm
@@ -53,16 +53,27 @@ const ITEM_RECORD_PROPERTIES = `
   addedBy
   addedOn
   storedOn
   markedReadBy
   markedReadOn
   readPosition
 `.trim().split(/\s+/);
 
+// Article objects that are passed to ReadingList.addItem may contain
+// some properties that are known but are not currently stored in the
+// ReadingList records. This is the list of properties that are knowingly
+// disregarded before the item is normalized.
+const ITEM_DISREGARDED_PROPERTIES = `
+  byline
+  dir
+  content
+  length
+`.trim().split(/\s+/);
+
 /**
  * A reading list contains ReadingListItems.
  *
  * A list maintains only one copy of an item per URL.  So if for example you use
  * an iterator to get two references to items with the same URL, your references
  * actually refer to the same JS object.
  *
  * Options Objects
@@ -848,16 +859,19 @@ ReadingListItemIterator.prototype = {
  * aren't in ITEM_RECORD_PROPERTIES.
  *
  * @param record A non-normalized record object.
  * @return The new normalized record.
  */
 function normalizeRecord(nonNormalizedRecord) {
   let record = {};
   for (let prop in nonNormalizedRecord) {
+    if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) {
+      continue;
+    }
     if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
       throw new Error("Unrecognized item property: " + prop);
     }
     switch (prop) {
     case "url":
     case "resolvedURL":
       if (nonNormalizedRecord[prop]) {
         record[prop] = normalizeURI(nonNormalizedRecord[prop]).spec;
new file mode 100644
--- /dev/null
+++ b/browser/components/readinglist/ServerClient.jsm
@@ -0,0 +1,166 @@
+/* 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/. */
+
+// The client used to access the ReadingList server.
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
+XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
+
+let log = Log.repository.getLogger("readinglist.serverclient");
+
+const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
+
+this.EXPORTED_SYMBOLS = [
+  "ServerClient",
+];
+
+// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
+// encode the request as utf-8 even though it wants to know the encoding.
+// It does, however, explicitly decode the response.  This seems insane, but is
+// what it is.
+// The end result being we need to utf-8 the request and let the response take
+// care of itself.
+function objectToUTF8Json(obj) {
+  // FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
+  return CommonUtils.encodeUTF8(JSON.stringify(obj));
+}
+
+function ServerClient(fxa = fxAccounts) {
+  this.fxa = fxa;
+}
+
+ServerClient.prototype = {
+
+  request(options) {
+    return this._request(options.path, options.method, options.body, options.headers);
+  },
+
+  get serverURL() {
+    return Services.prefs.getCharPref("readinglist.server");
+  },
+
+  _getURL(path) {
+    let result = this.serverURL;
+    // we expect the path to have a leading slash, so remove any trailing
+    // slashes on the pref.
+    if (result.endsWith("/")) {
+      result = result.slice(0, -1);
+    }
+    return result + path;
+  },
+
+  // Hook points for testing.
+  _getToken() {
+    // Assume token-caching is in place - if it's not we should avoid doing
+    // this each request.
+    return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
+  },
+
+  _removeToken(token) {
+    // XXX - remove this check once tokencaching landsin FxA.
+    if (!this.fxa.removeCachedOAuthToken) {
+      dump("XXX - token caching support is yet to land - can't remove token!");
+      return;
+    }
+    return this.fxa.removeCachedOAuthToken({token});
+  },
+
+  // Converts an error from the RESTRequest object to an error we export.
+  _convertRestError(error) {
+    return error; // XXX - errors?
+  },
+
+  // Converts an error from a try/catch handler to an error we export.
+  _convertJSError(error) {
+    return error; // XXX - errors?
+  },
+
+  /*
+   * Perform a request - handles authentication
+   */
+  _request: Task.async(function* (path, method, body, headers) {
+    let token = yield this._getToken();
+    let response = yield this._rawRequest(path, method, body, headers, token);
+    log.debug("initial request got status ${status}", response);
+    if (response.status == 401) {
+      // an auth error - assume our token has expired or similar.
+      this._removeToken(token);
+      token = yield this._getToken();
+      response = yield this._rawRequest(path, method, body, headers, token);
+      log.debug("retry of request got status ${status}", response);
+    }
+    return response;
+  }),
+
+  /*
+   * Perform a request *without* abstractions such as auth etc
+   *
+   * On success (which *includes* non-200 responses) returns an object like:
+   * {
+   *   status: 200, # http status code
+   *   headers: {}, # header values keyed by header name.
+   *   body: {},    # parsed json
+   }
+   */
+
+  _rawRequest(path, method, body, headers, oauthToken) {
+    return new Promise((resolve, reject) => {
+      let url = this._getURL(path);
+      log.debug("dispatching request to", url);
+      let request = new RESTRequest(url);
+      method = method.toUpperCase();
+
+      request.setHeader("Accept", "application/json");
+      request.setHeader("Content-Type", "application/json; charset=utf-8");
+      request.setHeader("Authorization", "Bearer " + oauthToken);
+      // and additional header specified for this request.
+      if (headers) {
+        for (let [headerName, headerValue] in Iterator(headers)) {
+          log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
+          request.setHeader(headerName, headerValue);
+        }
+      }
+
+      request.onComplete = error => {
+        if (error) {
+          return reject(this._convertRestError(error));
+        }
+
+        let response = request.response;
+        log.debug("received response status: ${status} ${statusText}", response);
+        // Handle response status codes we know about
+        let result = {
+          status: response.status,
+          headers: response.headers
+        };
+        try {
+          if (response.body) {
+            result.body = JSON.parse(response.body);
+          }
+        } catch (e) {
+          log.info("Failed to parse JSON body |${body}|: ${e}",
+                    {body: response.body, e});
+          // We don't reject due to this (and don't even make a huge amount of
+          // log noise - eg, a 50X error from a load balancer etc may not write
+          // JSON.
+        }
+
+        resolve(result);
+      }
+      // We are assuming the body has already been decoded and thus contains
+      // unicode, but the server expects utf-8. encodeURIComponent does that.
+      request.dispatch(method, objectToUTF8Json(body));
+    });
+  },
+};
--- a/browser/components/readinglist/moz.build
+++ b/browser/components/readinglist/moz.build
@@ -1,25 +1,23 @@
 # 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/.
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXTRA_JS_MODULES.readinglist += [
     'ReadingList.jsm',
+    'Scheduler.jsm',
+    'ServerClient.jsm',
     'SQLiteStore.jsm',
 ]
 
 TESTING_JS_MODULES += [
     'test/ReadingListTestUtils.jsm',
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
 
-EXTRA_JS_MODULES.readinglist += [
-    'Scheduler.jsm',
-]
-
 with Files('**'):
     BUG_COMPONENT = ('Firefox', 'Reading List')
--- a/browser/components/readinglist/sidebar.js
+++ b/browser/components/readinglist/sidebar.js
@@ -156,16 +156,17 @@ let RLSidebar = {
     itemNode.querySelector(".item-domain").textContent = domain;
 
     let thumb = itemNode.querySelector(".item-thumb-container");
     if (item.preview) {
       thumb.style.backgroundImage = "url(" + item.preview + ")";
     } else {
       thumb.style.removeProperty("background-image");
     }
+    thumb.classList.toggle("preview-available", !!item.preview);
   },
 
   /**
    * Ensure that the list is populated with the correct items.
    */
   ensureListItems: Task.async(function* () {
     yield ReadingList.forEachItem(item => {
       // TODO: Should be batch inserting via DocumentFragment
@@ -195,17 +196,17 @@ let RLSidebar = {
   },
 
   set activeItem(node) {
     if (node && node.parentNode != this.list) {
       log.error(`Unable to set activeItem to invalid node ${node}`);
       return;
     }
 
-    log.debug(`Setting activeItem: ${node ? node.id : null}`);
+    log.trace(`Setting activeItem: ${node ? node.id : null}`);
 
     if (node && node.classList.contains("active")) {
       return;
     }
 
     let prevItem = document.querySelector("#list > .item.active");
     if (prevItem) {
       prevItem.classList.remove("active");
@@ -228,17 +229,17 @@ let RLSidebar = {
   },
 
   set selectedItem(node) {
     if (node && node.parentNode != this.list) {
       log.error(`Unable to set selectedItem to invalid node ${node}`);
       return;
     }
 
-    log.debug(`Setting activeItem: ${node ? node.id : null}`);
+    log.trace(`Setting selectedItem: ${node ? node.id : null}`);
 
     let prevItem = document.querySelector("#list > .item.selected");
     if (prevItem) {
       prevItem.classList.remove("selected");
     }
 
     if (node) {
       node.classList.add("selected");
@@ -265,17 +266,17 @@ let RLSidebar = {
       if (item.classList.contains("selected")) {
         return i;
       }
     }
     return -1;
   },
 
   set selectedIndex(index) {
-    log.debug(`Setting selectedIndex: ${index}`);
+    log.trace(`Setting selectedIndex: ${index}`);
 
     if (index == -1) {
       this.selectedItem = null;
       return;
     }
 
     let item = this.list.children.item(index);
     if (!item) {
--- a/browser/components/readinglist/test/xpcshell/head.js
+++ b/browser/components/readinglist/test/xpcshell/head.js
@@ -1,7 +1,56 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+
+do_get_profile(); // fxa needs a profile directory for storage.
+
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+
+// Create a mocked FxAccounts object with a signed-in, verified user.
+function* createMockFxA() {
+
+  function MockFxAccountsClient() {
+    this._email = "nobody@example.com";
+    this._verified = false;
+
+    this.accountStatus = function(uid) {
+      let deferred = Promise.defer();
+      deferred.resolve(!!uid && (!this._deletedOnServer));
+      return deferred.promise;
+    };
+
+    this.signOut = function() { return Promise.resolve(); };
+
+    FxAccountsClient.apply(this);
+  }
+
+  MockFxAccountsClient.prototype = {
+    __proto__: FxAccountsClient.prototype
+  }
+
+  function MockFxAccounts() {
+    return new FxAccounts({
+      fxAccountsClient: new MockFxAccountsClient(),
+      getAssertion: () => Promise.resolve("assertion"),
+    });
+  }
+
+  let fxa = new MockFxAccounts();
+  let credentials = {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    assertion: "foobar",
+    sessionToken: "dead",
+    kA: "beef",
+    kB: "cafe",
+    verified: true
+  };
+
+  yield fxa.setSignedInUser(credentials);
+  return fxa;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/readinglist/test/xpcshell/test_ServerClient.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource:///modules/readinglist/ServerClient.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+
+let appender = new Log.DumpAppender();
+for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
+  Log.repository.getLogger(logName).addAppender(appender);
+}
+
+// Some test servers we use.
+let Server = function(handlers) {
+  this._server = null;
+  this._handlers = handlers;
+}
+
+Server.prototype = {
+  start() {
+    this._server = new HttpServer();
+    for (let [path, handler] in Iterator(this._handlers)) {
+      // httpd.js seems to swallow exceptions
+      let thisHandler = handler;
+      let wrapper = (request, response) => {
+        try {
+          thisHandler(request, response);
+        } catch (ex) {
+          print("**** Handler for", path, "failed:", ex, ex.stack);
+          throw ex;
+        }
+      }
+      this._server.registerPathHandler(path, wrapper);
+    }
+    this._server.start(-1);
+  },
+
+  stop() {
+    return new Promise(resolve => {
+      this._server.stop(resolve);
+      this._server = null;
+    });
+  },
+
+  get host() {
+    return "http://localhost:" + this._server.identity.primaryPort;
+  },
+};
+
+// An OAuth server that hands out tokens.
+function OAuthTokenServer() {
+  let server;
+  let handlers = {
+    "/v1/authorization": (request, response) => {
+      response.setStatusLine("1.1", 200, "OK");
+      let token = "token" + server.numTokenFetches;
+      print("Test OAuth server handing out token", token);
+      server.numTokenFetches += 1;
+      server.activeTokens.add(token);
+      response.write(JSON.stringify({access_token: token}));
+    },
+    "/v1/destroy": (request, response) => {
+      // Getting the body seems harder than it should be!
+      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+                .createInstance(Ci.nsIScriptableInputStream);
+      sis.init(request.bodyInputStream);
+      let body = JSON.parse(sis.read(sis.available()));
+      sis.close();
+      let token = body.token;
+      ok(server.activeTokens.delete(token));
+      print("after destroy have", server.activeTokens.size, "tokens left.")
+      response.setStatusLine("1.1", 200, "OK");
+      response.write('{}');
+    },
+  }
+  server = new Server(handlers);
+  server.numTokenFetches = 0;
+  server.activeTokens = new Set();
+  return server;
+}
+
+// The tests.
+function run_test() {
+  run_next_test();
+}
+
+// Arrange for the first token we hand out to be rejected - the client should
+// notice the 401 and silently get a new token and retry the request.
+add_task(function testAuthRetry() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      // We know the first token we will get is "token0", so we simulate that
+      // "expiring" by only accepting "token1". Then we just echo the response
+      // back.
+      let authHeader;
+      try {
+        authHeader = request.getHeader("Authorization");
+      } catch (ex) {}
+      if (authHeader != "Bearer token1") {
+        response.setStatusLine("1.1", 401, "Unauthorized");
+        response.write("wrong token");
+        return;
+      }
+      response.setStatusLine("1.1", 200, "OK");
+      response.write(JSON.stringify({ok: true}));
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  let authServer = OAuthTokenServer();
+  authServer.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+    Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      body: {foo: "bar"},
+    });
+    equal(response.status, 200, "got the 200 we expected");
+    equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
+    deepEqual(response.body, {ok: true});
+  } finally {
+    yield authServer.stop();
+    yield rlserver.stop();
+  }
+});
+
+// Check that specified headers are seen by the server, and that server headers
+// in the response are seen by the client.
+add_task(function testHeaders() {
+  let handlers = {
+    "/v1/batch": (request, response) => {
+      ok(request.hasHeader("x-foo"), "got our foo header");
+      equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
+      response.setHeader("Server-Sent-Header", "hello");
+      response.setStatusLine("1.1", 200, "OK");
+      response.write("{}");
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let response = yield sc.request({
+      path: "/batch",
+      method: "post",
+      headers: {"X-Foo": "bar"},
+      body: {foo: "bar"}});
+    equal(response.status, 200, "got the 200 we expected");
+    equal(response.headers["server-sent-header"], "hello", "got the server header");
+  } finally {
+    yield rlserver.stop();
+  }
+});
+
+// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
+// (Note the ServerClient assumes all strings in and out are UCS, and thus have
+// already been encoded/decoded (ie, it never expects to receive stuff already
+// utf-8 encoded, and never returns utf-8 encoded responses.)
+add_task(function testUTF8() {
+  let handlers = {
+    "/v1/hello": (request, response) => {
+      // Get the body as bytes.
+      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+                .createInstance(Ci.nsIScriptableInputStream);
+      sis.init(request.bodyInputStream);
+      let body = sis.read(sis.available());
+      sis.close();
+      // The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
+      // It should have been encoded as utf-8 which is \xc2\xa9
+      equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
+      // and just write it back unchanged.
+      response.setStatusLine("1.1", 200, "OK");
+      response.write(body);
+    }
+  };
+  let rlserver = new Server(handlers);
+  rlserver.start();
+  try {
+    Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
+
+    let fxa = yield createMockFxA();
+    let sc = new ServerClient(fxa);
+    sc._getToken = () => Promise.resolve();
+
+    let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
+    let response = yield sc.request({
+      path: "/hello",
+      method: "post",
+      body: body
+    });
+    equal(response.status, 200, "got the 200 we expected");
+    deepEqual(response.body, body);
+  } finally {
+    yield rlserver.stop();
+  }
+});
--- a/browser/components/readinglist/test/xpcshell/xpcshell.ini
+++ b/browser/components/readinglist/test/xpcshell/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head = head.js
 firefox-appdir = browser
 
 [test_ReadingList.js]
+[test_ServerClient.js]
 [test_scheduler.js]
 [test_SQLiteStore.js]
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -40,17 +40,17 @@ const WINDOW_ATTRIBUTES = ["width", "hei
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
 const WINDOW_HIDEABLE_FEATURES = [
   "menubar", "toolbar", "locationbar", "personalbar", "statusbar", "scrollbars"
 ];
 
 // Messages that will be received via the Frame Message Manager.
-const FMM_MESSAGES = [
+const MESSAGES = [
   // The content script gives us a reference to an object that performs
   // synchronous collection of session data.
   "SessionStore:setupSyncHandler",
 
   // The content script sends us data that has been invalidated and needs to
   // be saved to disk.
   "SessionStore:update",
 
@@ -65,37 +65,39 @@ const FMM_MESSAGES = [
   // consider restoring another tab in the queue. The document has
   // been restored, and forms have been filled. We trigger
   // SSTabRestored at this time.
   "SessionStore:restoreTabContentComplete",
 
   // A tab that is being restored was reloaded. We call restoreTabContent to
   // finish restoring it right away.
   "SessionStore:reloadPendingTab",
+
+  // A crashed tab was revived by navigating to a different page. Remove its
+  // browser from the list of crashed browsers to stop ignoring its messages.
+  "SessionStore:crashedTabRevived",
 ];
 
 // The list of messages we accept from <xul:browser>s that have no tab
 // assigned. Those are for example the ones that preload about:newtab pages.
-const FMM_NOTAB_MESSAGES = new Set([
+const NOTAB_MESSAGES = new Set([
   // For a description see above.
   "SessionStore:setupSyncHandler",
 
   // For a description see above.
   "SessionStore:update",
 ]);
 
-// Messages that will be received via the Parent Process Message Manager.
-const PPMM_MESSAGES = [
-  // A tab is being revived from the crashed state. The sender of this
-  // message should actually be running in the parent process, since this
-  // will be the crashed tab interface. We use the Child and Parent Process
-  // Message Managers because the message is sent during framescript unload
-  // when the Frame Message Manager is not available.
-  "SessionStore:RemoteTabRevived",
-];
+// The list of messages we want to receive even during the short period after a
+// frame has been removed from the DOM and before its frame script has finished
+// unloading.
+const CLOSED_MESSAGES = new Set([
+  // For a description see above.
+  "SessionStore:crashedTabRevived",
+]);
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
 // The number of milliseconds in a day
@@ -418,18 +420,16 @@ let SessionStoreInternal = {
       throw new Error("SessionStore.init() must only be called once!");
     }
 
     TelemetryTimestamps.add("sessionRestoreInitialized");
     OBSERVING.forEach(function(aTopic) {
       Services.obs.addObserver(this, aTopic, true);
     }, this);
 
-    PPMM_MESSAGES.forEach(msg => ppmm.addMessageListener(msg, this));
-
     this._initPrefs();
     this._initialized = true;
   },
 
   /**
    * Initialize the session using the state provided by SessionStartup
    */
   initSession: function () {
@@ -549,18 +549,16 @@ let SessionStoreInternal = {
       SessionSaver.run();
     }
 
     // clear out priority queue in case it's still holding refs
     TabRestoreQueue.reset();
 
     // Make sure to cancel pending saves.
     SessionSaver.cancel();
-
-    PPMM_MESSAGES.forEach(msg => ppmm.removeMessageListener(msg, this));
   },
 
   /**
    * Handle notifications
    */
   observe: function ssi_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "browser-window-before-show": // catch new windows
@@ -597,31 +595,25 @@ let SessionStoreInternal = {
   },
 
   /**
    * This method handles incoming messages sent by the session store content
    * script via the Frame Message Manager or Parent Process Message Manager,
    * and thus enables communication with OOP tabs.
    */
   receiveMessage(aMessage) {
-    // We'll deal with any Parent Process Message Manager messages first...
-    if (aMessage.name == "SessionStore:RemoteTabRevived") {
-      this._crashedBrowsers.delete(aMessage.objects.browser.permanentKey);
-      return;
-    }
-
     // If we got here, that means we're dealing with a frame message
     // manager message, so the target will be a <xul:browser>.
     var browser = aMessage.target;
     var win = browser.ownerDocument.defaultView;
     let tab = win.gBrowser.getTabForBrowser(browser);
 
     // Ensure we receive only specific messages from <xul:browser>s that
     // have no tab assigned, e.g. the ones that preload about:newtab pages.
-    if (!tab && !FMM_NOTAB_MESSAGES.has(aMessage.name)) {
+    if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
       throw new Error(`received unexpected message '${aMessage.name}' ` +
                       `from a browser that has no tab`);
     }
 
     switch (aMessage.name) {
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
@@ -704,16 +696,19 @@ let SessionStoreInternal = {
         break;
       case "SessionStore:reloadPendingTab":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
             this.restoreTabContent(tab);
           }
         }
         break;
+      case "SessionStore:crashedTabRevived":
+        this._crashedBrowsers.delete(browser.permanentKey);
+        break;
       default:
         throw new Error(`received unknown message '${aMessage.name}'`);
         break;
     }
   },
 
   /**
    * Record telemetry measurements stored in an object.
@@ -794,17 +789,20 @@ let SessionStoreInternal = {
     if (RunState.isQuitting)
       return;
 
     // Assign the window a unique identifier we can use to reference
     // internal data about the window.
     aWindow.__SSi = this._generateWindowID();
 
     let mm = aWindow.getGroupMessageManager("browsers");
-    FMM_MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
+    MESSAGES.forEach(msg => {
+      let listenWhenClosed = CLOSED_MESSAGES.has(msg);
+      mm.addMessageListener(msg, this, listenWhenClosed);
+    });
 
     // Load the frame script after registering listeners.
     mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
 
     // and create its data object
     this._windows[aWindow.__SSi] = { tabs: [], selected: 0, _closedTabs: [], busy: false };
 
     let isPrivateWindow = false;
@@ -1123,17 +1121,17 @@ let SessionStoreInternal = {
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
       this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
     }
 
     // Cache the window state until it is completely gone.
     DyingWindowCache.set(aWindow, winData);
 
     let mm = aWindow.getGroupMessageManager("browsers");
-    FMM_MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
+    MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
 
     delete aWindow.__SSi;
   },
 
   /**
    * On quit application requested
    */
   onQuitApplicationRequested: function ssi_onQuitApplicationRequested() {
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -744,22 +744,18 @@ function handleRevivedTab() {
     if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
       // Sanity check - we'd better be loading this in a non-remote browser.
       throw new Error("We seem to be navigating away from about:tabcrashed in " +
                       "a non-remote browser. This should really never happen.");
     }
 
     removeEventListener("pagehide", handleRevivedTab);
 
-    // We can't send a message using the frame message manager because by
-    // the time we reach the unload event handler, it's "too late", and messages
-    // won't be sent or received. The child-process message manager works though,
-    // despite the fact that we're really running in the parent process.
-    let browser = docShell.chromeEventHandler;
-    cpmm.sendAsyncMessage("SessionStore:RemoteTabRevived", null, {browser: browser});
+    // Notify the parent.
+    sendAsyncMessage("SessionStore:crashedTabRevived");
   }
 }
 
 // If we're browsing from the tab crashed UI to a blacklisted URI that keeps
 // this browser non-remote, we'll handle that in a pagehide event.
 addEventListener("pagehide", handleRevivedTab);
 
 addEventListener("unload", () => {
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -26,32 +26,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
                                   "resource://gre/modules/devtools/dbg-client.jsm");
 
 const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
 const Telemetry = devtools.require("devtools/shared/telemetry");
 
 const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
 const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
-const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL";
-const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL";
+const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
+const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
 const bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
 
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 this.DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._themes = new Map();    // Map<themeId, theme>
   this._toolboxes = new Map(); // Map<target, toolbox>
+  this._telemetry = new Telemetry();
 
   // destroy() is an observer's handler so we need to preserve context.
   this.destroy = this.destroy.bind(this);
   this._teardown = this._teardown.bind(this);
 
   this._testing = false;
 
   EventEmitter.decorate(this);
--- a/browser/devtools/shared/devices.js
+++ b/browser/devtools/shared/devices.js
@@ -1,603 +1,74 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { Ci, Cc } = require("chrome");
+const { getJSON } = require("devtools/shared/getjson");
 const { Services } = require("resource://gre/modules/Services.jsm");
+const promise = require("promise");
+
+const DEVICES_URL = "devtools.devices.url";
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/device.properties");
 
-/* `Devices` is a catalog of existing devices and their properties, intended
- * for (mobile) device emulation tools and features.
+/* This is a catalog of common web-enabled devices and their properties,
+ * intended for (mobile) device emulation.
  *
  * The properties of a device are:
- * - name: Device brand and model(s).
- * - width: Viewport width.
- * - height: Viewport height.
- * - pixelRatio: Screen pixel ratio to viewport.
- * - userAgent: Device UserAgent string.
- * - touch: Whether the screen is touch-enabled.
+ * - name: brand and model(s).
+ * - width: viewport width.
+ * - height: viewport height.
+ * - pixelRatio: ratio from viewport to physical screen pixels.
+ * - userAgent: UA string of the device's browser.
+ * - touch: whether it has a touch screen.
+ * - firefoxOS: whether Firefox OS is supported.
  *
- * To add more devices to this catalog, either patch this file, or push new
- * device descriptions from your own code (e.g. an addon) like so:
+ * The device types are:
+ *   ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
+ *
+ * You can easily add more devices to this catalog from your own code (e.g. an
+ * addon) like so:
  *
  *   var myPhone = { name: "My Phone", ... };
- *   require("devtools/shared/devices").Devices.Others.phones.push(myPhone);
+ *   require("devtools/shared/devices").AddDevice(myPhone, "phones");
  */
 
-let Devices = {
-  Types: ["phones", "tablets", "notebooks", "televisions", "watches"],
-
-  // Get the localized string of a device type.
-  GetString(deviceType) {
-    return Strings.GetStringFromName("device." + deviceType);
-  },
-};
-exports.Devices = Devices;
-
+// Local devices catalog that addons can add to.
+let localDevices = {};
 
-// The `Devices.FirefoxOS` list was put together from various sources online.
-Devices.FirefoxOS = {
-  phones: [
-    {
-      name: "Firefox OS Flame",
-      width: 320,
-      height: 570,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Alcatel One Touch Fire, Fire C",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Alcatel Fire E",
-      width: 320,
-      height: 480,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Geeksphone Keon",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Geeksphone Peak, Revolution",
-      width: 360,
-      height: 640,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Intex Cloud Fx",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "LG Fireweb",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; LG-D300; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Spice Fire One Mi-FX1",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Symphony GoFox F15",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "Zen Fire 105",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "ZTE Open",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; ZTEOPEN; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "ZTE Open C",
-      width: 320,
-      height: 450,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Mobile; OPENC; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-  ],
-  tablets: [
-    {
-      name: "Foxconn InFocus",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-    {
-      name: "VIA Vixen",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
-      touch: true,
-    },
-  ],
-  notebooks: [
-  ],
-  televisions: [
-    {
-      name: "720p HD Television",
-      width: 1280,
-      height: 720,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "1080p Full HD Television",
-      width: 1920,
-      height: 1080,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "4K Ultra HD Television",
-      width: 3840,
-      height: 2160,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-  ],
-  watches: [
-    {
-      name: "LG G Watch",
-      width: 280,
-      height: 280,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "LG G Watch R",
-      width: 320,
-      height: 320,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Moto 360",
-      width: 320,
-      height: 290,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Samsung Gear Live",
-      width: 320,
-      height: 320,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-  ],
-};
+// Add a device to the local catalog.
+function AddDevice(device, type = "phones") {
+  let list = localDevices[type];
+  if (!list) {
+    list = localDevices[type] = [];
+  }
+  list.push(device);
+}
+exports.AddDevice = AddDevice;
+
+// Get the complete devices catalog.
+function GetDevices(bypassCache = false) {
+  let deferred = promise.defer();
 
-// `Devices.Others` was derived from the Chromium source code:
-// - chromium/src/third_party/WebKit/Source/devtools/front_end/toolbox/OverridesUI.js
-Devices.Others = {
-  phones: [
-    {
-      name: "Apple iPhone 3GS",
-      width: 320,
-      height: 480,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 4",
-      width: 320,
-      height: 480,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 5",
-      width: 320,
-      height: 568,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 6",
-      width: 375,
-      height: 667,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
-      touch: true,
-    },
-    {
-      name: "Apple iPhone 6 Plus",
-      width: 414,
-      height: 736,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
-      touch: true,
-    },
-    {
-      name: "BlackBerry Z10",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
-      touch: true,
-    },
-    {
-      name: "BlackBerry Z30",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 4",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 5",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "Google Nexus S",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "HTC Evo, Touch HD, Desire HD, Desire",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "HTC One X, EVO LTE",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.0.3; HTC One X Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "HTC Sensation, Evo 3D",
-      width: 360,
-      height: 640,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "LG Optimus 2X, Optimus 3D, Optimus Black",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; LG-P990/V08c Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2",
-      touch: true,
-    },
-    {
-      name: "LG Optimus G",
-      width: 384,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.0; LG-E975 Build/IMM76L) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
-      touch: true,
-    },
-    {
-      name: "LG Optimus LTE, Optimus 4X HD",
-      width: 424,
-      height: 753,
-      pixelRatio: 1.7,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; LG-P930 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "LG Optimus One",
-      width: 213,
-      height: 320,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Motorola Defy, Droid, Droid X, Milestone",
-      width: 320,
-      height: 569,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
-      touch: true,
-    },
-    {
-      name: "Motorola Droid 3, Droid 4, Droid Razr, Atrix 4G, Atrix 2",
-      width: 540,
-      height: 960,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Motorola Droid Razr HD",
-      width: 720,
-      height: 1280,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; DROID RAZR 4G Build/6.5.1-73_DHD-11_M1-29) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Nokia C5, C6, C7, N97, N8, X7",
-      width: 360,
-      height: 640,
-      pixelRatio: 1,
-      userAgent: "NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4",
-      touch: true,
-    },
-    {
-      name: "Nokia Lumia 7X0, Lumia 8XX, Lumia 900, N800, N810, N900",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 820)",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note 3",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note II",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Note",
-      width: 400,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SAMSUNG-SGH-I717 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S III, Galaxy Nexus",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S, S II, W",
-      width: 320,
-      height: 533,
-      pixelRatio: 1.5,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.1; en-us; GT-I9000 Build/ECLAIR) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy S4",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia S, Ion",
-      width: 360,
-      height: 640,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; LT28at Build/6.1.C.1.111) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia Sola, U",
-      width: 480,
-      height: 854,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SonyEricssonST25i Build/6.0.B.1.564) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Sony Xperia Z, Z1",
-      width: 360,
-      height: 640,
-      pixelRatio: 3,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 4.2; en-us; SonyC6903 Build/14.1.G.1.518) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
-      touch: true,
-    },
-  ],
-  tablets: [
-    {
-      name: "Amazon Kindle Fire HDX 7″",
-      width: 1920,
-      height: 1200,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; en-us; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Amazon Kindle Fire HDX 8.9″",
-      width: 2560,
-      height: 1600,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Amazon Kindle Fire (First Generation)",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.141.16-Gen4_11004310) AppleWebkit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true",
-      touch: true,
-    },
-    {
-      name: "Apple iPad 1 / 2 / iPad Mini",
-      width: 1024,
-      height: 768,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
-      touch: true,
-    },
-    {
-      name: "Apple iPad 3 / 4",
-      width: 1024,
-      height: 768,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
-      touch: true,
-    },
-    {
-      name: "BlackBerry PlayBook",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 10",
-      width: 1280,
-      height: 800,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 7 2",
-      width: 960,
-      height: 600,
-      pixelRatio: 2,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Google Nexus 7",
-      width: 966,
-      height: 604,
-      pixelRatio: 1.325,
-      userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
-      touch: true,
-    },
-    {
-      name: "Motorola Xoom, Xyboard",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Tab 7.7, 8.9, 10.1",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-    {
-      name: "Samsung Galaxy Tab",
-      width: 1024,
-      height: 600,
-      pixelRatio: 1,
-      userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
-      touch: true,
-    },
-  ],
-  notebooks: [
-    {
-      name: "Notebook with touch",
-      width: 1280,
-      height: 950,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: true,
-    },
-    {
-      name: "Notebook with HiDPI screen",
-      width: 1440,
-      height: 900,
-      pixelRatio: 2,
-      userAgent: "",
-      touch: false,
-    },
-    {
-      name: "Generic notebook",
-      width: 1280,
-      height: 800,
-      pixelRatio: 1,
-      userAgent: "",
-      touch: false,
-    },
-  ],
-  televisions: [
-  ],
-  watches: [
-  ],
-};
+  // Fetch common devices from Mozilla's CDN.
+  getJSON(DEVICES_URL, bypassCache).then(devices => {
+    for (let type in localDevices) {
+      if (!devices[type]) {
+        devices.TYPES.push(type);
+        devices[type] = [];
+      }
+      devices[type] = localDevices[type].concat(devices[type]);
+    }
+    deferred.resolve(devices);
+  });
+
+  return deferred.promise;
+}
+exports.GetDevices = GetDevices;
+
+// Get the localized string for a device type.
+function GetDeviceString(deviceType) {
+  return Strings.GetStringFromName("device." + deviceType);
+}
+exports.GetDeviceString = GetDeviceString;
rename from browser/devtools/webide/modules/remote-resources.js
rename to browser/devtools/shared/getjson.js
--- a/browser/devtools/webide/modules/remote-resources.js
+++ b/browser/devtools/shared/getjson.js
@@ -1,54 +1,43 @@
 /* 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/. */
 
 const {Cu, CC} = require("chrome");
-const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const promise = require("promise");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 
 const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
 
-function getJSON(bypassCache, pref) {
+// Downloads and caches a JSON file from a URL given by the pref.
+exports.getJSON = function (prefName, bypassCache) {
   if (!bypassCache) {
     try {
-      let str = Services.prefs.getCharPref(pref + "_cache");
+      let str = Services.prefs.getCharPref(prefName + "_cache");
       let json = JSON.parse(str);
       return promise.resolve(json);
     } catch(e) {/* no pref or invalid json. Let's continue */}
   }
 
-
   let deferred = promise.defer();
-
   let xhr = new XMLHttpRequest();
 
   xhr.onload = () => {
     let json;
     try {
       json = JSON.parse(xhr.responseText);
     } catch(e) {
-      return deferred.reject("Not valid JSON");
+      return deferred.reject("Invalid JSON");
     }
-    Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
+    Services.prefs.setCharPref(prefName + "_cache", xhr.responseText);
     deferred.resolve(json);
   }
 
   xhr.onerror = (e) => {
     deferred.reject("Network error");
   }
 
-  xhr.open("get", Services.prefs.getCharPref(pref));
+  xhr.open("get", Services.prefs.getCharPref(prefName));
   xhr.send();
 
   return deferred.promise;
 }
-
-
-
-exports.GetTemplatesJSON = function(bypassCache) {
-  return getJSON(bypassCache, "devtools.webide.templatesURL");
-}
-
-exports.GetAddonsJSON = function(bypassCache) {
-  return getJSON(bypassCache, "devtools.webide.addonsURL");
-}
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -46,25 +46,27 @@ EXTRA_JS_MODULES.devtools.shared.timelin
 ]
 
 EXTRA_JS_MODULES.devtools.shared += [
     'autocomplete-popup.js',
     'd3.js',
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
+    'getjson.js',
     'inplace-editor.js',
     'observable-object.js',
     'options-view.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
+    'widgets/CubicBezierPresets.js',
     'widgets/CubicBezierWidget.js',
     'widgets/FastListWidget.js',
     'widgets/Spectrum.js',
     'widgets/TableWidget.js',
     'widgets/Tooltip.js',
     'widgets/TreeWidget.js',
 ]
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -2,24 +2,28 @@
 subsuite = devtools
 support-files =
   browser_layoutHelpers.html
   browser_layoutHelpers-getBoxQuads.html
   browser_layoutHelpers_iframe.html
   browser_templater_basic.html
   browser_toolbar_basic.html
   browser_toolbar_webconsole_errors_count.html
+  browser_devices.json
   doc_options-view.xul
   head.js
   leakhunt.js
 
 [browser_css_color.js]
 [browser_cubic-bezier-01.js]
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
+[browser_cubic-bezier-04.js]
+[browser_cubic-bezier-05.js]
+[browser_cubic-bezier-06.js]
 [browser_flame-graph-01.js]
 [browser_flame-graph-02.js]
 [browser_flame-graph-03a.js]
 [browser_flame-graph-03b.js]
 [browser_flame-graph-03c.js]
 [browser_flame-graph-04.js]
 [browser_flame-graph-utils-01.js]
 [browser_flame-graph-utils-02.js]
@@ -93,8 +97,9 @@ skip-if = e10s # Bug 1086492 - Disable t
 [browser_templater_basic.js]
 [browser_toolbar_basic.js]
 [browser_toolbar_tooltip.js]
 [browser_toolbar_webconsole_errors_count.js]
 skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't correct with e10s
 [browser_treeWidget_basic.js]
 [browser_treeWidget_keyboard_interaction.js]
 [browser_treeWidget_mouse_interaction.js]
+[browser_devices.js]
--- a/browser/devtools/shared/test/browser_cubic-bezier-01.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-01.js
@@ -2,26 +2,30 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that the CubicBezierWidget generates content in a given parent node
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {CubicBezierWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
-  info("Checking that the markup is created in the parent");
+  info("Checking that the graph markup is created in the parent");
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container);
 
+  ok(container.querySelector(".display-wrap"),
+    "The display has been added");
+
   ok(container.querySelector(".coordinate-plane"),
     "The coordinate plane has been added");
   let buttons = container.querySelectorAll("button");
   is(buttons.length, 2,
     "The 2 control points have been added");
   is(buttons[0].className, "control-point");
   is(buttons[0].id, "P1");
   is(buttons[1].className, "control-point");
--- a/browser/devtools/shared/test/browser_cubic-bezier-02.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-02.js
@@ -2,145 +2,192 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests the CubicBezierWidget events
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget, PREDEFINED} =
+const {CubicBezierWidget} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
+  // Required or widget will be clipped inside of 'bottom'
+  // host by -14. Setting `fixed` zeroes this which is needed for
+  // calculating offsets. Occurs in test env only.
+  doc.body.setAttribute("style", "position: fixed");
+
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
-  yield pointsCanBeDragged(w, win, doc);
-  yield curveCanBeClicked(w, win, doc);
-  yield pointsCanBeMovedWithKeyboard(w, win, doc);
+  let rect = w.curve.getBoundingClientRect();
+  rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+  rect.graphBottom = rect.height - rect.graphTop;
+  rect.graphHeight = rect.graphBottom - rect.graphTop;
+
+  yield pointsCanBeDragged(w, win, doc, rect);
+  yield curveCanBeClicked(w, win, doc, rect);
+  yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
 
   w.destroy();
   host.destroy();
   gBrowser.removeCurrentTab();
 });
 
-function* pointsCanBeDragged(widget, win, doc) {
+function* pointsCanBeDragged(widget, win, doc, offsets) {
   info("Checking that the control points can be dragged with the mouse");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P1");
   widget._onPointMouseDown({target: widget.p1});
-  doc.onmousemove({pageX: 0, pageY: 100});
+  doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
   doc.onmouseup();
 
   let bezier = yield onUpdated;
   ok(true, "The widget fired the updated event");
   ok(bezier, "The updated event contains a bezier argument");
   is(bezier.P1[0], 0, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 1, "The new P1 progress coordinate is correct");
 
   info("Listening for the update event");
   onUpdated = widget.once("updated");
 
   info("Generating a mousedown/move/up on P2");
   widget._onPointMouseDown({target: widget.p2});
-  doc.onmousemove({pageX: 200, pageY: 300});
+  doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
   doc.onmouseup();
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 1, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
 }
 
-function* curveCanBeClicked(widget, win, doc) {
+function* curveCanBeClicked(widget, win, doc, offsets) {
   info("Checking that clicking on the curve moves the closest control point");
 
   info("Listening for the update event");
   let onUpdated = widget.once("updated");
 
   info("Click close to P1");
-  widget._onCurveClick({pageX: 50, pageY: 150});
+  let x = offsets.left + (offsets.width / 4.0);
+  let y = offsets.graphTop + (offsets.graphHeight / 4.0);
+  widget._onCurveClick({pageX: x, pageY: y});
 
   let bezier = yield onUpdated;
   ok(true, "The widget fired the updated event");
   is(bezier.P1[0], 0.25, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
   is(bezier.P2[0], 1, "P2 time coordinate remained unchanged");
   is(bezier.P2[1], 0, "P2 progress coordinate remained unchanged");
 
   info("Listening for the update event");
   onUpdated = widget.once("updated");
 
   info("Click close to P2");
-  widget._onCurveClick({pageX: 150, pageY: 250});
+  x = offsets.right - (offsets.width / 4);
+  y = offsets.graphBottom - (offsets.graphHeight / 4);
+  widget._onCurveClick({pageX: x, pageY: y});
 
   bezier = yield onUpdated;
   is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
   is(bezier.P1[0], 0.25, "P1 time coordinate remained unchanged");
   is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
 }
 
-function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
+function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
   info("Checking that points respond to keyboard events");
 
+  let singleStep = 3;
+  let shiftStep = 30;
+
   info("Moving P1 to the left");
+  let newOffset = parseInt(widget.p1.style.left) - singleStep;
+  let x = widget.bezierCanvas.
+          offsetsToCoordinates({style: {left: newOffset}})[0];
+
   let onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
   let bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the left, fast");
+  newOffset = parseInt(widget.p1.style.left) - shiftStep;
+  x = widget.bezierCanvas.
+      offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the right, fast");
+  newOffset = parseInt(widget.p1.style.left) + shiftStep;
+  x = widget.bezierCanvas.
+    offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
   is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the bottom");
+  newOffset = parseInt(widget.p1.style.top) + singleStep;
+  let y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the bottom, fast");
+  newOffset = parseInt(widget.p1.style.top) + shiftStep;
+  y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Moving P1 to the top, fast");
+  newOffset = parseInt(widget.p1.style.top) - shiftStep;
+  y = widget.bezierCanvas.
+    offsetsToCoordinates({style: {top: newOffset}})[1];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
   bezier = yield onUpdated;
-  is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
-  is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
+  is(bezier.P1[0], x, "The new P1 time coordinate is correct");
+  is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
 
   info("Checking that keyboard events also work with P2");
   info("Moving P2 to the left");
+  newOffset = parseInt(widget.p2.style.left) - singleStep;
+  x = widget.bezierCanvas.
+    offsetsToCoordinates({style: {left: newOffset}})[0];
+
   onUpdated = widget.once("updated");
   widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
   bezier = yield onUpdated;
-  is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
+  is(bezier.P2[0], x, "The new P2 time coordinate is correct");
   is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
 }
 
 function getKeyEvent(target, keyCode, shift=false) {
   return {
     target: target,
     keyCode: keyCode,
     shiftKey: shift,
--- a/browser/devtools/shared/test/browser_cubic-bezier-03.js
+++ b/browser/devtools/shared/test/browser_cubic-bezier-03.js
@@ -2,18 +2,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Tests that coordinates can be changed programatically in the CubicBezierWidget
 
 const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
-const {CubicBezierWidget, PREDEFINED} =
+const {CubicBezierWidget} =
   devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
 
 add_task(function*() {
   yield promiseTab("about:blank");
   let [host, win, doc] = yield createHost("bottom", TEST_URI);
 
   let container = doc.querySelector("#container");
   let w = new CubicBezierWidget(container, PREDEFINED.linear);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-04.js
@@ -0,0 +1,51 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget generates markup.
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierPresetWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierPresetWidget(container);
+
+  info("Checking that the presets are created in the parent");
+  ok(container.querySelector(".preset-pane"),
+     "The preset pane has been added");
+
+  ok(container.querySelector("#preset-categories"),
+     "The preset categories have been added");
+  let categories = container.querySelectorAll(".category");
+  is(categories.length, Object.keys(PRESETS).length,
+     "The preset categories have been added");
+  Object.keys(PRESETS).forEach(category => {
+    ok(container.querySelector("#" + category), `${category} has been added`);
+    ok(container.querySelector("#preset-category-" + category),
+       `The preset list for ${category} has been added.`);
+  });
+
+  info("Checking that each of the presets and its preview have been added");
+  Object.keys(PRESETS).forEach(category => {
+    Object.keys(PRESETS[category]).forEach(presetLabel => {
+      let preset = container.querySelector("#" + presetLabel);
+      ok(preset, `${presetLabel} has been added`);
+      ok(preset.querySelector("canvas"),
+         `${presetLabel}'s canvas preview has been added`);
+      ok(preset.querySelector("p"),
+         `${presetLabel}'s label has been added`);
+    });
+  });
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-05.js
@@ -0,0 +1,49 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests that the CubicBezierPresetWidget cycles menus
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierPresetWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
+  require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierPresetWidget(container);
+
+  info("Checking that preset is selected if coordinates are known");
+
+  w.refreshMenu([0, 0, 0, 0]);
+  is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
+    "The default category is selected");
+  is(w._activePreset, null, "There is no selected category");
+
+  w.refreshMenu(PREDEFINED["linear"]);
+  is(w.activeCategory, container.querySelector("#ease-in-out"),
+     "The ease-in-out category is active");
+  is(w._activePreset, container.querySelector("#ease-in-out-linear"),
+     "The ease-in-out-linear preset is active");
+
+  w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
+  is(w.activeCategory, container.querySelector("#ease-out"),
+     "The ease-out category is active");
+  is(w._activePreset, container.querySelector("#ease-out-sine"),
+     "The ease-out-sine preset is active");
+
+  w.refreshMenu([0, 0, 0, 0]);
+  is(w.activeCategory, container.querySelector("#ease-out"),
+     "The ease-out category is still active");
+  is(w._activePreset, null, "No preset is active");
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_cubic-bezier-06.js
@@ -0,0 +1,80 @@
+
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the integration between CubicBezierWidget and CubicBezierPresets
+
+const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
+const {CubicBezierWidget} =
+  devtools.require("devtools/shared/widgets/CubicBezierWidget");
+const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
+
+add_task(function*() {
+  yield promiseTab("about:blank");
+  let [host, win, doc] = yield createHost("bottom", TEST_URI);
+
+  let container = doc.querySelector("#container");
+  let w = new CubicBezierWidget(container,
+              PRESETS["ease-in"]["ease-in-sine"]);
+  w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
+
+  let rect = w.curve.getBoundingClientRect();
+  rect.graphTop = rect.height * w.bezierCanvas.padding[0];
+
+  yield adjustingBezierUpdatesPreset(w, win, doc, rect);
+  yield selectingPresetUpdatesBezier(w, win, doc, rect);
+
+  w.destroy();
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
+
+function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
+  info("Checking that changing the bezier refreshes the preset menu");
+
+  is(widget.presets.activeCategory,
+     doc.querySelector("#ease-in"),
+     "The selected category is ease-in");
+
+  is(widget.presets._activePreset,
+     doc.querySelector("#ease-in-sine"),
+     "The selected preset is ease-in-sine");
+
+  info("Generating custom bezier curve by dragging");
+  widget._onPointMouseDown({target: widget.p1});
+  doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
+  doc.onmouseup();
+
+  is(widget.presets.activeCategory,
+     doc.querySelector("#ease-in"),
+     "The selected category is still ease-in");
+
+  is(widget.presets._activePreset, null,
+     "There is no active preset");
+ }
+
+function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
+  info("Checking that selecting a preset updates bezier curve");
+
+  info("Listening for the new coordinates event");
+  let onNewCoordinates = widget.presets.once("new-coordinates");
+  let onUpdated = widget.once("updated");
+
+  info("Click a preset");
+  let preset = doc.querySelector("#ease-in-sine");
+  widget.presets._onPresetClick({currentTarget: preset});
+
+  yield onNewCoordinates;
+  ok(true, "The preset widget fired the new-coordinates event");
+
+  let bezier = yield onUpdated;
+  ok(true, "The bezier canvas fired the updated event");
+
+  is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
+  is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
+  is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
+  is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_devices.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let { GetDevices, GetDeviceString, AddDevice } = devtools.require("devtools/shared/devices");
+
+add_task(function*() {
+  Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "browser_devices.json");
+
+  let devices = yield GetDevices();
+
+  is(devices.TYPES.length, 1, "Found 1 device type.");
+
+  let type1 = devices.TYPES[0];
+
+  is(devices[type1].length, 2, "Found 2 devices of type #1.");
+
+  let string = GetDeviceString(type1);
+  ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
+
+  let device1 = {
+    name: "SquarePhone",
+    width: 320,
+    height: 320,
+    pixelRatio: 2,
+    userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
+    touch: true,
+    firefoxOS: true
+  };
+  AddDevice(device1, type1);
+  devices = yield GetDevices();
+
+  is(devices[type1].length, 3, "Added new device of type #1.");
+  ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
+
+  let type2 = "appliances";
+  let device2 = {
+    name: "Mr Freezer",
+    width: 800,
+    height: 600,
+    pixelRatio: 5,
+    userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
+    touch: true,
+    firefoxOS: true
+  };
+  AddDevice(device2, type2);
+  devices = yield GetDevices();
+
+  is(devices.TYPES.length, 2, "Added device type #2.");
+  is(devices[type2].length, 1, "Added new device of type #2.");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_devices.json
@@ -0,0 +1,23 @@
+{
+  "TYPES": [ "phones" ],
+  "phones": [
+    {
+      "name": "Small Phone",
+      "width": 320,
+      "height": 480,
+      "pixelRatio": 1,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true
+    },
+    {
+      "name": "Big Phone",
+      "width": 360,
+      "height": 640,
+      "pixelRatio": 3,
+      "userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
+      "touch": true,
+      "firefoxOS": true
+    }
+  ]
+}
--- a/browser/devtools/shared/test/browser_graphs-07a.js
+++ b/browser/devtools/shared/test/browser_graphs-07a.js
@@ -12,24 +12,28 @@ add_task(function*() {
   yield performTest();
   gBrowser.removeCurrentTab();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   let graph = new LineGraphWidget(doc.body, "fps");
   yield graph.once("ready");
+  testGraph(graph, normalDragStop);
+  yield graph.destroy();
 
-  testGraph(graph);
+  let graph2 = new LineGraphWidget(doc.body, "fps");
+  yield graph2.once("ready");
+  testGraph(graph2, buggyDragStop);
+  yield graph2.destroy();
 
-  yield graph.destroy();
   host.destroy();
 }
 
-function testGraph(graph) {
+function testGraph(graph, dragStop) {
   graph.setData(TEST_DATA);
 
   info("Making a selection.");
 
   dragStart(graph, 300);
   ok(graph.hasSelectionInProgress(),
     "The selection should start (1).");
   is(graph.getSelection().start, 300,
@@ -181,21 +185,32 @@ function click(graph, x, y = 1) {
 
 function dragStart(graph, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseDown({ clientX: x, clientY: y });
 }
 
-function dragStop(graph, x, y = 1) {
+function normalDragStop(graph, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseUp({ clientX: x, clientY: y });
 }
 
+function buggyDragStop(graph, x, y = 1) {
+  x /= window.devicePixelRatio;
+  y /= window.devicePixelRatio;
+
+  // Only fire a mousemove instead of a mouseup.
+  // This happens when the mouseup happens outside of the toolbox,
+  // see Bug 1066504.
+  graph._onMouseMove({ clientX: x, clientY: y });
+  graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 });
+}
+
 function scroll(graph, wheel, x, y = 1) {
   x /= window.devicePixelRatio;
   y /= window.devicePixelRatio;
   graph._onMouseMove({ clientX: x, clientY: y });
   graph._onMouseWheel({ clientX: x, clientY: y, detail: wheel });
 }
--- a/browser/devtools/shared/test/unit/test_bezierCanvas.js
+++ b/browser/devtools/shared/test/unit/test_bezierCanvas.js
@@ -99,15 +99,18 @@ function getCanvasMock(w=200, h=400) {
         clearRect: () => {},
         beginPath: () => {},
         closePath: () => {},
         moveTo: () => {},
         lineTo: () => {},
         stroke: () => {},
         arc: () => {},
         fill: () => {},
-        bezierCurveTo: () => {}
+        bezierCurveTo: () => {},
+        save: () => {},
+        restore: () => {},
+        setTransform: () => {}
       };
     },
     width: w,
     height: h
   };
 }
--- a/browser/devtools/shared/test/unit/test_cubicBezier.js
+++ b/browser/devtools/shared/test/unit/test_cubicBezier.js
@@ -14,16 +14,17 @@ let {CubicBezier} = require("devtools/sh
 
 function run_test() {
   throwsWhenMissingCoordinates();
   throwsWhenIncorrectCoordinates();
   convertsStringCoordinates();
   coordinatesToStringOutputsAString();
   pointGettersReturnPointCoordinatesArrays();
   toStringOutputsCubicBezierValue();
+  toStringOutputsCssPresetValues();
 }
 
 function throwsWhenMissingCoordinates() {
   do_check_throws(() => {
     new CubicBezier();
   }, "Throws an exception when coordinates are missing");
 }
 
@@ -79,18 +80,37 @@ function pointGettersReturnPointCoordina
   do_check_eq(c.P1[1], .2);
   do_check_eq(c.P2[0], .5);
   do_check_eq(c.P2[1], 1);
 }
 
 function toStringOutputsCubicBezierValue() {
   do_print("toString() outputs the cubic-bezier() value");
 
+  let c = new CubicBezier([0, 1, 1, 0]);
+  do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
+}
+
+function toStringOutputsCssPresetValues() {
+  do_print("toString() outputs the css predefined values");
+
   let c = new CubicBezier([0, 0, 1, 1]);
-  do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
+  do_check_eq(c.toString(), "linear");
+
+  c = new CubicBezier([0.25, 0.1, 0.25, 1]);
+  do_check_eq(c.toString(), "ease");
+
+  c = new CubicBezier([0.42, 0, 1, 1]);
+  do_check_eq(c.toString(), "ease-in");
+
+  c = new CubicBezier([0, 0, 0.58, 1]);
+  do_check_eq(c.toString(), "ease-out");
+
+  c = new CubicBezier([0.42, 0, 0.58, 1]);
+  do_check_eq(c.toString(), "ease-in-out");
 }
 
 function do_check_throws(cb, info) {
   do_print(info);
 
   let hasThrown = false;
   try {
     cb();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/CubicBezierPresets.js
@@ -0,0 +1,64 @@
+/**
+ * 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/.
+ */
+
+// Set of preset definitions for use with CubicBezierWidget
+// Credit: http://easings.net
+
+"use strict";
+
+const PREDEFINED = {
+  "ease": [0.25, 0.1, 0.25, 1],
+  "linear": [0, 0, 1, 1],
+  "ease-in": [0.42, 0, 1, 1],
+  "ease-out": [0, 0, 0.58, 1],
+  "ease-in-out": [0.42, 0, 0.58, 1]
+};
+
+const PRESETS = {
+  "ease-in": {
+    "ease-in-linear": [0, 0, 1, 1],
+    "ease-in-ease-in": [0.42, 0, 1, 1],
+    "ease-in-sine": [0.47, 0, 0.74, 0.71],
+    "ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
+    "ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
+    "ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
+    "ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
+    "ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
+    "ease-in-circular": [0.6, 0.04, 0.98, 0.34],
+    "ease-in-backward": [0.6, -0.28, 0.74, 0.05]
+  },
+  "ease-out": {
+    "ease-out-linear": [0, 0, 1, 1],
+    "ease-out-ease-out": [0, 0, 0.58, 1],
+    "ease-out-sine": [0.39, 0.58, 0.57, 1],
+    "ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
+    "ease-out-cubic": [0.22, 0.61, 0.36, 1],
+    "ease-out-quartic": [0.17, 0.84, 0.44, 1],
+    "ease-out-quintic": [0.23, 1, 0.32, 1],
+    "ease-out-exponential": [0.19, 1, 0.22, 1],
+    "ease-out-circular": [0.08, 0.82, 0.17, 1],
+    "ease-out-backward": [0.18, 0.89, 0.32, 1.28]
+  },
+  "ease-in-out": {
+    "ease-in-out-linear": [0, 0, 1, 1],
+    "ease-in-out-ease": [0.25, 0.1, 0.25, 1],
+    "ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
+    "ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
+    "ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
+    "ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
+    "ease-in-out-quartic": [0.77, 0, 0.18, 1],
+    "ease-in-out-quintic": [0.86, 0, 0.07, 1],
+    "ease-in-out-exponential": [1, 0, 0, 1],
+    "ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
+    "ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
+  }
+};
+
+const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
+
+exports.PRESETS = PRESETS;
+exports.PREDEFINED = PREDEFINED;
+exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
--- a/browser/devtools/shared/widgets/CubicBezierWidget.js
+++ b/browser/devtools/shared/widgets/CubicBezierWidget.js
@@ -22,24 +22,17 @@
 
 // Based on www.cubic-bezier.com by Lea Verou
 // See https://github.com/LeaVerou/cubic-bezier
 
 "use strict";
 
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const {setTimeout, clearTimeout} = require("sdk/timers");
-
-const PREDEFINED = exports.PREDEFINED = {
-  "ease": [.25, .1, .25, 1],
-  "linear": [0, 0, 1, 1],
-  "ease-in": [.42, 0, 1, 1],
-  "ease-out": [0, 0, .58, 1],
-  "ease-in-out": [.42, 0, .58, 1]
-};
+const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets");
 
 /**
  * CubicBezier data structure helper
  * Accepts an array of coordinates and exposes a few useful getters
  * @param {Array} coordinates i.e. [.42, 0, .58, 1]
  */
 function CubicBezier(coordinates) {
   if (!coordinates) {
@@ -54,32 +47,36 @@ function CubicBezier(coordinates) {
       throw "Wrong coordinate at " + i + "(" + xy + ")";
     }
   }
 
   this.coordinates.toString = function() {
     return this.map(n => {
       return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
     }) + "";
-  }
+  };
 }
 
 exports.CubicBezier = CubicBezier;
 
 CubicBezier.prototype = {
   get P1() {
     return this.coordinates.slice(0, 2);
   },
 
   get P2() {
     return this.coordinates.slice(2);
   },
 
   toString: function() {
-    return 'cubic-bezier(' + this.coordinates + ')';
+    // Check first if current coords are one of css predefined functions
+    let predefName = Object.keys(PREDEFINED)
+                           .find(key => coordsAreEqual(PREDEFINED[key], this.coordinates));
+
+    return predefName || 'cubic-bezier(' + this.coordinates + ')';
   }
 };
 
 /**
  * Bezier curve canvas plotting class
  * @param {DOMNode} canvas
  * @param {CubicBezier} bezier
  * @param {Array} padding Amount of horizontal,vertical padding around the graph
@@ -92,17 +89,17 @@ function BezierCanvas(canvas, bezier, pa
   // Convert to a cartesian coordinate system with axes from 0 to 1
   this.ctx = this.canvas.getContext('2d');
   let p = this.padding;
 
   this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
                  -canvas.height * (1 - p[0] - p[2]));
   this.ctx.translate(p[3] / (1 - p[1] - p[3]),
                      -1 - p[0] / (1 - p[0] - p[2]));
-};
+}
 
 exports.BezierCanvas = BezierCanvas;
 
 BezierCanvas.prototype = {
   /**
    * Get P1 and P2 current top/left offsets so they can be positioned
    * @return {Array} Returns an array of 2 {top:String,left:String} objects
    */
@@ -110,78 +107,86 @@ BezierCanvas.prototype = {
     let p = this.padding, w = this.canvas.width, h = this.canvas.height;
 
     return [{
       left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px',
       top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px'
     }, {
       left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
       top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
-    }]
+    }];
   },
 
   /**
    * Convert an element's left/top offsets into coordinates
    */
   offsetsToCoordinates: function(element) {
     let p = this.padding, w = this.canvas.width, h = this.canvas.height;
 
     // Convert padding percentage to actual padding
     p = p.map(function(a, i) { return a * (i % 2? w : h)});
 
     return [
-      (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
-      (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
+      (parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
+      (h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
     ];
   },
 
   /**
    * Draw the cubic bezier curve for the current coordinates
    */
   plot: function(settings={}) {
     let xy = this.bezier.coordinates;
 
     let defaultSettings = {
       handleColor: '#666',
       handleThickness: .008,
       bezierColor: '#4C9ED9',
-      bezierThickness: .015
+      bezierThickness: .015,
+      drawHandles: true
     };
 
     for (let setting in settings) {
       defaultSettings[setting] = settings[setting];
     }
 
-    this.ctx.clearRect(-.5,-.5, 2, 2);
+    // Clear the canvas –making sure to clear the
+    // whole area by resetting the transform first.
+    this.ctx.save();
+    this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+    this.ctx.restore();
 
-    // Draw control handles
-    this.ctx.beginPath();
-    this.ctx.fillStyle = defaultSettings.handleColor;
-    this.ctx.lineWidth = defaultSettings.handleThickness;
-    this.ctx.strokeStyle = defaultSettings.handleColor;
-
-    this.ctx.moveTo(0, 0);
-    this.ctx.lineTo(xy[0], xy[1]);
-    this.ctx.moveTo(1,1);
-    this.ctx.lineTo(xy[2], xy[3]);
+    if (defaultSettings.drawHandles) {
+      // Draw control handles
+      this.ctx.beginPath();
+      this.ctx.fillStyle = defaultSettings.handleColor;
+      this.ctx.lineWidth = defaultSettings.handleThickness;
+      this.ctx.strokeStyle = defaultSettings.handleColor;
 
-    this.ctx.stroke();
-    this.ctx.closePath();
+      this.ctx.moveTo(0, 0);
+      this.ctx.lineTo(xy[0], xy[1]);
+      this.ctx.moveTo(1,1);
+      this.ctx.lineTo(xy[2], xy[3]);
+
+      this.ctx.stroke();
+      this.ctx.closePath();
 
-    function circle(ctx, cx, cy, r) {
-      return ctx.beginPath();
-      ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
-      ctx.closePath();
+      var circle = function(ctx, cx, cy, r) {
+        return ctx.beginPath();
+        ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
+        ctx.closePath();
+      };
+
+      circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+      this.ctx.fill();
+      circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+      this.ctx.fill();
     }
 
-    circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
-    this.ctx.fill();
-    circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
-    this.ctx.fill();
-
     // Draw bezier curve
     this.ctx.beginPath();
     this.ctx.lineWidth = defaultSettings.bezierThickness;
     this.ctx.strokeStyle = defaultSettings.bezierColor;
     this.ctx.moveTo(0,0);
     this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1);
     this.ctx.stroke();
     this.ctx.closePath();
@@ -192,104 +197,118 @@ BezierCanvas.prototype = {
  * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
  * adds the control points and user interaction
  * @param {DOMNode} parent The container where the graph should be created
  * @param {Array} coordinates Coordinates of the curve to be drawn
  *
  * Emits "updated" events whenever the curve is changed. Along with the event is
  * sent a CubicBezier object
  */
-function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
+function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) {
+  EventEmitter.decorate(this);
+
   this.parent = parent;
   let {curve, p1, p2} = this._initMarkup();
 
+  this.curveBoundingBox = curve.getBoundingClientRect();
   this.curve = curve;
-  this.curveBoundingBox = curve.getBoundingClientRect();
   this.p1 = p1;
   this.p2 = p2;
 
   // Create and plot the bezier curve
   this.bezierCanvas = new BezierCanvas(this.curve,
-    new CubicBezier(coordinates), [.25, 0]);
+    new CubicBezier(coordinates), [0.30, 0]);
   this.bezierCanvas.plot();
 
   // Place the control points
   let offsets = this.bezierCanvas.offsets;
   this.p1.style.left = offsets[0].left;
   this.p1.style.top = offsets[0].top;
   this.p2.style.left = offsets[1].left;
   this.p2.style.top = offsets[1].top;
 
   this._onPointMouseDown = this._onPointMouseDown.bind(this);
   this._onPointKeyDown = this._onPointKeyDown.bind(this);
   this._onCurveClick = this._onCurveClick.bind(this);
-  this._initEvents();
+  this._onNewCoordinates = this._onNewCoordinates.bind(this);
+
+  // Add preset preview menu
+  this.presets = new CubicBezierPresetWidget(parent);
 
   // Add the timing function previewer
   this.timingPreview = new TimingFunctionPreviewWidget(parent);
 
-  EventEmitter.decorate(this);
+  this._initEvents();
 }
 
 exports.CubicBezierWidget = CubicBezierWidget;
 
 CubicBezierWidget.prototype = {
   _initMarkup: function() {
     let doc = this.parent.ownerDocument;
 
+    let wrap = doc.createElement("div");
+    wrap.className = "display-wrap";
+
     let plane = doc.createElement("div");
     plane.className = "coordinate-plane";
 
     let p1 = doc.createElement("button");
     p1.className = "control-point";
     p1.id = "P1";
     plane.appendChild(p1);
 
     let p2 = doc.createElement("button");
     p2.className = "control-point";
     p2.id = "P2";
     plane.appendChild(p2);
 
     let curve = doc.createElement("canvas");
-    curve.setAttribute("height", "400");
-    curve.setAttribute("width", "200");
+    curve.setAttribute("width", 150);
+    curve.setAttribute("height", 370);
     curve.id = "curve";
-    plane.appendChild(curve);
 
-    this.parent.appendChild(plane);
+    plane.appendChild(curve);
+    wrap.appendChild(plane);
+
+    this.parent.appendChild(wrap);
 
     return {
       p1: p1,
       p2: p2,
       curve: curve
-    }
+    };
   },
 
   _removeMarkup: function() {
-    this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
+    this.parent.ownerDocument.querySelector(".display-wrap").remove();
   },
 
   _initEvents: function() {
     this.p1.addEventListener("mousedown", this._onPointMouseDown);
     this.p2.addEventListener("mousedown", this._onPointMouseDown);
 
     this.p1.addEventListener("keydown", this._onPointKeyDown);
     this.p2.addEventListener("keydown", this._onPointKeyDown);
 
     this.curve.addEventListener("click", this._onCurveClick);
+
+    this.presets.on("new-coordinates", this._onNewCoordinates);
   },
 
   _removeEvents: function() {
     this.p1.removeEventListener("mousedown", this._onPointMouseDown);
     this.p2.removeEventListener("mousedown", this._onPointMouseDown);
 
     this.p1.removeEventListener("keydown", this._onPointKeyDown);
     this.p2.removeEventListener("keydown", this._onPointKeyDown);
 
     this.curve.removeEventListener("click", this._onCurveClick);
+
+    this.presets.off("new-coordinates", this._onNewCoordinates);
   },
 
   _onPointMouseDown: function(event) {
     // Updating the boundingbox in case it has changed
     this.curveBoundingBox = this.curve.getBoundingClientRect();
 
     let point = event.target;
     let doc = point.ownerDocument;
@@ -312,17 +331,17 @@ CubicBezierWidget.prototype = {
       point.style.top = y - top + "px";
 
       self._updateFromPoints();
     };
 
     doc.onmouseup = function () {
       point.focus();
       doc.onmousemove = doc.onmouseup = null;
-    }
+    };
   },
 
   _onPointKeyDown: function(event) {
     let point = event.target;
     let code = event.keyCode;
 
     if (code >= 37 && code <= 40) {
       event.preventDefault();
@@ -339,16 +358,18 @@ CubicBezierWidget.prototype = {
         case 40: point.style.top = top + offset + 'px'; break;
       }
 
       this._updateFromPoints();
     }
   },
 
   _onCurveClick: function(event) {
+    this.curveBoundingBox = this.curve.getBoundingClientRect();
+
     let left = this.curveBoundingBox.left;
     let top = this.curveBoundingBox.top;
     let x = event.pageX - left;
     let y = event.pageY - top;
 
     // Find which point is closer
     let distP1 = distance(x, y,
       parseInt(this.p1.style.left), parseInt(this.p1.style.top));
@@ -357,24 +378,29 @@ CubicBezierWidget.prototype = {
 
     let point = distP1 < distP2 ? this.p1 : this.p2;
     point.style.left = x + "px";
     point.style.top = y + "px";
 
     this._updateFromPoints();
   },
 
+  _onNewCoordinates: function(event, coordinates) {
+    this.coordinates = coordinates;
+  },
+
   /**
    * Get the current point coordinates and redraw the curve to match
    */
   _updateFromPoints: function() {
     // Get the new coordinates from the point's offsets
-    let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
+    let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
     coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
 
+    this.presets.refreshMenu(coordinates);
     this._redraw(coordinates);
   },
 
   /**
    * Redraw the curve
    * @param {Array} coordinates The array of control point coordinates
    */
   _redraw: function(coordinates) {
@@ -386,17 +412,17 @@ CubicBezierWidget.prototype = {
     this.timingPreview.preview(this.bezierCanvas.bezier + "");
   },
 
   /**
    * Set new coordinates for the control points and redraw the curve
    * @param {Array} coordinates
    */
   set coordinates(coordinates) {
-    this._redraw(coordinates)
+    this._redraw(coordinates);
 
     // Move the points
     let offsets = this.bezierCanvas.offsets;
     this.p1.style.left = offsets[0].left;
     this.p1.style.top = offsets[0].top;
     this.p2.style.left = offsets[1].left;
     this.p2.style.top = offsets[1].top;
   },
@@ -415,30 +441,282 @@ CubicBezierWidget.prototype = {
     // Try with one of the predefined values
     let coordinates = PREDEFINED[value];
 
     // Otherwise parse the coordinates from the cubic-bezier function
     if (!coordinates && value.startsWith("cubic-bezier")) {
       coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
     }
 
+    this.presets.refreshMenu(coordinates);
     this.coordinates = coordinates;
   },
 
   destroy: function() {
     this._removeEvents();
     this._removeMarkup();
 
     this.timingPreview.destroy();
+    this.presets.destroy();
 
     this.curve = this.p1 = this.p2 = null;
   }
 };
 
 /**
+ * CubicBezierPreset widget.
+ * Builds a menu of presets from CubicBezierPresets
+ * @param {DOMNode} parent The container where the preset panel should be created
+ *
+ * Emits "new-coordinate" event along with the coordinates
+ * whenever a preset is selected.
+ */
+function CubicBezierPresetWidget(parent) {
+  this.parent = parent;
+
+  let {presetPane, presets, categories} = this._initMarkup();
+  this.presetPane = presetPane;
+  this.presets = presets;
+  this.categories = categories;
+
+  this._activeCategory = null;
+  this._activePresetList = null;
+  this._activePreset = null;
+
+  this._onCategoryClick = this._onCategoryClick.bind(this);
+  this._onPresetClick = this._onPresetClick.bind(this);
+
+  EventEmitter.decorate(this);
+  this._initEvents();
+}
+
+exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
+
+CubicBezierPresetWidget.prototype = {
+  /*
+   * Constructs a list of all preset categories and a list
+   * of presets for each category.
+   *
+   * High level markup:
+   *  div .preset-pane
+   *    div .preset-categories
+   *      div .category
+   *      div .category
+   *      ...
+   *    div .preset-container
+   *      div .presetList
+   *        div .preset
+   *        ...
+   *      div .presetList
+   *        div .preset
+   *        ...
+   */
+  _initMarkup: function() {
+    let doc = this.parent.ownerDocument;
+
+    let presetPane = doc.createElement("div");
+    presetPane.className = "preset-pane";
+
+    let categoryList = doc.createElement("div");
+    categoryList.id = "preset-categories";
+
+    let presetContainer = doc.createElement("div");
+    presetContainer.id = "preset-container";
+
+    Object.keys(PRESETS).forEach(categoryLabel => {
+      let category = this._createCategory(categoryLabel);
+      categoryList.appendChild(category);
+
+      let presetList = this._createPresetList(categoryLabel);
+      presetContainer.appendChild(presetList);
+    });
+
+    presetPane.appendChild(categoryList);
+    presetPane.appendChild(presetContainer);
+
+    this.parent.appendChild(presetPane);
+
+    let allCategories = presetPane.querySelectorAll(".category");
+    let allPresets = presetPane.querySelectorAll(".preset");
+
+    return {
+      presetPane: presetPane,
+      presets: allPresets,
+      categories: allCategories
+    };
+  },
+
+  _createCategory: function(categoryLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let category = doc.createElement("div");
+    category.id = categoryLabel;
+    category.classList.add("category");
+
+    let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
+    category.textContent = categoryDisplayLabel;
+
+    return category;
+  },
+
+  _normalizeCategoryLabel: function(categoryLabel) {
+    return categoryLabel.replace("/-/g", " ");
+  },
+
+  _createPresetList: function(categoryLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let presetList = doc.createElement("div");
+    presetList.id = "preset-category-" + categoryLabel;
+    presetList.classList.add("preset-list");
+
+    Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+      let preset = this._createPreset(categoryLabel, presetLabel);
+      presetList.appendChild(preset);
+    });
+
+    return presetList;
+  },
+
+  _createPreset: function(categoryLabel, presetLabel) {
+    let doc = this.parent.ownerDocument;
+
+    let preset = doc.createElement("div");
+    preset.classList.add("preset");
+    preset.id = presetLabel;
+    preset.coordinates = PRESETS[categoryLabel][presetLabel];
+
+    // Create preset preview
+    let curve = doc.createElement("canvas");
+    let bezier = new CubicBezier(preset.coordinates);
+
+    curve.setAttribute("height", 55);
+    curve.setAttribute("width", 55);
+
+    preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
+    preset.bezierCanvas.plot({
+      drawHandles: false,
+      bezierThickness: 0.025
+    });
+
+    preset.appendChild(curve);
+
+    // Create preset label
+    let presetLabelElem = doc.createElement("p");
+    let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel);
+    presetLabelElem.textContent = presetDisplayLabel;
+    preset.appendChild(presetLabelElem);
+
+    return preset;
+  },
+
+  _normalizePresetLabel: function(categoryLabel, presetLabel) {
+    return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
+  },
+
+  _initEvents: function() {
+    for (let category of this.categories) {
+      category.addEventListener("click", this._onCategoryClick);
+    }
+
+    for (let preset of this.presets) {
+      preset.addEventListener("click", this._onPresetClick);
+    }
+  },
+
+  _removeEvents: function() {
+    for (let category of this.categories) {
+      category.removeEventListener("click", this._onCategoryClick);
+    }
+
+    for (let preset of this.presets) {
+      preset.removeEventListener("click", this._onPresetClick);
+    }
+  },
+
+  _onPresetClick: function(event) {
+    this.emit("new-coordinates", event.currentTarget.coordinates);
+    this.activePreset = event.currentTarget;
+  },
+
+  _onCategoryClick: function(event) {
+    this.activeCategory = event.target;
+  },
+
+  _setActivePresetList: function(presetListId) {
+    let presetList = this.presetPane.querySelector("#" + presetListId);
+    swapClassName("active-preset-list", this._activePresetList, presetList);
+    this._activePresetList = presetList;
+  },
+
+  set activeCategory(category) {
+    swapClassName("active-category", this._activeCategory, category);
+    this._activeCategory = category;
+    this._setActivePresetList("preset-category-" + category.id);
+  },
+
+  get activeCategory() {
+    return this._activeCategory;
+  },
+
+  set activePreset(preset) {
+    swapClassName("active-preset", this._activePreset, preset);
+    this._activePreset = preset;
+  },
+
+  get activePreset() {
+    return this._activePreset;
+  },
+
+  /**
+   * Called by CubicBezierWidget onload and when
+   * the curve is modified via the canvas.
+   * Attempts to match the new user setting with an
+   * existing preset.
+   * @param {Array} coordinates new coords [i, j, k, l]
+   */
+  refreshMenu: function(coordinates) {
+    // If we cannot find a matching preset, keep
+    // menu on last known preset category.
+    let category = this._activeCategory;
+
+    // If we cannot find a matching preset
+    // deselect any selected preset.
+    let preset = null;
+
+    // If a category has never been viewed before
+    // show the default category.
+    if (!category) {
+      category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
+    }
+
+    // If the new coordinates do match a preset,
+    // set its category and preset button as active.
+    Object.keys(PRESETS).forEach(categoryLabel => {
+
+      Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
+        if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
+          category = this.parent.querySelector("#" + categoryLabel);
+          preset = this.parent.querySelector("#" + presetLabel);
+        }
+      });
+
+    });
+
+    this.activeCategory = category;
+    this.activePreset = preset;
+  },
+
+  destroy: function() {
+    this._removeEvents();
+    this.parent.querySelector(".preset-pane").remove();
+  }
+};
+
+/**
  * The TimingFunctionPreviewWidget animates a dot on a scale with a given
  * timing-function
  * @param {DOMNode} parent The container where this widget should go
  */
 function TimingFunctionPreviewWidget(parent) {
   this.previousValue = null;
   this.autoRestartAnimation = null;
 
@@ -549,8 +827,34 @@ function isValidTimingFunction(value) {
 
   // Or it has to match a cubic-bezier expression
   if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) {
     return true;
   }
 
   return false;
 }
+
+/**
+ * Removes a class from a node and adds it to another.
+ * @param {String} className the class to swap
+ * @param {DOMNode} from the node to remove the class from
+ * @param {DOMNode} to the node to add the class to
+ */
+function swapClassName(className, from, to) {
+  if (from !== null) {
+    from.classList.remove(className);
+  }
+
+  if (to !== null) {
+    to.classList.add(className);
+  }
+}
+
+/**
+ * Compares two arrays of coordinates [i, j, k, l]
+ * @param {Array} c1 first coordinate array to compare
+ * @param {Array} c2 second coordinate array to compare
+ * @return {Boolean}
+ */
+function coordsAreEqual(c1, c2) {
+  return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
+}
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -176,16 +176,17 @@ this.AbstractCanvasGraph = function(pare
     this._height = canvas.height = bounds.height * this._pixelRatio;
     this._ctx = canvas.getContext("2d");
     this._ctx.mozImageSmoothingEnabled = false;
 
     this._cursor = new GraphCursor();
     this._selection = new GraphArea();
     this._selectionDragger = new GraphAreaDragger();
     this._selectionResizer = new GraphAreaResizer();
+    this._isMouseActive = false;
 
     this._onAnimationFrame = this._onAnimationFrame.bind(this);
     this._onMouseMove = this._onMouseMove.bind(this);
     this._onMouseDown = this._onMouseDown.bind(this);
     this._onMouseUp = this._onMouseUp.bind(this);
     this._onMouseWheel = this._onMouseWheel.bind(this);
     this._onMouseOut = this._onMouseOut.bind(this);
     this._onResize = this._onResize.bind(this);
@@ -947,31 +948,40 @@ AbstractCanvasGraph.prototype = {
 
     return { left: x, top: y };
   },
 
   /**
    * Listener for the "mousemove" event on the graph's container.
    */
   _onMouseMove: function(e) {
+    let resizer = this._selectionResizer;
+    let dragger = this._selectionDragger;
+
+    // If a mouseup happened outside the toolbox and the current operation
+    // is causing the selection changed, then end it.
+    if (e.buttons == 0 && (this.hasSelectionInProgress() ||
+                           resizer.margin != null ||
+                           dragger.origin != null)) {
+      return this._onMouseUp(e);
+    }
+
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
     let mouseY = (e.clientY - offset.top) * this._pixelRatio;
     this._cursor.x = mouseX;
     this._cursor.y = mouseY;
 
-    let resizer = this._selectionResizer;
     if (resizer.margin != null) {
       this._selection[resizer.margin] = mouseX;
       this._shouldRedraw = true;
       this.emit("selecting");
       return;
     }
 
-    let dragger = this._selectionDragger;
     if (dragger.origin != null) {
       this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
       this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
       this._shouldRedraw = true;
       this.emit("selecting");
       return;
     }
 
@@ -1008,16 +1018,17 @@ AbstractCanvasGraph.prototype = {
 
     this._shouldRedraw = true;
   },
 
   /**
    * Listener for the "mousedown" event on the graph's container.
    */
   _onMouseDown: function(e) {
+    this._isMouseActive = true;
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
@@ -1046,16 +1057,17 @@ AbstractCanvasGraph.prototype = {
     this._shouldRedraw = true;
     this.emit("mousedown");
   },
 
   /**
    * Listener for the "mouseup" event on the graph's container.
    */
   _onMouseUp: function(e) {
+    this._isMouseActive = false;
     let offset = this._getContainerOffset();
     let mouseX = (e.clientX - offset.left) * this._pixelRatio;
 
     switch (this._canvas.getAttribute("input")) {
       case "hovering-background":
       case "hovering-region":
         if (!this.selectionEnabled) {
           break;
@@ -1156,31 +1168,27 @@ AbstractCanvasGraph.prototype = {
       selection.end = midPoint + GRAPH_WHEEL_MIN_SELECTION_WIDTH / 2;
     }
 
     this._shouldRedraw = true;
     this.emit("selecting");
     this.emit("scroll");
   },
 
-  /**
+   /**
    * Listener for the "mouseout" event on the graph's container.
+   * Clear any active cursors if a drag isn't happening.
    */
-  _onMouseOut: function() {
-    if (this.hasSelectionInProgress()) {
-      this.dropSelection();
+  _onMouseOut: function(e) {
+    if (!this._isMouseActive) {
+      this._cursor.x = null;
+      this._cursor.y = null;
+      this._canvas.removeAttribute("input");
+      this._shouldRedraw = true;
     }
-
-    this._cursor.x = null;
-    this._cursor.y = null;
-    this._selectionResizer.margin = null;
-    this._selectionDragger.origin = null;
-
-    this._canvas.removeAttribute("input");
-    this._shouldRedraw = true;
   },
 
   /**
    * Listener for the "resize" event on the graph's parent node.
    */
   _onResize: function() {
     if (this.hasData()) {
       setNamedTimeout(this._uid, GRAPH_RESIZE_EVENTS_DRAIN, this.refresh);
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -790,18 +790,18 @@ Tooltip.prototype = {
    * the instance of the widget
    */
   setCubicBezierContent: function(bezier) {
     let def = promise.defer();
 
     // Create an iframe to host the cubic-bezier widget
     let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
     iframe.setAttribute("transparent", true);
-    iframe.setAttribute("width", "200");
-    iframe.setAttribute("height", "415");
+    iframe.setAttribute("width", "410");
+    iframe.setAttribute("height", "360");
     iframe.setAttribute("flex", "1");
     iframe.setAttribute("class", "devtools-tooltip-iframe");
 
     let panel = this.panel;
     let xulWin = this.doc.ownerGlobal;
 
     // Wait for the load to initialize the widget
     function onLoad() {
--- a/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
+++ b/browser/devtools/shared/widgets/cubic-bezier-frame.xhtml
@@ -3,23 +3,24 @@
    - 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>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
-  <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
+  <link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" type="text/css"/>
   <script type="application/javascript;version=1.8" src="theme-switching.js"/>
   <style>
-    body {
+    html, body {
       margin: 0;
       padding: 0;
-      width: 200px;
-      height: 415px;
+      overflow: hidden;
+      width: 410px;
+      height: 370px;
     }
   </style>
 </head>
 <body role="application">
   <div id="container"></div>
 </body>
 </html>
--- a/browser/devtools/shared/widgets/cubic-bezier.css
+++ b/browser/devtools/shared/widgets/cubic-bezier.css
@@ -1,41 +1,37 @@
 /* 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/. */
 
 /* Based on Lea Verou www.cubic-bezier.com
    See https://github.com/LeaVerou/cubic-bezier */
 
-.coordinate-plane {
-  position: absolute;
-  line-height: 0;
-  height: 400px;
-  width: 200px;
+#container {
+  display: flex;
+  width: 410px;
+  height: 370px;
+  flex-direction: row-reverse;
+  overflow: hidden;
 }
 
-.coordinate-plane:before,
-.coordinate-plane:after {
-  position: absolute;
-  bottom: 25%;
-  left: 0;
-  width: 100%;
+.display-wrap {
+  width: 50%;
+  height: 100%;
+  text-align: center;
+  overflow: hidden;
 }
 
-.coordinate-plane:before {
-  content: "";
-  border-bottom: 2px solid;
-  transform: rotate(-90deg) translateY(2px);
-  transform-origin: bottom left;
-}
+/* Coordinate Plane */
 
-.coordinate-plane:after {
-  content: "";
-  border-top: 2px solid;
-  margin-bottom: -2px;
+.coordinate-plane {
+  width: 150px;
+  height: 370px;
+  margin: 0 auto;
+  position: relative;
 }
 
 .theme-dark .coordinate-plane:before,
 .theme-dark .coordinate-plane:after {
   border-color: #eee;
 }
 
 .control-point {
@@ -45,64 +41,60 @@
   width: 10px;
   border: 0;
   background: #666;
   display: block;
   margin: -5px 0 0 -5px;
   outline: none;
   border-radius: 5px;
   padding: 0;
-
   cursor: pointer;
 }
 
-#P1x, #P1y {
-  color: #f08;
-}
-
-#P2x, #P2y {
-  color: #0ab;
-}
-
-canvas#curve {
-  background:
-    linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
-    repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
-    repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
-
-  background-size: 100% 50%, 100% 50%, 100% 50%;
-  background-position: 25%, 0, 0;
+.display-wrap {
+  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
+  background-size: 100% 100%, 100% 100%;
+  background-position: -2px 5px, -2px 5px;
 
   -moz-user-select: none;
 }
 
-.theme-dark canvas#curve {
-  background:
-    linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
-    repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
-    repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
+.theme-dark .display-wrap {
+  background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
+  background-size: 100% 100%, 100% 100%;
+  background-position: -2px 5px, -2px 5px;
 
-  background-size: 100% 50%, 100% 50%, 100% 50%;
-  background-position: 25%, 0, 0;
+  -moz-user-select: none;
+}
+canvas#curve {
+  background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
+  background-size: 100% 100%;
+  background-position: 0 0;
 }
 
-/* Timing function preview widget */
+.theme-dark canvas#curve {
+  background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
+}
+
+/* Timing Function Preview Widget */
 
 .timing-function-preview {
   position: absolute;
-  top: 400px;
+  bottom: 20px;
+  right: 27px;
+  width: 150px;
 }
 
 .timing-function-preview .scale {
   position: absolute;
   top: 6px;
   left: 0;
   z-index: 1;
 
-  width: 200px;
+  width: 150px;
   height: 1px;
 
   background: #ccc;
 }
 
 .timing-function-preview .dot {
   position: absolute;
   top: 0;
@@ -123,20 +115,126 @@ canvas#curve {
   animation-name: timing-function-preview;
 }
 
 @keyframes timing-function-preview {
   0% {
     left: -7px;
   }
   33% {
-    left: 193px;
+    left: 143px;
   }
   50% {
-    left: 193px;
+    left: 143px;
   }
   83% {
     left: -7px;
   }
   100% {
     left: -7px;
   }
 }
+
+/* Preset Widget */
+
+.preset-pane {
+  width:50%;
+  height: 100%;
+  border-right: 1px solid var(--theme-splitter-color);
+}
+
+#preset-categories {
+  display: flex;
+  width: 94%;
+  border: 1px solid var(--theme-splitter-color);
+  border-radius: 2px;
+  background-color: var(--theme-toolbar-background);
+  margin-left: 4px;
+  margin-top: 3px;
+}
+
+#preset-categories .category:last-child {
+  border-right: none;
+}
+
+.category {
+  flex: 1 1 auto;
+  padding: 5px;
+  width: 33.33%;
+  text-align: center;
+  text-transform: capitalize;
+  border-right: 1px solid var(--theme-splitter-color);
+  cursor: default;
+  color: var(--theme-body-color);
+}
+
+.category:hover {
+  background-color: var(--theme-tab-toolbar-background);
+}
+
+.active-category {
+  background-color: var(--theme-selection-background);
+  color: var(--theme-selection-color);
+}
+
+.active-category:hover {
+  background-color: var(--theme-selection-background);
+}
+
+#preset-container {
+  padding: 0px;
+  width: 100%;
+  height: 331px;
+  overflow-y: scroll;
+}
+
+.preset-list {
+  display: none;
+}
+
+.active-preset-list {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: left;
+  padding-left: 4px;
+  padding-top: 3px;
+}
+
+.preset {
+  cursor: pointer;
+  width: 55px;
+  margin: 5px 11px 0px 0px;
+  text-transform: capitalize;
+  text-align: center;
+}
+
+.preset canvas {
+  display: block;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  background-color: var(--theme-body-background);
+}
+
+.theme-dark .preset canvas {
+  border-color: #444e58;
+}
+
+.preset p {
+  text-align: center;
+  font-size: 0.9em;
+  line-height: 0px;
+  margin: 2px 0px 0px 0p;
+  color: var(--theme-body-color-alt);
+}
+
+.active-preset p, .active-preset:hover p {
+  color: var(--theme-body-color);
+}
+
+.preset:hover canvas {
+  border-color: var(--theme-selection-background);
+}
+
+.active-preset canvas, .active-preset:hover canvas,
+.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
+  background-color: var(--theme-selection-background-semitransparent);
+  border-color: var(--theme-selection-background);
+}
--- a/browser/devtools/webide/content/newapp.js
+++ b/browser/devtools/webide/content/newapp.js
@@ -10,36 +10,37 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
 
 const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
-const APP_CREATOR_LIST = "devtools.webide.templatesURL";
 const {AppManager} = require("devtools/webide/app-manager");
-const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
+
+const TEMPLATES_URL = "devtools.webide.templatesURL";
 
 let gTemplateList = null;
 
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   let projectNameNode = document.querySelector("#project-name");
   projectNameNode.addEventListener("input", canValidate, true);
-  getJSON();
+  getTemplatesJSON();
 }, true);
 
-function getJSON() {
-  GetTemplatesJSON().then(list => {
+function getTemplatesJSON() {
+  getJSON(TEMPLATES_URL).then(list => {
     if (!Array.isArray(list)) {
       throw new Error("JSON response not an array");
     }
     if (list.length == 0) {
       throw new Error("JSON response is an empty array");
     }
     gTemplateList = list;
     let templatelistNode = document.querySelector("#templatelist");
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -14,33 +14,34 @@ const {require} = devtools;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {Connection} = require("devtools/client/connection-manager");
 const {AppManager} = require("devtools/webide/app-manager");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const ProjectEditor = require("projecteditor/projecteditor");
 const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
 const {GetAvailableAddons} = require("devtools/webide/addons");
-const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
 const utils = require("devtools/webide/utils");
 const Telemetry = require("devtools/shared/telemetry");
 const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
 const {showDoorhanger} = require("devtools/shared/doorhanger");
 const ProjectList = require("devtools/webide/project-list");
 
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
 const HTML = "http://www.w3.org/1999/xhtml";
 const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
 
 const MAX_ZOOM = 1.4;
 const MIN_ZOOM = 0.6;
 
-// download template index early
-GetTemplatesJSON(true);
+// Download remote resources early
+getJSON("devtools.webide.addonsURL", true);
+getJSON("devtools.webide.templatesURL", true);
 
 // See bug 989619
 console.log = console.log.bind(console);
 console.warn = console.warn.bind(console);
 console.error = console.error.bind(console);
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
--- a/browser/devtools/webide/modules/addons.js
+++ b/browser/devtools/webide/modules/addons.js
@@ -1,18 +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/. */
 
 const {Cu} = require("chrome");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
 const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
-const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
-const {GetAddonsJSON} = require("devtools/webide/remote-resources");
+const {getJSON} = require("devtools/shared/getjson");
+const EventEmitter = require("devtools/toolkit/event-emitter");
+
+const ADDONS_URL = "devtools.webide.addonsURL";
 
 let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
 let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
 let ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
 let SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
 let ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
 let ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
 
@@ -49,17 +51,17 @@ let GetAvailableAddons_promise = null;
 let GetAvailableAddons = exports.GetAvailableAddons = function() {
   if (!GetAvailableAddons_promise) {
     let deferred = promise.defer();
     GetAvailableAddons_promise = deferred.promise;
     let addons = {
       simulators: [],
       adb: null
     }
-    GetAddonsJSON(true).then(json => {
+    getJSON(ADDONS_URL, true).then(json => {
       for (let stability in json) {
         for (let version of json[stability]) {
           addons.simulators.push(new SimulatorAddon(stability, version));
         }
       }
       addons.adb = new ADBAddon();
       addons.adapters = new AdaptersAddon();
       deferred.resolve(addons);
--- a/browser/devtools/webide/moz.build
+++ b/browser/devtools/webide/moz.build
@@ -20,17 +20,16 @@ MOCHITEST_CHROME_MANIFESTS += [
 ]
 
 EXTRA_JS_MODULES.devtools.webide += [
     'modules/addons.js',
     'modules/app-manager.js',
     'modules/build.js',
     'modules/config-view.js',
     'modules/project-list.js',
-    'modules/remote-resources.js',
     'modules/runtimes.js',
     'modules/simulator-process.js',
     'modules/simulators.js',
     'modules/tab-store.js',
     'modules/utils.js'
 ]
 
 JS_PREFERENCE_FILES += [
--- a/browser/locales/en-US/chrome/browser/devtools/device.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/device.properties
@@ -9,11 +9,12 @@
 # language in which you'd find the best documentation on web development on the
 # web.
 
 # LOCALIZATION NOTE:
 # These strings are category names in a list of devices that a user can choose
 # to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc).
 device.phones=Phones
 device.tablets=Tablets
-device.notebooks=Notebooks
+device.laptops=Laptops
 device.televisions=TVs
+device.consoles=Gaming consoles
 device.watches=Watches
--- a/browser/metro/profile/metro.js
+++ b/browser/metro/profile/metro.js
@@ -539,17 +539,17 @@ pref("editor.singleLine.pasteNewlines", 
 
 #ifdef MOZ_SERVICES_SYNC
 // sync service
 pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs");
 
 // prefs to sync by default
 pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
 pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true);
-pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
+pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
 pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
 pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
 pref("services.sync.prefs.sync.privacy.donottrackheader.value", true);
 pref("services.sync.prefs.sync.signon.rememberSignons", true);
 #endif
 
 // threshold where a tap becomes a drag, in 1/240" reference pixels
 // The names of the preferences are to be in sync with EventStateManager.cpp
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -8,16 +8,17 @@
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = [ "ReaderParent" ];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
 
 const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 let ReaderParent = {
 
   MESSAGES: [
@@ -51,17 +52,28 @@ let ReaderParent = {
           // Make sure the target browser is still alive before trying to send data back.
           if (message.target.messageManager) {
             message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
           }
         });
         break;
 
       case "Reader:FaviconRequest": {
-        // XXX: To implement.
+        if (message.target.messageManager) {
+          let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url);
+          faviconUrl.then(function onResolution(favicon) {
+            message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
+              url: message.data.url,
+              faviconUrl: favicon.path.replace(/^favicon:/, "")
+            })
+          },
+          function onRejection(reason) {
+            Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
+          }).catch(Cu.reportError);
+        }
         break;
       }
       case "Reader:ListStatusRequest":
         ReadingList.hasItemForURL(message.data.url).then(inList => {
           let mm = message.target.messageManager
           // Make sure the target browser is still alive before trying to send data back.
           if (mm) {
             mm.sendAsyncMessage("Reader:ListStatusData",
--- a/browser/themes/shared/readinglist/sidebar.inc.css
+++ b/browser/themes/shared/readinglist/sidebar.inc.css
@@ -45,23 +45,28 @@ body {
 .item-thumb-container {
   min-width: 64px;
   max-width: 64px;
   min-height: 40px;
   max-height: 40px;
   border: 1px solid white;
   box-shadow: 0px 1px 2px rgba(0,0,0,.35);
   margin: 5px;
-  background-color: #fff;
-  background-size: cover;
+  background-color: #ebebeb;
+  background-size: contain;
   background-repeat: no-repeat;
   background-position: center;
   background-image: url("chrome://branding/content/silhouette-40.svg");
 }
 
+.item-thumb-container.preview-available {
+  background-color: #fff;
+  background-size: cover;
+}
+
 .item-summary-container {
   display: flex;
   flex-flow: column;
   -moz-padding-start: 4px;
   overflow: hidden;
   flex-grow: 1;
 }
 
@@ -84,17 +89,17 @@ body {
   color: #0095DD;
 }
 
 .item:hover .item-domain {
   color: #008ACB;
 }
 
 .item:not(:hover):not(.selected) .remove-button {
-  display: none;
+  visibility: hidden;
 }
 
 .remove-button {
   padding: 0;
   width: 16px;
   height: 16px;
   background-size: contain;
   background-color: transparent;
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4646,18 +4646,17 @@ nsDocShell::IsPrintingOrPP(bool aDisplay
   return mIsPrintingOrPP;
 }
 
 bool
 nsDocShell::IsNavigationAllowed(bool aDisplayPrintErrorDialog,
                                 bool aCheckIfUnloadFired)
 {
   bool isAllowed = !IsPrintingOrPP(aDisplayPrintErrorDialog) &&
-                   (!aCheckIfUnloadFired || !mFiredUnloadEvent) &&
-                   !mBlockNavigation;
+                   (!aCheckIfUnloadFired || !mFiredUnloadEvent);
   if (!isAllowed) {
     return false;
   }
   if (!mContentViewer) {
     return true;
   }
   bool firingBeforeUnload;
   mContentViewer->GetBeforeUnloadFiring(&firingBeforeUnload);
@@ -9558,18 +9557,16 @@ nsDocShell::InternalLoad(nsIURI* aURI,
                          nsISHEntry* aSHEntry,
                          bool aFirstParty,
                          const nsAString& aSrcdoc,
                          nsIDocShell* aSourceDocShell,
                          nsIURI* aBaseURI,
                          nsIDocShell** aDocShell,
                          nsIRequest** aRequest)
 {
-  MOZ_RELEASE_ASSERT(!mBlockNavigation);
-
   nsresult rv = NS_OK;
   mOriginalUriString.Truncate();
 
 #ifdef PR_LOGGING
   if (gDocShellLeakLog && PR_LOG_TEST(gDocShellLeakLog, PR_LOG_DEBUG)) {
     nsAutoCString spec;
     if (aURI) {
       aURI->GetSpec(spec);
@@ -10015,29 +10012,16 @@ nsDocShell::InternalLoad(nsIURI* aURI,
        sameExceptHashes && !newHash.IsEmpty());
 
     if (doShortCircuitedLoad) {
       // Save the position of the scrollers.
       nscoord cx = 0, cy = 0;
       GetCurScrollPos(ScrollOrientation_X, &cx);
       GetCurScrollPos(ScrollOrientation_Y, &cy);
 
-      {
-        AutoRestore<bool> scrollingToAnchor(mBlockNavigation);
-        mBlockNavigation = true;
-
-        // ScrollToAnchor doesn't necessarily cause us to scroll the window;
-        // the function decides whether a scroll is appropriate based on the
-        // arguments it receives.  But even if we don't end up scrolling,
-        // ScrollToAnchor performs other important tasks, such as informing
-        // the presShell that we have a new hash.  See bug 680257.
-        rv = ScrollToAnchor(curHash, newHash, aLoadType);
-        NS_ENSURE_SUCCESS(rv, rv);
-      }
-
       // Reset mLoadType to its original value once we exit this block,
       // because this short-circuited load might have started after a
       // normal, network load, and we don't want to clobber its load type.
       // See bug 737307.
       AutoRestore<uint32_t> loadTypeResetter(mLoadType);
 
       // If a non-short-circuit load (i.e., a network load) is pending,
       // make this a replacement load, so that we don't add a SHEntry here
@@ -10117,26 +10101,16 @@ nsDocShell::InternalLoad(nsIURI* aURI,
 
         // Make sure we won't just repost without hitting the
         // cache first
         if (cacheKey) {
           mOSHE->SetCacheKey(cacheKey);
         }
       }
 
-      /* restore previous position of scroller(s), if we're moving
-       * back in history (bug 59774)
-       */
-      if (mOSHE && (aLoadType == LOAD_HISTORY ||
-                    aLoadType == LOAD_RELOAD_NORMAL)) {
-        nscoord bx, by;
-        mOSHE->GetScrollPosition(&bx, &by);
-        SetCurScrollPosEx(bx, by);
-      }
-
       /* Restore the original LSHE if we were loading something
        * while short-circuited load was initiated.
        */
       SetHistoryEntry(&mLSHE, oldLSHE);
       /* Set the title for the SH entry for this target url. so that
        * SH menus in go/back/forward buttons won't be empty for this.
        */
       if (mSessionHistory) {
@@ -10161,41 +10135,61 @@ nsDocShell::InternalLoad(nsIURI* aURI,
 
       // Set the doc's URI according to the new history entry's URI.
       nsCOMPtr<nsIDocument> doc = GetDocument();
       NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
       doc->SetDocumentURI(aURI);
 
       SetDocCurrentStateObj(mOSHE);
 
+      // Inform the favicon service that the favicon for oldURI also
+      // applies to aURI.
+      CopyFavicon(currentURI, aURI, mInPrivateBrowsing);
+
+      nsRefPtr<nsGlobalWindow> win = mScriptGlobal ?
+        mScriptGlobal->GetCurrentInnerWindowInternal() : nullptr;
+
+      // ScrollToAnchor doesn't necessarily cause us to scroll the window;
+      // the function decides whether a scroll is appropriate based on the
+      // arguments it receives.  But even if we don't end up scrolling,
+      // ScrollToAnchor performs other important tasks, such as informing
+      // the presShell that we have a new hash.  See bug 680257.
+      rv = ScrollToAnchor(curHash, newHash, aLoadType);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      /* restore previous position of scroller(s), if we're moving
+       * back in history (bug 59774)
+       */
+      if (mOSHE && (aLoadType == LOAD_HISTORY ||
+                    aLoadType == LOAD_RELOAD_NORMAL)) {
+        nscoord bx, by;
+        mOSHE->GetScrollPosition(&bx, &by);
+        SetCurScrollPosEx(bx, by);
+      }
+
       // Dispatch the popstate and hashchange events, as appropriate.
       //
       // The event dispatch below can cause us to re-enter script and
       // destroy the docshell, nulling out mScriptGlobal. Hold a stack
       // reference to avoid null derefs. See bug 914521.
-      nsRefPtr<nsGlobalWindow> win = mScriptGlobal;
       if (win) {
         // Fire a hashchange event URIs differ, and only in their hashes.
         bool doHashchange = sameExceptHashes && !curHash.Equals(newHash);
 
         if (historyNavBetweenSameDoc || doHashchange) {
           win->DispatchSyncPopState();
         }
 
         if (doHashchange) {
           // Note that currentURI hasn't changed because it's on the
           // stack, so we can just use it directly as the old URI.
           win->DispatchAsyncHashchange(currentURI, aURI);
         }
       }
 
-      // Inform the favicon service that the favicon for oldURI also
-      // applies to aURI.
-      CopyFavicon(currentURI, aURI, mInPrivateBrowsing);
-
       return NS_OK;
     }
   }
 
   // Check if the webbrowser chrome wants the load to proceed; this can be
   // used to cancel attempts to load URIs in the wrong process.
   nsCOMPtr<nsIWebBrowserChrome3> browserChrome3 = do_GetInterface(mTreeOwner);
   if (browserChrome3) {
@@ -13990,17 +13984,18 @@ nsDocShell::ChannelIntercepted(nsIInterc
 
   if (!isNavigation) {
     doc = GetDocument();
     if (!doc) {
       return NS_ERROR_NOT_AVAILABLE;
     }
   }
 
-  return swm->DispatchFetchEvent(doc, aChannel);
+  bool isReload = mLoadType & LOAD_CMD_RELOAD;
+  return swm->DispatchFetchEvent(doc, aChannel, isReload);
 }
 
 NS_IMETHODIMP
 nsDocShell::SetPaymentRequestId(const nsAString& aPaymentRequestId)
 {
   mPaymentRequestId = aPaymentRequestId;
   return NS_OK;
 }
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -893,17 +893,16 @@ protected:
   bool mIsActive;
   bool mIsPrerendered;
   bool mIsAppTab;
   bool mUseGlobalHistory;
   bool mInPrivateBrowsing;
   bool mUseRemoteTabs;
   bool mDeviceSizeIsPageSize;
   bool mWindowDraggingAllowed;
-  bool mBlockNavigation;
 
   // Because scriptability depends on the mAllowJavascript values of our
   // ancestors, we cache the effective scriptability and recompute it when
   // it might have changed;
   bool mCanExecuteScripts;
   void RecomputeCanExecuteScripts();
 
   // This boolean is set to true right before we fire pagehide and generally
--- a/dom/animation/AnimationPlayer.cpp
+++ b/dom/animation/AnimationPlayer.cpp
@@ -437,16 +437,19 @@ AnimationPlayer::DoPlay()
 
   if (mHoldTime.IsNull()) {
     return;
   }
 
   // Clear ready promise. We'll create a new one lazily.
   mReady = nullptr;
 
+  // Clear the start time until we resolve a new one
+  mStartTime.SetNull();
+
   mIsPending = true;
 
   nsIDocument* doc = GetRenderedDocument();
   if (!doc) {
     StartOnNextTick(Nullable<TimeDuration>());
     return;
   }
 
--- a/dom/animation/test/css-animations/test_animation-player-starttime.html
+++ b/dom/animation/test/css-animations/test_animation-player-starttime.html
@@ -194,37 +194,16 @@ function EventWatcher(watchedNode, event
 // animation. The terms can be found here:
 //
 //   http://w3c.github.io/web-animations/#animation-node-phases-and-states
 //
 // Note the distinction between "player start time" and "animation start time".
 // The former is the start of the start delay. The latter is the start of the
 // active interval. (If there is no delay, they are the same.)
 
-// Called when startTime is set to the time the start delay would ideally
-// start (not accounting for any delay to next paint tick).
-function checkStateOnSettingStartTimeToAnimationCreationTime(player)
-{
-  // We don't test player.startTime since our caller just set it.
-
-  assert_equals(player.playState, 'running',
-    'AnimationPlayer.playState should be "running" at the start of ' +
-    'the start delay');
-
-  assert_equals(player.source.target.style.animationPlayState, 'running',
-    'AnimationPlayer.source.target.style.animationPlayState should be ' +
-    '"running" at the start of the start delay');
-
-  var div = player.source.target;
-  var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
-  assert_equals(marginLeft, UNANIMATED_POSITION,
-                'the computed value of margin-left should be unaffected ' +
-                'at the beginning of the start delay');
-}
-
 // Called when the ready Promise's callbacks should happen
 function checkStateOnReadyPromiseResolved(player)
 {
   assert_less_than_equal(player.startTime, player.timeline.currentTime,
     'AnimationPlayer.startTime should be less than the timeline\'s ' +
     'currentTime on the first paint tick after animation creation');
 
   assert_equals(player.playState, 'running',
@@ -289,51 +268,102 @@ function checkStateAtActiveIntervalEndTi
   var div = player.source.target;
   var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
   assert_equals(marginLeft, UNANIMATED_POSITION,
     'the computed value of margin-left should be unaffected ' +
     'by the animation at the end of the active duration when the ' +
     'animation-fill-mode is none');
 }
 
+test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s' });
+  var player = div.getAnimationPlayers()[0];
+  assert_equals(player.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (play-pending) animation is unresolved');
+
+test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+  var player = div.getAnimationPlayers()[0];
+  assert_equals(player.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created (pause-pending) animation is unresolved');
+
+async_test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s' });
+  var player = div.getAnimationPlayers()[0];
+  player.ready.then(t.step_func(function() {
+    assert_true(player.startTime > 0,
+                'startTime is resolved when running');
+    t.done();
+  }));
+}, 'startTime is resolved when running');
+
+async_test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
+  var player = div.getAnimationPlayers()[0];
+  player.ready.then(t.step_func(function() {
+    assert_equals(player.startTime, null,
+                  'startTime is unresolved when paused');
+    t.done();
+  }));
+}, 'startTime is unresolved when paused');
+
+async_test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s' });
+  var player = div.getAnimationPlayers()[0];
+  player.ready.then(t.step_func(function() {
+    div.style.animationPlayState = 'paused';
+    getComputedStyle(div).animationPlayState;
+    /* FIXME: Switch this on once deferred pausing is enabled
+    assert_not_equals(player.startTime, null,
+                      'startTime is resolved when pause-pending');
+    */
+
+    div.style.animationPlayState = 'running';
+    getComputedStyle(div).animationPlayState;
+    assert_equals(player.startTime, null,
+                  'startTime is unresolved when play-pending');
+    t.done();
+  }));
+}, 'startTime while pause-pending and play-pending');
+
+async_test(function(t)
+{
+  var div = addDiv(t, { 'style': 'animation: anim 100s' });
+  var player = div.getAnimationPlayers()[0];
+  // Seek to end to put us in the finished state
+  // FIXME: Once we implement finish(), use that here.
+  player.currentTime = 100 * 1000;
+  player.ready.then(t.step_func(function() {
+    // Call play() which puts us back in the running state
+    player.play();
+    // FIXME: Enable this once we implement finishing behavior (bug 1074630)
+    /*
+    assert_equals(player.startTime, null, 'startTime is unresolved');
+    */
+    t.done();
+  }));
+}, 'startTime while play-pending from finished state');
+
 
 test(function(t)
 {
   var div = addDiv(t, {'class': 'animated-div'});
-
   div.style.animation = ANIM_PROPERTY_VAL;
 
   var player = div.getAnimationPlayers()[0];
-
-  // Animations shouldn't start until the next paint tick, so:
-  assert_equals(player.startTime, null,
-    'AnimationPlayer.startTime should be unresolved when an animation ' +
-    'is initially created');
-
-  assert_equals(player.playState, "pending",
-    'AnimationPlayer.playState should be "pending" when an animation ' +
-    'is initially created');
-
-  assert_equals(player.source.target.style.animationPlayState, 'running',
-    'AnimationPlayer.source.target.style.animationPlayState should be ' +
-    '"running" when an animation is initially created');
-
-  // XXX Ideally we would have a test to check the ready Promise is initially
-  // unresolved, but currently there is no Web API to do that. Waiting for the
-  // ready Promise with a timeout doesn't work because the resolved callback
-  // will be called (async) regardless of whether the Promise was resolved in
-  // the past or is resolved in the future.
-
   var currentTime = player.timeline.currentTime;
   player.startTime = currentTime;
   assert_approx_equals(player.startTime, currentTime, 0.0001, // rounding error
     'Check setting of startTime actually works');
-
-  checkStateOnSettingStartTimeToAnimationCreationTime(player);
-}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
+}, 'Sanity test to check round-tripping assigning to a new animation\'s ' +
    'startTime');
 
 
 async_test(function(t) {
   var div = addDiv(t, {'class': 'animated-div'});
   var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
 
   div.style.animation = ANIM_PROPERTY_VAL;
--- a/dom/animation/test/css-transitions/test_animation-player-starttime.html
+++ b/dom/animation/test/css-transitions/test_animation-player-starttime.html
@@ -185,33 +185,16 @@ function EventWatcher(watchedNode, event
 // animation. The terms can be found here:
 //
 //   http://w3c.github.io/web-animations/#animation-node-phases-and-states
 //
 // Note the distinction between "player start time" and "animation start time".
 // The former is the start of the start delay. The latter is the start of the
 // active interval. (If there is no delay, they are the same.)
 
-// Called when startTime is set to the time the start delay would ideally
-// start (not accounting for any delay to next paint tick).
-function checkStateOnSettingStartTimeToAnimationCreationTime(player)
-{
-  // We don't test player.startTime since our caller just set it.
-
-  assert_equals(player.playState, 'running',
-    'AnimationPlayer.playState should be "running" at the start of ' +
-    'the start delay');
-
-  var div = player.source.target;
-  var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
-  assert_equals(marginLeft, INITIAL_POSITION,
-                'the computed value of margin-left should be unaffected ' +
-                'at the beginning of the start delay');
-}
-
 // Called when the ready Promise's callbacks should happen
 function checkStateOnReadyPromiseResolved(player)
 {
   assert_less_than_equal(player.startTime, player.timeline.currentTime,
     'AnimationPlayer.startTime should be less than the timeline\'s ' +
     'currentTime on the first paint tick after animation creation');
 
   assert_equals(player.playState, 'running',
@@ -263,47 +246,38 @@ function checkStateAtActiveIntervalEndTi
 
   var div = player.source.target;
   var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
   assert_equals(marginLeft, END_POSITION,
     'the computed value of margin-left should be the final transitioned-to ' +
     'value at the end of the active duration');
 }
 
-
 test(function(t)
 {
   var div = addDiv(t, {'class': 'animated-div'});
-
   flushComputedStyle(div);
   div.style.marginLeft = '200px'; // initiate transition
 
   var player = div.getAnimationPlayers()[0];
+  assert_equals(player.startTime, null, 'startTime is unresolved');
+}, 'startTime of a newly created transition is unresolved');
 
-  // Animations shouldn't start until the next paint tick, so:
-  assert_equals(player.startTime, null,
-    'AnimationPlayer.startTime should be unresolved when an animation ' +
-    'is initially created');
 
-  assert_equals(player.playState, "pending",
-    'AnimationPlayer.playState should be "pending" when an animation ' +
-    'is initially created');
+test(function(t)
+{
+  var div = addDiv(t, {'class': 'animated-div'});
+  flushComputedStyle(div);
+  div.style.marginLeft = '200px'; // initiate transition
 
-  // XXX Ideally we would have a test to check the ready Promise is initially
-  // unresolved, but currently there is no Web API to do that. Waiting for the
-  // ready Promise with a timeout doesn't work because the resolved callback
-  // will be called (async) regardless of whether the Promise was resolved in
-  // the past or is resolved in the future.
-
+  var player = div.getAnimationPlayers()[0];
   var currentTime = player.timeline.currentTime;
   player.startTime = currentTime;
   assert_approx_equals(player.startTime, currentTime, 0.0001, // rounding error
     'Check setting of startTime actually works');
-
-  checkStateOnSettingStartTimeToAnimationCreationTime(player);
 }, 'Sanity test to check round-tripping assigning to new animation\'s ' +
    'startTime');
 
 
 async_test(function(t) {
   var div = addDiv(t, {'class': 'animated-div'});
   var eventWatcher = new EventWatcher(div, 'transitionend');
 
--- a/dom/base/nsPerformance.cpp
+++ b/dom/base/nsPerformance.cpp
@@ -19,16 +19,17 @@
 #include "PerformanceMeasure.h"
 #include "PerformanceResourceTiming.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/PerformanceBinding.h"
 #include "mozilla/dom/PerformanceTimingBinding.h"
 #include "mozilla/dom/PerformanceNavigationBinding.h"
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/TimeStamp.h"
+#include "js/HeapAPI.h"
 
 #ifdef MOZ_WIDGET_GONK
 #define PERFLOG(msg, ...)  __android_log_print(ANDROID_LOG_INFO, "PerformanceTiming", msg, ##__VA_ARGS__)
 #else
 #define PERFLOG(msg, ...) printf_stderr(msg, ##__VA_ARGS__)
 #endif
 
 using namespace mozilla;
@@ -384,20 +385,37 @@ nsPerformanceNavigation::~nsPerformanceN
 
 JSObject*
 nsPerformanceNavigation::WrapObject(JSContext *cx, JS::Handle<JSObject*> aGivenProto)
 {
   return PerformanceNavigationBinding::Wrap(cx, this, aGivenProto);
 }
 
 
-NS_IMPL_CYCLE_COLLECTION_INHERITED(nsPerformance, DOMEventTargetHelper,
-                                   mWindow, mTiming,
-                                   mNavigation, mEntries,
-                                   mParentPerformance)
+NS_IMPL_CYCLE_COLLECTION_CLASS(nsPerformance)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsPerformance, DOMEventTargetHelper)
+NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow, mTiming,
+                                mNavigation, mEntries,
+                                mParentPerformance)
+  tmp->mMozMemory = nullptr;
+  mozilla::DropJSObjects(this);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsPerformance, DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mTiming,
+                                    mNavigation, mEntries,
+                                    mParentPerformance)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(nsPerformance, DOMEventTargetHelper)
+  NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMozMemory)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
 NS_IMPL_ADDREF_INHERITED(nsPerformance, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(nsPerformance, DOMEventTargetHelper)
 
 nsPerformance::nsPerformance(nsPIDOMWindow* aWindow,
                              nsDOMNavigationTiming* aDOMTiming,
                              nsITimedChannel* aChannel,
                              nsPerformance* aParentPerformance)
   : DOMEventTargetHelper(aWindow),
@@ -407,24 +425,37 @@ nsPerformance::nsPerformance(nsPIDOMWind
     mParentPerformance(aParentPerformance),
     mPrimaryBufferSize(kDefaultBufferSize)
 {
   MOZ_ASSERT(aWindow, "Parent window object should be provided");
 }
 
 nsPerformance::~nsPerformance()
 {
+  mozilla::DropJSObjects(this);
 }
 
 // QueryInterface implementation for nsPerformance
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPerformance)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
+void
+nsPerformance::GetMozMemory(JSContext *aCx, JS::MutableHandle<JSObject*> aObj)
+{
+  if (!mMozMemory) {
+    mMozMemory = js::gc::NewMemoryInfoObject(aCx);
+    if (mMozMemory) {
+      mozilla::HoldJSObjects(this);
+    }
+  }
+
+  aObj.set(mMozMemory);
+}
 
 nsPerformanceTiming*
 nsPerformance::Timing()
 {
   if (!mTiming) {
     // For navigation timing, the third argument (an nsIHtttpChannel) is null
     // since the cross-domain redirect were already checked.
     // The last argument (zero time) for performance.timing is the navigation
--- a/dom/base/nsPerformance.h
+++ b/dom/base/nsPerformance.h
@@ -8,16 +8,17 @@
 #include "nsCOMPtr.h"
 #include "nsAutoPtr.h"
 #include "mozilla/Attributes.h"
 #include "nsWrapperCache.h"
 #include "nsDOMNavigationTiming.h"
 #include "nsContentUtils.h"
 #include "nsPIDOMWindow.h"
 #include "js/TypeDecls.h"
+#include "js/RootingAPI.h"
 #include "mozilla/dom/BindingDeclarations.h"
 #include "mozilla/DOMEventTargetHelper.h"
 
 class nsITimedChannel;
 class nsPerformance;
 class nsIHttpChannel;
 
 namespace mozilla {
@@ -292,17 +293,17 @@ class nsPerformance final : public mozil
 public:
   typedef mozilla::dom::PerformanceEntry PerformanceEntry;
   nsPerformance(nsPIDOMWindow* aWindow,
                 nsDOMNavigationTiming* aDOMTiming,
                 nsITimedChannel* aChannel,
                 nsPerformance* aParentPerformance);
 
   NS_DECL_ISUPPORTS_INHERITED
-  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsPerformance, DOMEventTargetHelper)
+  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(nsPerformance, DOMEventTargetHelper)
 
   nsDOMNavigationTiming* GetDOMTiming() const
   {
     return mDOMTiming;
   }
 
   nsITimedChannel* GetChannel() const
   {
@@ -339,16 +340,18 @@ public:
   void Mark(const nsAString& aName, mozilla::ErrorResult& aRv);
   void ClearMarks(const mozilla::dom::Optional<nsAString>& aName);
   void Measure(const nsAString& aName,
                const mozilla::dom::Optional<nsAString>& aStartMark,
                const mozilla::dom::Optional<nsAString>& aEndMark,
                mozilla::ErrorResult& aRv);
   void ClearMeasures(const mozilla::dom::Optional<nsAString>& aName);
 
+  void GetMozMemory(JSContext *aCx, JS::MutableHandle<JSObject*> aObj);
+
   IMPL_EVENT_HANDLER(resourcetimingbufferfull)
 
 private:
   ~nsPerformance();
   bool IsPerformanceTimingAttribute(const nsAString& aName);
   DOMHighResTimeStamp ResolveTimestampFromName(const nsAString& aName, mozilla::ErrorResult& aRv);
   DOMTimeMilliSec GetPerformanceTimingFromString(const nsAString& aTimingName);
   DOMHighResTimeStamp ConvertDOMMilliSecToHighRes(const DOMTimeMilliSec aTime);
@@ -359,16 +362,17 @@ private:
   nsCOMPtr<nsPIDOMWindow> mWindow;
   nsRefPtr<nsDOMNavigationTiming> mDOMTiming;
   nsCOMPtr<nsITimedChannel> mChannel;
   nsRefPtr<nsPerformanceTiming> mTiming;
   nsRefPtr<nsPerformanceNavigation> mNavigation;
   nsTArray<nsRefPtr<PerformanceEntry> > mEntries;
   nsRefPtr<nsPerformance> mParentPerformance;
   uint64_t mPrimaryBufferSize;
+  JS::Heap<JSObject*> mMozMemory;
 
   static const uint64_t kDefaultBufferSize = 150;
 
   // Helper classes
   class PerformanceEntryComparator {
     public:
       bool Equals(const PerformanceEntry* aElem1,
                   const PerformanceEntry* aElem2) const;
--- a/dom/cache/Action.cpp
+++ b/dom/cache/Action.cpp
@@ -5,23 +5,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/cache/Action.h"
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
-NS_IMPL_ISUPPORTS0(mozilla::dom::cache::Action::Resolver);
-
 void
 Action::CancelOnInitiatingThread()
 {
   NS_ASSERT_OWNINGTHREAD(Action);
-  MOZ_ASSERT(!mCanceled);
+  // It is possible for cancellation to be duplicated.  For example, an
+  // individual Cache could have its Actions canceled and then shutdown
+  // could trigger a second action.
   mCanceled = true;
 }
 
 Action::Action()
   : mCanceled(false)
 {
 }
 
--- a/dom/cache/Action.h
+++ b/dom/cache/Action.h
@@ -13,31 +13,29 @@
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 class Action
 {
 public:
-  class Resolver : public nsISupports
+  class Resolver
   {
-  protected:
-    // virtual because deleted through base class pointer
-    virtual ~Resolver() { }
-
   public:
     // Note: Action must drop Resolver ref after calling Resolve()!
     // Note: Must be called on the same thread used to execute
     //       Action::RunOnTarget().
     virtual void Resolve(nsresult aRv) = 0;
 
-    // We must use ISUPPORTS for our refcounting here because sub-classes also
-    // want to inherit interfaces like nsIRunnable.
-    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_IMETHOD_(MozExternalRefCountType)
+    AddRef(void) = 0;
+
+    NS_IMETHOD_(MozExternalRefCountType)
+    Release(void) = 0;
   };
 
   // Execute operations on the target thread.  Once complete call
   // Resolver::Resolve().  This can be done sync or async.
   // Note: Action should hold Resolver ref until its ready to call Resolve().
   // Note: The "target" thread is determined when the Action is scheduled on
   //       Context.  The Action should not assume any particular thread is used.
   virtual void RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo) = 0;
@@ -46,30 +44,32 @@ public:
   // responsible for calling Resolver::Resolve() as normal; either with a
   // normal error code or NS_ERROR_ABORT.  If CancelOnInitiatingThread() is
   // called after Resolve() has already occurred, then the cancel can be
   // ignored.
   //
   // Cancellation is a best effort to stop processing as soon as possible, but
   // does not guarantee the Action will not run.
   //
+  // CancelOnInitiatingThread() may be called more than once.  Subsequent
+  // calls should have no effect.
+  //
   // Default implementation sets an internal cancellation flag that can be
   // queried with IsCanceled().
   virtual void CancelOnInitiatingThread();
 
   // Executed on the initiating thread and is passed the nsresult given to
   // Resolver::Resolve().
   virtual void CompleteOnInitiatingThread(nsresult aRv) { }
 
   // Executed on the initiating thread.  If this Action will operate on the
   // given cache ID then override this to return true.
   virtual bool MatchesCacheId(CacheId aCacheId) const { return false; }
 
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(cache::Action)
-  NS_DECL_OWNINGTHREAD
+  NS_INLINE_DECL_REFCOUNTING(cache::Action)
 
 protected:
   Action();
 
   // virtual because deleted through base class pointer
   virtual ~Action();
 
   // Check if this Action has been canceled.  May be called from any thread,
--- a/dom/cache/Context.cpp
+++ b/dom/cache/Context.cpp
@@ -1,62 +1,62 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/cache/Context.h"
 
+#include "mozilla/AutoRestore.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/cache/Action.h"
 #include "mozilla/dom/cache/Manager.h"
 #include "mozilla/dom/cache/ManagerId.h"
+#include "mozilla/dom/cache/OfflineStorage.h"
 #include "mozilla/dom/quota/OriginOrPatternString.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "nsIFile.h"
 #include "nsIPrincipal.h"
 #include "nsIRunnable.h"
 #include "nsThreadUtils.h"
 
 namespace {
 
 using mozilla::dom::Nullable;
 using mozilla::dom::cache::QuotaInfo;
+using mozilla::dom::quota::Client;
 using mozilla::dom::quota::OriginOrPatternString;
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
 using mozilla::dom::quota::PersistenceType;
 
-// Executed when the context is destroyed to release our lock on the
-// QuotaManager.
+// Release our lock on the QuotaManager directory asynchronously.
 class QuotaReleaseRunnable final : public nsRunnable
 {
 public:
-  QuotaReleaseRunnable(const QuotaInfo& aQuotaInfo, const nsACString& aQuotaId)
+  explicit QuotaReleaseRunnable(const QuotaInfo& aQuotaInfo)
     : mQuotaInfo(aQuotaInfo)
-    , mQuotaId(aQuotaId)
   { }
 
   NS_IMETHOD Run() override
   {
     MOZ_ASSERT(NS_IsMainThread());
     QuotaManager* qm = QuotaManager::Get();
     MOZ_ASSERT(qm);
     qm->AllowNextSynchronizedOp(OriginOrPatternString::FromOrigin(mQuotaInfo.mOrigin),
                                 Nullable<PersistenceType>(PERSISTENCE_TYPE_DEFAULT),
-                                mQuotaId);
+                                mQuotaInfo.mStorageId);
     return NS_OK;
   }
 
 private:
   ~QuotaReleaseRunnable() { }
 
   const QuotaInfo mQuotaInfo;
-  const nsCString mQuotaId;
 };
 
 } // anonymous namespace
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
@@ -65,30 +65,29 @@ using mozilla::dom::quota::OriginOrPatte
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
 using mozilla::dom::quota::PersistenceType;
 
 // Executed to perform the complicated dance of steps necessary to initialize
 // the QuotaManager.  This must be performed for each origin before any disk
 // IO occurrs.
 class Context::QuotaInitRunnable final : public nsIRunnable
-                                           , public Action::Resolver
 {
 public:
   QuotaInitRunnable(Context* aContext,
                     Manager* aManager,
-                    const nsACString& aQuotaId,
                     Action* aQuotaIOThreadAction)
     : mContext(aContext)
+    , mThreadsafeHandle(aContext->CreateThreadsafeHandle())
     , mManager(aManager)
-    , mQuotaId(aQuotaId)
     , mQuotaIOThreadAction(aQuotaIOThreadAction)
     , mInitiatingThread(NS_GetCurrentThread())
+    , mResult(NS_OK)
     , mState(STATE_INIT)
-    , mResult(NS_OK)
+    , mNeedsQuotaRelease(false)
   {
     MOZ_ASSERT(mContext);
     MOZ_ASSERT(mManager);
     MOZ_ASSERT(mInitiatingThread);
   }
 
   nsresult Dispatch()
   {
@@ -99,36 +98,45 @@ public:
     nsresult rv = NS_DispatchToMainThread(this, nsIThread::DISPATCH_NORMAL);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       mState = STATE_COMPLETE;
       Clear();
     }
     return rv;
   }
 
-  virtual void Resolve(nsresult aRv) override
+private:
+  class SyncResolver final : public Action::Resolver
   {
-    // Depending on the error or success path, this can run on either the
-    // main thread or the QuotaManager IO thread.  The IO thread is an
-    // idle thread which may be destroyed and recreated, so its hard to
-    // assert on.
-    MOZ_ASSERT(mState == STATE_RUNNING || NS_FAILED(aRv));
+  public:
+    SyncResolver()
+      : mResolved(false)
+      , mResult(NS_OK)
+    { }
 
-    mResult = aRv;
-    mState = STATE_COMPLETING;
+    virtual void
+    Resolve(nsresult aRv) override
+    {
+      MOZ_ASSERT(!mResolved);
+      mResolved = true;
+      mResult = aRv;
+    };
 
-    nsresult rv = mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL);
-    if (NS_FAILED(rv)) {
-      // Shutdown must be delayed until all Contexts are destroyed.  Crash for
-      // this invariant violation.
-      MOZ_CRASH("Failed to dispatch QuotaInitRunnable to initiating thread.");
-    }
-  }
+    bool Resolved() const { return mResolved; }
+    nsresult Result() const { return mResult; }
+
+  private:
+    ~SyncResolver() { }
 
-private:
+    bool mResolved;
+    nsresult mResult;
+
+    NS_INLINE_DECL_REFCOUNTING(Context::QuotaInitRunnable::SyncResolver, override)
+  };
+
   ~QuotaInitRunnable()
   {
     MOZ_ASSERT(mState == STATE_COMPLETE);
     MOZ_ASSERT(!mContext);
     MOZ_ASSERT(!mQuotaIOThreadAction);
   }
 
   enum State
@@ -147,31 +155,32 @@ private:
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     MOZ_ASSERT(mContext);
     mContext = nullptr;
     mManager = nullptr;
     mQuotaIOThreadAction = nullptr;
   }
 
   nsRefPtr<Context> mContext;
+  nsRefPtr<ThreadsafeHandle> mThreadsafeHandle;
   nsRefPtr<Manager> mManager;
-  const nsCString mQuotaId;
   nsRefPtr<Action> mQuotaIOThreadAction;
   nsCOMPtr<nsIThread> mInitiatingThread;
-  State mState;
   nsresult mResult;
   QuotaInfo mQuotaInfo;
+  nsMainThreadPtrHandle<OfflineStorage> mOfflineStorage;
+  State mState;
+  bool mNeedsQuotaRelease;
 
 public:
-  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIRUNNABLE
 };
 
-NS_IMPL_ISUPPORTS_INHERITED(mozilla::dom::cache::Context::QuotaInitRunnable,
-                            Action::Resolver, nsIRunnable);
+NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::QuotaInitRunnable, nsIRunnable);
 
 // The QuotaManager init state machine is represented in the following diagram:
 //
 //    +---------------+
 //    |     Start     |      Resolve(error)
 //    | (Orig Thread) +---------------------+
 //    +-------+-------+                     |
 //            |                             |
@@ -186,79 +195,94 @@ NS_IMPL_ISUPPORTS_INHERITED(mozilla::dom
 //   +--------+---------+                   |
 //            |                             |
 // +----------v------------+                |
 // |EnsureOriginInitialized| Resolve(error) |
 // |   (Quota IO Thread)   +----------------+
 // +----------+------------+                |
 //            |                             |
 //  +---------v---------+            +------v------+
-//  |      Running      |  Resolve() |  Completing |
+//  |      Running      |            |  Completing |
 //  | (Quota IO Thread) +------------>(Orig Thread)|
 //  +-------------------+            +------+------+
 //                                          |
 //                                    +-----v----+
 //                                    | Complete |
 //                                    +----------+
 //
 // The initialization process proceeds through the main states.  If an error
-// occurs, then we transition back to Completing state back on the original
-// thread.
+// occurs, then we transition to Completing state back on the original thread.
 NS_IMETHODIMP
 Context::QuotaInitRunnable::Run()
 {
   // May run on different threads depending on the state.  See individual
   // state cases for thread assertions.
 
+  nsRefPtr<SyncResolver> resolver = new SyncResolver();
+
   switch(mState) {
     // -----------------------------------
     case STATE_CALL_WAIT_FOR_OPEN_ALLOWED:
     {
       MOZ_ASSERT(NS_IsMainThread());
       QuotaManager* qm = QuotaManager::GetOrCreate();
       if (!qm) {
-        Resolve(NS_ERROR_FAILURE);
-        return NS_OK;
+        resolver->Resolve(NS_ERROR_FAILURE);
+        break;
       }
 
       nsRefPtr<ManagerId> managerId = mManager->GetManagerId();
       nsCOMPtr<nsIPrincipal> principal = managerId->Principal();
       nsresult rv = qm->GetInfoFromPrincipal(principal,
                                              &mQuotaInfo.mGroup,
                                              &mQuotaInfo.mOrigin,
                                              &mQuotaInfo.mIsApp);
       if (NS_WARN_IF(NS_FAILED(rv))) {
-        Resolve(rv);
-        return NS_OK;
+        resolver->Resolve(rv);
+        break;
       }
 
+      QuotaManager::GetStorageId(PERSISTENCE_TYPE_DEFAULT,
+                                 mQuotaInfo.mOrigin,
+                                 Client::DOMCACHE,
+                                 NS_LITERAL_STRING("cache"),
+                                 mQuotaInfo.mStorageId);
+
       // QuotaManager::WaitForOpenAllowed() will hold a reference to us as
       // a callback.  We will then get executed again on the main thread when
       // it is safe to open the quota directory.
       mState = STATE_WAIT_FOR_OPEN_ALLOWED;
       rv = qm->WaitForOpenAllowed(OriginOrPatternString::FromOrigin(mQuotaInfo.mOrigin),
                                   Nullable<PersistenceType>(PERSISTENCE_TYPE_DEFAULT),
-                                  mQuotaId, this);
+                                  mQuotaInfo.mStorageId, this);
       if (NS_FAILED(rv)) {
-        Resolve(rv);
-        return NS_OK;
+        resolver->Resolve(rv);
+        break;
       }
       break;
     }
     // ------------------------------
     case STATE_WAIT_FOR_OPEN_ALLOWED:
     {
       MOZ_ASSERT(NS_IsMainThread());
+
+      mNeedsQuotaRelease = true;
+
       QuotaManager* qm = QuotaManager::Get();
       MOZ_ASSERT(qm);
+
+      nsRefPtr<OfflineStorage> offlineStorage =
+        OfflineStorage::Register(mThreadsafeHandle, mQuotaInfo);
+      mOfflineStorage = new nsMainThreadPtrHolder<OfflineStorage>(offlineStorage);
+
       mState = STATE_ENSURE_ORIGIN_INITIALIZED;
       nsresult rv = qm->IOThread()->Dispatch(this, nsIThread::DISPATCH_NORMAL);
       if (NS_WARN_IF(NS_FAILED(rv))) {
-        Resolve(rv);
-        return NS_OK;
+        resolver->Resolve(rv);
+        break;
       }
       break;
     }
     // ----------------------------------
     case STATE_ENSURE_ORIGIN_INITIALIZED:
     {
       // Can't assert quota IO thread because its an idle thread that can get
       // recreated.  At least assert we're not on main thread or owning thread.
@@ -268,75 +292,94 @@ Context::QuotaInitRunnable::Run()
       QuotaManager* qm = QuotaManager::Get();
       MOZ_ASSERT(qm);
       nsresult rv = qm->EnsureOriginIsInitialized(PERSISTENCE_TYPE_DEFAULT,
                                                   mQuotaInfo.mGroup,
                                                   mQuotaInfo.mOrigin,
                                                   mQuotaInfo.mIsApp,
                                                   getter_AddRefs(mQuotaInfo.mDir));
       if (NS_FAILED(rv)) {
-        Resolve(rv);
-        return NS_OK;
+        resolver->Resolve(rv);
+        break;
       }
 
       mState = STATE_RUNNING;
 
       if (!mQuotaIOThreadAction) {
-        Resolve(NS_OK);
-        return NS_OK;
+        resolver->Resolve(NS_OK);
+        break;
       }
 
-      // Execute the provided initialization Action.  We pass ourselves as the
-      // Resolver.  The Action must either call Resolve() immediately or hold
-      // a ref to us and call Resolve() later.
-      mQuotaIOThreadAction->RunOnTarget(this, mQuotaInfo);
+      // Execute the provided initialization Action.  The Action must Resolve()
+      // before returning.
+      mQuotaIOThreadAction->RunOnTarget(resolver, mQuotaInfo);
+      MOZ_ASSERT(resolver->Resolved());
 
       break;
     }
     // -------------------
     case STATE_COMPLETING:
     {
       NS_ASSERT_OWNINGTHREAD(Action::Resolver);
       if (mQuotaIOThreadAction) {
         mQuotaIOThreadAction->CompleteOnInitiatingThread(mResult);
       }
-      mContext->OnQuotaInit(mResult, mQuotaInfo);
+      mContext->OnQuotaInit(mResult, mQuotaInfo, mOfflineStorage);
       mState = STATE_COMPLETE;
+
+      if (mNeedsQuotaRelease) {
+        // Unlock the quota dir if we locked it previously
+        nsCOMPtr<nsIRunnable> runnable = new QuotaReleaseRunnable(mQuotaInfo);
+        MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable)));
+      }
+
       // Explicitly cleanup here as the destructor could fire on any of
       // the threads we have bounced through.
       Clear();
       break;
     }
     // -----
     default:
     {
       MOZ_CRASH("unexpected state in QuotaInitRunnable");
-      break;
     }
   }
 
+  if (resolver->Resolved()) {
+    MOZ_ASSERT(mState == STATE_RUNNING || NS_FAILED(resolver->Result()));
+
+    MOZ_ASSERT(NS_SUCCEEDED(mResult));
+    mResult = resolver->Result();
+
+    mState = STATE_COMPLETING;
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+      mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL)));
+  }
+
   return NS_OK;
 }
 
 // Runnable wrapper around Action objects dispatched on the Context.  This
 // runnable executes the Action on the appropriate threads while the Context
 // is initialized.
 class Context::ActionRunnable final : public nsIRunnable
                                     , public Action::Resolver
+                                    , public Context::Activity
 {
 public:
   ActionRunnable(Context* aContext, nsIEventTarget* aTarget, Action* aAction,
                  const QuotaInfo& aQuotaInfo)
     : mContext(aContext)
     , mTarget(aTarget)
     , mAction(aAction)
     , mQuotaInfo(aQuotaInfo)
     , mInitiatingThread(NS_GetCurrentThread())
     , mState(STATE_INIT)
     , mResult(NS_OK)
+    , mExecutingRunOnTarget(false)
   {
     MOZ_ASSERT(mContext);
     MOZ_ASSERT(mTarget);
     MOZ_ASSERT(mAction);
     MOZ_ASSERT(mQuotaInfo.mDir);
     MOZ_ASSERT(mInitiatingThread);
   }
 
@@ -349,99 +392,123 @@ public:
     nsresult rv = mTarget->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       mState = STATE_COMPLETE;
       Clear();
     }
     return rv;
   }
 
-  bool MatchesCacheId(CacheId aCacheId) {
+  virtual bool
+  MatchesCacheId(CacheId aCacheId) const override
+  {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     return mAction->MatchesCacheId(aCacheId);
   }
 
-  void Cancel()
+  virtual void
+  Cancel() override
   {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     mAction->CancelOnInitiatingThread();
   }
 
   virtual void Resolve(nsresult aRv) override
   {
     MOZ_ASSERT(mTarget == NS_GetCurrentThread());
     MOZ_ASSERT(mState == STATE_RUNNING);
+
     mResult = aRv;
-    mState = STATE_COMPLETING;
-    nsresult rv = mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL);
-    if (NS_FAILED(rv)) {
-      // Shutdown must be delayed until all Contexts are destroyed.  Crash
-      // for this invariant violation.
-      MOZ_CRASH("Failed to dispatch ActionRunnable to initiating thread.");
+
+    // We ultimately must complete on the initiating thread, but bounce through
+    // the current thread again to ensure that we don't destroy objects and
+    // state out from under the currently running action's stack.
+    mState = STATE_RESOLVING;
+
+    // If we were resolved synchronously within Action::RunOnTarget() then we
+    // can avoid a thread bounce and just resolve once RunOnTarget() returns.
+    // The Run() method will handle this by looking at mState after
+    // RunOnTarget() returns.
+    if (mExecutingRunOnTarget) {
+      return;
     }
+
+    // Otherwise we are in an asynchronous resolve.  And must perform a thread
+    // bounce to run on the target thread again.
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+      mTarget->Dispatch(this, nsIThread::DISPATCH_NORMAL)));
   }
 
 private:
   ~ActionRunnable()
   {
     MOZ_ASSERT(mState == STATE_COMPLETE);
     MOZ_ASSERT(!mContext);
     MOZ_ASSERT(!mAction);
   }
 
   void Clear()
   {
     NS_ASSERT_OWNINGTHREAD(Action::Resolver);
     MOZ_ASSERT(mContext);
     MOZ_ASSERT(mAction);
-    mContext->OnActionRunnableComplete(this);
+    mContext->RemoveActivity(this);
     mContext = nullptr;
     mAction = nullptr;
   }
 
   enum State
   {
     STATE_INIT,
     STATE_RUN_ON_TARGET,
     STATE_RUNNING,
+    STATE_RESOLVING,
     STATE_COMPLETING,
     STATE_COMPLETE
   };
 
   nsRefPtr<Context> mContext;
   nsCOMPtr<nsIEventTarget> mTarget;
   nsRefPtr<Action> mAction;
   const QuotaInfo mQuotaInfo;
   nsCOMPtr<nsIThread> mInitiatingThread;
   State mState;
   nsresult mResult;
 
+  // Only accessible on target thread;
+  bool mExecutingRunOnTarget;
+
 public:
-  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIRUNNABLE
 };
 
-NS_IMPL_ISUPPORTS_INHERITED(mozilla::dom::cache::Context::ActionRunnable,
-                            Action::Resolver, nsIRunnable);
+NS_IMPL_ISUPPORTS(mozilla::dom::cache::Context::ActionRunnable, nsIRunnable);
 
 // The ActionRunnable has a simpler state machine.  It basically needs to run
 // the action on the target thread and then complete on the original thread.
 //
 //   +-------------+
 //   |    Start    |
 //   |(Orig Thread)|
 //   +-----+-------+
 //         |
 // +-------v---------+
 // |  RunOnTarget    |
-// |Target IO Thread)+-------------------------------+
-// +-------+---------+                               |
-//         |                                         |
-// +-------v----------+ Resolve()            +-------v-----+
-// |     Running      |                      |  Completing |
+// |Target IO Thread)+---+ Resolve()
+// +-------+---------+   |
+//         |             |
+// +-------v----------+  |
+// |     Running      |  |
+// |(Target IO Thread)|  |
+// +------------------+  |
+//         | Resolve()   |
+// +-------v----------+  |
+// |     Resolving    <--+                   +-------------+
+// |                  |                      |  Completing |
 // |(Target IO Thread)+---------------------->(Orig Thread)|
 // +------------------+                      +-------+-----+
 //                                                   |
 //                                                   |
 //                                              +----v---+
 //                                              |Complete|
 //                                              +--------+
 //
@@ -452,18 +519,49 @@ NS_IMPL_ISUPPORTS_INHERITED(mozilla::dom
 NS_IMETHODIMP
 Context::ActionRunnable::Run()
 {
   switch(mState) {
     // ----------------------
     case STATE_RUN_ON_TARGET:
     {
       MOZ_ASSERT(NS_GetCurrentThread() == mTarget);
+      MOZ_ASSERT(!mExecutingRunOnTarget);
+
+      // Note that we are calling RunOnTarget().  This lets us detect
+      // if Resolve() is called synchronously.
+      AutoRestore<bool> executingRunOnTarget(mExecutingRunOnTarget);
+      mExecutingRunOnTarget = true;
+
       mState = STATE_RUNNING;
       mAction->RunOnTarget(this, mQuotaInfo);
+
+      // Resolve was called synchronously from RunOnTarget().  We can
+      // immediately move to completing now since we are sure RunOnTarget()
+      // completed.
+      if (mState == STATE_RESOLVING) {
+        // Use recursion instead of switch case fall-through...  Seems slightly
+        // easier to understand.
+        Run();
+      }
+
+      break;
+    }
+    // -----------------
+    case STATE_RESOLVING:
+    {
+      MOZ_ASSERT(NS_GetCurrentThread() == mTarget);
+      // The call to Action::RunOnTarget() must have returned now if we
+      // are running on the target thread again.  We may now proceed
+      // with completion.
+      mState = STATE_COMPLETING;
+      // Shutdown must be delayed until all Contexts are destroyed.  Crash
+      // for this invariant violation.
+      MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+        mInitiatingThread->Dispatch(this, nsIThread::DISPATCH_NORMAL)));
       break;
     }
     // -------------------
     case STATE_COMPLETING:
     {
       NS_ASSERT_OWNINGTHREAD(Action::Resolver);
       mAction->CompleteOnInitiatingThread(mResult);
       mState = STATE_COMPLETE;
@@ -477,25 +575,117 @@ Context::ActionRunnable::Run()
     {
       MOZ_CRASH("unexpected state in ActionRunnable");
       break;
     }
   }
   return NS_OK;
 }
 
+void
+Context::ThreadsafeHandle::AllowToClose()
+{
+  if (mOwningThread == NS_GetCurrentThread()) {
+    AllowToCloseOnOwningThread();
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewRunnableMethod(this, &ThreadsafeHandle::AllowToCloseOnOwningThread);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+void
+Context::ThreadsafeHandle::InvalidateAndAllowToClose()
+{
+  if (mOwningThread == NS_GetCurrentThread()) {
+    InvalidateAndAllowToCloseOnOwningThread();
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewRunnableMethod(this, &ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+Context::ThreadsafeHandle::ThreadsafeHandle(Context* aContext)
+  : mStrongRef(aContext)
+  , mWeakRef(aContext)
+  , mOwningThread(NS_GetCurrentThread())
+{
+}
+
+Context::ThreadsafeHandle::~ThreadsafeHandle()
+{
+  // Normally we only touch mStrongRef on the owning thread.  This is safe,
+  // however, because when we do use mStrongRef on the owning thread we are
+  // always holding a strong ref to the ThreadsafeHandle via the owning
+  // runnable.  So we cannot run the ThreadsafeHandle destructor simultaneously.
+  if (!mStrongRef || mOwningThread == NS_GetCurrentThread()) {
+    return;
+  }
+
+  // Dispatch is guaranteed to succeed here because we block shutdown until
+  // all Contexts have been destroyed.
+  nsCOMPtr<nsIRunnable> runnable =
+    NS_NewNonOwningRunnableMethod(mStrongRef.forget().take(), &Context::Release);
+  MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+    mOwningThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
+}
+
+void
+Context::ThreadsafeHandle::AllowToCloseOnOwningThread()
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  // A Context "closes" when its ref count drops to zero.  Dropping this
+  // strong ref is necessary, but not sufficient for the close to occur.
+  // Any outstanding IO will continue and keep the Context alive.  Once
+  // the Context is idle, it will be destroyed.
+  mStrongRef = nullptr;
+}
+
+void
+Context::ThreadsafeHandle::InvalidateAndAllowToCloseOnOwningThread()
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  // Cancel the Context through the weak reference.  This means we can
+  // allow the Context to close by dropping the strong ref, but then
+  // still cancel ongoing IO if necessary.
+  if (mWeakRef) {
+    mWeakRef->Invalidate();
+  }
+  // We should synchronously have AllowToCloseOnOwningThread called when
+  // the Context is canceled.
+  MOZ_ASSERT(!mStrongRef);
+}
+
+void
+Context::ThreadsafeHandle::ContextDestroyed(Context* aContext)
+{
+  MOZ_ASSERT(mOwningThread == NS_GetCurrentThread());
+  MOZ_ASSERT(!mStrongRef);
+  MOZ_ASSERT(mWeakRef);
+  MOZ_ASSERT(mWeakRef == aContext);
+  mWeakRef = nullptr;
+}
+
 // static
 already_AddRefed<Context>
 Context::Create(Manager* aManager, Action* aQuotaIOThreadAction)
 {
   nsRefPtr<Context> context = new Context(aManager);
 
   nsRefPtr<QuotaInitRunnable> runnable =
-    new QuotaInitRunnable(context, aManager, NS_LITERAL_CSTRING("Cache"),
-                          aQuotaIOThreadAction);
+    new QuotaInitRunnable(context, aManager, aQuotaIOThreadAction);
   nsresult rv = runnable->Dispatch();
   if (NS_FAILED(rv)) {
     // Shutdown must be delayed until all Contexts are destroyed.  Shutdown
     // must also prevent any new Contexts from being constructed.  Crash
     // for this invariant violation.
     MOZ_CRASH("Failed to dispatch QuotaInitRunnable.");
   }
 
@@ -530,52 +720,71 @@ Context::Dispatch(nsIEventTarget* aTarge
 }
 
 void
 Context::CancelAll()
 {
   NS_ASSERT_OWNINGTHREAD(Context);
   mState = STATE_CONTEXT_CANCELED;
   mPendingActions.Clear();
-  for (uint32_t i = 0; i < mActionRunnables.Length(); ++i) {
-    nsRefPtr<ActionRunnable> runnable = mActionRunnables[i];
-    runnable->Cancel();
+  {
+    ActivityList::ForwardIterator iter(mActivityList);
+    while (iter.HasMore()) {
+      iter.GetNext()->Cancel();
+    }
+  }
+  AllowToClose();
+}
+
+void
+Context::Invalidate()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  mManager->Invalidate();
+  CancelAll();
+}
+
+void
+Context::AllowToClose()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  if (mThreadsafeHandle) {
+    mThreadsafeHandle->AllowToClose();
   }
 }
 
 void
 Context::CancelForCacheId(CacheId aCacheId)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
-  for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
+
+  // Remove matching pending actions
+  for (int32_t i = mPendingActions.Length() - 1; i >= 0; --i) {
     if (mPendingActions[i].mAction->MatchesCacheId(aCacheId)) {
       mPendingActions.RemoveElementAt(i);
     }
   }
-  for (uint32_t i = 0; i < mActionRunnables.Length(); ++i) {
-    nsRefPtr<ActionRunnable> runnable = mActionRunnables[i];
-    if (runnable->MatchesCacheId(aCacheId)) {
-      runnable->Cancel();
+
+  // Cancel activities and let them remove themselves
+  ActivityList::ForwardIterator iter(mActivityList);
+  while (iter.HasMore()) {
+    Activity* activity = iter.GetNext();
+    if (activity->MatchesCacheId(aCacheId)) {
+      activity->Cancel();
     }
   }
 }
 
 Context::~Context()
 {
   NS_ASSERT_OWNINGTHREAD(Context);
   MOZ_ASSERT(mManager);
 
-  // Unlock the quota dir as we go out of scope.
-  nsCOMPtr<nsIRunnable> runnable =
-    new QuotaReleaseRunnable(mQuotaInfo, NS_LITERAL_CSTRING("Cache"));
-  nsresult rv = NS_DispatchToMainThread(runnable, nsIThread::DISPATCH_NORMAL);
-  if (NS_FAILED(rv)) {
-    // Shutdown must be delayed until all Contexts are destroyed.  Crash
-    // for this invariant violation.
-    MOZ_CRASH("Failed to dispatch QuotaReleaseRunnable to main thread.");
+  if (mThreadsafeHandle) {
+    mThreadsafeHandle->ContextDestroyed(this);
   }
 
   mManager->RemoveContext(this);
 }
 
 void
 Context::DispatchAction(nsIEventTarget* aTarget, Action* aAction)
 {
@@ -584,47 +793,75 @@ Context::DispatchAction(nsIEventTarget* 
   nsRefPtr<ActionRunnable> runnable =
     new ActionRunnable(this, aTarget, aAction, mQuotaInfo);
   nsresult rv = runnable->Dispatch();
   if (NS_FAILED(rv)) {
     // Shutdown must be delayed until all Contexts are destroyed.  Crash
     // for this invariant violation.
     MOZ_CRASH("Failed to dispatch ActionRunnable to target thread.");
   }
-  mActionRunnables.AppendElement(runnable);
+  AddActivity(runnable);
 }
 
 void
-Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo)
+Context::OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
+                     nsMainThreadPtrHandle<OfflineStorage>& aOfflineStorage)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
 
   mQuotaInfo = aQuotaInfo;
 
+  // Always save the offline storage to ensure QuotaManager does not shutdown
+  // before the Context has gone away.
+  MOZ_ASSERT(!mOfflineStorage);
+  mOfflineStorage = aOfflineStorage;
+
   if (mState == STATE_CONTEXT_CANCELED || NS_FAILED(aRv)) {
     for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
       mPendingActions[i].mAction->CompleteOnInitiatingThread(aRv);
     }
     mPendingActions.Clear();
+    mThreadsafeHandle->AllowToClose();
     // Context will destruct after return here and last ref is released.
     return;
   }
 
   MOZ_ASSERT(mState == STATE_CONTEXT_INIT);
   mState = STATE_CONTEXT_READY;
 
   for (uint32_t i = 0; i < mPendingActions.Length(); ++i) {
     DispatchAction(mPendingActions[i].mTarget, mPendingActions[i].mAction);
   }
   mPendingActions.Clear();
 }
 
 void
-Context::OnActionRunnableComplete(ActionRunnable* aActionRunnable)
+Context::AddActivity(Activity* aActivity)
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  MOZ_ASSERT(aActivity);
+  MOZ_ASSERT(!mActivityList.Contains(aActivity));
+  mActivityList.AppendElement(aActivity);
+}
+
+void
+Context::RemoveActivity(Activity* aActivity)
 {
   NS_ASSERT_OWNINGTHREAD(Context);
-  MOZ_ASSERT(aActionRunnable);
-  MOZ_ALWAYS_TRUE(mActionRunnables.RemoveElement(aActionRunnable));
+  MOZ_ASSERT(aActivity);
+  MOZ_ALWAYS_TRUE(mActivityList.RemoveElement(aActivity));
+  MOZ_ASSERT(!mActivityList.Contains(aActivity));
+}
+
+already_AddRefed<Context::ThreadsafeHandle>
+Context::CreateThreadsafeHandle()
+{
+  NS_ASSERT_OWNINGTHREAD(Context);
+  if (!mThreadsafeHandle) {
+    mThreadsafeHandle = new ThreadsafeHandle(this);
+  }
+  nsRefPtr<ThreadsafeHandle> ref = mThreadsafeHandle;
+  return ref.forget();
 }
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/Context.h
+++ b/dom/cache/Context.h
@@ -6,71 +6,153 @@
 
 #ifndef mozilla_dom_cache_Context_h
 #define mozilla_dom_cache_Context_h
 
 #include "mozilla/dom/cache/Types.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsISupportsImpl.h"
+#include "nsProxyRelease.h"
 #include "nsString.h"
 #include "nsTArray.h"
+#include "nsTObserverArray.h"
 
 class nsIEventTarget;
+class nsIThread;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 class Action;
 class Manager;
+class OfflineStorage;
 
 // The Context class is RAII-style class for managing IO operations within the
 // Cache.
 //
 // When a Context is created it performs the complicated steps necessary to
 // initialize the QuotaManager.  Action objects dispatched on the Context are
 // delayed until this initialization is complete.  They are then allow to
 // execute on any specified thread.  Once all references to the Context are
 // gone, then the steps necessary to release the QuotaManager are performed.
-// Since pending Action objects reference the Context, this allows overlapping
-// IO to opportunistically run without re-initializing the QuotaManager again.
+// After initialization the Context holds a self reference, so it will stay
+// alive until one of three conditions occur:
+//
+//  1) The Manager will call Context::AllowToClose() when all of the actors
+//     have removed themselves as listener.  This means an idle context with
+//     no active DOM objects will close gracefully.
+//  2) The QuotaManager invalidates the storage area so it can delete the
+//     files.  In this case the OfflineStorage calls Cache::Invalidate() which
+//     in turn cancels all existing Action objects and then marks the Manager
+//     as invalid.
+//  3) Browser shutdown occurs and the Manager calls Context::CancelAll().
+//
+// In either case, though, the Action objects must be destroyed first to
+// allow the Context to be destroyed.
 //
 // While the Context performs operations asynchronously on threads, all of
 // methods in its public interface must be called on the same thread
 // originally used to create the Context.
 //
 // As an invariant, all Context objects must be destroyed before permitting
 // the "profile-before-change" shutdown event to complete.  This is ensured
 // via the code in ShutdownObserver.cpp.
 class Context final
 {
 public:
+  // Define a class allowing other threads to hold the Context alive.  This also
+  // allows these other threads to safely close or cancel the Context.
+  class ThreadsafeHandle final
+  {
+    friend class Context;
+  public:
+    void AllowToClose();
+    void InvalidateAndAllowToClose();
+  private:
+    explicit ThreadsafeHandle(Context* aContext);
+    ~ThreadsafeHandle();
+
+    // disallow copying
+    ThreadsafeHandle(const ThreadsafeHandle&) = delete;
+    ThreadsafeHandle& operator=(const ThreadsafeHandle&) = delete;
+
+    void AllowToCloseOnOwningThread();
+    void InvalidateAndAllowToCloseOnOwningThread();
+
+    void ContextDestroyed(Context* aContext);
+
+    // Cleared to allow the Context to close.  Only safe to access on
+    // owning thread.
+    nsRefPtr<Context> mStrongRef;
+
+    // Used to support cancelation even while the Context is already allowed
+    // to close.  Cleared by ~Context() calling ContextDestroyed().  Only
+    // safe to access on owning thread.
+    Context* mWeakRef;
+
+    nsCOMPtr<nsIThread> mOwningThread;
+
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(cache::Context::ThreadsafeHandle)
+  };
+
+  // Different objects hold references to the Context while some work is being
+  // performed asynchronously.  These objects must implement the Activity
+  // interface and register themselves with the AddActivity().  When they are
+  // destroyed they must call RemoveActivity().  This allows the Context to
+  // cancel any outstanding Activity work when the Context is cancelled.
+  class Activity
+  {
+  public:
+    virtual void Cancel() = 0;
+    virtual bool MatchesCacheId(CacheId aCacheId) const = 0;
+  };
+
+  // Create a Context attached to the given Manager.  The given Action
+  // will run on the QuotaManager IO thread.  Note, this Action must
+  // be execute synchronously.
   static already_AddRefed<Context>
   Create(Manager* aManager, Action* aQuotaIOThreadAction);
 
   // Execute given action on the target once the quota manager has been
   // initialized.
   //
   // Only callable from the thread that created the Context.
   void Dispatch(nsIEventTarget* aTarget, Action* aAction);
 
   // Cancel any Actions running or waiting to run.  This should allow the
   // Context to be released and Listener::RemoveContext() will be called
   // when complete.
   //
   // Only callable from the thread that created the Context.
   void CancelAll();
 
+  // Like CancelAll(), but also marks the Manager as "invalid".
+  void Invalidate();
+
+  // Remove any self references and allow the Context to be released when
+  // there are no more Actions to process.
+  void AllowToClose();
+
   // Cancel any Actions running or waiting to run that operate on the given
   // cache ID.
   //
   // Only callable from the thread that created the Context.
   void CancelForCacheId(CacheId aCacheId);
 
+  void AddActivity(Activity* aActivity);
+  void RemoveActivity(Activity* aActivity);
+
+  const QuotaInfo&
+  GetQuotaInfo() const
+  {
+    return mQuotaInfo;
+  }
+
 private:
   class QuotaInitRunnable;
   class ActionRunnable;
 
   enum State
   {
     STATE_CONTEXT_INIT,
     STATE_CONTEXT_READY,
@@ -81,26 +163,38 @@ private:
   {
     nsCOMPtr<nsIEventTarget> mTarget;
     nsRefPtr<Action> mAction;
   };
 
   explicit Context(Manager* aManager);
   ~Context();
   void DispatchAction(nsIEventTarget* aTarget, Action* aAction);
-  void OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo);
-  void OnActionRunnableComplete(ActionRunnable* const aAction);
+  void OnQuotaInit(nsresult aRv, const QuotaInfo& aQuotaInfo,
+                   nsMainThreadPtrHandle<OfflineStorage>& aOfflineStorage);
+
+  already_AddRefed<ThreadsafeHandle>
+  CreateThreadsafeHandle();
 
   nsRefPtr<Manager> mManager;
   State mState;
   QuotaInfo mQuotaInfo;
   nsTArray<PendingAction> mPendingActions;
 
-  // weak refs since ~ActionRunnable() removes itself from this list
-  nsTArray<ActionRunnable*> mActionRunnables;
+  // Weak refs since activites must remove themselves from this list before
+  // being destroyed by calling RemoveActivity().
+  typedef nsTObserverArray<Activity*> ActivityList;
+  ActivityList mActivityList;
+
+  // The ThreadsafeHandle may have a strong ref back to us.  This creates
+  // a ref-cycle that keeps the Context alive.  The ref-cycle is broken
+  // when ThreadsafeHandle::AllowToClose() is called.
+  nsRefPtr<ThreadsafeHandle> mThreadsafeHandle;
+
+  nsMainThreadPtrHandle<OfflineStorage> mOfflineStorage;
 
 public:
   NS_INLINE_DECL_REFCOUNTING(cache::Context)
 };
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/DBSchema.cpp
+++ b/dom/cache/DBSchema.cpp
@@ -17,18 +17,18 @@
 #include "nsCRT.h"
 #include "nsHttp.h"
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 
-const int32_t DBSchema::kMaxWipeSchemaVersion = 3;
-const int32_t DBSchema::kLatestSchemaVersion = 3;
+const int32_t DBSchema::kMaxWipeSchemaVersion = 4;
+const int32_t DBSchema::kLatestSchemaVersion = 4;
 const int32_t DBSchema::kMaxEntriesPerStatement = 255;
 
 using mozilla::void_t;
 
 // static
 nsresult
 DBSchema::CreateSchema(mozIStorageConnection* aConn)
 {
@@ -84,16 +84,17 @@ DBSchema::CreateSchema(mozIStorageConnec
         "id INTEGER NOT NULL PRIMARY KEY, "
         "request_method TEXT NOT NULL, "
         "request_url TEXT NOT NULL, "
         "request_url_no_query TEXT NOT NULL, "
         "request_referrer TEXT NOT NULL, "
         "request_headers_guard INTEGER NOT NULL, "
         "request_mode INTEGER NOT NULL, "
         "request_credentials INTEGER NOT NULL, "
+        "request_cache INTEGER NOT NULL, "
         "request_body_id TEXT NULL, "
         "response_type INTEGER NOT NULL, "
         "response_url TEXT NOT NULL, "
         "response_status INTEGER NOT NULL, "
         "response_status_text TEXT NOT NULL, "
         "response_headers_guard INTEGER NOT NULL, "
         "response_body_id TEXT NULL, "
         "response_security_info BLOB NULL, "
@@ -965,26 +966,27 @@ DBSchema::InsertEntry(mozIStorageConnect
     "INSERT INTO entries ("
       "request_method, "
       "request_url, "
       "request_url_no_query, "
       "request_referrer, "
       "request_headers_guard, "
       "request_mode, "
       "request_credentials, "
+      "request_cache, "
       "request_body_id, "
       "response_type, "
       "response_url, "
       "response_status, "
       "response_status_text, "
       "response_headers_guard, "
       "response_body_id, "
       "response_security_info, "
       "cache_id "
-    ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)"
+    ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)"
   ), getter_AddRefs(state));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = state->BindUTF8StringParameter(0, aRequest.method());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = state->BindStringParameter(1, aRequest.url());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
@@ -1001,44 +1003,48 @@ DBSchema::InsertEntry(mozIStorageConnect
 
   rv = state->BindInt32Parameter(5, static_cast<int32_t>(aRequest.mode()));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = state->BindInt32Parameter(6,
     static_cast<int32_t>(aRequest.credentials()));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = BindId(state, 7, aRequestBodyId);
+  rv = state->BindInt32Parameter(7,
+    static_cast<int32_t>(aRequest.requestCache()));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindInt32Parameter(8, static_cast<int32_t>(aResponse.type()));
+  rv = BindId(state, 8, aRequestBodyId);
+  if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+
+  rv = state->BindInt32Parameter(9, static_cast<int32_t>(aResponse.type()));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindStringParameter(9, aResponse.url());
+  rv = state->BindStringParameter(10, aResponse.url());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindInt32Parameter(10, aResponse.status());
+  rv = state->BindInt32Parameter(11, aResponse.status());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindUTF8StringParameter(11, aResponse.statusText());
+  rv = state->BindUTF8StringParameter(12, aResponse.statusText());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindInt32Parameter(12,
+  rv = state->BindInt32Parameter(13,
     static_cast<int32_t>(aResponse.headersGuard()));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = BindId(state, 13, aResponseBodyId);
+  rv = BindId(state, 14, aResponseBodyId);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindBlobParameter(14, reinterpret_cast<const uint8_t*>
+  rv = state->BindBlobParameter(15, reinterpret_cast<const uint8_t*>
                                   (aResponse.securityInfo().get()),
                                 aResponse.securityInfo().Length());
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
-  rv = state->BindInt32Parameter(15, aCacheId);
+  rv = state->BindInt32Parameter(16, aCacheId);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = state->Execute();
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = aConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT last_insert_rowid()"
   ), getter_AddRefs(state));
@@ -1214,16 +1220,17 @@ DBSchema::ReadRequest(mozIStorageConnect
     "SELECT "
       "request_method, "
       "request_url, "
       "request_url_no_query, "
       "request_referrer, "
       "request_headers_guard, "
       "request_mode, "
       "request_credentials, "
+      "request_cache, "
       "request_body_id "
     "FROM entries "
     "WHERE id=?1;"
   ), getter_AddRefs(state));
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
 
   rv = state->BindInt32Parameter(0, aEntryId);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
@@ -1256,23 +1263,29 @@ DBSchema::ReadRequest(mozIStorageConnect
   aSavedRequestOut->mValue.mode() = static_cast<RequestMode>(mode);
 
   int32_t credentials;
   rv = state->GetInt32(6, &credentials);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
   aSavedRequestOut->mValue.credentials() =
     static_cast<RequestCredentials>(credentials);
 
+  int32_t requestCache;
+  rv = state->GetInt32(7, &requestCache);
+  if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
+  aSavedRequestOut->mValue.requestCache() =
+    static_cast<RequestCache>(requestCache);
+
   bool nullBody = false;
-  rv = state->GetIsNull(7, &nullBody);
+  rv = state->GetIsNull(8, &nullBody);
   if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
   aSavedRequestOut->mHasBodyId = !nullBody;
 
   if (aSavedRequestOut->mHasBodyId) {
-    rv = ExtractId(state, 7, &aSavedRequestOut->mBodyId);
+    rv = ExtractId(state, 8, &aSavedRequestOut->mBodyId);
     if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
   }
 
   rv = aConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT "
       "name, "
       "value "
     "FROM request_headers "
--- a/dom/cache/Manager.cpp
+++ b/dom/cache/Manager.cpp
@@ -172,17 +172,20 @@ public:
     mozilla::ipc::AssertIsOnBackgroundThread();
 
     nsresult rv = MaybeCreateInstance();
     if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; }
 
     ManagerList::ForwardIterator iter(sFactory->mManagerList);
     while (iter.HasMore()) {
       nsRefPtr<Manager> manager = iter.GetNext();
-      if (*manager->mManagerId == *aManagerId) {
+      // If there is an invalid Manager finishing up and a new Manager
+      // is created for the same origin, then the new Manager will
+      // be blocked until QuotaManager finishes clearing the origin.
+      if (manager->IsValid() && *manager->mManagerId == *aManagerId) {
         return manager.forget();
       }
     }
 
     return nullptr;
   }
 
   static void
@@ -906,25 +909,29 @@ private:
     mCopyContextList.Clear();
   }
 
   static void
   AsyncCopyCompleteFunc(void* aClosure, nsresult aRv)
   {
     // May be on any thread, including STS event target.
     MOZ_ASSERT(aClosure);
-    nsRefPtr<CachePutAllAction> action = static_cast<CachePutAllAction*>(aClosure);
+    // Weak ref as we are guaranteed to the action is alive until
+    // CompleteOnInitiatingThread is called.
+    CachePutAllAction* action = static_cast<CachePutAllAction*>(aClosure);
     action->CallOnAsyncCopyCompleteOnTargetThread(aRv);
   }
 
   void
   CallOnAsyncCopyCompleteOnTargetThread(nsresult aRv)
   {
-    // May be on any thread, including STS event target.
-    nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableMethodWithArg<nsresult>(
+    // May be on any thread, including STS event target.  Non-owning runnable
+    // here since we are guaranteed the Action will survive until
+    // CompleteOnInitiatingThread is called.
+    nsCOMPtr<nsIRunnable> runnable = NS_NewNonOwningRunnableMethodWithArgs<nsresult>(
       this, &CachePutAllAction::OnAsyncCopyComplete, aRv);
     MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
       mTargetThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL)));
   }
 
   void
   DoResolve(nsresult aRv)
   {
@@ -1399,16 +1406,19 @@ void
 Manager::RemoveListener(Listener* aListener)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   // There may not be a listener here in the case where an actor is killed
   // before it can perform any actual async requests on Manager.
   mListeners.RemoveElement(aListener, ListenerEntryListenerComparator());
   MOZ_ASSERT(!mListeners.Contains(aListener,
                                   ListenerEntryListenerComparator()));
+  if (mListeners.IsEmpty() && mContext) {
+    mContext->AllowToClose();
+  }
 }
 
 void
 Manager::RemoveContext(Context* aContext)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(mContext);
   MOZ_ASSERT(mContext == aContext);
@@ -1417,16 +1427,31 @@ Manager::RemoveContext(Context* aContext
   // If we're trying to shutdown, then note that we're done.  This is the
   // delayed case from Manager::Shutdown().
   if (mShuttingDown) {
     Factory::Remove(this);
   }
 }
 
 void
+Manager::Invalidate()
+{
+  NS_ASSERT_OWNINGTHREAD(Manager);
+  // QuotaManager can trigger this more than once.
+  mValid = false;
+}
+
+bool
+Manager::IsValid() const
+{
+  NS_ASSERT_OWNINGTHREAD(Manager);
+  return mValid;
+}
+
+void
 Manager::AddRefCacheId(CacheId aCacheId)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   for (uint32_t i = 0; i < mCacheIdRefs.Length(); ++i) {
     if (mCacheIdRefs[i].mCacheId == aCacheId) {
       mCacheIdRefs[i].mCount += 1;
       return;
     }
@@ -1445,17 +1470,17 @@ Manager::ReleaseCacheId(CacheId aCacheId
     if (mCacheIdRefs[i].mCacheId == aCacheId) {
       DebugOnly<uint32_t> oldRef = mCacheIdRefs[i].mCount;
       mCacheIdRefs[i].mCount -= 1;
       MOZ_ASSERT(mCacheIdRefs[i].mCount < oldRef);
       if (mCacheIdRefs[i].mCount == 0) {
         bool orphaned = mCacheIdRefs[i].mOrphaned;
         mCacheIdRefs.RemoveElementAt(i);
         // TODO: note that we need to check this cache for staleness on startup (bug 1110446)
-        if (orphaned && !mShuttingDown) {
+        if (orphaned && !mShuttingDown && mValid) {
           nsRefPtr<Context> context = CurrentContext();
           context->CancelForCacheId(aCacheId);
           nsRefPtr<Action> action = new DeleteOrphanedCacheAction(this,
                                                                   aCacheId);
           context->Dispatch(mIOThread, action);
         }
       }
       return;
@@ -1488,17 +1513,17 @@ Manager::ReleaseBodyId(const nsID& aBody
     if (mBodyIdRefs[i].mBodyId == aBodyId) {
       DebugOnly<uint32_t> oldRef = mBodyIdRefs[i].mCount;
       mBodyIdRefs[i].mCount -= 1;
       MOZ_ASSERT(mBodyIdRefs[i].mCount < oldRef);
       if (mBodyIdRefs[i].mCount < 1) {
         bool orphaned = mBodyIdRefs[i].mOrphaned;
         mBodyIdRefs.RemoveElementAt(i);
         // TODO: note that we need to check this body for staleness on startup (bug 1110446)
-        if (orphaned && !mShuttingDown) {
+        if (orphaned && !mShuttingDown && mValid) {
           nsRefPtr<Action> action = new DeleteOrphanedBodyAction(aBodyId);
           nsRefPtr<Context> context = CurrentContext();
           context->Dispatch(mIOThread, action);
         }
       }
       return;
     }
   }
@@ -1530,19 +1555,18 @@ Manager::RemoveStreamList(StreamList* aS
 
 void
 Manager::CacheMatch(Listener* aListener, RequestId aRequestId, CacheId aCacheId,
                     const PCacheRequest& aRequest,
                     const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheMatch(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
-                            nullptr, nullptr);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheMatch(aRequestId, NS_ERROR_FAILURE, nullptr, nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheMatchAction(this, listenerId, aRequestId,
                                                  aCacheId, aRequest, aParams,
                                                  streamList);
@@ -1551,18 +1575,18 @@ Manager::CacheMatch(Listener* aListener,
 
 void
 Manager::CacheMatchAll(Listener* aListener, RequestId aRequestId,
                        CacheId aCacheId, const PCacheRequestOrVoid& aRequest,
                        const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheMatchAll(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheMatchAll(aRequestId, NS_ERROR_FAILURE,
                                nsTArray<SavedResponse>(), nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheMatchAllAction(this, listenerId, aRequestId,
                                                     aCacheId, aRequest, aParams,
@@ -1573,18 +1597,18 @@ Manager::CacheMatchAll(Listener* aListen
 void
 Manager::CachePutAll(Listener* aListener, RequestId aRequestId, CacheId aCacheId,
                      const nsTArray<CacheRequestResponse>& aPutList,
                      const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreamList,
                      const nsTArray<nsCOMPtr<nsIInputStream>>& aResponseStreamList)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCachePutAll(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCachePutAll(aRequestId, NS_ERROR_FAILURE);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CachePutAllAction(this, listenerId, aRequestId,
                                                   aCacheId, aPutList,
                                                   aRequestStreamList,
                                                   aResponseStreamList);
   nsRefPtr<Context> context = CurrentContext();
@@ -1593,36 +1617,36 @@ Manager::CachePutAll(Listener* aListener
 
 void
 Manager::CacheDelete(Listener* aListener, RequestId aRequestId,
                      CacheId aCacheId, const PCacheRequest& aRequest,
                      const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheDelete(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN, false);
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheDelete(aRequestId, NS_ERROR_FAILURE, false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheDeleteAction(this, listenerId, aRequestId,
                                                   aCacheId, aRequest, aParams);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::CacheKeys(Listener* aListener, RequestId aRequestId,
                    CacheId aCacheId, const PCacheRequestOrVoid& aRequestOrVoid,
                    const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnCacheKeys(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnCacheKeys(aRequestId, NS_ERROR_FAILURE,
                            nsTArray<SavedRequest>(), nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new CacheKeysAction(this, listenerId, aRequestId,
                                                 aCacheId, aRequestOrVoid,
@@ -1632,18 +1656,18 @@ Manager::CacheKeys(Listener* aListener, 
 
 void
 Manager::StorageMatch(Listener* aListener, RequestId aRequestId,
                       Namespace aNamespace, const PCacheRequest& aRequest,
                       const PCacheQueryParams& aParams)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageMatch(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageMatch(aRequestId, NS_ERROR_FAILURE,
                               nullptr, nullptr);
     return;
   }
   nsRefPtr<Context> context = CurrentContext();
   nsRefPtr<StreamList> streamList = new StreamList(this, context);
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageMatchAction(this, listenerId, aRequestId,
                                                    aNamespace, aRequest,
@@ -1652,86 +1676,87 @@ Manager::StorageMatch(Listener* aListene
 }
 
 void
 Manager::StorageHas(Listener* aListener, RequestId aRequestId,
                     Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageHas(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageHas(aRequestId, NS_ERROR_FAILURE,
                             false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageHasAction(this, listenerId, aRequestId,
                                                  aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageOpen(Listener* aListener, RequestId aRequestId,
                      Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageOpen(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN, 0);
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageOpen(aRequestId, NS_ERROR_FAILURE, 0);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageOpenAction(this, listenerId, aRequestId,
                                                   aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageDelete(Listener* aListener, RequestId aRequestId,
                        Namespace aNamespace, const nsAString& aKey)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageDelete(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageDelete(aRequestId, NS_ERROR_FAILURE,
                                false);
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageDeleteAction(this, listenerId, aRequestId,
                                                     aNamespace, aKey);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 void
 Manager::StorageKeys(Listener* aListener, RequestId aRequestId,
                      Namespace aNamespace)
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   MOZ_ASSERT(aListener);
-  if (mShuttingDown) {
-    aListener->OnStorageKeys(aRequestId, NS_ERROR_ILLEGAL_DURING_SHUTDOWN,
+  if (mShuttingDown || !mValid) {
+    aListener->OnStorageKeys(aRequestId, NS_ERROR_FAILURE,
                              nsTArray<nsString>());
     return;
   }
   ListenerId listenerId = SaveListener(aListener);
   nsRefPtr<Action> action = new StorageKeysAction(this, listenerId, aRequestId,
                                                   aNamespace);
   nsRefPtr<Context> context = CurrentContext();
   context->Dispatch(mIOThread, action);
 }
 
 Manager::Manager(ManagerId* aManagerId, nsIThread* aIOThread)
   : mManagerId(aManagerId)
   , mIOThread(aIOThread)
   , mContext(nullptr)
   , mShuttingDown(false)
+  , mValid(true)
 {
   MOZ_ASSERT(mManagerId);
   MOZ_ASSERT(mIOThread);
 }
 
 Manager::~Manager()
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
@@ -1760,21 +1785,16 @@ Manager::Shutdown()
     return;
   }
 
   // Set a flag to prevent any new requests from coming in and creating
   // a new Context.  We must ensure all Contexts and IO operations are
   // complete before shutdown proceeds.
   mShuttingDown = true;
 
-  for (uint32_t i = 0; i < mStreamLists.Length(); ++i) {
-    nsRefPtr<StreamList> streamList = mStreamLists[i];
-    streamList->CloseAll();
-  }
-
   // If there is a context, then we must wait for it to complete.  Cancel and
   // only note that we are done after its cleaned up.
   if (mContext) {
     nsRefPtr<Context> context = mContext;
     context->CancelAll();
     return;
   }
 
@@ -1784,16 +1804,17 @@ Manager::Shutdown()
 
 already_AddRefed<Context>
 Manager::CurrentContext()
 {
   NS_ASSERT_OWNINGTHREAD(Manager);
   nsRefPtr<Context> ref = mContext;
   if (!ref) {
     MOZ_ASSERT(!mShuttingDown);
+    MOZ_ASSERT(mValid);
     nsRefPtr<Action> setupAction = new SetupAction();
     ref = Context::Create(this, setupAction);
     mContext = ref;
   }
   return ref.forget();
 }
 
 Manager::ListenerId
--- a/dom/cache/Manager.h
+++ b/dom/cache/Manager.h
@@ -120,16 +120,21 @@ public:
   static void ShutdownAllOnMainThread();
 
   // Must be called by Listener objects before they are destroyed.
   void RemoveListener(Listener* aListener);
 
   // Must be called by Context objects before they are destroyed.
   void RemoveContext(Context* aContext);
 
+  // Marks the Manager "invalid".  Once the Context completes no new operations
+  // will be permitted with this Manager.  New actors will get a new Manager.
+  void Invalidate();
+  bool IsValid() const;
+
   // If an actor represents a long term reference to a cache or body stream,
   // then they must call AddRefCacheId() or AddRefBodyId().  This will
   // cause the Manager to keep the backing data store alive for the given
   // object.  The actor must then call ReleaseCacheId() or ReleaseBodyId()
   // exactly once for every AddRef*() call it made.  Any delayed deletion
   // will then be performed.
   void AddRefCacheId(CacheId aCacheId);
   void ReleaseCacheId(CacheId aCacheId);
@@ -209,18 +214,18 @@ private:
 
   // Weak reference cleared by RemoveContext() in Context destructor.
   Context* MOZ_NON_OWNING_REF mContext;
 
   // Weak references cleared by RemoveListener() in Listener destructors.
   struct ListenerEntry
   {
     ListenerEntry()
-      : mId(UINT64_MAX),
-      mListener(nullptr)
+      : mId(UINT64_MAX)
+      , mListener(nullptr)
     {
     }
 
     ListenerEntry(ListenerId aId, Listener* aListener)
       : mId(aId)
       , mListener(aListener)
     {
     }
@@ -250,16 +255,17 @@ private:
   typedef nsTArray<ListenerEntry> ListenerList;
   ListenerList mListeners;
   static ListenerId sNextListenerId;
 
   // Weak references cleared by RemoveStreamList() in StreamList destructors.
   nsTArray<StreamList*> mStreamLists;
 
   bool mShuttingDown;
+  bool mValid;
 
   struct CacheIdRefCounter
   {
     CacheId mCacheId;
     MozRefCountType mCount;
     bool mOrphaned;
   };
   nsTArray<CacheIdRefCounter> mCacheIdRefs;
new file mode 100644
--- /dev/null
+++ b/dom/cache/OfflineStorage.cpp
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/cache/OfflineStorage.h"
+
+#include "mozilla/dom/cache/Context.h"
+#include "mozilla/dom/cache/QuotaClient.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace dom {
+namespace cache {
+
+using mozilla::dom::quota::Client;
+using mozilla::dom::quota::PERSISTENCE_TYPE_DEFAULT;
+using mozilla::dom::quota::QuotaManager;
+
+NS_IMPL_ISUPPORTS(OfflineStorage, nsIOfflineStorage);
+
+// static
+already_AddRefed<OfflineStorage>
+OfflineStorage::Register(Context::ThreadsafeHandle* aContext,
+                         const QuotaInfo& aQuotaInfo)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  QuotaManager* qm = QuotaManager::Get();
+  if (NS_WARN_IF(!qm)) {
+    return nullptr;
+  }
+
+  nsRefPtr<Client> client = qm->GetClient(Client::DOMCACHE);
+
+  nsRefPtr<OfflineStorage> storage =
+    new OfflineStorage(aContext, aQuotaInfo, client);
+
+  if (NS_WARN_IF(!qm->RegisterStorage(storage))) {
+    return nullptr;
+  }
+
+  return storage.forget();
+}
+
+void
+OfflineStorage::AddDestroyCallback(nsIRunnable* aCallback)
+{
+  MOZ_ASSERT(aCallback);
+  MOZ_ASSERT(!mDestroyCallbacks.Contains(aCallback));
+  mDestroyCallbacks.AppendElement(aCallback);
+}
+
+OfflineStorage::OfflineStorage(Context::ThreadsafeHandle* aContext,
+                               const QuotaInfo& aQuotaInfo,
+                               Client* aClient)
+  : mContext(aContext)
+  , mQuotaInfo(aQuotaInfo)
+  , mClient(aClient)
+{
+  MOZ_ASSERT(mContext);
+  MOZ_ASSERT(mClient);
+
+  mPersistenceType = PERSISTENCE_TYPE_DEFAULT;
+  mGroup = mQuotaInfo.mGroup;
+}
+
+OfflineStorage::~OfflineStorage()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  QuotaManager* qm = QuotaManager::Get();
+  MOZ_ASSERT(qm);
+  qm->UnregisterStorage(this);
+  for (uint32_t i = 0; i < mDestroyCallbacks.Length(); ++i) {
+    mDestroyCallbacks[i]->Run();
+  }
+}
+
+NS_IMETHODIMP_(const nsACString&)
+OfflineStorage::Id()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mQuotaInfo.mStorageId;
+}
+
+NS_IMETHODIMP_(Client*)
+OfflineStorage::GetClient()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mClient;
+}
+
+NS_IMETHODIMP_(bool)
+OfflineStorage::IsOwnedByProcess(ContentParent* aOwner)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  // The Cache and Context can be shared by multiple client processes.  They
+  // are not exclusively owned by a single process.
+  //
+  // As far as I can tell this is used by QuotaManager to shutdown storages
+  // when a particular process goes away.  We definitely don't want this
+  // since we are shared.  Also, the Cache actor code already properly
+  // handles asynchronous actor destruction when the child process dies.
+  //
+  // Therefore, always return false here.
+  return false;
+}
+
+NS_IMETHODIMP_(const nsACString&)
+OfflineStorage::Origin()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return mQuotaInfo.mOrigin;
+}
+
+NS_IMETHODIMP_(nsresult)
+OfflineStorage::Close()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mContext->AllowToClose();
+  return NS_OK;
+}
+
+NS_IMETHODIMP_(void)
+OfflineStorage::Invalidate()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  mContext->InvalidateAndAllowToClose();
+}
+
+} // namespace cache
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/cache/OfflineStorage.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_cache_QuotaOfflineStorage_h
+#define mozilla_dom_cache_QuotaOfflineStorage_h
+
+#include "nsISupportsImpl.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/cache/Context.h"
+#include "nsIOfflineStorage.h"
+#include "nsTArray.h"
+
+class nsIThread;
+
+namespace mozilla {
+namespace dom {
+namespace cache {
+
+class OfflineStorage final : public nsIOfflineStorage
+{
+public:
+  static already_AddRefed<OfflineStorage>
+  Register(Context::ThreadsafeHandle* aContext, const QuotaInfo& aQuotaInfo);
+
+  void
+  AddDestroyCallback(nsIRunnable* aCallback);
+
+private:
+  OfflineStorage(Context::ThreadsafeHandle* aContext,
+                 const QuotaInfo& aQuotaInfo,
+                 Client* aClient);
+  ~OfflineStorage();
+
+  nsRefPtr<Context::ThreadsafeHandle> mContext;
+  const QuotaInfo mQuotaInfo;
+  nsRefPtr<Client> mClient;
+  nsTArray<nsCOMPtr<nsIRunnable>> mDestroyCallbacks;
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIOFFLINESTORAGE
+};
+
+} // namespace cache
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_cache_QuotaOfflineStorage_h
--- a/dom/cache/PCacheTypes.ipdlh
+++ b/dom/cache/PCacheTypes.ipdlh
@@ -5,16 +5,17 @@
 include protocol PCachePushStream;
 include protocol PCacheStreamControl;
 include PHeaders;
 include InputStreamParams;
 
 using HeadersGuardEnum from "mozilla/dom/FetchIPCUtils.h";
 using RequestCredentials from "mozilla/dom/FetchIPCUtils.h";
 using RequestMode from "mozilla/dom/FetchIPCUtils.h";
+using RequestCache from "mozilla/dom/FetchIPCUtils.h";
 using mozilla::dom::ResponseType from "mozilla/dom/FetchIPCUtils.h";
 using mozilla::void_t from "ipc/IPCMessageUtils.h";
 using struct nsID from "nsID.h";
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
@@ -49,16 +50,17 @@ struct PCacheRequest
   nsString urlWithoutQuery;
   PHeadersEntry[] headers;
   HeadersGuardEnum headersGuard;
   nsString referrer;
   RequestMode mode;
   RequestCredentials credentials;
   PCacheReadStreamOrVoid body;
   uint32_t context;
+  RequestCache requestCache;
 };
 
 union PCacheRequestOrVoid
 {
   void_t;
   PCacheRequest;
 };
 
--- a/dom/cache/QuotaClient.cpp
+++ b/dom/cache/QuotaClient.cpp
@@ -3,26 +3,28 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/cache/QuotaClient.h"
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/dom/cache/Manager.h"
+#include "mozilla/dom/cache/OfflineStorage.h"
 #include "mozilla/dom/quota/QuotaManager.h"
 #include "mozilla/dom/quota/UsageInfo.h"
 #include "nsIFile.h"
 #include "nsISimpleEnumerator.h"
 #include "nsThreadUtils.h"
 
 namespace {
 
 using mozilla::DebugOnly;
 using mozilla::dom::cache::Manager;
+using mozilla::dom::cache::OfflineStorage;
 using mozilla::dom::quota::Client;
 using mozilla::dom::quota::PersistenceType;
 using mozilla::dom::quota::QuotaManager;
 using mozilla::dom::quota::UsageInfo;
 
 static nsresult
 GetBodyUsage(nsIFile* aDir, UsageInfo* aUsageInfo)
 {
@@ -55,16 +57,51 @@ GetBodyUsage(nsIFile* aDir, UsageInfo* a
     MOZ_ASSERT(fileSize >= 0);
 
     aUsageInfo->AppendToFileUsage(fileSize);
   }
 
   return NS_OK;
 }
 
+class StoragesDestroyedRunnable final : public nsRunnable
+{
+  uint32_t mExpectedCalls;
+  nsCOMPtr<nsIRunnable> mCallback;
+
+public:
+  StoragesDestroyedRunnable(uint32_t aExpectedCalls, nsIRunnable* aCallback)
+    : mExpectedCalls(aExpectedCalls)
+    , mCallback(aCallback)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mExpectedCalls);
+    MOZ_ASSERT(mCallback);
+  }
+
+  NS_IMETHOD Run() override
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mExpectedCalls);
+    mExpectedCalls -= 1;
+    if (!mExpectedCalls) {
+      mCallback->Run();
+    }
+    return NS_OK;
+  }
+
+private:
+  ~StoragesDestroyedRunnable()
+  {
+    // This is a callback runnable and not used for thread dispatch.  It should
+    // always be destroyed on the main thread.
+    MOZ_ASSERT(NS_IsMainThread());
+  }
+};
+
 class CacheQuotaClient final : public Client
 {
 public:
   virtual Type
   GetType() override
   {
     return DOMCACHE;
   }
@@ -146,59 +183,73 @@ public:
 
     return NS_OK;
   }
 
   virtual void
   OnOriginClearCompleted(PersistenceType aPersistenceType,
                          const nsACString& aOrigin) override
   {
-    // nothing to do
+    // Nothing to do here.
   }
 
   virtual void
   ReleaseIOThreadObjects() override
   {
-    // nothing to do
+    // Nothing to do here as the Context handles cleaning everything up
+    // automatically.
   }
 
   virtual bool
   IsFileServiceUtilized() override
   {
     return false;
   }
 
   virtual bool
   IsTransactionServiceActivated() override
   {
-    // TODO: implement nsIOfflineStorage interface (bug 1110487)
-    return false;
+    return true;
   }
 
   virtual void
   WaitForStoragesToComplete(nsTArray<nsIOfflineStorage*>& aStorages,
                             nsIRunnable* aCallback) override
   {
-    // TODO: implement nsIOfflineStorage interface (bug 1110487)
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(!aStorages.IsEmpty());
+
+    nsCOMPtr<nsIRunnable> callback =
+      new StoragesDestroyedRunnable(aStorages.Length(), aCallback);
+
+    for (uint32_t i = 0; i < aStorages.Length(); ++i) {
+      MOZ_ASSERT(aStorages[i]->GetClient());
+      MOZ_ASSERT(aStorages[i]->GetClient()->GetType() == Client::DOMCACHE);
+      nsRefPtr<OfflineStorage> storage =
+        static_cast<OfflineStorage*>(aStorages[i]);
+      storage->AddDestroyCallback(callback);
+    }
   }
 
 
   virtual void
   ShutdownTransactionService() override
   {
     MOZ_ASSERT(NS_IsMainThread());
 
     // spins the event loop and synchronously shuts down all Managers
     Manager::ShutdownAllOnMainThread();
   }
 
 private:
-  ~CacheQuotaClient() { }
+  ~CacheQuotaClient()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+  }
 
-public:
   NS_INLINE_DECL_REFCOUNTING(CacheQuotaClient, override)
 };
 
 } // anonymous namespace;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
--- a/dom/cache/QuotaClient.h
+++ b/dom/cache/QuotaClient.h
@@ -9,15 +9,16 @@
 
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/quota/Client.h"
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
-already_AddRefed<quota::Client> CreateQuotaClient();
+already_AddRefed<quota::Client>
+CreateQuotaClient();
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_cache_QuotaClient_h
--- a/dom/cache/StreamList.cpp
+++ b/dom/cache/StreamList.cpp
@@ -18,17 +18,17 @@ namespace cache {
 StreamList::StreamList(Manager* aManager, Context* aContext)
   : mManager(aManager)
   , mContext(aContext)
   , mCacheId(0)
   , mStreamControl(nullptr)
   , mActivated(false)
 {
   MOZ_ASSERT(mManager);
-  MOZ_ASSERT(mContext);
+  mContext->AddActivity(this);
 }
 
 void
 StreamList::SetStreamControl(CacheStreamControlParent* aStreamControl)
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   MOZ_ASSERT(aStreamControl);
 
@@ -137,24 +137,39 @@ void
 StreamList::CloseAll()
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   if (mStreamControl) {
     mStreamControl->CloseAll();
   }
 }
 
+void
+StreamList::Cancel()
+{
+  NS_ASSERT_OWNINGTHREAD(StreamList);
+  CloseAll();
+}
+
+bool
+StreamList::MatchesCacheId(CacheId aCacheId) const
+{
+  NS_ASSERT_OWNINGTHREAD(StreamList);
+  return aCacheId == mCacheId;
+}
+
 StreamList::~StreamList()
 {
   NS_ASSERT_OWNINGTHREAD(StreamList);
   MOZ_ASSERT(!mStreamControl);
   if (mActivated) {
     mManager->RemoveStreamList(this);
     for (uint32_t i = 0; i < mList.Length(); ++i) {
       mManager->ReleaseBodyId(mList[i].mId);
     }
     mManager->ReleaseCacheId(mCacheId);
   }
+  mContext->RemoveActivity(this);
 }
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
--- a/dom/cache/StreamList.h
+++ b/dom/cache/StreamList.h
@@ -2,31 +2,31 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_cache_StreamList_h
 #define mozilla_dom_cache_StreamList_h
 
+#include "mozilla/dom/cache/Context.h"
 #include "mozilla/dom/cache/Types.h"
 #include "nsRefPtr.h"
 #include "nsTArray.h"
 
 class nsIInputStream;
 
 namespace mozilla {
 namespace dom {
 namespace cache {
 
 class CacheStreamControlParent;
-class Context;
 class Manager;
 
-class StreamList final
+class StreamList final : public Context::Activity
 {
 public:
   StreamList(Manager* aManager, Context* aContext);
 
   void SetStreamControl(CacheStreamControlParent* aStreamControl);
   void RemoveStreamControl(CacheStreamControlParent* aStreamControl);
 
   void Activate(CacheId aCacheId);
@@ -34,16 +34,20 @@ public:
   void Add(const nsID& aId, nsIInputStream* aStream);
   already_AddRefed<nsIInputStream> Extract(const nsID& aId);
 
   void NoteClosed(const nsID& aId);
   void NoteClosedAll();
   void Close(const nsID& aId);
   void CloseAll();
 
+  // Context::Activity methods
+  virtual void Cancel() override;
+  virtual bool MatchesCacheId(CacheId aCacheId) const override;
+
 private:
   ~StreamList();
   struct Entry
   {
     nsID mId;
     nsCOMPtr<nsIInputStream> mStream;
   };
   nsRefPtr<Manager> mManager;
--- a/dom/cache/TypeUtils.cpp
+++ b/dom/cache/TypeUtils.cpp
@@ -220,16 +220,17 @@ TypeUtils::ToPCacheRequest(PCacheRequest
 
   nsRefPtr<InternalHeaders> headers = aIn->Headers();
   MOZ_ASSERT(headers);
   headers->GetPHeaders(aOut.headers());
   aOut.headersGuard() = headers->Guard();
   aOut.mode() = aIn->Mode();
   aOut.credentials() = aIn->GetCredentialsMode();
   aOut.context() = aIn->ContentPolicyType();
+  aOut.requestCache() = aIn->GetCacheMode();
 
   if (aBodyAction == IgnoreBody) {
     aOut.body() = void_t();
     return;
   }
 
   // BodyUsed flag is checked and set previously in ToInternalRequest()
 
@@ -362,16 +363,17 @@ TypeUtils::ToInternalRequest(const PCach
   nsRefPtr<InternalRequest> internalRequest = new InternalRequest();
 
   internalRequest->SetMethod(aIn.method());
   internalRequest->SetURL(NS_ConvertUTF16toUTF8(aIn.url()));
   internalRequest->SetReferrer(aIn.referrer());
   internalRequest->SetMode(aIn.mode());
   internalRequest->SetCredentialsMode(aIn.credentials());
   internalRequest->SetContentPolicyType(aIn.context());
+  internalRequest->SetCacheMode(aIn.requestCache());
 
   nsRefPtr<InternalHeaders> internalHeaders =
     new InternalHeaders(aIn.headers(), aIn.headersGuard());
   ErrorResult result;
   internalRequest->Headers()->SetGuard(aIn.headersGuard(), result);
   MOZ_ASSERT(!result.Failed());
   internalRequest->Headers()->Fill(*internalHeaders, result);
   MOZ_ASSERT(!result.Failed());
--- a/dom/cache/Types.h
+++ b/dom/cache/Types.h
@@ -29,16 +29,17 @@ static const RequestId INVALID_REQUEST_I
 typedef int32_t CacheId;
 
 struct QuotaInfo
 {
   QuotaInfo() : mIsApp(false) { }
   nsCOMPtr<nsIFile> mDir;
   nsCString mGroup;
   nsCString mOrigin;
+  nsCString mStorageId;
   bool mIsApp;
 };
 
 } // namespace cache
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_cache_Types_h
--- a/dom/cache/moz.build
+++ b/dom/cache/moz.build
@@ -23,16 +23,17 @@ EXPORTS.mozilla.dom.cache += [
     'DBAction.h',
     'DBSchema.h',
     'Feature.h',
     'FetchPut.h',
     'FileUtils.h',
     'IPCUtils.h',
     'Manager.h',
     'ManagerId.h',
+    'OfflineStorage.h',
     'PrincipalVerifier.h',
     'QuotaClient.h',
     'ReadStream.h',
     'SavedTypes.h',
     'StreamControl.h',
     'StreamList.h',
     'StreamUtils.h',
     'Types.h',
@@ -56,16 +57,17 @@ UNIFIED_SOURCES += [
     'Context.cpp',
     'DBAction.cpp',
     'DBSchema.cpp',
     'Feature.cpp',
     'FetchPut.cpp',
     'FileUtils.cpp',
     'Manager.cpp',
     'ManagerId.cpp',
+    'OfflineStorage.cpp',
     'PrincipalVerifier.cpp',
     'QuotaClient.cpp',
     'ReadStream.cpp',
     'StreamControl.cpp',
     'StreamList.cpp',
     'StreamUtils.cpp',
     'TypeUtils.cpp',
 ]
--- a/dom/cache/test/mochitest/driver.js
+++ b/dom/cache/test/mochitest/driver.js
@@ -25,16 +25,32 @@ function runTests(testFile, order) {
                 ["dom.serviceWorkers.testing.enabled", true],
                 ["dom.serviceWorkers.exemptFromPerDomainMax", true]]
       }, function() {
         resolve();
       });
     });
   }
 
+  // adapted from dom/indexedDB/test/helpers.js
+  function clearStorage() {
+    return new Promise(function(resolve, reject) {
+      var principal = SpecialPowers.wrap(document).nodePrincipal;
+      var appId, inBrowser;
+      var nsIPrincipal = SpecialPowers.Components.interfaces.nsIPrincipal;
+      if (principal.appId != nsIPrincipal.UNKNOWN_APP_ID &&
+          principal.appId != nsIPrincipal.NO_APP_ID) {
+        appId = principal.appId;
+        inBrowser = principal.isInBrowserElement;
+      }
+      SpecialPowers.clearStorageForURI(document.documentURI, resolve, appId,
+                                       inBrowser);
+    });
+  }
+
   function loadScript(script) {
     return new Promise(function(resolve, reject) {
       var s = document.createElement("script");
       s.src = script;
       s.onerror = reject;
       s.onload = resolve;
       document.body.appendChild(s);
     });
@@ -95,22 +111,26 @@ function runTests(testFile, order) {
           info("Running tests in parallel mode");
           return runTests(testFile, "parallel");
         });
   }
   if (order == "sequential") {
     return setupPrefs()
         .then(importDrivers)
         .then(runWorkerTest)
+        .then(clearStorage)
         .then(runServiceWorkerTest)
+        .then(clearStorage)
         .then(runFrameTest)
+        .then(clearStorage)
         .catch(function(e) {
           ok(false, "A promise was rejected during test execution: " + e);
         });
   }
   return setupPrefs()
       .then(importDrivers)
       .then(() => Promise.all([runWorkerTest(), runServiceWorkerTest(), runFrameTest()]))
+      .then(clearStorage)
       .catch(function(e) {
         ok(false, "A promise was rejected during test execution: " + e);
       });
 }
 
--- a/dom/cache/test/mochitest/mochitest.ini
+++ b/dom/cache/test/mochitest/mochitest.ini
@@ -12,18 +12,20 @@ support-files =
   test_cache_matchAll_request.js
   test_cache_overwrite.js
   mirror.sjs
   test_cache_match_vary.js
   vary.sjs
   test_caches.js
   test_cache_keys.js
   test_cache_put.js
+  test_cache_requestCache.js
 
 [test_cache.html]
 [test_cache_add.html]
 [test_cache_match_request.html]
 [test_cache_matchAll_request.html]
 [test_cache_overwrite.html]
 [test_cache_match_vary.html]
 [test_caches.html]
 [test_cache_keys.html]
 [test_cache_put.html]
+[test_cache_requestCache.html]
--- a/dom/cache/test/mochitest/test_cache_keys.js
+++ b/dom/cache/test/mochitest/test_cache_keys.js
@@ -52,18 +52,16 @@ caches.open(name).then(function(cache) {
       })
   );
 }).then(function() {
   // But HEAD should be allowed even without ignoreMethod
   return c.keys(new Request(tests[0], {method: "HEAD"}));
 }).then(function(keys) {
   is(keys.length, 1, "One match should be found");
   ok(keys[0].url.indexOf(tests[0]) >= 0, "Valid URL");
-  // TODO: Add tests for ignoreVary
-
   // Make sure cacheName is ignored.
   return c.keys(tests[0], {cacheName: "non-existing-cache"});
 }).then(function(keys) {
   is(keys.length, 1, "One match should be found");
   ok(keys[0].url.indexOf(tests[0]) >= 0, "Valid URL");
   return caches.delete(name);
 }).then(function(deleted) {
   ok(deleted, "The cache should be successfully deleted");
--- a/dom/cache/test/mochitest/test_cache_match_vary.js
+++ b/dom/cache/test/mochitest/test_cache_match_vary.js
@@ -74,16 +74,43 @@ function testBasics() {
       // Ensure that searching with a non-matching value for the Cookie header but with ignoreVary set succeeds.
       return test.cache.match(new Request(requestURL, {headers: {"Cookie": "foo=bar"}}),
                               {ignoreVary: true});
     }).then(function(r) {
       return checkResponse(r, test.response, test.responseText);
     });
 }
 
+function testBasicKeys() {
+  function checkRequest(reqs) {
+    is(reqs.length, 1, "One request expected");
+    ok(reqs[0].url.indexOf(requestURL) >= 0, "The correct request expected");
+    ok(reqs[0].headers.get("WhatToVary"), "Cookie", "The correct request headers expected");
+  }
+  var test;
+  return setupTest({"WhatToVary": "Cookie"})
+    .then(function(t) {
+      test = t;
+      // Ensure that searching without specifying a Cookie header succeeds.
+      return test.cache.keys(requestURL);
+    }).then(function(r) {
+      return checkRequest(r);
+    }).then(function() {
+      // Ensure that searching with a non-matching value for the Cookie header fails.
+      return test.cache.keys(new Request(requestURL, {headers: {"Cookie": "foo=bar"}}));
+    }).then(function(r) {
+      is(r.length, 0, "Searching for a request with an unknown Vary header should not succeed");
+      // Ensure that searching with a non-matching value for the Cookie header but with ignoreVary set succeeds.
+      return test.cache.keys(new Request(requestURL, {headers: {"Cookie": "foo=bar"}}),
+                             {ignoreVary: true});
+    }).then(function(r) {
+      return checkRequest(r);
+    });
+}
+
 function testStar() {
   var test;
   return setupTest({"WhatToVary": "*", "Cookie": "foo=bar"})
     .then(function(t) {
       test = t;
       // Ensure that searching with a different Cookie header with Vary:* succeeds.
       return test.cache.match(new Request(requestURL, {headers: {"Cookie": "bar=baz"}}));
     }).then(function(r) {
@@ -257,16 +284,18 @@ function testMultipleCacheEntries() {
 // Make sure to clean up after each test step.
 function step(testPromise) {
   return testPromise.then(function() {
     caches.delete(name);
   });
 }
 
 step(testBasics()).then(function() {
+  return step(testBasicKeys());
+}).then(function() {
   return step(testStar());
 }).then(function() {
   return step(testMatch());
 }).then(function() {
   return step(testStarAndAnotherHeader());
 }).then(function() {
   return step(testInvalidHeaderName());
 }).then(function() {
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_requestCache.html
@@ -0,0 +1,20 @@
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Validate the Cache.keys() method</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <script type="text/javascript" src="driver.js"></script>
+</head>
+<body>
+<iframe id="frame"></iframe>
+<script class="testbody" type="text/javascript">
+  runTests("test_cache_requestCache.js")
+    .then(function() {
+      SimpleTest.finish();
+    });
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/cache/test/mochitest/test_cache_requestCache.js
@@ -0,0 +1,27 @@
+var name = "requestCache" + context;
+var c;
+
+var reqWithoutCache = new Request("//mochi.test:8888/?noCache" + context);
+var reqWithCache = new Request("//mochi.test:8888/?withCache" + context,
+                               {cache: "force-cache"});
+
+// Sanity check
+is(reqWithoutCache.cache, "default", "Correct default value");
+is(reqWithCache.cache, "force-cache", "Correct value set by the ctor");
+
+caches.open(name).then(function(cache) {
+  c = cache;
+  return c.addAll([reqWithoutCache, reqWithCache]);
+}).then(function() {
+  return c.keys();
+}).then(function(keys) {
+  is(keys.length, 2, "Correct number of requests");
+  is(keys[0].url, reqWithoutCache.url, "Correct URL");
+  is(keys[0].cache, reqWithoutCache.cache, "Correct cache attribute");
+  is(keys[1].url, reqWithCache.url, "Correct URL");
+  is(keys[1].cache, reqWithCache.cache, "Correct cache attribute");
+  return caches.delete(name);
+}).then(function(deleted) {
+  ok(deleted, "The cache should be successfully deleted");
+  testDone();
+});
--- a/dom/canvas/WebGL1Context.h
+++ b/dom/canvas/WebGL1Context.h
@@ -29,15 +29,16 @@ public:
     // nsWrapperCache
     virtual JSObject* WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto) override;
 
 private:
     virtual bool ValidateAttribPointerType(bool integerMode, GLenum type, GLsizei* alignment, const char* info) override;
     virtual bool ValidateBufferTarget(GLenum target, const char* info) override;
     virtual bool ValidateBufferIndexedTarget(GLenum target, const char* info) override;
     virtual bool ValidateBufferForTarget(GLenum target, WebGLBuffer* buffer, const char* info) override;
-
+    virtual bool ValidateBufferUsageEnum(GLenum usage, const char* info) override;
+    
     virtual bool ValidateUniformMatrixTranspose(bool transpose, const char* info) override;
 };
 
 } // namespace mozilla
 
 #endif // WEBGL_1_CONTEXT_H_
--- a/dom/canvas/WebGL1ContextBuffers.cpp
+++ b/dom/canvas/WebGL1ContextBuffers.cpp
@@ -45,8 +45,24 @@ WebGL1Context::ValidateBufferForTarget(G
 
     if (buffer->HasEverBeenBound() && target != buffer->Target()) {
         ErrorInvalidOperation("%s: buffer already bound to a different target", info);
         return false;
     }
 
     return true;
 }
+
+bool
+WebGL1Context::ValidateBufferUsageEnum(GLenum usage, const char* info)
+{
+    switch (usage) {
+    case LOCAL_GL_STREAM_DRAW:
+    case LOCAL_GL_STATIC_DRAW:
+    case LOCAL_GL_DYNAMIC_DRAW:
+        return true;
+    default:
+        break;
+    }
+
+    ErrorInvalidEnumInfo(info, usage);
+    return false;
+}
--- a/dom/canvas/WebGL2Context.cpp
+++ b/dom/canvas/WebGL2Context.cpp
@@ -82,17 +82,18 @@ static const gl::GLFeature kRequiredFeat
     gl::GLFeature::instanced_arrays,
     gl::GLFeature::instanced_non_arrays,
     gl::GLFeature::map_buffer_range,
     gl::GLFeature::occlusion_query2,
     gl::GLFeature::packed_depth_stencil,
     gl::GLFeature::query_objects,
     gl::GLFeature::renderbuffer_color_float,
     gl::GLFeature::renderbuffer_color_half_float,
-    gl::GLFeature::sRGB,
+    gl::GLFeature::sRGB_framebuffer,
+    gl::GLFeature::sRGB_texture,
     gl::GLFeature::sampler_objects,
     gl::GLFeature::standard_derivatives,
     gl::GLFeature::texture_3D,
     gl::GLFeature::texture_3D_compressed,
     gl::GLFeature::texture_3D_copy,
     gl::GLFeature::texture_float,
     gl::GLFeature::texture_float_linear,
     gl::GLFeature::texture_half_float,
@@ -115,22 +116,32 @@ WebGLContext::InitWebGL2()
         !gl->IsSupported(gl::GLFeature::occlusion_query_boolean))
     {
         // On desktop, we fake occlusion_query_boolean with occlusion_query if
         // necessary. (See WebGL2ContextQueries.cpp)
         GenerateWarning("WebGL 2 unavailable. Requires occlusion queries.");
         return false;
     }
 
+    std::vector<gl::GLFeature> missingList;
+
     for (size_t i = 0; i < ArrayLength(kRequiredFeatures); i++) {
-        if (!gl->IsSupported(kRequiredFeatures[i])) {
-            GenerateWarning("WebGL 2 unavailable. Requires feature %s.",
-                            gl::GLContext::GetFeatureName(kRequiredFeatures[i]));
-            return false;
+        if (!gl->IsSupported(kRequiredFeatures[i]))
+            missingList.push_back(kRequiredFeatures[i]);
+    }
+
+    if (missingList.size()) {
+        nsAutoCString exts;
+        for (auto itr = missingList.begin(); itr != missingList.end(); ++itr) {
+            exts.AppendLiteral("\n  ");
+            exts.Append(gl::GLContext::GetFeatureName(*itr));
         }
+        GenerateWarning("WebGL 2 unavailable. The following required features are"
+                        " unavailible: %s", exts.BeginReading());
+        return false;
     }
 
     // ok WebGL 2 is compatible, we can enable natively supported extensions.
     for (size_t i = 0; i < ArrayLength(kNativelySupportedExtensions); i++) {
         EnableExtension(kNativelySupportedExtensions[i]);
 
         MOZ_ASSERT(IsExtensionEnabled(kNativelySupportedExtensions[i]));
     }
--- a/dom/canvas/WebGL2Context.h
+++ b/dom/canvas/WebGL2Context.h
@@ -343,15 +343,16 @@ private:
     bool ValidateTexStorage(GLenum target, GLsizei levels, GLenum internalformat,
                                 GLsizei width, GLsizei height, GLsizei depth,
                                 const char* info);
 
     virtual bool ValidateAttribPointerType(bool integerMode, GLenum type, GLsizei* alignment, const char* info) override;
     virtual bool ValidateBufferTarget(GLenum target, const char* info) override;
     virtual bool ValidateBufferIndexedTarget(GLenum target, const char* info) override;
     virtual bool ValidateBufferForTarget(GLenum target, WebGLBuffer* buffer, const char* info) override;
+    virtual bool ValidateBufferUsageEnum(GLenum usage, const char* info) override;
 
     virtual bool ValidateUniformMatrixTranspose(bool transpose, const char* info) override;
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/canvas/WebGL2ContextBuffers.cpp
+++ b/dom/canvas/WebGL2ContextBuffers.cpp
@@ -70,16 +70,38 @@ WebGL2Context::ValidateBufferForTarget(G
             buffer->Target() != LOCAL_GL_ELEMENT_ARRAY_BUFFER;
     }
 
     ErrorInvalidOperation("%s: buffer already bound to a incompatible target %s",
                           info, EnumName(buffer->Target().get()));
     return false;
 }
 
+bool
+WebGL2Context::ValidateBufferUsageEnum(GLenum usage, const char* info)
+{
+    switch (usage) {
+    case LOCAL_GL_DYNAMIC_COPY:
+    case LOCAL_GL_DYNAMIC_DRAW:
+    case LOCAL_GL_DYNAMIC_READ:
+    case LOCAL_GL_STATIC_COPY:
+    case LOCAL_GL_STATIC_DRAW:
+    case LOCAL_GL_STATIC_READ:
+    case LOCAL_GL_STREAM_COPY:
+    case LOCAL_GL_STREAM_DRAW:
+    case LOCAL_GL_STREAM_READ:
+        return true;
+    default:
+        break;
+    }
+
+    ErrorInvalidEnumInfo(info, usage);
+    return false;
+}
+
 // -------------------------------------------------------------------------
 // Buffer objects
 
 void
 WebGL2Context::CopyBufferSubData(GLenum readTarget, GLenum writeTarget,
                                  GLintptr readOffset, GLintptr writeOffset,
                                  GLsizeiptr size)
 {
--- a/dom/canvas/WebGLContext.h
+++ b/dom/canvas/WebGLContext.h
@@ -927,17 +927,16 @@ protected:
     WebGLRefPtr<WebGLBuffer> mBoundUniformBuffer;
 
     nsTArray<WebGLRefPtr<WebGLBuffer>> mBoundUniformBuffers;
     nsTArray<WebGLRefPtr<WebGLBuffer>> mBoundTransformFeedbackBuffers;
 
     WebGLRefPtr<WebGLBuffer>& GetBufferSlotByTarget(GLenum target);
     WebGLRefPtr<WebGLBuffer>& GetBufferSlotByTargetIndexed(GLenum target,
                                                            GLuint index);
-    bool ValidateBufferUsageEnum(GLenum target, const char* info);
 
 // -----------------------------------------------------------------------------
 // Queries (WebGL2ContextQueries.cpp)
 protected:
     WebGLRefPtr<WebGLQuery>* GetQueryTargetSlot(GLenum target);
 
     WebGLRefPtr<WebGLQuery> mActiveOcclusionQuery;
     WebGLRefPtr<WebGLQuery> mActiveTransformFeedbackQuery;
@@ -1384,16 +1383,17 @@ private:
 
 private:
     // -------------------------------------------------------------------------
     // Context customization points
     virtual bool ValidateAttribPointerType(bool integerMode, GLenum type, GLsizei* alignment, const char* info) = 0;
     virtual bool ValidateBufferTarget(GLenum target, const char* info) = 0;
     virtual bool ValidateBufferIndexedTarget(GLenum target, const char* info) = 0;
     virtual bool ValidateBufferForTarget(GLenum target, WebGLBuffer* buffer, const char* info) = 0;
+    virtual bool ValidateBufferUsageEnum(GLenum usage, const char* info) = 0;
     virtual bool ValidateUniformMatrixTranspose(bool transpose, const char* info) = 0;
 
 protected:
     int32_t MaxTextureSizeForTarget(TexTarget target) const {
         return (target == LOCAL_GL_TEXTURE_2D) ? mGLMaxTextureSize
                                                : mGLMaxCubeMapTextureSize;
     }
 
--- a/dom/canvas/WebGLContextGL.cpp
+++ b/dom/canvas/WebGLContextGL.cpp
@@ -885,16 +885,21 @@ WebGLContext::FramebufferTexture2D(GLenu
                                    GLint level)
 {
     if (IsContextLost())
         return;
 
     if (!ValidateFramebufferTarget(target, "framebufferTexture2D"))
         return;
 
+    if (!IsWebGL2() && level != 0) {
+        ErrorInvalidValue("framebufferTexture2D: level must be 0.");
+        return;
+    }
+
     WebGLFramebuffer* fb;
     switch (target) {
     case LOCAL_GL_FRAMEBUFFER:
     case LOCAL_GL_DRAW_FRAMEBUFFER:
         fb = mBoundDrawFramebuffer;
         break;
 
     case LOCAL_GL_READ_FRAMEBUFFER:
--- a/dom/canvas/WebGLExtensionSRGB.cpp
+++ b/dom/canvas/WebGLExtensionSRGB.cpp
@@ -29,15 +29,16 @@ WebGLExtensionSRGB::~WebGLExtensionSRGB(
 {
 }
 
 bool
 WebGLExtensionSRGB::IsSupported(const WebGLContext* webgl)
 {
     gl::GLContext* gl = webgl->GL();
 
-    return gl->IsSupported(gl::GLFeature::sRGB);
+    return gl->IsSupported(gl::GLFeature::sRGB_framebuffer) &&
+           gl->IsSupported(gl::GLFeature::sRGB_texture);
 }
 
 
 IMPL_WEBGL_EXTENSION_GOOP(WebGLExtensionSRGB)
 
 } // namespace mozilla
--- a/dom/canvas/WebGLFramebuffer.cpp
+++ b/dom/canvas/WebGLFramebuffer.cpp
@@ -494,37 +494,29 @@ WebGLFramebuffer::FramebufferRenderbuffe
 void
 WebGLFramebuffer::FramebufferTexture2D(FBAttachment attachPoint,
                                        TexImageTarget texImageTarget,
                                        WebGLTexture* tex, GLint level)
 {
     MOZ_ASSERT(mContext->mBoundDrawFramebuffer == this ||
                mContext->mBoundReadFramebuffer == this);
 
-    if (!mContext->ValidateObjectAllowNull("framebufferTexture2D: texture",
-                                           tex))
-    {
+    if (!mContext->ValidateObjectAllowNull("framebufferTexture2D: texture", tex))
         return;
-    }
 
     if (tex) {
         bool isTexture2D = tex->Target() == LOCAL_GL_TEXTURE_2D;
         bool isTexTarget2D = texImageTarget == LOCAL_GL_TEXTURE_2D;
         if (isTexture2D != isTexTarget2D) {
             mContext->ErrorInvalidOperation("framebufferTexture2D: Mismatched"
                                             " texture and texture target.");
             return;
         }
     }
 
-    if (level != 0) {
-        mContext->ErrorInvalidValue("framebufferTexture2D: Level must be 0.");
-        return;
-    }
-
     /* Get the requested attachment. If result is NULL, attachment is invalid
      * and an error is generated.
      *
      * Don't use GetAttachment(...) here because it opt builds it returns
      * mColorAttachment[0] for invalid attachment, which we really don't want to
      * mess with.
      */
     Attachment* attachment = GetAttachmentOrNull(attachPoint);
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -4641,17 +4641,17 @@ EventStateManager::DoStateChange(nsICont
 /* static */
 void
 EventStateManager::UpdateAncestorState(nsIContent* aStartNode,
                                        nsIContent* aStopBefore,
                                        EventStates aState,
                                        bool aAddState)
 {
   for (; aStartNode && aStartNode != aStopBefore;
-       aStartNode = aStartNode->GetParent()) {
+       aStartNode = aStartNode->GetParentElementCrossingShadowRoot()) {
     // We might be starting with a non-element (e.g. a text node) and
     // if someone is doing something weird might be ending with a
     // non-element too (e.g. a document fragment)
     if (!aStartNode->IsElement()) {
       continue;
     }
     Element* element = aStartNode->AsElement();
     DoStateChange(element, aState, aAddState);
@@ -4669,17 +4669,17 @@ EventStateManager::UpdateAncestorState(n
     // same node, and while one is no longer hovered the other still
     // is.  In that situation, the label that's still hovered will be
     // aStopBefore or some ancestor of it, and the call we just made
     // to UpdateAncestorState with aAddState = false would have
     // removed the hover state from the node.  But the node should
     // still be in hover state.  To handle this situation we need to
     // keep walking up the tree and any time we find a label mark its
     // corresponding node as still in our state.
-    for ( ; aStartNode; aStartNode = aStartNode->GetParent()) {
+    for ( ; aStartNode; aStartNode = aStartNode->GetParentElementCrossingShadowRoot()) {
       if (!aStartNode->IsElement()) {
         continue;
       }
 
       Element* labelTarget = GetLabelTarget(aStartNode->AsElement());
       if (labelTarget && !labelTarget->State().HasState(aState)) {
         DoStateChange(labelTarget, aState, true);
       }
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -136,16 +136,17 @@ skip-if = buildapp == 'b2g'
 skip-if = toolkit == "gonk" || e10s
 [test_bug985988.html]
 [test_bug998809.html]
 [test_bug1017086_disable.html]
 support-files = bug1017086_inner.html
 [test_bug1017086_enable.html]
 support-files = bug1017086_inner.html
 [test_bug1079236.html]
+[test_bug1145910.html]
 [test_clickevent_on_input.html]
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_continuous_wheel_events.html]
 skip-if = buildapp == 'b2g' || e10s # b2g(5535 passed, 108 failed - more tests running than desktop) b2g-debug(5535 passed, 108 failed - more tests running than desktop) b2g-desktop(5535 passed, 108 failed - more tests running than desktop)
 [test_dblclick_explicit_original_target.html]
 [test_dom_keyboard_event.html]
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_dom_mouse_event.html]
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_bug1145910.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1145910
+-->
+<head>
+  <title>Test for Bug 1145910</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<style>
+div:active {
+  color: rgb(0, 255, 0);
+}
+</style>
+<div id="host">Foo</div>
+<script type="application/javascript">
+
+/** Test for Bug 1145910 **/
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(function() {
+  var host = document.getElementById("host");
+  var shadow = host.createShadowRoot();
+  shadow.innerHTML = '<style>div:active { color: rgb(0, 255, 0); }</style><div id="inner">Bar</div>';
+  var inner = shadow.getElementById("inner");
+
+  is(window.getComputedStyle(host).color, "rgb(0, 0, 0)", "The host should not be active");
+  is(window.getComputedStyle(inner).color, "rgb(0, 0, 0)", "The div inside the shadow root should not be active.");
+
+  synthesizeMouseAtCenter(host, { type: "mousedown" });
+
+  is(window.getComputedStyle(inner).color, "rgb(0, 255, 0)", "Div inside shadow root should be active.");
+  is(window.getComputedStyle(host).color, "rgb(0, 255, 0)", "Host should be active when the inner div is made active.");
+
+  synthesizeMouseAtCenter(host, { type: "mouseup" });
+
+  is(window.getComputedStyle(inner).color, "rgb(0, 0, 0)", "Div inside shadow root should no longer be active.");
+  is(window.getComputedStyle(host).color, "rgb(0, 0, 0)", "Host should no longer be active.");
+
+  SimpleTest.finish();
+});
+
+</script>
+</body>
+</html>
--- a/dom/fetch/FetchIPCUtils.h
+++ b/dom/fetch/FetchIPCUtils.h
@@ -27,15 +27,20 @@ namespace IPC {
                                     mozilla::dom::RequestMode::Same_origin,
                                     mozilla::dom::RequestMode::EndGuard_> {};
   template<>
   struct ParamTraits<mozilla::dom::RequestCredentials> :
     public ContiguousEnumSerializer<mozilla::dom::RequestCredentials,
                                     mozilla::dom::RequestCredentials::Omit,
                                     mozilla::dom::RequestCredentials::EndGuard_> {};
   template<>
+  struct ParamTraits<mozilla::dom::RequestCache> :
+    public ContiguousEnumSerializer<mozilla::dom::RequestCache,
+                                    mozilla::dom::RequestCache::Default,
+                                    mozilla::dom::RequestCache::EndGuard_> {};
+  template<>
   struct ParamTraits<mozilla::dom::ResponseType> :
     public ContiguousEnumSerializer<mozilla::dom::ResponseType,
                                     mozilla::dom::ResponseType::Basic,
                                     mozilla::dom::ResponseType::EndGuard_> {};
 }
 
 #endif // mozilla_dom_FetchIPCUtils_h
--- a/dom/fetch/InternalRequest.h
+++ b/dom/fetch/InternalRequest.h
@@ -203,16 +203,22 @@ public:
   }
 
   RequestCache
   GetCacheMode() const
   {
     return mCacheMode;
   }
 
+  void
+  SetCacheMode(RequestCache aCacheMode)
+  {
+    mCacheMode = aCacheMode;
+  }
+
   nsContentPolicyType
   ContentPolicyType() const
   {
     return mContentPolicyType;
   }
 
   void
   SetContentPolicyType(nsContentPolicyType aContentPolicyType)
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -77,16 +77,17 @@ Request::Constructor(const GlobalObject&
 
   request = request->GetRequestConstructorCopy(global, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
   RequestMode fallbackMode = RequestMode::EndGuard_;
   RequestCredentials fallbackCredentials = RequestCredentials::EndGuard_;
+  RequestCache fallbackCache = RequestCache::EndGuard_;
   if (aInput.IsUSVString()) {
     nsString input;
     input.Assign(aInput.GetAsUSVString());
 
     nsString requestURL;
     if (NS_IsMainThread()) {
       nsCOMPtr<nsPIDOMWindow> window = do_QueryInterface(global);
       MOZ_ASSERT(window);
@@ -122,16 +123,17 @@ Request::Constructor(const GlobalObject&
       url->Stringify(requestURL, aRv);
       if (aRv.Failed()) {
         return nullptr;
       }
     }
     request->SetURL(NS_ConvertUTF16toUTF8(requestURL));
     fallbackMode = RequestMode::Cors;
     fallbackCredentials = RequestCredentials::Omit;
+    fallbackCache = RequestCache::Default;
   }
 
   // CORS-with-forced-preflight is not publicly exposed and should not be
   // considered a valid value.
   if (aInit.mMode.WasPassed() &&
       aInit.mMode.Value() == RequestMode::Cors_with_forced_preflight) {
     NS_NAMED_LITERAL_STRING(sourceDescription, "'mode' member of RequestInit");
     NS_NAMED_LITERAL_STRING(value, "cors-with-forced-preflight");
@@ -147,16 +149,22 @@ Request::Constructor(const GlobalObject&
   if (mode != RequestMode::EndGuard_) {
     request->SetMode(mode);
   }
 
   if (credentials != RequestCredentials::EndGuard_) {
     request->SetCredentialsMode(credentials);
   }
 
+  RequestCache cache = aInit.mCache.WasPassed() ?
+                       aInit.mCache.Value() : fallbackCache;
+  if (cache != RequestCache::EndGuard_) {
+    request->SetCacheMode(cache);
+  }
+
   // Request constructor step 14.
   if (aInit.mMethod.WasPassed()) {
     nsAutoCString method(aInit.mMethod.Value());
     nsAutoCString upperCaseMethod = method;
     ToUpperCase(upperCaseMethod);
 
     // Step 14.1. Disallow forbidden methods, and anything that is not a HTTP
     // token, since HTTP states that Method may be any of the defined values or
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -223,23 +223,10 @@ Headers*
 Response::Headers_()
 {
   if (!mHeaders) {
     mHeaders = new Headers(mOwner, mInternalResponse->Headers());
   }
 
   return mHeaders;
 }
-
-void
-Response::SetFinalURL(bool aFinalURL, ErrorResult& aRv)
-{
-  nsCString url;
-  mInternalResponse->GetUrl(url);
-  if (url.IsEmpty()) {
-    aRv.ThrowTypeError(MSG_RESPONSE_URL_IS_NULL);
-    return;
-  }
-
-  mInternalResponse->SetFinalURL(aFinalURL);
-}
 } // namespace dom
 } // namespace mozilla
--- a/dom/fetch/Response.h
+++ b/dom/fetch/Response.h
@@ -51,25 +51,16 @@ public:
   void
   GetUrl(DOMString& aUrl) const
   {
     nsCString url;
     mInternalResponse->GetUrl(url);
     aUrl.AsAString() = NS_ConvertUTF8toUTF16(url);
   }
 
-  bool
-  GetFinalURL(ErrorResult& aRv) const
-  {
-    return mInternalResponse->FinalURL();
-  }
-
-  void
-  SetFinalURL(bool aFinalURL, ErrorResult& aRv);
-
   uint16_t
   Status() const
   {
     return mInternalResponse->GetStatus();
   }
 
   bool
   Ok() const
--- a/dom/interfaces/base/nsIServiceWorkerManager.idl
+++ b/dom/interfaces/base/nsIServiceWorkerManager.idl
@@ -14,17 +14,17 @@ interface nsIURI;
 interface nsIServiceWorkerUnregisterCallback : nsISupports
 {
   // aState is true if the unregistration succeded.
   // It's false if this ServiceWorkerRegistration doesn't exist.
   [noscript] void UnregisterSucceeded(in bool aState);
   [noscript] void UnregisterFailed();
 };
 
-[builtinclass, uuid(706c3e6b-c9d2-4857-893d-4b4845fec48f)]
+[builtinclass, uuid(e4c8baa5-237a-4bf6-82d4-ea06eb4b76ba)]
 interface nsIServiceWorkerManager : nsISupports
 {
   /**
    * Registers a ServiceWorker with script loaded from `aScriptURI` to act as
    * the ServiceWorker for aScope.  Requires a valid entry settings object on
    * the stack. This means you must call this from content code 'within'
    * a window.
    *
@@ -54,17 +54,18 @@ interface nsIServiceWorkerManager : nsIS
 
   // Returns true if a ServiceWorker is available for the scope of aURI.
   bool isAvailableForURI(in nsIURI aURI);
 
   // Returns true if a given document is currently controlled by a ServiceWorker
   bool isControlled(in nsIDocument aDocument);
 
   // Cause a fetch event to be dispatched to the worker global associated with the given document.
-  void dispatchFetchEvent(in nsIDocument aDoc, in nsIInterceptedChannel aChannel);
+  void dispatchFetchEvent(in nsIDocument aDoc, in nsIInterceptedChannel aChannel,
+                          in boolean aIsReload);
 
   // aTarget MUST be a ServiceWorkerRegistration.
   [noscript] void AddRegistrationEventListener(in DOMString aScope, in nsIDOMEventTarget aTarget);
   [noscript] void RemoveRegistrationEventListener(in DOMString aScope, in nsIDOMEventTarget aTarget);
 
   /**
    * Call this to request that document `aDoc` be controlled by a ServiceWorker
    * if a registration exists for it's scope.
--- a/dom/media/MediaDecoder.cpp
+++ b/dom/media/MediaDecoder.cpp
@@ -1426,17 +1426,16 @@ void MediaDecoder::UpdateEstimatedMediaD
     return;
   }
   NS_ENSURE_TRUE_VOID(GetStateMachine());
   GetStateMachine()->UpdateEstimatedDuration(aDuration);
 }
 
 void MediaDecoder::SetMediaSeekable(bool aMediaSeekable) {
   ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
-  MOZ_ASSERT(NS_IsMainThread() || OnDecodeThread());
   mMediaSeekable = aMediaSeekable;
 }
 
 bool
 MediaDecoder::IsTransportSeekable()
 {
   ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
   return GetResource()->IsTransportSeekable();
@@ -1636,19 +1635,21 @@ void MediaDecoder::NotifyDataArrived(con
 // Provide access to the state machine object
 MediaDecoderStateMachine* MediaDecoder::GetStateMachine() const {
   return mDecoderStateMachine;
 }
 
 void
 MediaDecoder::NotifyWaitingForResourcesStatusChanged()
 {
-  ReentrantMonitorAutoEnter mon(GetReentrantMonitor());
   if (mDecoderStateMachine) {
-    mDecoderStateMachine->NotifyWaitingForResourcesStatusChanged();
+    RefPtr<nsRunnable> task =
+      NS_NewRunnableMethod(mDecoderStateMachine,
+                           &MediaDecoderStateMachine::NotifyWaitingForResourcesStatusChanged);
+    mDecoderStateMachine->TaskQueue()->Dispatch(task.forget());
   }
 }
 
 bool MediaDecoder::IsShutdown() const {
   NS_ENSURE_TRUE(GetStateMachine(), true);
   return GetStateMachine()->IsShutdown();
 }
 
@@ -1736,17 +1737,16 @@ MediaDecoder::SetCDMProxy(CDMProxy* aPro
   NotifyWaitingForResourcesStatusChanged();
   return NS_OK;
 }
 
 CDMProxy*
 MediaDecoder::GetCDMProxy()
 {
   GetReentrantMonitor().AssertCurrentThreadIn();
-  MOZ_ASSERT(OnDecodeThread() || NS_IsMainThread());
   return mProxy;
 }
 #endif
 
 #ifdef MOZ_RAW
 bool
 MediaDecoder::IsRawEnabled()
 {
--- a/dom/media/MediaDecoderReader.cpp
+++ b/dom/media/MediaDecoderReader.cpp
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "MediaDecoderReader.h"
 #include "AbstractMediaDecoder.h"
 #include "MediaResource.h"
 #include "VideoUtils.h"
 #include "ImageContainer.h"
 
+#include "nsPrintfCString.h"
 #include "mozilla/mozalloc.h"
 #include <stdint.h>
 #include <algorithm>
 
 namespace mozilla {
 
 // Un-comment to enable logging of seek bisections.
 //#define SEEK_LOGGING
@@ -22,16 +23,22 @@ namespace mozilla {
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* gMediaDecoderLog;
 #define DECODER_LOG(x, ...) \
   PR_LOG(gMediaDecoderLog, PR_LOG_DEBUG, ("Decoder=%p " x, mDecoder, ##__VA_ARGS__))
 #else
 #define DECODER_LOG(x, ...)
 #endif
 
+// Same workaround as MediaDecoderStateMachine.cpp.
+#define DECODER_WARN_HELPER(a, b) NS_WARNING b
+#define DECODER_WARN(x, ...) \
+  DECODER_WARN_HELPER(0, (nsPrintfCString("Decoder=%p " x, mDecoder, ##__VA_ARGS__).get()))
+
+
 PRLogModuleInfo* gMediaPromiseLog;
 
 void
 EnsureMediaPromiseLog()
 {
   if (!gMediaPromiseLog) {
     gMediaPromiseLog = PR_NewLogModule("MediaPromise");
   }
@@ -178,16 +185,53 @@ MediaDecoderReader::ComputeStartTime(con
     startTime = 0;
   }
   DECODER_LOG("ComputeStartTime first video frame start %lld", aVideo ? aVideo->mTime : -1);
   DECODER_LOG("ComputeStartTime first audio frame start %lld", aAudio ? aAudio->mTime : -1);
   NS_ASSERTION(startTime >= 0, "Start time is negative");
   return startTime;
 }
 
+nsRefPtr<MediaDecoderReader::MetadataPromise>
+MediaDecoderReader::CallReadMetadata()
+{
+  typedef ReadMetadataFailureReason Reason;
+
+  MOZ_ASSERT(OnDecodeThread());
+  mDecoder->GetReentrantMonitor().AssertNotCurrentThreadIn();
+  DECODER_LOG("MediaDecoderReader::CallReadMetadata");
+
+  // PreReadMetadata causes us to try to allocate various hardware and OS
+  // resources, which may not be available at the moment.
+  PreReadMetadata();
+  if (IsWaitingMediaResources()) {
+    return MetadataPromise::CreateAndReject(Reason::WAITING_FOR_RESOURCES, __func__);
+  }
+
+  // Attempt to read the metadata.
+  nsRefPtr<MetadataHolder> metadata = new MetadataHolder();
+  nsresult rv = ReadMetadata(&metadata->mInfo, getter_Transfers(metadata->mTags));
+
+  // Reading metadata can cause us to discover that we need resources (like
+  // encryption keys).
+  if (IsWaitingMediaResources()) {
+    return MetadataPromise::CreateAndReject(Reason::WAITING_FOR_RESOURCES, __func__);
+  }
+
+  // We're not waiting for anything. If we didn't get the metadata, that's an
+  // error.
+  if (NS_FAILED(rv) || !metadata->mInfo.HasValidMedia()) {
+    DECODER_WARN("ReadMetadata failed, rv=%x HasValidMedia=%d", rv, metadata->mInfo.HasValidMedia());
+    return MetadataPromise::CreateAndReject(Reason::METADATA_ERROR, __func__);
+  }
+
+  // Success!
+  return MetadataPromise::CreateAndResolve(metadata, __func__);
+}
+
 class ReRequestVideoWithSkipTask : public nsRunnable
 {
 public:
   ReRequestVideoWithSkipTask(MediaDecoderReader* aReader,
                              int64_t aTimeThreshold)
     : mReader(aReader)
     , mTimeThreshold(aTimeThreshold)
   {
@@ -352,8 +396,12 @@ MediaDecoderReader::Shutdown()
   }
 
   mDecoder = nullptr;
 
   return p;
 }
 
 } // namespace mozilla
+
+#undef DECODER_LOG
+#undef DECODER_WARN
+#undef DECODER_WARN_HELPER
--- a/dom/media/MediaDecoderReader.h
+++ b/dom/media/MediaDecoderReader.h
@@ -17,43 +17,62 @@ namespace mozilla {
 
 namespace dom {
 class TimeRanges;
 }
 
 class MediaDecoderReader;
 class SharedDecoderManager;
 
-struct WaitForDataRejectValue {
+struct WaitForDataRejectValue
+{
   enum Reason {
     SHUTDOWN,
     CANCELED
   };
 
   WaitForDataRejectValue(MediaData::Type aType, Reason aReason)
     :mType(aType), mReason(aReason) {}
   MediaData::Type mType;
   Reason mReason;
 };
 
+class MetadataHolder
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MetadataHolder)
+  MediaInfo mInfo;
+  nsAutoPtr<MetadataTags> mTags;
+
+private:
+  virtual ~MetadataHolder() {}
+};
+
+enum class ReadMetadataFailureReason : int8_t
+{
+  WAITING_FOR_RESOURCES,
+  METADATA_ERROR
+};
+
 // Encapsulates the decoding and reading of media data. Reading can either
 // synchronous and done on the calling "decode" thread, or asynchronous and
 // performed on a background thread, with the result being returned by
 // callback. Never hold the decoder monitor when calling into this class.
 // Unless otherwise specified, methods and fields of this class can only
 // be accessed on the decode task queue.
 class MediaDecoderReader {
 public:
   enum NotDecodedReason {
     END_OF_STREAM,
     DECODE_ERROR,
     WAITING_FOR_DATA,
     CANCELED
   };
 
+  typedef MediaPromise<nsRefPtr<MetadataHolder>, ReadMetadataFailureReason, /* IsExclusive = */ true> MetadataPromise;
   typedef MediaPromise<nsRefPtr<AudioData>, NotDecodedReason, /* IsExclusive = */ true> AudioDataPromise;
   typedef MediaPromise<nsRefPtr<VideoData>, NotDecodedReason, /* IsExclusive = */ true> VideoDataPromise;
   typedef MediaPromise<int64_t, nsresult, /* IsExclusive = */ true> SeekPromise;
 
   // Note that, conceptually, WaitForData makes sense in a non-exclusive sense.
   // But in the current architecture it's only ever used exclusively (by MDSM),
   // so we mark it that way to verify our assumptions. If you have a use-case
   // for multiple WaitForData consumers, feel free to flip the exclusivity here.
@@ -143,16 +162,21 @@ public:
   // in buffering mode. Some readers support a promise-based mechanism by which
   // they notify the state machine when the data arrives.
   virtual bool IsWaitForDataSupported() { return false; }
   virtual nsRefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType) { MOZ_CRASH(); }
 
   virtual bool HasAudio() = 0;
   virtual bool HasVideo() = 0;
 
+  // The ReadMetadata API is unfortunately synchronous. We should fix that at
+  // some point, but for now we can make things a bit better by using a
+  // promise-y API on top of a synchronous call.
+  nsRefPtr<MetadataPromise> CallReadMetadata();
+
   // A function that is called before ReadMetadata() call.
   virtual void PreReadMetadata() {};
 
   // Read header data for all bitstreams in the file. Fills aInfo with
   // the data required to present the media, and optionally fills *aTags
   // with tag metadata from the file.
   // Returns NS_OK on success, or NS_ERROR_FAILURE on failure.
   virtual nsresult ReadMetadata(MediaInfo* aInfo,
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -1494,24 +1494,30 @@ void MediaDecoderStateMachine::SetDorman
       nsRefPtr<MediaDecoder::SeekPromise> unused = mQueuedSeek.mPromise.Ensure(__func__);
     }
     mPendingSeek.RejectIfExists(__func__);
     mCurrentSeek.RejectIfExists(__func__);
     SetState(DECODER_STATE_DORMANT);
     if (IsPlaying()) {
       StopPlayback();
     }
-    StopAudioThread();
-    FlushDecoding();
-    // Now that those threads are stopped, there's no possibility of
-    // mPendingWakeDecoder being needed again. Revoke it.
-    mPendingWakeDecoder = nullptr;
+
+    Reset();
+
+    // Note that we do not wait for the decode task queue to go idle before
+    // queuing the ReleaseMediaResources task - instead, we disconnect promises,
+    // reset state, and put a ResetDecode in the decode task queue. Any tasks
+    // that run after ResetDecode are supposed to run with a clean slate. We rely
+    // on that in other places (i.e. seeking), so it seems reasonable to rely on
+    // it here as well.
     DebugOnly<nsresult> rv = DecodeTaskQueue()->Dispatch(
     NS_NewRunnableMethod(mReader, &MediaDecoderReader::ReleaseMediaResources));
     MOZ_ASSERT(NS_SUCCEEDED(rv));
+    // There's now no possibility of mPendingWakeDecoder being needed again. Revoke it.
+    mPendingWakeDecoder = nullptr;
     mDecoder->GetReentrantMonitor().NotifyAll();
   } else if ((aDormant != true) && (mState == DECODER_STATE_DORMANT)) {
     mDecodingFrozenAtStateDecoding = true;
     ScheduleStateMachine();
     mCurrentFrameTime = 0;
     SetState(DECODER_STATE_DECODING_NONE);
     mDecoder->GetReentrantMonitor().NotifyAll();
   }
@@ -1555,53 +1561,31 @@ void MediaDecoderStateMachine::StartDeco
   mIsVideoPrerolling = !DonePrerollingVideo();
 
   // Ensure that we've got tasks enqueued to decode data if we need to.
   DispatchDecodeTasksIfNeeded();
 
   ScheduleStateMachine();
 }
 
-void MediaDecoderStateMachine::StartWaitForResources()
-{
-  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
-  NS_ASSERTION(OnDecodeThread(),
-               "Should be on decode thread.");
-  SetState(DECODER_STATE_WAIT_FOR_RESOURCES);
-  DECODER_LOG("StartWaitForResources");
-}
-
 void MediaDecoderStateMachine::NotifyWaitingForResourcesStatusChanged()
 {
-  AssertCurrentThreadInMonitor();
+  MOZ_ASSERT(OnStateMachineThread());
+  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
   DECODER_LOG("NotifyWaitingForResourcesStatusChanged");
-  RefPtr<nsIRunnable> task(
-    NS_NewRunnableMethod(this,
-      &MediaDecoderStateMachine::DoNotifyWaitingForResourcesStatusChanged));
-  DecodeTaskQueue()->Dispatch(task);
-}
-
-void MediaDecoderStateMachine::DoNotifyWaitingForResourcesStatusChanged()
-{
-  NS_ASSERTION(OnDecodeThread(), "Should be on decode thread.");
-  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
-
-  DECODER_LOG("DoNotifyWaitingForResourcesStatusChanged");
 
   if (mState == DECODER_STATE_WAIT_FOR_RESOURCES) {
-    // The reader is no longer waiting for resources (say a hardware decoder),
-    // we can now proceed to decode metadata.
+    // Try again.
     SetState(DECODER_STATE_DECODING_NONE);
+    ScheduleStateMachine();
   } else if (mState == DECODER_STATE_WAIT_FOR_CDM &&
              !mReader->IsWaitingOnCDMResource()) {
     SetState(DECODER_STATE_DECODING_FIRSTFRAME);
     EnqueueDecodeFirstFrameTask();
   }
-
-  ScheduleStateMachine();
 }
 
 void MediaDecoderStateMachine::PlayInternal()
 {
   NS_ASSERTION(OnStateMachineThread(), "Should be on state machine thread.");
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
 
   // Once we start playing, we don't want to minimize our prerolling, as we
@@ -1636,48 +1620,16 @@ void MediaDecoderStateMachine::PlayInter
   // when the state machine notices the decoder's state change to PLAYING.
   if (mState == DECODER_STATE_BUFFERING) {
     StartDecoding();
   }
 
   ScheduleStateMachine();
 }
 
-void MediaDecoderStateMachine::ResetPlayback()
-{
-  MOZ_ASSERT(OnStateMachineThread());
-
-  // We should be reseting because we're seeking, shutting down, or
-  // entering dormant state. We could also be in the process of going dormant,
-  // and have just switched to exiting dormant before we finished entering
-  // dormant, hence the DECODING_NONE case below.
-  AssertCurrentThreadInMonitor();
-  MOZ_ASSERT(mState == DECODER_STATE_SEEKING ||
-             mState == DECODER_STATE_SHUTDOWN ||
-             mState == DECODER_STATE_DORMANT ||
-             mState == DECODER_STATE_DECODING_NONE);
-
-  // Audio thread should've been stopped at the moment. Otherwise, AudioSink
-  // might be accessing AudioQueue outside of the decoder monitor while we
-  // are clearing the queue and causes crash for no samples to be popped.
-  MOZ_ASSERT(!mAudioSink);
-
-  mVideoFrameEndTime = -1;
-  mDecodedVideoEndTime = -1;
-  mAudioStartTime = -1;
-  mAudioEndTime = -1;
-  mDecodedAudioEndTime = -1;
-  mAudioCompleted = false;
-  AudioQueue().Reset();
-  VideoQueue().Reset();
-  mFirstVideoFrameAfterSeek = nullptr;
-  mDropAudioUntilNextDiscontinuity = true;
-  mDropVideoUntilNextDiscontinuity = true;
-}
-
 void MediaDecoderStateMachine::NotifyDataArrived(const char* aBuffer,
                                                      uint32_t aLength,
                                                      int64_t aOffset)
 {
   NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
   mReader->NotifyDataArrived(aBuffer, aLength, aOffset);
 
   // While playing an unseekable stream of unknown duration, mEndTime is
@@ -1767,29 +1719,16 @@ void MediaDecoderStateMachine::StopAudio
     }
     mAudioSink = nullptr;
   }
   // Wake up those waiting for audio sink to finish.
   mDecoder->GetReentrantMonitor().NotifyAll();
 }
 
 nsresult
-MediaDecoderStateMachine::EnqueueDecodeMetadataTask()
-{
-  AssertCurrentThreadInMonitor();
-  MOZ_ASSERT(mState == DECODER_STATE_DECODING_METADATA);
-
-  RefPtr<nsIRunnable> task(
-    NS_NewRunnableMethod(this, &MediaDecoderStateMachine::CallDecodeMetadata));
-  nsresult rv = DecodeTaskQueue()->Dispatch(task);
-  NS_ENSURE_SUCCESS(rv, rv);
-  return NS_OK;
-}
-
-nsresult
 MediaDecoderStateMachine::EnqueueDecodeFirstFrameTask()
 {
   AssertCurrentThreadInMonitor();
   MOZ_ASSERT(mState == DECODER_STATE_DECODING_FIRSTFRAME);
 
   RefPtr<nsIRunnable> task(
     NS_NewRunnableMethod(this, &MediaDecoderStateMachine::CallDecodeFirstFrame));
   nsresult rv = TaskQueue()->Dispatch(task);
@@ -1921,24 +1860,18 @@ MediaDecoderStateMachine::InitiateSeek()
   // to display
   nsCOMPtr<nsIRunnable> startEvent =
       NS_NewRunnableMethodWithArg<MediaDecoderEventVisibility>(
         mDecoder,
         &MediaDecoder::SeekingStarted,
         mCurrentSeek.mTarget.mEventVisibility);
   NS_DispatchToMainThread(startEvent, NS_DISPATCH_NORMAL);
 
-  // The seek target is different than the current playback position,
-  // we'll need to seek the playback position, so shutdown our decode
-  // thread and audio sink.
-  StopAudioThread();
-  ResetPlayback();
-
-  // Put a reset in the pipe before seek.
-  ResetDecode();
+  // Reset our state machine and decoding pipeline before seeking.
+  Reset();
 
   // Do the seek.
   mSeekRequest.Begin(ProxyMediaCall(DecodeTaskQueue(), mReader.get(), __func__,
                                     &MediaDecoderReader::Seek, mCurrentSeek.mTarget.mTime,
                                     GetEndTime())
     ->RefableThen(TaskQueue(), __func__, this,
                   &MediaDecoderStateMachine::OnSeekCompleted,
                   &MediaDecoderStateMachine::OnSeekFailed));
@@ -2200,69 +2133,26 @@ MediaDecoderStateMachine::DecodeError()
     nsCOMPtr<nsIRunnable> event =
       NS_NewRunnableMethod(mDecoder, &MediaDecoder::DecodeError);
     ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
     NS_DispatchToMainThread(event, NS_DISPATCH_SYNC);
   }
 }
 
 void
-MediaDecoderStateMachine::CallDecodeMetadata()
+MediaDecoderStateMachine::OnMetadataRead(MetadataHolder* aMetadata)
 {
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
-  if (mState != DECODER_STATE_DECODING_METADATA) {
-    return;
-  }
-  if (NS_FAILED(DecodeMetadata())) {
-    DECODER_WARN("Decode metadata failed, shutting down decoder");
-    DecodeError();
-  }
-}
-
-nsresult MediaDecoderStateMachine::DecodeMetadata()
-{
-  AssertCurrentThreadInMonitor();
-  NS_ASSERTION(OnDecodeThread(), "Should be on decode thread.");
+  MOZ_ASSERT(OnStateMachineThread());
   MOZ_ASSERT(mState == DECODER_STATE_DECODING_METADATA);
-  DECODER_LOG("Decoding Media Headers");
-
-  nsresult res;
-  MediaInfo info;
-  bool isAwaitingResources = false;
-  {
-    ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
-    mReader->PreReadMetadata();
-
-    if (mReader->IsWaitingMediaResources()) {
-      StartWaitForResources();
-      return NS_OK;
-    }
-    res = mReader->ReadMetadata(&info, getter_Transfers(mMetadataTags));
-    isAwaitingResources = mReader->IsWaitingMediaResources();
-  }
-
-  if (NS_SUCCEEDED(res) &&
-      mState == DECODER_STATE_DECODING_METADATA &&
-      isAwaitingResources) {
-    // change state to DECODER_STATE_WAIT_FOR_RESOURCES
-    StartWaitForResources();
-    // affect values only if ReadMetadata succeeds
-    return NS_OK;
-  }
-
-  if (NS_FAILED(res) || (!info.HasValidMedia())) {
-    DECODER_WARN("ReadMetadata failed, res=%x HasValidMedia=%d", res, info.HasValidMedia());
-    return NS_ERROR_FAILURE;
-  }
-
-  if (NS_SUCCEEDED(res)) {
-    mDecoder->SetMediaSeekable(mReader->IsMediaSeekable());
-  }
-
-  mInfo = info;
+  mMetadataRequest.Complete();
+
+  mDecoder->SetMediaSeekable(mReader->IsMediaSeekable());
+  mInfo = aMetadata->mInfo;
+  mMetadataTags = aMetadata->mTags.forget();
 
   if (HasVideo()) {
     DECODER_LOG("Video decode isAsync=%d HWAccel=%d videoQueueSize=%d",
                 mReader->IsAsync(),
                 mReader->VideoIsHardwareAccelerated(),
                 GetAmpleVideoFrames());
   }
 
@@ -2270,33 +2160,43 @@ nsresult MediaDecoderStateMachine::Decod
   mGotDurationFromMetaData = (GetDuration() != -1) || mDurationSet;
 
   if (mGotDurationFromMetaData) {
     // We have all the information required: duration and size
     // Inform the element that we've loaded the metadata.
     EnqueueLoadedMetadataEvent();
   }
 
-  if (mState == DECODER_STATE_DECODING_METADATA) {
-    if (mReader->IsWaitingOnCDMResource()) {
-      // Metadata parsing was successful but we're still waiting for CDM caps
-      // to become available so that we can build the correct decryptor/decoder.
-      SetState(DECODER_STATE_WAIT_FOR_CDM);
-      return NS_OK;
-    }
-
-    SetState(DECODER_STATE_DECODING_FIRSTFRAME);
-    res = EnqueueDecodeFirstFrameTask();
-    if (NS_FAILED(res)) {
-      return NS_ERROR_FAILURE;
-    }
+  if (mReader->IsWaitingOnCDMResource()) {
+    // Metadata parsing was successful but we're still waiting for CDM caps
+    // to become available so that we can build the correct decryptor/decoder.
+    SetState(DECODER_STATE_WAIT_FOR_CDM);
+    return;
   }
+
+  SetState(DECODER_STATE_DECODING_FIRSTFRAME);
+  EnqueueDecodeFirstFrameTask();
   ScheduleStateMachine();
-
-  return NS_OK;
+}
+
+void
+MediaDecoderStateMachine::OnMetadataNotRead(ReadMetadataFailureReason aReason)
+{
+  ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
+  MOZ_ASSERT(OnStateMachineThread());
+  MOZ_ASSERT(mState == DECODER_STATE_DECODING_METADATA);
+  mMetadataRequest.Complete();
+
+  if (aReason == ReadMetadataFailureReason::WAITING_FOR_RESOURCES) {
+    SetState(DECODER_STATE_WAIT_FOR_RESOURCES);
+  } else {
+    MOZ_ASSERT(aReason == ReadMetadataFailureReason::METADATA_ERROR);
+    DECODER_WARN("Decode metadata failed, shutting down decoder");
+    DecodeError();
+  }
 }
 
 void
 MediaDecoderStateMachine::EnqueueLoadedMetadataEvent()
 {
   nsAutoPtr<MediaInfo> info(new MediaInfo());
   *info = mInfo;
   MediaDecoderEventVisibility visibility = mSentLoadedMetadataEvent?
@@ -2669,18 +2569,17 @@ nsresult MediaDecoderStateMachine::RunSt
       mQueuedSeek.RejectIfExists(__func__);
       mPendingSeek.RejectIfExists(__func__);
       mCurrentSeek.RejectIfExists(__func__);
 
       if (IsPlaying()) {
         StopPlayback();
       }
 
-      StopAudioThread();
-      FlushDecoding();
+      Reset();
 
       // Put a task in the decode queue to shutdown the reader.
       // the queue to spin down.
       RefPtr<nsIRunnable> task;
       task = NS_NewRunnableMethod(this, &MediaDecoderStateMachine::ShutdownReader);
       DebugOnly<nsresult> rv = DecodeTaskQueue()->Dispatch(task);
       MOZ_ASSERT(NS_SUCCEEDED(rv));
 
@@ -2694,26 +2593,35 @@ nsresult MediaDecoderStateMachine::RunSt
 
     case DECODER_STATE_WAIT_FOR_CDM:
     case DECODER_STATE_WAIT_FOR_RESOURCES: {
       return NS_OK;
     }
 
     case DECODER_STATE_DECODING_NONE: {
       SetState(DECODER_STATE_DECODING_METADATA);
-      // Ensure we have a decode thread to decode metadata.
-      return EnqueueDecodeMetadataTask();
+      ScheduleStateMachine();
+      return NS_OK;
     }
 
     case DECODER_STATE_DECODING_METADATA: {
+      if (!mMetadataRequest.Exists()) {
+        DECODER_LOG("Dispatching CallReadMetadata");
+        mMetadataRequest.Begin(ProxyMediaCall(DecodeTaskQueue(), mReader.get(), __func__,
+                                              &MediaDecoderReader::CallReadMetadata)
+          ->RefableThen(TaskQueue(), __func__, this,
+                        &MediaDecoderStateMachine::OnMetadataRead,
+                        &MediaDecoderStateMachine::OnMetadataNotRead));
+
+      }
       return NS_OK;
     }
 
     case DECODER_STATE_DECODING_FIRSTFRAME: {
-      // DECODER_STATE_DECODING_FIRSTFRAME will be started by DecodeMetadata
+      // DECODER_STATE_DECODING_FIRSTFRAME will be started by OnMetadataRead.
       return NS_OK;
     }
 
     case DECODER_STATE_DECODING: {
       if (mDecoder->GetState() != MediaDecoder::PLAY_STATE_PLAYING &&
           IsPlaying())
       {
         // We're playing, but the element/decoder is in paused state. Stop
@@ -2833,58 +2741,56 @@ nsresult MediaDecoderStateMachine::RunSt
       return NS_OK;
     }
   }
 
   return NS_OK;
 }
 
 void
-MediaDecoderStateMachine::FlushDecoding()
+MediaDecoderStateMachine::Reset()
 {
   MOZ_ASSERT(OnStateMachineThread());
   AssertCurrentThreadInMonitor();
-
-  // Put a task in the decode queue to abort any decoding operations.
-  // The reader is not supposed to put any tasks to deliver samples into
-  // the queue after this runs (unless we request another sample from it).
-  ResetDecode();
-  {
-    // Wait for the ResetDecode to run and for the decoder to abort
-    // decoding operations and run any pending callbacks. This is
-    // important, as we don't want any pending tasks posted to the task
-    // queue by the reader to deliver any samples after we've posted the
-    // reader Shutdown() task below, as the sample-delivery tasks will
-    // keep video frames alive until after we've called Reader::Shutdown(),
-    // and shutdown on B2G will fail as there are outstanding video frames
-    // alive.
-    ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor());
-    DecodeTaskQueue()->AwaitIdle();
-  }
-
-  // We must reset playback so that all references to frames queued
-  // in the state machine are dropped, else subsequent calls to Shutdown()
-  // or ReleaseMediaResources() can fail on B2G.
-  ResetPlayback();
-}
-
-void
-MediaDecoderStateMachine::ResetDecode()
-{
-  MOZ_ASSERT(OnStateMachineThread());
-  AssertCurrentThreadInMonitor();
-
+  DECODER_LOG("MediaDecoderStateMachine::Reset");
+
+  // We should be resetting because we're seeking, shutting down, or entering
+  // dormant state. We could also be in the process of going dormant, and have
+  // just switched to exiting dormant before we finished entering dormant,
+  // hence the DECODING_NONE case below.
+  MOZ_ASSERT(mState == DECODER_STATE_SEEKING ||
+             mState == DECODER_STATE_SHUTDOWN ||
+             mState == DECODER_STATE_DORMANT ||
+             mState == DECODER_STATE_DECODING_NONE);
+
+  // Stop the audio thread. Otherwise, AudioSink might be accessing AudioQueue
+  // outside of the decoder monitor while we are clearing the queue and causes
+  // crash for no samples to be popped.
+  StopAudioThread();
+
+  mVideoFrameEndTime = -1;
+  mDecodedVideoEndTime = -1;
+  mAudioStartTime = -1;
+  mAudioEndTime = -1;
+  mDecodedAudioEndTime = -1;
+  mAudioCompleted = false;
+  AudioQueue().Reset();
+  VideoQueue().Reset();
+  mFirstVideoFrameAfterSeek = nullptr;
+  mDropAudioUntilNextDiscontinuity = true;
+  mDropVideoUntilNextDiscontinuity = true;
+  mDecodeToSeekTarget = false;
+
+  mMetadataRequest.DisconnectIfExists();
   mAudioDataRequest.DisconnectIfExists();
   mAudioWaitRequest.DisconnectIfExists();
   mVideoDataRequest.DisconnectIfExists();
   mVideoWaitRequest.DisconnectIfExists();
   mSeekRequest.DisconnectIfExists();
 
-  mDecodeToSeekTarget = false;
-
   RefPtr<nsRunnable> resetTask =
     NS_NewRunnableMethod(mReader, &MediaDecoderReader::ResetDecode);
   DecodeTaskQueue()->Dispatch(resetTask);
 }
 
 void MediaDecoderStateMachine::RenderVideoFrame(VideoData* aData,
                                                 TimeStamp aTarget)
 {
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -362,19 +362,18 @@ public:
   void QueueMetadata(int64_t aPublishTime,
                      nsAutoPtr<MediaInfo> aInfo,
                      nsAutoPtr<MetadataTags> aTags);
 
   // Returns true if we're currently playing. The decoder monitor must
   // be held.
   bool IsPlaying() const;
 
-  // Dispatch DoNotifyWaitingForResourcesStatusChanged task to the task queue.
   // Called when the reader may have acquired the hardware resources required
-  // to begin decoding. The decoder monitor must be held while calling this.
+  // to begin decoding.
   void NotifyWaitingForResourcesStatusChanged();
 
   // Notifies the state machine that should minimize the number of samples
   // decoded we preroll, until playback starts. The first time playback starts
   // the state machine is free to return to prerolling normally. Note
   // "prerolling" in this context refers to when we decode and buffer decoded
   // samples in advance of when they're needed for playback.
   void SetMinimizePrerollUntilPlaybackStarts();
@@ -402,18 +401,19 @@ public:
   }
 
   void OnWaitForDataRejected(WaitForDataRejectValue aRejection)
   {
     ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
     WaitRequestRef(aRejection.mType).Complete();
   }
 
-  // Resets all state related to decoding, emptying all buffers etc.
-  void ResetDecode();
+  // Resets all state related to decoding and playback, emptying all buffers
+  // and aborting all pending operations on the decode task queue.
+  void Reset();
 
 private:
   void AcquireMonitorAndInvokeDecodeError();
 
 protected:
   virtual ~MediaDecoderStateMachine();
 
   void AssertCurrentThreadInMonitor() const { mDecoder->GetReentrantMonitor().AssertCurrentThreadIn(); }
@@ -460,18 +460,16 @@ protected:
   };
   WakeDecoderRunnable* GetWakeDecoderRunnable();
 
   MediaQueue<AudioData>& AudioQueue() { return mAudioQueue; }
   MediaQueue<VideoData>& VideoQueue() { return mVideoQueue; }
 
   nsresult FinishDecodeFirstFrame();
 
-  nsAutoPtr<MetadataTags> mMetadataTags;
-
   // True if our buffers of decoded audio are not full, and we should
   // decode more.
   bool NeedToDecodeAudio();
 
   // True if our buffers of decoded video are not full, and we should
   // decode more.
   bool NeedToDecodeVideo();
 
@@ -509,24 +507,16 @@ protected:
   bool HasFutureAudio();
 
   // Returns true if we recently exited "quick buffering" mode.
   bool JustExitedQuickBuffering();
 
   // Dispatches an asynchronous event to update the media element's ready state.
   void UpdateReadyState();
 
-  // Resets playback timing data. Called when we seek, on the decode thread.
-  void ResetPlayback();
-
-  // Orders the Reader to stop decoding, and blocks until the Reader
-  // has stopped decoding and finished delivering samples, then calls
-  // ResetPlayback() to discard all enqueued data.
-  void FlushDecoding();
-
   // Called when AudioSink reaches the end. |mPlayStartTime| and
   // |mPlayDuration| are updated to provide a good base for calculating video
   // stream time.
   void ResyncAudioClock();
 
   // Returns the audio clock, if we have audio, or -1 if we don't.
   // Called on the state machine thread.
   int64_t GetAudioClock() const;
@@ -585,18 +575,16 @@ protected:
   void StartDecoding();
 
   // Moves the decoder into the shutdown state, and dispatches an error
   // event to the media element. This begins shutting down the decoder.
   // The decoder monitor must be held. This is only called on the
   // decode thread.
   void DecodeError();
 
-  void StartWaitForResources();
-
   // Dispatches a task to the decode task queue to begin decoding metadata.
   // This is threadsafe and can be called on any thread.
   // The decoder monitor must be held.
   nsresult EnqueueDecodeMetadataTask();
 
   // Dispatches a LoadedMetadataEvent.
   // This is threadsafe and can be called on any thread.
   // The decoder monitor must be held.
@@ -654,22 +642,19 @@ protected:
   // decoded and playable. This is the sum of the number of usecs of audio which
   // is decoded and in the reader's audio queue, and the usecs of unplayed audio
   // which has been pushed to the audio hardware for playback. Note that after
   // calling this, the audio hardware may play some of the audio pushed to
   // hardware, so this can only be used as a upper bound. The decoder monitor
   // must be held when calling this. Called on the decode thread.
   int64_t GetDecodedAudioDuration();
 
-  // Load metadata. Called on the decode thread. The decoder monitor
-  // must be held with exactly one lock count.
-  nsresult DecodeMetadata();
-
-  // Wraps the call to DecodeMetadata(), signals a DecodeError() on failure.
-  void CallDecodeMetadata();
+  // Promise callbacks for metadata reading.
+  void OnMetadataRead(MetadataHolder* aMetadata);
+  void OnMetadataNotRead(ReadMetadataFailureReason aReason);
 
   // Initiate first content decoding. Called on the state machine thread.
   // The decoder monitor must be held with exactly one lock count.
   nsresult DecodeFirstFrame();
 
   // Wraps the call to DecodeFirstFrame(), signals a DecodeError() on failure.
   void CallDecodeFirstFrame();
 
@@ -728,20 +713,16 @@ protected:
 
   // Called by the AudioSink to signal that all outstanding work is complete
   // and the sink is shutting down.
   void OnAudioSinkComplete();
 
   // Called by the AudioSink to signal errors.
   void OnAudioSinkError();
 
-  // The state machine may move into DECODING_METADATA if we are in
-  // DECODER_STATE_WAIT_FOR_RESOURCES.
-  void DoNotifyWaitingForResourcesStatusChanged();
-
   // Return true if the video decoder's decode speed can not catch up the
   // play time.
   bool NeedToSkipToNextKeyframe();
 
   // The decoder object that created this state machine. The state machine
   // holds a strong reference to the decoder to ensure that the decoder stays
   // alive once media element has started the decoder shutdown process, and has
   // dropped its reference to the decoder. This enables the state machine to
@@ -1181,20 +1162,25 @@ protected:
   // Track the current seek promise made by the reader.
   MediaPromiseConsumerHolder<MediaDecoderReader::SeekPromise> mSeekRequest;
 
   // We record the playback position before we seek in order to
   // determine where the seek terminated relative to the playback position
   // we were at before the seek.
   int64_t mCurrentTimeBeforeSeek;
 
+  // Track our request for metadata from the reader.
+  MediaPromiseConsumerHolder<MediaDecoderReader::MetadataPromise> mMetadataRequest;
+
   // Stores presentation info required for playback. The decoder monitor
   // must be held when accessing this.
   MediaInfo mInfo;
 
+  nsAutoPtr<MetadataTags> mMetadataTags;
+
   mozilla::MediaMetadataManager mMetadataManager;
 
   MediaDecoderOwner::NextFrameStatus mLastFrameStatus;
 
   mozilla::RollingMean<uint32_t, uint32_t> mCorruptFrames;
 
   bool mDisabledHardwareAcceleration;
 
--- a/dom/media/fmp4/AVCCDecoderModule.cpp
+++ b/dom/media/fmp4/AVCCDecoderModule.cpp
@@ -269,18 +269,18 @@ already_AddRefed<MediaDataDecoder>
 AVCCDecoderModule::CreateVideoDecoder(const mp4_demuxer::VideoDecoderConfig& aConfig,
                                       layers::LayersBackend aLayersBackend,
                                       layers::ImageContainer* aImageContainer,
                                       FlushableMediaTaskQueue* aVideoTaskQueue,
                                       MediaDataDecoderCallback* aCallback)
 {
   nsRefPtr<MediaDataDecoder> decoder;
 
-  if ((strcmp(aConfig.mime_type, "video/avc") &&
-       strcmp(aConfig.mime_type, "video/mp4")) ||
+  if ((!aConfig.mime_type.EqualsLiteral("video/avc") &&
+       !aConfig.mime_type.EqualsLiteral("video/mp4")) ||
       !mPDM->DecoderNeedsAVCC(aConfig)) {
     // There is no need for an AVCC wrapper for non-AVC content.
     decoder = mPDM->CreateVideoDecoder(aConfig,
                                        aLayersBackend,
                                        aImageContainer,
                                        aVideoTaskQueue,
                                        aCallback);
   } else {
@@ -300,20 +300,20 @@ AVCCDecoderModule::CreateAudioDecoder(co
                                       MediaDataDecoderCallback* aCallback)
 {
   return mPDM->CreateAudioDecoder(aConfig,
                                   aAudioTaskQueue,
                                   aCallback);
 }
 
 bool
-AVCCDecoderModule::SupportsAudioMimeType(const char* aMimeType)
+AVCCDecoderModule::SupportsAudioMimeType(const nsACString& aMimeType)
 {
   return mPDM->SupportsAudioMimeType(aMimeType);
 }
 
 bool
-AVCCDecoderModule::SupportsVideoMimeType(const char* aMimeType)
+AVCCDecoderModule::SupportsVideoMimeType(const nsACString& aMimeType)
 {
   return mPDM->SupportsVideoMimeType(aMimeType);
 }
 
 } // namespace mozilla
--- a/dom/media/fmp4/AVCCDecoderModule.h
+++ b/dom/media/fmp4/AVCCDecoderModule.h
@@ -36,18 +36,18 @@ public:
                      FlushableMediaTaskQueue* aVideoTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
   virtual already_AddRefed<MediaDataDecoder>
   CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
-  virtual bool SupportsAudioMimeType(const char* aMimeType) override;
-  virtual bool SupportsVideoMimeType(const char* aMimeType) override;
+  virtual bool SupportsAudioMimeType(const nsACString& aMimeType) override;
+  virtual bool SupportsVideoMimeType(const nsACString& aMimeType) override;
 
 private:
   nsRefPtr<PlatformDecoderModule> mPDM;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_AVCCDecoderModule_h
--- a/dom/media/fmp4/BlankDecoderModule.cpp
+++ b/dom/media/fmp4/BlankDecoderModule.cpp
@@ -233,17 +233,17 @@ public:
     nsRefPtr<MediaDataDecoder> decoder =
       new BlankMediaDataDecoder<BlankAudioDataCreator>(creator,
                                                        aAudioTaskQueue,
                                                        aCallback);
     return decoder.forget();
   }
 
   virtual bool
-  SupportsAudioMimeType(const char* aMimeType) override
+  SupportsAudioMimeType(const nsACString& aMimeType) override
   {
     return true;
   }
 
 };
 
 already_AddRefed<PlatformDecoderModule> CreateBlankDecoderModule()
 {
--- a/dom/media/fmp4/MP4Reader.cpp
+++ b/dom/media/fmp4/MP4Reader.cpp
@@ -306,29 +306,29 @@ MP4Reader::ExtractCryptoInitData(nsTArra
   MOZ_ASSERT(mCrypto.valid);
   const nsTArray<mp4_demuxer::PsshInfo>& psshs = mCrypto.pssh;
   for (uint32_t i = 0; i < psshs.Length(); i++) {
     aInitData.AppendElements(psshs[i].data);
   }
 }
 
 bool
-MP4Reader::IsSupportedAudioMimeType(const char* aMimeType)
+MP4Reader::IsSupportedAudioMimeType(const nsACString& aMimeType)
 {
-  return (!strcmp(aMimeType, "audio/mpeg") ||
-          !strcmp(aMimeType, "audio/mp4a-latm")) &&
+  return (aMimeType.EqualsLiteral("audio/mpeg") ||
+          aMimeType.EqualsLiteral("audio/mp4a-latm")) &&
          mPlatform->SupportsAudioMimeType(aMimeType);
 }
 
 bool
-MP4Reader::IsSupportedVideoMimeType(const char* aMimeType)
+MP4Reader::IsSupportedVideoMimeType(const nsACString& aMimeType)
 {
-  return (!strcmp(aMimeType, "video/mp4") ||
-          !strcmp(aMimeType, "video/avc") ||
-          !strcmp(aMimeType, "video/x-vnd.on2.vp6")) &&
+  return (aMimeType.EqualsLiteral("video/mp4") ||
+          aMimeType.EqualsLiteral("video/avc") ||
+          aMimeType.EqualsLiteral("video/x-vnd.on2.vp6")) &&
          mPlatform->SupportsVideoMimeType(aMimeType);
 }
 
 void
 MP4Reader::PreReadMetadata()
 {
   if (mPlatform) {
     RequestCodecResource();
--- a/dom/media/fmp4/MP4Reader.h
+++ b/dom/media/fmp4/MP4Reader.h
@@ -128,18 +128,18 @@ private:
   // DecoderCallback proxies the MediaDataDecoderCallback calls to these
   // functions.
   void Output(mp4_demuxer::TrackType aType, MediaData* aSample);
   void InputExhausted(mp4_demuxer::TrackType aTrack);
   void Error(mp4_demuxer::TrackType aTrack);
   void Flush(mp4_demuxer::TrackType aTrack);
   void DrainComplete(mp4_demuxer::TrackType aTrack);
   void UpdateIndex();
-  bool IsSupportedAudioMimeType(const char* aMimeType);
-  bool IsSupportedVideoMimeType(const char* aMimeType);
+  bool IsSupportedAudioMimeType(const nsACString& aMimeType);
+  bool IsSupportedVideoMimeType(const nsACString& aMimeType);
   void NotifyResourcesStatusChanged();
   void RequestCodecResource();
   virtual bool IsWaitingOnCDMResource() override;
 
   Microseconds GetNextKeyframeTime();
   bool ShouldSkip(bool aSkipToNextKeyframe, int64_t aTimeThreshold);
 
   size_t SizeOfQueue(TrackType aTrack);
--- a/dom/media/fmp4/PlatformDecoderModule.cpp
+++ b/dom/media/fmp4/PlatformDecoderModule.cpp
@@ -175,25 +175,25 @@ PlatformDecoderModule::CreatePDM()
   if (sGMPDecoderEnabled) {
     nsRefPtr<PlatformDecoderModule> m(new AVCCDecoderModule(new GMPDecoderModule()));
     return m.forget();
   }
   return nullptr;
 }
 
 bool
-PlatformDecoderModule::SupportsAudioMimeType(const char* aMimeType)
+PlatformDecoderModule::SupportsAudioMimeType(const nsACString& aMimeType)
 {
-  return !strcmp(aMimeType, "audio/mp4a-latm");
+  return aMimeType.EqualsLiteral("audio/mp4a-latm");
 }
 
 bool
-PlatformDecoderModule::SupportsVideoMimeType(const char* aMimeType)
+PlatformDecoderModule::SupportsVideoMimeType(const nsACString& aMimeType)
 {
-  return !strcmp(aMimeType, "video/mp4") || !strcmp(aMimeType, "video/avc");
+  return aMimeType.EqualsLiteral("video/mp4") || aMimeType.EqualsLiteral("video/avc");
 }
 
 bool
 PlatformDecoderModule::DecoderNeedsAVCC(const mp4_demuxer::VideoDecoderConfig& aConfig)
 {
   return false;
 }
 
--- a/dom/media/fmp4/PlatformDecoderModule.h
+++ b/dom/media/fmp4/PlatformDecoderModule.h
@@ -117,18 +117,18 @@ public:
   virtual already_AddRefed<MediaDataDecoder>
   CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) = 0;
 
   // An audio decoder module must support AAC by default.
   // If more audio codec is to be supported, SupportsAudioMimeType will have
   // to be extended
-  virtual bool SupportsAudioMimeType(const char* aMimeType);
-  virtual bool SupportsVideoMimeType(const char* aMimeType);
+  virtual bool SupportsAudioMimeType(const nsACString& aMimeType);
+  virtual bool SupportsVideoMimeType(const nsACString& aMimeType);
 
   // Indicates if the video decoder requires AVCC format.
   virtual bool DecoderNeedsAVCC(const mp4_demuxer::VideoDecoderConfig& aConfig);
 
   virtual void DisableHardwareAcceleration() {}
 
   virtual bool SupportsSharedDecoders(const mp4_demuxer::VideoDecoderConfig& aConfig) const {
     return true;
--- a/dom/media/fmp4/android/AndroidDecoderModule.cpp
+++ b/dom/media/fmp4/android/AndroidDecoderModule.cpp
@@ -13,34 +13,31 @@
 
 #include "MediaData.h"
 
 #include "mp4_demuxer/AnnexB.h"
 #include "mp4_demuxer/DecoderData.h"
 
 #include "nsThreadUtils.h"
 #include "nsAutoPtr.h"
+#include "nsPromiseFlatString.h"
 
 #include <jni.h>
 #include <string.h>
 
 using namespace mozilla;
 using namespace mozilla::gl;
 using namespace mozilla::widget::sdk;
 
 namespace mozilla {
 
-static MediaCodec::LocalRef CreateDecoder(const char* aMimeType)
+static MediaCodec::LocalRef CreateDecoder(const nsACString& aMimeType)
 {
-  if (!aMimeType) {
-    return nullptr;
-  }
-
   MediaCodec::LocalRef codec;
-  NS_ENSURE_SUCCESS(MediaCodec::CreateDecoderByType(aMimeType, &codec), nullptr);
+  NS_ENSURE_SUCCESS(MediaCodec::CreateDecoderByType(PromiseFlatCString(aMimeType).get(), &codec), nullptr);
   return codec;
 }
 
 class VideoDataDecoder : public MediaCodecDataDecoder {
 public:
   VideoDataDecoder(const mp4_demuxer::VideoDecoderConfig& aConfig,
                    MediaFormat::Param aFormat, MediaDataDecoderCallback* aCallback,
                    layers::ImageContainer* aImageContainer)
@@ -246,17 +243,17 @@ public:
                                              numChannels,
                                              sampleRate);
     mCallback->Output(data);
     return NS_OK;
   }
 };
 
 
-bool AndroidDecoderModule::SupportsAudioMimeType(const char* aMimeType) {
+bool AndroidDecoderModule::SupportsAudioMimeType(const nsACString& aMimeType) {
   return static_cast<bool>(CreateDecoder(aMimeType));
 }
 
 already_AddRefed<MediaDataDecoder>
 AndroidDecoderModule::CreateVideoDecoder(
                                 const mp4_demuxer::VideoDecoderConfig& aConfig,
                                 layers::LayersBackend aLayersBackend,
                                 layers::ImageContainer* aImageContainer,
@@ -295,21 +292,21 @@ AndroidDecoderModule::CreateAudioDecoder
   nsRefPtr<MediaDataDecoder> decoder =
     new AudioDataDecoder(aConfig, format, aCallback);
 
   return decoder.forget();
 
 }
 
 MediaCodecDataDecoder::MediaCodecDataDecoder(MediaData::Type aType,
-                                             const char* aMimeType,
+                                             const nsACString& aMimeType,
                                              MediaFormat::Param aFormat,
                                              MediaDataDecoderCallback* aCallback)
   : mType(aType)
-  , mMimeType(strdup(aMimeType))
+  , mMimeType(aMimeType)
   , mFormat(aFormat)
   , mCallback(aCallback)
   , mInputBuffers(nullptr)
   , mOutputBuffers(nullptr)
   , mMonitor("MediaCodecDataDecoder::mMonitor")
   , mFlushing(false)
   , mDraining(false)
   , mStopping(false)
--- a/dom/media/fmp4/android/AndroidDecoderModule.h
+++ b/dom/media/fmp4/android/AndroidDecoderModule.h
@@ -30,41 +30,41 @@ public:
   CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
 
   AndroidDecoderModule() {}
   virtual ~AndroidDecoderModule() {}
 
-  virtual bool SupportsAudioMimeType(const char* aMimeType) override;
+  virtual bool SupportsAudioMimeType(const nsACString& aMimeType) override;
 };
 
 class MediaCodecDataDecoder : public MediaDataDecoder {
 public:
 
   MediaCodecDataDecoder(MediaData::Type aType,
-                        const char* aMimeType,
+                        const nsACString& aMimeType,
                         widget::sdk::MediaFormat::Param aFormat,
                         MediaDataDecoderCallback* aCallback);
 
   virtual ~MediaCodecDataDecoder();
 
   virtual nsresult Init() override;
   virtual nsresult Flush() override;
   virtual nsresult Drain() override;
   virtual nsresult Shutdown() override;
   virtual nsresult Input(mp4_demuxer::MP4Sample* aSample);
 
 protected:
   friend class AndroidDecoderModule;
 
   MediaData::Type mType;
 
-  nsAutoPtr<char> mMimeType;
+  nsAutoCString mMimeType;
   widget::sdk::MediaFormat::GlobalRef mFormat;
 
   MediaDataDecoderCallback* mCallback;
 
   widget::sdk::MediaCodec::GlobalRef mDecoder;
 
   jni::ObjectArray::GlobalRef mInputBuffers;
   jni::ObjectArray::GlobalRef mOutputBuffers;
--- a/dom/media/fmp4/apple/AppleATDecoder.cpp
+++ b/dom/media/fmp4/apple/AppleATDecoder.cpp
@@ -30,24 +30,24 @@ AppleATDecoder::AppleATDecoder(const mp4
   , mTaskQueue(aAudioTaskQueue)
   , mCallback(aCallback)
   , mConverter(nullptr)
   , mStream(nullptr)
 {
   MOZ_COUNT_CTOR(AppleATDecoder);
   LOG("Creating Apple AudioToolbox decoder");
   LOG("Audio Decoder configuration: %s %d Hz %d channels %d bits per channel",
-      mConfig.mime_type,
+      mConfig.mime_type.get(),
       mConfig.samples_per_second,
       mConfig.channel_count,
       mConfig.bits_per_sample);
 
-  if (!strcmp(mConfig.mime_type, "audio/mpeg")) {
+  if (mConfig.mime_type.EqualsLiteral("audio/mpeg")) {
     mFormatID = kAudioFormatMPEGLayer3;
-  } else if (!strcmp(mConfig.mime_type, "audio/mp4a-latm")) {
+  } else if (mConfig.mime_type.EqualsLiteral("audio/mp4a-latm")) {
     mFormatID = kAudioFormatMPEG4AAC;
   } else {
     mFormatID = 0;
   }
 }
 
 AppleATDecoder::~AppleATDecoder()
 {
--- a/dom/media/fmp4/apple/AppleDecoderModule.cpp
+++ b/dom/media/fmp4/apple/AppleDecoderModule.cpp
@@ -187,19 +187,19 @@ AppleDecoderModule::CreateAudioDecoder(c
                                        MediaDataDecoderCallback* aCallback)
 {
   nsRefPtr<MediaDataDecoder> decoder =
     new AppleATDecoder(aConfig, aAudioTaskQueue, aCallback);
   return decoder.forget();
 }
 
 bool
-AppleDecoderModule::SupportsAudioMimeType(const char* aMimeType)
+AppleDecoderModule::SupportsAudioMimeType(const nsACString& aMimeType)
 {
-  return !strcmp(aMimeType, "audio/mp4a-latm") || !strcmp(aMimeType, "audio/mpeg");
+  return aMimeType.EqualsLiteral("audio/mp4a-latm") || aMimeType.EqualsLiteral("audio/mpeg");
 }
 
 bool
 AppleDecoderModule::DecoderNeedsAVCC(const mp4_demuxer::VideoDecoderConfig& aConfig)
 {
   return true;
 }
 
--- a/dom/media/fmp4/apple/AppleDecoderModule.h
+++ b/dom/media/fmp4/apple/AppleDecoderModule.h
@@ -27,17 +27,17 @@ public:
                      MediaDataDecoderCallback* aCallback) override;
 
   // Decode thread.
   virtual already_AddRefed<MediaDataDecoder>
   CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
-  virtual bool SupportsAudioMimeType(const char* aMimeType) override;
+  virtual bool SupportsAudioMimeType(const nsACString& aMimeType) override;
   virtual bool
   DecoderNeedsAVCC(const mp4_demuxer::VideoDecoderConfig& aConfig) override;
 
   static void Init();
   static nsresult CanDecode();
 
 private:
   friend class InitTask;
--- a/dom/media/fmp4/ffmpeg/FFmpegAudioDecoder.cpp
+++ b/dom/media/fmp4/ffmpeg/FFmpegAudioDecoder.cpp
@@ -165,23 +165,23 @@ nsresult
 FFmpegAudioDecoder<LIBAV_VER>::Drain()
 {
   mTaskQueue->AwaitIdle();
   mCallback->DrainComplete();
   return Flush();
 }
 
 AVCodecID
-FFmpegAudioDecoder<LIBAV_VER>::GetCodecId(const char* aMimeType)
+FFmpegAudioDecoder<LIBAV_VER>::GetCodecId(const nsACString& aMimeType)
 {
-  if (!strcmp(aMimeType, "audio/mpeg")) {
+  if (aMimeType.EqualsLiteral("audio/mpeg")) {
     return AV_CODEC_ID_MP3;
   }
 
-  if (!strcmp(aMimeType, "audio/mp4a-latm")) {
+  if (aMimeType.EqualsLiteral("audio/mp4a-latm")) {
     return AV_CODEC_ID_AAC;
   }
 
   return AV_CODEC_ID_NONE;
 }
 
 FFmpegAudioDecoder<LIBAV_VER>::~FFmpegAudioDecoder()
 {
--- a/dom/media/fmp4/ffmpeg/FFmpegAudioDecoder.h
+++ b/dom/media/fmp4/ffmpeg/FFmpegAudioDecoder.h
@@ -3,17 +3,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef __FFmpegAACDecoder_h__
 #define __FFmpegAACDecoder_h__
 
 #include "FFmpegDataDecoder.h"
-#include "mp4_demuxer/DecoderData.h"
 
 namespace mozilla
 {
 
 template <int V> class FFmpegAudioDecoder
 {
 };
 
@@ -24,17 +23,17 @@ public:
   FFmpegAudioDecoder(FlushableMediaTaskQueue* aTaskQueue,
                      MediaDataDecoderCallback* aCallback,
                      const mp4_demuxer::AudioDecoderConfig& aConfig);
   virtual ~FFmpegAudioDecoder();
 
   virtual nsresult Init() override;
   virtual nsresult Input(mp4_demuxer::MP4Sample* aSample) override;
   virtual nsresult Drain() override;
-  static AVCodecID GetCodecId(const char* aMimeType);
+  static AVCodecID GetCodecId(const nsACString& aMimeType);
 
 private:
   void DecodePacket(mp4_demuxer::MP4Sample* aSample);
 
   MediaDataDecoderCallback* mCallback;
 };
 
 } // namespace mozilla
--- a/dom/media/fmp4/ffmpeg/FFmpegDecoderModule.h
+++ b/dom/media/fmp4/ffmpeg/FFmpegDecoderModule.h
@@ -46,22 +46,22 @@ public:
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) override
   {
     nsRefPtr<MediaDataDecoder> decoder =
       new FFmpegAudioDecoder<V>(aAudioTaskQueue, aCallback, aConfig);
     return decoder.forget();
   }
 
-  virtual bool SupportsAudioMimeType(const char* aMimeType) override
+  virtual bool SupportsAudioMimeType(const nsACString& aMimeType) override
   {
     return FFmpegAudioDecoder<V>::GetCodecId(aMimeType) != AV_CODEC_ID_NONE;
   }
 
-  virtual bool SupportsVideoMimeType(const char* aMimeType) override
+  virtual bool SupportsVideoMimeType(const nsACString& aMimeType) override
   {
     return FFmpegH264Decoder<V>::GetCodecId(aMimeType) != AV_CODEC_ID_NONE;
   }
 
   virtual bool DecoderNeedsAVCC(const mp4_demuxer::VideoDecoderConfig& aConfig) override
   {
     return true;
   }
--- a/dom/media/fmp4/ffmpeg/FFmpegH264Decoder.cpp
+++ b/dom/media/fmp4/ffmpeg/FFmpegH264Decoder.cpp
@@ -282,22 +282,22 @@ FFmpegH264Decoder<LIBAV_VER>::Flush()
 }
 
 FFmpegH264Decoder<LIBAV_VER>::~FFmpegH264Decoder()
 {
   MOZ_COUNT_DTOR(FFmpegH264Decoder);
 }
 
 AVCodecID
-FFmpegH264Decoder<LIBAV_VER>::GetCodecId(const char* aMimeType)
+FFmpegH264Decoder<LIBAV_VER>::GetCodecId(const nsACString& aMimeType)
 {
-  if (!strcmp(aMimeType, "video/avc") || !strcmp(aMimeType, "video/mp4")) {
+  if (aMimeType.EqualsLiteral("video/avc") || aMimeType.EqualsLiteral("video/mp4")) {
     return AV_CODEC_ID_H264;
   }
 
-  if (!strcmp(aMimeType, "video/x-vnd.on2.vp6")) {
+  if (aMimeType.EqualsLiteral("video/x-vnd.on2.vp6")) {
     return AV_CODEC_ID_VP6F;
   }
 
   return AV_CODEC_ID_NONE;
 }
 
 } // namespace mozilla
--- a/dom/media/fmp4/ffmpeg/FFmpegH264Decoder.h
+++ b/dom/media/fmp4/ffmpeg/FFmpegH264Decoder.h
@@ -35,17 +35,17 @@ public:
                     const mp4_demuxer::VideoDecoderConfig& aConfig,
                     ImageContainer* aImageContainer);
   virtual ~FFmpegH264Decoder();
 
   virtual nsresult Init() override;
   virtual nsresult Input(mp4_demuxer::MP4Sample* aSample) override;
   virtual nsresult Drain() override;
   virtual nsresult Flush() override;
-  static AVCodecID GetCodecId(const char* aMimeType);
+  static AVCodecID GetCodecId(const nsACString& aMimeType);
 
 private:
   void DecodeFrame(mp4_demuxer::MP4Sample* aSample);
   DecodeResult DoDecodeFrame(mp4_demuxer::MP4Sample* aSample);
   void DoDrain();
   void OutputDelayedFrames();
 
   /**
--- a/dom/media/fmp4/gmp/GMPDecoderModule.cpp
+++ b/dom/media/fmp4/gmp/GMPDecoderModule.cpp
@@ -41,17 +41,17 @@ CreateDecoderWrapper(MediaDataDecoderCal
 
 already_AddRefed<MediaDataDecoder>
 GMPDecoderModule::CreateVideoDecoder(const mp4_demuxer::VideoDecoderConfig& aConfig,
                                      layers::LayersBackend aLayersBackend,
                                      layers::ImageContainer* aImageContainer,
                                      FlushableMediaTaskQueue* aVideoTaskQueue,
                                      MediaDataDecoderCallback* aCallback)
 {
-  if (strcmp(aConfig.mime_type, "video/avc") != 0) {
+  if (!aConfig.mime_type.EqualsLiteral("video/avc")) {
     return nullptr;
   }
 
   nsRefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback);
   wrapper->SetProxyTarget(new GMPVideoDecoder(aConfig,
                                               aLayersBackend,
                                               aImageContainer,
                                               aVideoTaskQueue,
@@ -59,17 +59,17 @@ GMPDecoderModule::CreateVideoDecoder(con
   return wrapper.forget();
 }
 
 already_AddRefed<MediaDataDecoder>
 GMPDecoderModule::CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                                      FlushableMediaTaskQueue* aAudioTaskQueue,
                                      MediaDataDecoderCallback* aCallback)
 {
-  if (strcmp(aConfig.mime_type, "audio/mp4a-latm") != 0) {
+  if (!aConfig.mime_type.EqualsLiteral("audio/mp4a-latm")) {
     return nullptr;
   }
 
   nsRefPtr<MediaDataDecoderProxy> wrapper = CreateDecoderWrapper(aCallback);
   wrapper->SetProxyTarget(new GMPAudioDecoder(aConfig,
                                               aAudioTaskQueue,
                                               wrapper->Callback()));
   return wrapper.forget();
--- a/dom/media/fmp4/gonk/GonkAudioDecoderManager.cpp
+++ b/dom/media/fmp4/gonk/GonkAudioDecoderManager.cpp
@@ -46,17 +46,17 @@ GonkAudioDecoderManager::GonkAudioDecode
   , mUseAdts(true)
   , mAudioBuffer(nullptr)
 {
   MOZ_COUNT_CTOR(GonkAudioDecoderManager);
   MOZ_ASSERT(mAudioChannels);
   mUserData.AppendElements(aConfig.audio_specific_config->Elements(),
                            aConfig.audio_specific_config->Length());
   // Pass through mp3 without applying an ADTS header.
-  if (strcmp(aConfig.mime_type, "audio/mp4a-latm") != 0) {
+  if (!aConfig.mime_type.EqualsLiteral("audio/mp4a-latm")) {
       mUseAdts = false;
   }
 }
 
 GonkAudioDecoderManager::~GonkAudioDecoderManager()
 {
   MOZ_COUNT_DTOR(GonkAudioDecoderManager);
 }
--- a/dom/media/fmp4/wmf/WMFAudioMFTManager.cpp
+++ b/dom/media/fmp4/wmf/WMFAudioMFTManager.cpp
@@ -72,19 +72,19 @@ WMFAudioMFTManager::WMFAudioMFTManager(
   : mAudioChannels(aConfig.channel_count)
   , mAudioRate(aConfig.samples_per_second)
   , mAudioFrameOffset(0)
   , mAudioFrameSum(0)
   , mMustRecaptureAudioPosition(true)
 {
   MOZ_COUNT_CTOR(WMFAudioMFTManager);
 
-  if (!strcmp(aConfig.mime_type, "audio/mpeg")) {
+  if (aConfig.mime_type.EqualsLiteral("audio/mpeg")) {
     mStreamType = MP3;
-  } else if (!strcmp(aConfig.mime_type, "audio/mp4a-latm")) {
+  } else if (aConfig.mime_type.EqualsLiteral("audio/mp4a-latm")) {
     mStreamType = AAC;
     AACAudioSpecificConfigToUserData(aConfig.aac_profile,
                                      aConfig.audio_specific_config->Elements(),
                                      aConfig.audio_specific_config->Length(),
                                      mUserData);
   } else {
     mStreamType = Unknown;
   }
--- a/dom/media/fmp4/wmf/WMFDecoderModule.cpp
+++ b/dom/media/fmp4/wmf/WMFDecoderModule.cpp
@@ -113,29 +113,29 @@ bool
 WMFDecoderModule::SupportsSharedDecoders(const mp4_demuxer::VideoDecoderConfig& aConfig) const
 {
   // If DXVA is enabled, but we're not going to use it for this specific config, then
   // we can't use the shared decoder.
   return !sDXVAEnabled || ShouldUseDXVA(aConfig);
 }
 
 bool
-WMFDecoderModule::SupportsVideoMimeType(const char* aMimeType)
+WMFDecoderModule::SupportsVideoMimeType(const nsACString& aMimeType)
 {
-  return !strcmp(aMimeType, "video/mp4") ||
-         !strcmp(aMimeType, "video/avc") ||
-         !strcmp(aMimeType, "video/webm; codecs=vp8") ||
-         !strcmp(aMimeType, "video/webm; codecs=vp9");
+  return aMimeType.EqualsLiteral("video/mp4") ||
+         aMimeType.EqualsLiteral("video/avc") ||
+         aMimeType.EqualsLiteral("video/webm; codecs=vp8") ||
+         aMimeType.EqualsLiteral("video/webm; codecs=vp9");
 }
 
 bool
-WMFDecoderModule::SupportsAudioMimeType(const char* aMimeType)
+WMFDecoderModule::SupportsAudioMimeType(const nsACString& aMimeType)
 {
-  return !strcmp(aMimeType, "audio/mp4a-latm") ||
-         !strcmp(aMimeType, "audio/mpeg");
+  return aMimeType.EqualsLiteral("audio/mp4a-latm") ||
+         aMimeType.EqualsLiteral("audio/mpeg");
 }
 
 static bool
 ClassesRootRegKeyExists(const nsAString& aRegKeyPath)
 {
   nsresult rv;
 
   nsCOMPtr<nsIWindowsRegKey> regKey =
--- a/dom/media/fmp4/wmf/WMFDecoderModule.h
+++ b/dom/media/fmp4/wmf/WMFDecoderModule.h
@@ -26,18 +26,18 @@ public:
                      FlushableMediaTaskQueue* aVideoTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
   virtual already_AddRefed<MediaDataDecoder>
   CreateAudioDecoder(const mp4_demuxer::AudioDecoderConfig& aConfig,
                      FlushableMediaTaskQueue* aAudioTaskQueue,
                      MediaDataDecoderCallback* aCallback) override;
 
-  bool SupportsVideoMimeType(const char* aMimeType) override;
-  bool SupportsAudioMimeType(const char* aMimeType) override;
+  bool SupportsVideoMimeType(const nsACString& aMimeType) override;
+  bool SupportsAudioMimeType(const nsACString& aMimeType) override;
 
   virtual void DisableHardwareAcceleration() override
   {
     sDXVAEnabled = false;
   }
 
   virtual bool SupportsSharedDecoders(const mp4_demuxer::VideoDecoderConfig& aConfig) const override;
 
--- a/dom/media/fmp4/wmf/WMFVideoMFTManager.cpp
+++ b/dom/media/fmp4/wmf/WMFVideoMFTManager.cpp
@@ -79,22 +79,22 @@ WMFVideoMFTManager::WMFVideoMFTManager(
   // mVideoStride, mVideoWidth, mVideoHeight, mUseHwAccel are initialized in
   // Init().
 {
   NS_ASSERTION(!NS_IsMainThread(), "Should not be on main thread.");
   MOZ_ASSERT(mImageContainer);
   MOZ_COUNT_CTOR(WMFVideoMFTManager);
 
   // Need additional checks/params to check vp8/vp9
-  if (!strcmp(aConfig.mime_type, "video/mp4") ||
-      !strcmp(aConfig.mime_type, "video/avc")) {
+  if (aConfig.mime_type.EqualsLiteral("video/mp4") ||
+      aConfig.mime_type.EqualsLiteral("video/avc")) {
     mStreamType = H264;
-  } else if (!strcmp(aConfig.mime_type, "video/webm; codecs=vp8")) {
+  } else if (aConfig.mime_type.EqualsLiteral("video/webm; codecs=vp8")) {
     mStreamType = VP8;
-  } else if (!strcmp(aConfig.mime_type, "video/webm; codecs=vp9")) {
+  } else if (aConfig.mime_type.EqualsLiteral("video/webm; codecs=vp9")) {
     mStreamType = VP9;
   } else {
     mStreamType = Unknown;
   }
 }
 
 WMFVideoMFTManager::~WMFVideoMFTManager()
 {
--- a/dom/media/mediasource/MediaSourceReader.cpp
+++ b/dom/media/mediasource/MediaSourceReader.cpp
@@ -66,17 +66,19 @@ MediaSourceReader::MediaSourceReader(Med
 
 void
 MediaSourceReader::PrepareInitialization()
 {
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
   MSE_DEBUG("trackBuffers=%u", mTrackBuffers.Length());
   mEssentialTrackBuffers.AppendElements(mTrackBuffers);
   mHasEssentialTrackBuffers = true;
-  mDecoder->NotifyWaitingForResourcesStatusChanged();
+  if (!IsWaitingMediaResources()) {
+    mDecoder->NotifyWaitingForResourcesStatusChanged();
+  }
 }
 
 bool
 MediaSourceReader::IsWaitingMediaResources()
 {
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
 
   for (uint32_t i = 0; i < mEssentialTrackBuffers.Length(); ++i) {
@@ -756,17 +758,20 @@ MediaSourceReader::OnTrackBufferConfigur
   if (aInfo.HasAudio() && !mAudioTrack) {
     MSE_DEBUG("%p audio", aTrackBuffer);
     mAudioTrack = aTrackBuffer;
   }
   if (aInfo.HasVideo() && !mVideoTrack) {
     MSE_DEBUG("%p video", aTrackBuffer);
     mVideoTrack = aTrackBuffer;
   }
-  mDecoder->NotifyWaitingForResourcesStatusChanged();
+
+  if (!IsWaitingMediaResources()) {
+    mDecoder->NotifyWaitingForResourcesStatusChanged();
+  }
 }
 
 bool
 MediaSourceReader::TrackBuffersContainTime(int64_t aTime)
 {
   ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor());
   if (mAudioTrack && !mAudioTrack->ContainsTime(aTime, EOS_FUZZ_US)) {
     return false;
--- a/dom/media/webm/IntelWebMVideoDecoder.cpp
+++ b/dom/media/webm/IntelWebMVideoDecoder.cpp
@@ -107,20 +107,20 @@ IntelWebMVideoDecoder::Create(WebMReader
 
   decoder->mTaskQueue = aReader->GetVideoTaskQueue();
   NS_ENSURE_TRUE(decoder->mTaskQueue, nullptr);
 
   return decoder.forget();
 }
 
 bool
-IntelWebMVideoDecoder::IsSupportedVideoMimeType(const char* aMimeType)
+IntelWebMVideoDecoder::IsSupportedVideoMimeType(const nsACString& aMimeType)
 {
-  return (!strcmp(aMimeType, "video/webm; codecs=vp8") ||
-          !strcmp(aMimeType, "video/webm; codecs=vp9")) &&
+  return (aMimeType.EqualsLiteral("video/webm; codecs=vp8") ||
+          aMimeType.EqualsLiteral("video/webm; codecs=vp9")) &&
          mPlatform->SupportsVideoMimeType(aMimeType);
 }
 
 nsresult
 IntelWebMVideoDecoder::Init(unsigned int aWidth, unsigned int aHeight)
 {
   mPlatform = PlatformDecoderModule::Create();
   if (!mPlatform) {
--- a/dom/media/webm/IntelWebMVideoDecoder.h
+++ b/dom/media/webm/IntelWebMVideoDecoder.h
@@ -49,17 +49,17 @@ private:
   void InitLayersBackendType();
 
   bool Decode();
 
   bool Demux(nsAutoPtr<VP8Sample>& aSample, bool* aEOS);
 
   bool SkipVideoDemuxToNextKeyFrame(int64_t aTimeThreshold, uint32_t& parsed);
 
-  bool IsSupportedVideoMimeType(const char* aMimeType);
+  bool IsSupportedVideoMimeType(const nsACString& aMimeType);
 
   VP8Sample* PopSample();
 
   nsRefPtr<WebMReader> mReader;
   nsRefPtr<PlatformDecoderModule> mPlatform;
   nsRefPtr<MediaDataDecoder> mMediaDataDecoder;
 
   // TaskQueue on which decoder can choose to decode.
--- a/dom/security/nsCORSListenerProxy.cpp
+++ b/dom/security/nsCORSListenerProxy.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/Assertions.h"
 #include "mozilla/LinkedList.h"
 
 #include "nsCORSListenerProxy.h"
 #include "nsIChannel.h"
 #include "nsIHttpChannel.h"
+#include "nsIHttpChannelInternal.h"
 #include "nsError.h"
 #include "nsContentUtils.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsNetUtil.h"
 #include "nsMimeTypes.h"
 #include "nsIStreamConverterService.h"
 #include "nsStringStream.h"
 #include "nsGkAtoms.h"
@@ -814,16 +815,32 @@ nsCORSListenerProxy::UpdateChannel(nsICh
     bool dataScheme = false;
     rv = uri->SchemeIs("data", &dataScheme);
     NS_ENSURE_SUCCESS(rv, rv);
     if (dataScheme) {
       return NS_OK;
     }
   }
 
+  // Set CORS attributes on channel so that intercepted requests get correct
+  // values. We have to do this here because the CheckMayLoad checks may lead
+  // to early return. We can't be sure this is an http channel though, so we
+  // can't return early on failure.
+  nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aChannel);
+  if (internal) {
+    if (mIsPreflight) {
+      rv = internal->SetCorsMode(nsIHttpChannelInternal::CORS_MODE_CORS_WITH_FORCED_PREFLIGHT);
+    } else {
+      rv = internal->SetCorsMode(nsIHttpChannelInternal::CORS_MODE_CORS);
+    }
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = internal->SetCorsIncludeCredentials(mWithCredentials);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
   // Check that the uri is ok to load
   rv = nsContentUtils::GetSecurityManager()->
     CheckLoadURIWithPrincipal(mRequestingPrincipal, uri,
                               nsIScriptSecurityManager::STANDARD);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (originalURI != uri) {
     rv = nsContentUtils::GetSecurityManager()->
--- a/dom/tests/mochitest/fetch/test_request.js
+++ b/dom/tests/mochitest/fetch/test_request.js
@@ -2,48 +2,52 @@ function testDefaultCtor() {
   var req = new Request("");
   is(req.method, "GET", "Default Request method is GET");
   ok(req.headers instanceof Headers, "Request should have non-null Headers object");
   is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
   is(req.context, "fetch", "Default context is fetch.");
   is(req.referrer, "about:client", "Default referrer is `client` which serializes to about:client.");
   is(req.mode, "cors", "Request mode for string input is cors");
   is(req.credentials, "omit", "Default Request credentials is omit");
+  is(req.cache, "default", "Default Request cache is default");
 
   var req = new Request(req);
   is(req.method, "GET", "Default Request method is GET");
   ok(req.headers instanceof Headers, "Request should have non-null Headers object");
   is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
   is(req.context, "fetch", "Default context is fetch.");
   is(req.referrer, "about:client", "Default referrer is `client` which serializes to about:client.");