Merge m-c to fx-team.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 26 Jul 2013 22:27:22 -0400
changeset 140210 4874fa438b1c9316ae97485a28531959149465a3
parent 140111 db96c1377702f6ad584039f9074979d77dc2fee1 (diff)
parent 140209 fb48c7d58b8be9ce281d9dc032aaa10d5125efd0 (current diff)
child 140212 55ac60d61c0308cb06ea09a0eef4e53ce7cead52
push id1945
push userryanvm@gmail.com
push dateSat, 27 Jul 2013 02:27:26 +0000
treeherderfx-team@4874fa438b1c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
Merge m-c to fx-team.
browser/devtools/webconsole/test/test-bug-595934-canvas.html
browser/devtools/webconsole/test/test-bug-595934-canvas.js
dom/mobilemessage/interfaces/nsIDOMSmsManager.idl
dom/mobilemessage/src/SmsManager.cpp
dom/mobilemessage/src/SmsManager.h
toolkit/themes/linux/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/linux/mozapps/plugins/pluginDisabled.png
toolkit/themes/osx/mozapps/plugins/contentPluginBlocked.png
toolkit/themes/osx/mozapps/plugins/contentPluginClickToPlay.png
toolkit/themes/osx/mozapps/plugins/contentPluginClickToPlayPlain.png
toolkit/themes/osx/mozapps/plugins/contentPluginClose.png
toolkit/themes/osx/mozapps/plugins/contentPluginCrashed.png
toolkit/themes/osx/mozapps/plugins/contentPluginDisabled.png
toolkit/themes/osx/mozapps/plugins/contentPluginDownload.png
toolkit/themes/osx/mozapps/plugins/contentPluginMissing.png
toolkit/themes/osx/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/osx/mozapps/plugins/pluginDisabled.png
toolkit/themes/osx/mozapps/plugins/pluginProblem.css
toolkit/themes/windows/mozapps/plugins/contentPluginBlocked.png
toolkit/themes/windows/mozapps/plugins/contentPluginClickToPlay.png
toolkit/themes/windows/mozapps/plugins/contentPluginClickToPlayPlain.png
toolkit/themes/windows/mozapps/plugins/contentPluginClose.png
toolkit/themes/windows/mozapps/plugins/contentPluginCrashed.png
toolkit/themes/windows/mozapps/plugins/contentPluginDisabled.png
toolkit/themes/windows/mozapps/plugins/contentPluginDownload.png
toolkit/themes/windows/mozapps/plugins/contentPluginMissing.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-16-aero.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-16.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled-aero.png
toolkit/themes/windows/mozapps/plugins/pluginDisabled.png
toolkit/themes/windows/mozapps/plugins/pluginProblem.css
xpcom/glue/SSE.cpp
xpcom/glue/SSE.h
xpcom/glue/arm.cpp
xpcom/glue/arm.h
xpcom/tests/ShowSSEConfig.cpp
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -1,40 +1,77 @@
 /* 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/. */
 
 function debug(msg) {
   Services.console.logStringMessage("SessionStoreContent: " + msg);
 }
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+
 /**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
   DOM_EVENTS: [
-    "pageshow", "change", "input"
+    "pageshow", "change", "input", "MozStorageChanged"
   ],
 
   init: function () {
     this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "pageshow":
         if (event.persisted)
           sendAsyncMessage("SessionStore:pageshow");
         break;
       case "input":
       case "change":
         sendAsyncMessage("SessionStore:input");
         break;
+      case "MozStorageChanged": {
+        let isSessionStorage = true;
+        // We are only interested in sessionStorage events
+        try {
+          if (event.storageArea != content.sessionStorage) {
+            isSessionStorage = false;
+          }
+        } catch (ex) {
+          // This page does not even have sessionStorage
+          // (this is typically the case of about: pages)
+          isSessionStorage = false;
+        }
+        if (isSessionStorage) {
+          sendAsyncMessage("SessionStore:MozStorageChanged");
+        }
+        break;
+      }
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
+EventListener.init();
 
-EventListener.init();
+let ProgressListener = {
+  init: function() {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+  },
+  onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
+    // We are changing page, so time to invalidate the state of the tab
+    sendAsyncMessage("SessionStore:loadStart");
+  },
+  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {},
+  onProgressChange: function() {},
+  onStatusChange: function() {},
+  onSecurityChange: function() {},
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
+ProgressListener.init();
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -53,17 +53,25 @@ const MESSAGES = [
   // The content script tells us that its form data (or that of one of its
   // subframes) might have changed. This can be the contents or values of
   // standard form fields or of ContentEditables.
   "SessionStore:input",
 
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
-  "SessionStore:pageshow"
+  "SessionStore:pageshow",
+
+  // The content script has received a MozStorageChanged event dealing
+  // with a change in the contents of the sessionStorage.
+  "SessionStore:MozStorageChanged",
+
+  // The content script tells us that a new page just started loading in a
+  // browser.
+  "SessionStore:loadStart"
 ];
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -115,19 +123,26 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
   "resource:///modules/sessionstore/_SessionFile.jsm");
 
 #ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   "@mozilla.org/xre/app-info;1", "nsICrashReporter");
 #endif
 
+/**
+ * |true| if we are in debug mode, |false| otherwise.
+ * Debug mode is controlled by preference browser.sessionstore.debug
+ */
+let gDebuggingEnabled = false;
 function debug(aMsg) {
-  aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
-  Services.console.logStringMessage(aMsg);
+  if (gDebuggingEnabled) {
+    aMsg = ("SessionStore: " + aMsg).replace(/\S{80}/g, "$&\n");
+    Services.console.logStringMessage(aMsg);
+  }
 }
 
 this.SessionStore = {
   get promiseInitialized() {
     return SessionStoreInternal.promiseInitialized.promise;
   },
 
   get canRestoreLastSession() {
@@ -461,46 +476,26 @@ let SessionStoreInternal = {
           this._initialState.windows.forEach(function(aWindow) {
             delete aWindow.__lastSessionWindowID;
           });
         }
       }
       catch (ex) { debug("The session file is invalid: " + ex); }
     }
 
-    // A Lazy getter for the sessionstore.js backup promise.
-    XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () {
-      // We're creating a backup of sessionstore.js by moving it to .bak
-      // because that's a lot faster than creating a copy. sessionstore.js
-      // would be overwritten shortly afterwards anyway so we can save time
-      // and just move instead of copy.
-      return _SessionFile.moveToBackupPath();
-    });
-
     // at this point, we've as good as resumed the session, so we can
     // clear the resume_session_once flag, if it's set
     if (this._loadState != STATE_QUITTING &&
         this._prefBranch.getBoolPref("sessionstore.resume_session_once"))
       this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
 
     this._initEncoding();
 
     this._performUpgradeBackup();
 
-    // The service is ready. Backup-on-upgrade might still be in progress,
-    // but we do not have a race condition:
-    //
-    // - if the file to backup is named sessionstore.js, secondary
-    // backup will be started in this tick, so any further I/O will be
-    // scheduled to start after the secondary backup is complete;
-    //
-    // - if the file is named sessionstore.bak, it will only be erased
-    // by the getter to |_backupSessionFileOnce|, which specifically
-    // waits until the secondary backup has been completed or deemed
-    // useless before causing any side-effects.
     this._sessionInitialized = true;
     this._promiseInitialization.resolve();
   },
 
   /**
    * If this is the first time we launc this build of Firefox,
    * backup sessionstore.js.
    */
@@ -532,19 +527,23 @@ let SessionStoreInternal = {
   _initEncoding : function ssi_initEncoding() {
     // The (UTF-8) encoder used to write to files.
     XPCOMUtils.defineLazyGetter(this, "_writeFileEncoder", function () {
       return new TextEncoder();
     });
   },
 
   _initPrefs : function() {
-    XPCOMUtils.defineLazyGetter(this, "_prefBranch", function () {
-      return Services.prefs.getBranch("browser.");
-    });
+    this._prefBranch = Services.prefs.getBranch("browser.");
+
+    gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+
+    Services.prefs.addObserver("browser.sessionstore.debug", () => {
+      gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
+    }, false);
 
     // minimal interval between two save operations (in milliseconds)
     XPCOMUtils.defineLazyGetter(this, "_interval", function () {
       // used often, so caching/observing instead of fetching on-demand
       this._prefBranch.addObserver("sessionstore.interval", this, true);
       return this._prefBranch.getIntPref("sessionstore.interval");
     });
 
@@ -658,16 +657,23 @@ let SessionStoreInternal = {
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
       case "SessionStore:input":
         this.onTabInput(win, browser);
         break;
+      case "SessionStore:MozStorageChanged":
+        TabStateCache.delete(browser);
+        this.saveStateDelayed(win);
+        break;
+      case "SessionStore:loadStart":
+        TabStateCache.delete(browser);
+        break;
       default:
         debug("received unknown message '" + aMessage.name + "'");
         break;
     }
 
     this._clearRestoringWindows();
   },
 
@@ -682,16 +688,17 @@ let SessionStoreInternal = {
 
     var win = aEvent.currentTarget.ownerDocument.defaultView;
     switch (aEvent.type) {
       case "load":
         // If __SS_restore_data is set, then we need to restore the document
         // (form data, scrolling, etc.). This will only happen when a tab is
         // first restored.
         let browser = aEvent.currentTarget;
+        TabStateCache.delete(browser);
         if (browser.__SS_restore_data)
           this.restoreDocument(win, browser, aEvent);
         this.onTabLoad(win, browser);
         break;
       case "TabOpen":
         this.onTabAdd(win, aEvent.originalTarget);
         break;
       case "TabClose":
@@ -705,21 +712,26 @@ let SessionStoreInternal = {
         break;
       case "TabShow":
         this.onTabShow(win, aEvent.originalTarget);
         break;
       case "TabHide":
         this.onTabHide(win, aEvent.originalTarget);
         break;
       case "TabPinned":
-      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", true);
         this.saveStateDelayed(win);
         break;
-    }
-
+      case "TabUnpinned":
+        // If possible, update cached data without having to invalidate it
+        TabStateCache.update(aEvent.originalTarget, "pinned", false);
+        this.saveStateDelayed(win);
+        break;
+    }
     this._clearRestoringWindows();
   },
 
   /**
    * If it's the first window load since app start...
    * - determine if we're reloading after a crash or a forced-restart
    * - restore window state
    * - restart downloads
@@ -1079,16 +1091,17 @@ let SessionStoreInternal = {
     // session data on disk as this notification fires after the
     // quit-application notification so the browser is about to exit.
     if (this._loadState == STATE_QUITTING)
       return;
     this._lastSessionState = null;
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
+        TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
         delete aTab.linkedBrowser.__SS_tabStillLoading;
         delete aTab.linkedBrowser.__SS_formDataSaved;
         delete aTab.linkedBrowser.__SS_hostSchemeData;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
       });
       openWindows[aWindow.__SSi] = true;
@@ -1302,19 +1315,18 @@ let SessionStoreInternal = {
     event.initEvent("SSTabClosing", true, false);
     aTab.dispatchEvent(event);
 
     // don't update our internal state if we don't have to
     if (this._max_tabs_undo == 0) {
       return;
     }
 
-    // make sure that the tab related data is up-to-date
-    var tabState = this._collectTabData(aTab);
-    this._updateTextAndScrollDataForTab(aWindow, aTab.linkedBrowser, tabState);
+    // Get the latest data for this tab (generally, from the cache)
+    let tabState = this._collectTabData(aTab);
 
     // store closed-tab data for undo
     if (this._shouldSaveTabState(tabState)) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
       tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
 
       this._windows[aWindow.__SSi]._closedTabs.unshift({
@@ -1325,32 +1337,35 @@ let SessionStoreInternal = {
       });
       var length = this._windows[aWindow.__SSi]._closedTabs.length;
       if (length > this._max_tabs_undo)
         this._windows[aWindow.__SSi]._closedTabs.splice(this._max_tabs_undo, length - this._max_tabs_undo);
     }
   },
 
   /**
-   * When a tab loads, save state.
+   * When a tab loads, invalidate its cached state, trigger async save.
+   *
    * @param aWindow
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabLoad: function ssi_onTabLoad(aWindow, aBrowser) {
     // react on "load" and solitary "pageshow" events (the first "pageshow"
     // following "load" is too late for deleting the data caches)
     // It's possible to get a load event after calling stop on a browser (when
     // overwriting tabs). We want to return early if the tab hasn't been restored yet.
     if (aBrowser.__SS_restoreState &&
         aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       return;
     }
 
+    TabStateCache.delete(aBrowser);
+
     delete aBrowser.__SS_data;
     delete aBrowser.__SS_tabStillLoading;
     delete aBrowser.__SS_formDataSaved;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
@@ -1361,16 +1376,18 @@ let SessionStoreInternal = {
    *        Window reference
    * @param aBrowser
    *        Browser reference
    */
   onTabInput: function ssi_onTabInput(aWindow, aBrowser) {
     // deleting __SS_formDataSaved will cause us to recollect form data
     delete aBrowser.__SS_formDataSaved;
 
+    TabStateCache.delete(aBrowser);
+
     this.saveStateDelayed(aWindow, 3000);
   },
 
   /**
    * When a tab is selected, save session data
    * @param aWindow
    *        Window reference
    */
@@ -1397,28 +1414,34 @@ let SessionStoreInternal = {
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.hiddenToVisible(aTab);
 
       // let's kick off tab restoration again to ensure this tab gets restored
       // with "restore_hidden_tabs" == false (now that it has become visible)
       this.restoreNextTab();
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", false);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabShow
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   onTabHide: function ssi_onTabHide(aWindow, aTab) {
     // If the tab hasn't been restored yet, move it into the right bucket
     if (aTab.linkedBrowser.__SS_restoreState &&
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.visibleToHidden(aTab);
     }
 
+    // If possible, update cached data without having to invalidate it
+    TabStateCache.update(aTab, "hidden", true);
+
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   /* ........ nsISessionStore API .............. */
 
   getBrowserState: function ssi_getBrowserState() {
@@ -1484,42 +1507,63 @@ let SessionStoreInternal = {
 
     this.restoreWindow(aWindow, aState, aOverwrite);
   },
 
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab);
-
-    var window = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(window, aTab.linkedBrowser, tabState);
+    let tabState = this._collectTabData(aTab);
 
     return this._toJSONString(tabState);
   },
 
   setTabState: function ssi_setTabState(aTab, aState) {
-    var tabState = JSON.parse(aState);
-    if (!tabState.entries || !aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi)
+    // Remove the tab state from the cache.
+    // Note that we cannot simply replace the contents of the cache
+    // as |aState| can be an incomplete state that will be completed
+    // by |restoreHistoryPrecursor|.
+    let tabState = JSON.parse(aState);
+    if (!tabState) {
+      debug("Empty state argument");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (typeof tabState != "object") {
+      debug("State argument does not represent an object");
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
-
-    var window = aTab.ownerDocument.defaultView;
+    }
+    if (!("entries" in tabState)) {
+      debug("State argument must contain field 'entries'");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+    if (!aTab.ownerDocument) {
+      debug("Tab argument must have an owner document");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    let window = aTab.ownerDocument.defaultView;
+    if (!("__SSi" in window)) {
+      debug("Default view of ownerDocument must have a unique identifier");
+      throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+    }
+
+    TabStateCache.delete(aTab);
     this._setWindowStateBusy(window);
     this.restoreHistoryPrecursor(window, [aTab], [tabState], 0, 0, 0);
   },
 
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
         !aWindow.getBrowser)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
-    var tabState = this._collectTabData(aTab, true);
-    var sourceWindow = aTab.ownerDocument.defaultView;
-    this._updateTextAndScrollDataForTab(sourceWindow, aTab.linkedBrowser, tabState, true);
+    // Duplicate the tab state
+    let tabState = this._cloneFullTabData(aTab);
+
     tabState.index += aDelta;
     tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
     tabState.pinned = false;
 
     this._setWindowStateBusy(aWindow);
     let newTab = aTab == aWindow.gBrowser.selectedTab ?
       aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
       aWindow.gBrowser.addTab();
@@ -1679,31 +1723,33 @@ let SessionStoreInternal = {
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
     }
   },
 
   deleteWindowValue: function ssi_deleteWindowValue(aWindow, aKey) {
     if (aWindow.__SSi && this._windows[aWindow.__SSi].extData &&
         this._windows[aWindow.__SSi].extData[aKey])
       delete this._windows[aWindow.__SSi].extData[aKey];
+    this.saveStateDelayed(aWindow);
   },
 
   getTabValue: function ssi_getTabValue(aTab, aKey) {
     let data = {};
     if (aTab.__SS_extdata) {
       data = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       // If the tab hasn't been fully restored, get the data from the to-be-restored data
       data = aTab.linkedBrowser.__SS_data.extData;
     }
     return data[aKey] || "";
   },
 
   setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
+    TabStateCache.delete(aTab);
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
     let saveTo;
     if (aTab.__SS_extdata) {
       saveTo = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       saveTo = aTab.linkedBrowser.__SS_data.extData;
@@ -1712,33 +1758,36 @@ let SessionStoreInternal = {
       aTab.__SS_extdata = {};
       saveTo = aTab.__SS_extdata;
     }
     saveTo[aKey] = aStringValue;
     this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
+    TabStateCache.delete(aTab);
     // We want to make sure that if data is accessed early, we attempt to delete
     // that data from __SS_data as well. Otherwise we'll throw in cases where
     // data can be set or read.
     let deleteFrom;
     if (aTab.__SS_extdata) {
       deleteFrom = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       deleteFrom = aTab.linkedBrowser.__SS_data.extData;
     }
 
     if (deleteFrom && deleteFrom[aKey])
       delete deleteFrom[aKey];
+    this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
+      TabStateCache.clear();
       this.saveStateDelayed();
     }
   },
 
   /**
    * Restores the session state stored in _lastSessionState. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
@@ -1902,47 +1951,66 @@ let SessionStoreInternal = {
     }
 
     return [true, canOverwriteTabs];
   },
 
   /* ........ Saving Functionality .............. */
 
   /**
-   * Store all session data for a window
-   * @param aWindow
-   *        Window reference
+   * Collect data related to a single tab
+   *
+   * @param aTab
+   *        tabbrowser tab
+   *
+   * @returns {TabData} An object with the data for this tab.  If the
+   * tab has not been invalidated since the last call to
+   * _collectTabData(aTab), the same object is returned.
    */
-  _saveWindowHistory: function ssi_saveWindowHistory(aWindow) {
-    var tabbrowser = aWindow.gBrowser;
-    var tabs = tabbrowser.tabs;
-    var tabsData = this._windows[aWindow.__SSi].tabs = [];
-
-    for (var i = 0; i < tabs.length; i++)
-      tabsData.push(this._collectTabData(tabs[i]));
-
-    this._windows[aWindow.__SSi].selected = tabbrowser.mTabBox.selectedIndex + 1;
+  _collectTabData: function ssi_collectTabData(aTab) {
+    if (!aTab) {
+      throw new TypeError("Expecting a tab");
+    }
+    let tabData;
+    if ((tabData = TabStateCache.get(aTab))) {
+      return tabData;
+    }
+    tabData = new TabData(this._collectBaseTabData(aTab));
+    if (this._updateTextAndScrollDataForTab(aTab, tabData)) {
+      TabStateCache.set(aTab, tabData);
+    }
+    return tabData;
   },
 
   /**
-   * Collect data related to a single tab
+   * Collect data related to a single tab, including private data.
+   * Use with caution.
+   *
    * @param aTab
    *        tabbrowser tab
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
-   * @returns object
+   *
+   * @returns {object} An object with the data for this tab. This object
+   * is recomputed at every call.
    */
-  _collectTabData: function ssi_collectTabData(aTab, aFullData) {
-    var tabData = { entries: [], lastAccessed: aTab.lastAccessed };
-    var browser = aTab.linkedBrowser;
-
-    if (!browser || !browser.currentURI)
+  _cloneFullTabData: function ssi_cloneFullTabData(aTab) {
+    let options = { includePrivateData: true };
+    let tabData = this._collectBaseTabData(aTab, options);
+    this._updateTextAndScrollDataForTab(aTab, tabData, options);
+    return tabData;
+  },
+
+  _collectBaseTabData: function ssi_collectBaseTabData(aTab, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let tabData = {entries: [], lastAccessed: aTab.lastAccessed };
+    let browser = aTab.linkedBrowser;
+    if (!browser || !browser.currentURI) {
       // can happen when calling this function right after .addTab()
       return tabData;
-    else if (browser.__SS_data && browser.__SS_tabStillLoading) {
+    }
+    if (browser.__SS_data && browser.__SS_tabStillLoading) {
       // use the data to be restored when the tab hasn't been completely loaded
       tabData = browser.__SS_data;
       if (aTab.pinned)
         tabData.pinned = true;
       else
         delete tabData.pinned;
       tabData.hidden = aTab.hidden;
 
@@ -1962,26 +2030,26 @@ let SessionStoreInternal = {
     }
     catch (ex) { } // this could happen if we catch a tab during (de)initialization
 
     // XXXzeniko anchor navigation doesn't reset __SS_data, so we could reuse
     //           data even when we shouldn't (e.g. Back, different anchor)
     if (history && browser.__SS_data &&
         browser.__SS_data.entries[history.index] &&
         browser.__SS_data.entries[history.index].url == browser.currentURI.spec &&
-        history.index < this._sessionhistory_max_entries - 1 && !aFullData) {
+        history.index < this._sessionhistory_max_entries - 1 && !includePrivateData) {
       tabData = browser.__SS_data;
       tabData.index = history.index + 1;
     }
     else if (history && history.count > 0) {
       browser.__SS_hostSchemeData = [];
       try {
         for (var j = 0; j < history.count; j++) {
           let entry = this._serializeHistoryEntry(history.getEntryAtIndex(j, false),
-                                                  aFullData, aTab.pinned, browser.__SS_hostSchemeData);
+                                                  includePrivateData, aTab.pinned, browser.__SS_hostSchemeData);
           tabData.entries.push(entry);
         }
         // If we make it through the for loop, then we're ok and we should clear
         // any indicator of brokenness.
         delete aTab.__SS_broken_history;
       }
       catch (ex) {
         // In some cases, getEntryAtIndex will throw. This seems to be due to
@@ -1997,17 +2065,17 @@ let SessionStoreInternal = {
           NS_ASSERT(false, "SessionStore failed gathering complete history " +
                            "for the focused window/tab. See bug 669196.");
           aTab.__SS_broken_history = true;
         }
       }
       tabData.index = history.index + 1;
 
       // make sure not to cache privacy sensitive data which shouldn't get out
-      if (!aFullData)
+      if (!includePrivateData)
         browser.__SS_data = tabData;
     }
     else if (browser.currentURI.spec != "about:blank" ||
              browser.contentDocument.body.hasChildNodes()) {
       tabData.entries[0] = { url: browser.currentURI.spec };
       tabData.index = 1;
     }
 
@@ -2046,39 +2114,39 @@ let SessionStoreInternal = {
     tabData.image = tabbrowser.getIcon(aTab);
 
     if (aTab.__SS_extdata)
       tabData.extData = aTab.__SS_extdata;
     else if (tabData.extData)
       delete tabData.extData;
 
     if (history && browser.docShell instanceof Ci.nsIDocShell) {
-      let storageData = SessionStorage.serialize(browser.docShell, aFullData)
+      let storageData = SessionStorage.serialize(browser.docShell, includePrivateData)
       if (Object.keys(storageData).length)
         tabData.storage = storageData;
     }
 
     return tabData;
   },
 
   /**
    * Get an object that is a serialized representation of a History entry
    * Used for data storage
    * @param aEntry
    *        nsISHEntry instance
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    * @param aHostSchemeData
    *        an array of objects with host & scheme keys
    * @returns object
    */
   _serializeHistoryEntry:
-    function ssi_serializeHistoryEntry(aEntry, aFullData, aIsPinned, aHostSchemeData) {
+    function ssi_serializeHistoryEntry(aEntry, aIncludePrivateData, aIsPinned, aHostSchemeData) {
     var entry = { url: aEntry.URI.spec };
 
     try {
       // throwing is expensive, we know that about: pages will throw
       if (entry.url.indexOf("about:") != 0)
         aHostSchemeData.push({ host: aEntry.URI.host, scheme: aEntry.URI.scheme });
     }
     catch (ex) {
@@ -2119,26 +2187,26 @@ let SessionStoreInternal = {
 
     var x = {}, y = {};
     aEntry.getScrollPosition(x, y);
     if (x.value != 0 || y.value != 0)
       entry.scroll = x.value + "," + y.value;
 
     try {
       var prefPostdata = this._prefBranch.getIntPref("sessionstore.postdata");
-      if (aEntry.postData && (aFullData || prefPostdata &&
+      if (aEntry.postData && (aIncludePrivateData || prefPostdata &&
             this.checkPrivacyLevel(aEntry.URI.schemeIs("https"), aIsPinned))) {
         aEntry.postData.QueryInterface(Ci.nsISeekableStream).
                         seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
         var stream = Cc["@mozilla.org/binaryinputstream;1"].
                      createInstance(Ci.nsIBinaryInputStream);
         stream.setInputStream(aEntry.postData);
         var postBytes = stream.readByteArray(stream.available());
         var postdata = String.fromCharCode.apply(null, postBytes);
-        if (aFullData || prefPostdata == -1 ||
+        if (aIncludePrivateData || prefPostdata == -1 ||
             postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length <=
               prefPostdata) {
           // We can stop doing base64 encoding once our serialization into JSON
           // is guaranteed to handle all chars in strings, including embedded
           // nulls.
           entry.postdata_b64 = btoa(postdata);
         }
       }
@@ -2189,119 +2257,113 @@ let SessionStoreInternal = {
 
         if (child) {
           // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
           if (child.URI.schemeIs("wyciwyg")) {
             children = [];
             break;
           }
 
-          children.push(this._serializeHistoryEntry(child, aFullData,
+          children.push(this._serializeHistoryEntry(child, aIncludePrivateData,
                                                     aIsPinned, aHostSchemeData));
         }
       }
 
       if (children.length)
         entry.children = children;
     }
 
     return entry;
   },
 
   /**
-   * go through all tabs and store the current scroll positions
+   * Go through all frames and store the current scroll positions
    * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   */
-  _updateTextAndScrollData: function ssi_updateTextAndScrollData(aWindow) {
-    var browsers = aWindow.gBrowser.browsers;
-    this._windows[aWindow.__SSi].tabs.forEach(function (tabData, i) {
-      try {
-        this._updateTextAndScrollDataForTab(aWindow, browsers[i], tabData);
-      }
-      catch (ex) { debug(ex); } // get as much data as possible, ignore failures (might succeed the next time)
-    }, this);
-  },
-
-  /**
-   * go through all frames and store the current scroll positions
-   * and innerHTML content of WYSIWYG editors
-   * @param aWindow
-   *        Window reference
-   * @param aBrowser
-   *        single browser reference
+   *
+   * @param aTab
+   *        tabbrowser tab
    * @param aTabData
    *        tabData object to add the information to
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
+   * @param options
+   *        An optional object that may contain the following field:
+   *        - includePrivateData: always return privacy sensitive data
+   *          (use with care)
+   * @return false if data should not be cached because the tab
+   *        has not been fully initialized yet.
    */
   _updateTextAndScrollDataForTab:
-    function ssi_updateTextAndScrollDataForTab(aWindow, aBrowser, aTabData, aFullData) {
+    function ssi_updateTextAndScrollDataForTab(aTab, aTabData, aOptions = null) {
+    let includePrivateData = aOptions && aOptions.includePrivateData;
+    let window = aTab.ownerDocument.defaultView;
+    let browser = aTab.linkedBrowser;
     // we shouldn't update data for incompletely initialized tabs
-    if (aBrowser.__SS_data && aBrowser.__SS_tabStillLoading)
-      return;
-
-    var tabIndex = (aTabData.index || aTabData.entries.length) - 1;
+    if (!browser.currentURI
+        || (browser.__SS_data && browser.__SS_tabStillLoading)) {
+      return false;
+    }
+
+    let tabIndex = (aTabData.index || aTabData.entries.length) - 1;
     // entry data needn't exist for tabs just initialized with an incomplete session state
-    if (!aTabData.entries[tabIndex])
-      return;
-
-    let selectedPageStyle = aBrowser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
-                            this._getSelectedPageStyle(aBrowser.contentWindow);
+    if (!aTabData.entries[tabIndex]) {
+      return false;
+    }
+
+    let selectedPageStyle = browser.markupDocumentViewer.authorStyleDisabled ? "_nostyle" :
+                            this._getSelectedPageStyle(browser.contentWindow);
     if (selectedPageStyle)
       aTabData.pageStyle = selectedPageStyle;
     else if (aTabData.pageStyle)
       delete aTabData.pageStyle;
 
-    this._updateTextAndScrollDataForFrame(aWindow, aBrowser.contentWindow,
+    this._updateTextAndScrollDataForFrame(window, browser.contentWindow,
                                           aTabData.entries[tabIndex],
-                                          !aBrowser.__SS_formDataSaved, aFullData,
+                                          !browser.__SS_formDataSaved, includePrivateData,
                                           !!aTabData.pinned);
-    aBrowser.__SS_formDataSaved = true;
-    if (aBrowser.currentURI.spec == "about:config")
+    browser.__SS_formDataSaved = true;
+    if (browser.currentURI.spec == "about:config")
       aTabData.entries[tabIndex].formdata = {
         id: {
-          "textbox": aBrowser.contentDocument.getElementById("textbox").value
+          "textbox": browser.contentDocument.getElementById("textbox").value
         },
         xpath: {}
       };
+      return true;
   },
 
   /**
    * go through all subframes and store all form data, the current
    * scroll positions and innerHTML content of WYSIWYG editors
    * @param aWindow
    *        Window reference
    * @param aContent
    *        frame reference
    * @param aData
    *        part of a tabData object to add the information to
    * @param aUpdateFormData
    *        update all form data for this tab
-   * @param aFullData
+   * @param aIncludePrivateData
    *        always return privacy sensitive data (use with care)
    * @param aIsPinned
    *        the tab is pinned and should be treated differently for privacy
    */
   _updateTextAndScrollDataForFrame:
     function ssi_updateTextAndScrollDataForFrame(aWindow, aContent, aData,
-                                                 aUpdateFormData, aFullData, aIsPinned) {
+                                                 aUpdateFormData, aIncludePrivateData, aIsPinned) {
     for (var i = 0; i < aContent.frames.length; i++) {
       if (aData.children && aData.children[i])
         this._updateTextAndScrollDataForFrame(aWindow, aContent.frames[i],
                                               aData.children[i], aUpdateFormData,
-                                              aFullData, aIsPinned);
+                                              aIncludePrivateData, aIsPinned);
     }
     var isHTTPS = this._getURIFromString((aContent.parent || aContent).
                                          document.location.href).schemeIs("https");
     let topURL = aContent.top.document.location.href;
     let isAboutSR = topURL == "about:sessionrestore" || topURL == "about:welcomeback";
-    if (aFullData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
-      if (aFullData || aUpdateFormData) {
+    if (aIncludePrivateData || this.checkPrivacyLevel(isHTTPS, aIsPinned) || isAboutSR) {
+      if (aIncludePrivateData || aUpdateFormData) {
         let formData = DocumentUtils.getFormData(aContent.document);
 
         // We want to avoid saving data for about:sessionrestore as a string.
         // Since it's stored in the form as stringified JSON, stringifying further
         // causes an explosion of escape characters. cf. bug 467409
         if (formData && isAboutSR) {
           formData.id["sessionData"] = JSON.parse(formData.id["sessionData"]);
         }
@@ -2415,38 +2477,16 @@ let SessionStoreInternal = {
       aHosts[aHost] = aIsPinned;
     }
     else if (aScheme == "file") {
       aHosts[aHost] = true;
     }
   },
 
   /**
-   * store all hosts for a URL
-   * @param aWindow
-   *        Window reference
-   */
-  _updateCookieHosts: function ssi_updateCookieHosts(aWindow) {
-    var hosts = this._internalWindows[aWindow.__SSi].hosts = {};
-
-    // Since _updateCookiesHosts is only ever called for open windows during a
-    // session, we can call into _extractHostsForCookiesFromHostScheme directly
-    // using data that is attached to each browser.
-    for (let i = 0; i < aWindow.gBrowser.tabs.length; i++) {
-      let tab = aWindow.gBrowser.tabs[i];
-      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
-      for (let j = 0; j < hostSchemeData.length; j++) {
-        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
-                                                   hostSchemeData[j].scheme,
-                                                   hosts, true, tab.pinned);
-      }
-    }
-  },
-
-  /**
    * Serialize cookie data
    * @param aWindows
    *        JS object containing window data references
    *        { id: winData, etc. }
    */
   _updateCookies: function ssi_updateCookies(aWindows) {
     function addCookieToHash(aHash, aHost, aPath, aName, aCookie) {
       // lazily build up a 3-dimensional hash, with
@@ -2673,20 +2713,39 @@ let SessionStoreInternal = {
 
     return { windows: [winData] };
   },
 
   _collectWindowData: function ssi_collectWindowData(aWindow) {
     if (!this._isWindowLoaded(aWindow))
       return;
 
+    let tabbrowser = aWindow.gBrowser;
+    let tabs = tabbrowser.tabs;
+    let winData = this._windows[aWindow.__SSi];
+    let tabsData = winData.tabs = [];
+    let hosts = this._internalWindows[aWindow.__SSi].hosts = {};
+
     // update the internal state data for this window
-    this._saveWindowHistory(aWindow);
-    this._updateTextAndScrollData(aWindow);
-    this._updateCookieHosts(aWindow);
+    for (let tab of tabs) {
+      tabsData.push(this._collectTabData(tab));
+
+      // Since we are only ever called for open
+      // windows during a session, we can call into
+      // _extractHostsForCookiesFromHostScheme directly using data
+      // that is attached to each browser.
+      let hostSchemeData = tab.linkedBrowser.__SS_hostSchemeData || [];
+      for (let j = 0; j < hostSchemeData.length; j++) {
+        this._extractHostsForCookiesFromHostScheme(hostSchemeData[j].host,
+                                                   hostSchemeData[j].scheme,
+                                                   hosts, true, tab.pinned);
+      }
+    }
+    winData.selected = tabbrowser.mTabBox.selectedIndex + 1;
+
     this._updateWindowFeatures(aWindow);
 
     // Make sure we keep __SS_lastSessionWindowID around for cases like entering
     // or leaving PB mode.
     if (aWindow.__SS_lastSessionWindowID)
       this._windows[aWindow.__SSi].__lastSessionWindowID =
         aWindow.__SS_lastSessionWindowID;
 
@@ -2810,20 +2869,23 @@ let SessionStoreInternal = {
       winData.tabs[0].hidden = false;
       tabbrowser.showTab(tabs[0]);
     }
 
     // If overwriting tabs, we want to reset each tab's "restoring" state. Since
     // we're overwriting those tabs, they should no longer be restoring. The
     // tabs will be rebuilt and marked if they need to be restored after loading
     // state (in restoreHistoryPrecursor).
+    // We also want to invalidate any cached information on the tab state.
     if (aOverwriteTabs) {
       for (let i = 0; i < tabbrowser.tabs.length; i++) {
+        let tab = tabbrowser.tabs[i];
+        TabStateCache.delete(tab);
         if (tabbrowser.browsers[i].__SS_restoreState)
-          this._resetTabRestoringState(tabbrowser.tabs[i]);
+          this._resetTabRestoringState(tab);
       }
     }
 
     // We want to set up a counter on the window that indicates how many tabs
     // in this window are unrestored. This will be used in restoreNextTab to
     // determine if gRestoreTabsProgressListener should be removed from the window.
     // If we aren't overwriting existing tabs, then we want to add to the existing
     // count in case there are still tabs restoring.
@@ -2975,32 +3037,33 @@ let SessionStoreInternal = {
    *        Counter for number of times delaying b/c browser or history aren't ready
    * @param aRestoreImmediately
    *        Flag to indicate whether the given set of tabs aTabs should be
    *        restored/loaded immediately even if restore_on_demand = true
    */
   restoreHistoryPrecursor:
     function ssi_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount, aRestoreImmediately = false) {
+
     var tabbrowser = aWindow.gBrowser;
 
     // make sure that all browsers and their histories are available
     // - if one's not, resume this check in 100ms (repeat at most 10 times)
     for (var t = aIx; t < aTabs.length; t++) {
       try {
         if (!tabbrowser.getBrowserForTab(aTabs[t]).webNavigation.sessionHistory) {
           throw new Error();
         }
       }
       catch (ex) { // in case browser or history aren't ready yet
         if (aCount < 10) {
           var restoreHistoryFunc = function(self) {
             self.restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab,
                                          aIx, aCount + 1, aRestoreImmediately);
-          }
+          };
           aWindow.setTimeout(restoreHistoryFunc, 100, this);
           return;
         }
       }
     }
 
     if (!this._isWindowLoaded(aWindow)) {
       // from now on, the data will come from the actual window
@@ -3126,17 +3189,16 @@ let SessionStoreInternal = {
       // At this point we're essentially ready for consumers to read/write data
       // via the sessionstore API so we'll send the SSWindowStateReady event.
       this._setWindowStateReady(aWindow);
       return; // no more tabs to restore
     }
 
     var tab = aTabs.shift();
     var tabData = aTabData.shift();
-
     var browser = aWindow.gBrowser.getBrowserForTab(tab);
     var history = browser.webNavigation.sessionHistory;
 
     if (history.count > 0) {
       history.PurgeHistory(history.count);
     }
     history.QueryInterface(Ci.nsISHistoryInternal);
 
@@ -3691,27 +3753,27 @@ let SessionStoreInternal = {
   /**
    * save state delayed by N ms
    * marks window as dirty (i.e. data update can't be skipped)
    * @param aWindow
    *        Window reference
    * @param aDelay
    *        Milliseconds to delay
    */
-  saveStateDelayed: function ssi_saveStateDelayed(aWindow, aDelay) {
+  saveStateDelayed: function ssi_saveStateDelayed(aWindow = null, aDelay = 2000) {
     if (aWindow) {
       this._dirtyWindows[aWindow.__SSi] = true;
     }
 
     if (!this._saveTimer) {
       // interval until the next disk operation is allowed
       var minimalDelay = this._lastSaveTime + this._interval - Date.now();
 
       // if we have to wait, set a timer, otherwise saveState directly
-      aDelay = Math.max(minimalDelay, aDelay || 2000);
+      aDelay = Math.max(minimalDelay, aDelay);
       if (aDelay > 0) {
         this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this._saveTimer.init(this, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
       }
       else {
         this.saveState();
       }
     }
@@ -3817,35 +3879,19 @@ let SessionStoreInternal = {
     Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
     data = stateString.data;
 
     // Don't touch the file if an observer has deleted all state data.
     if (!data) {
       return;
     }
 
-    let promise;
-    // If "sessionstore.resume_from_crash" is true, attempt to backup the
-    // session file first, before writing to it.
-    if (this._resume_from_crash) {
-      // Note that we do not have race conditions here as _SessionFile
-      // guarantees that any I/O operation is completed before proceeding to
-      // the next I/O operation.
-      // Note backup happens only once, on initial save.
-      promise = this._backupSessionFileOnce;
-    } else {
-      promise = Promise.resolve();
-    }
-
-    // Attempt to write to the session file (potentially, depending on
-    // "sessionstore.resume_from_crash" preference, after successful backup).
-    promise = promise.then(function onSuccess() {
-      // Write (atomically) to a session file, using a tmp file.
-      return _SessionFile.write(data);
-    });
+    // Write (atomically) to a session file, using a tmp file.
+    let promise =
+      _SessionFile.write(data, {backupOnFirstWrite: this._resume_from_crash});
 
     // Once the session file is successfully updated, save the time stamp of the
     // last save and notify the observers.
     promise = promise.then(() => {
       this._lastSaveTime = Date.now();
       Services.obs.notifyObservers(null, "sessionstore-state-write-complete",
         "");
     });
@@ -3973,16 +4019,17 @@ let SessionStoreInternal = {
    */
   _getTabForBrowser: function ssi_getTabForBrowser(aBrowser) {
     let window = aBrowser.ownerDocument.defaultView;
     for (let i = 0; i < window.gBrowser.tabs.length; i++) {
       let tab = window.gBrowser.tabs[i];
       if (tab.linkedBrowser == aBrowser)
         return tab;
     }
+    return undefined;
   },
 
   /**
    * Whether or not to resume session, if not recovering from a crash.
    * @returns bool
    */
   _doResumeSession: function ssi_doResumeSession() {
     return this._prefBranch.getIntPref("startup.page") == 3 ||
@@ -4795,21 +4842,35 @@ function SessionStoreSHistoryListener(aT
   this.tab = aTab;
 }
 SessionStoreSHistoryListener.prototype = {
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsISHistoryListener,
     Ci.nsISupportsWeakReference
   ]),
   browser: null,
-  OnHistoryNewEntry: function(aNewURI) { },
-  OnHistoryGoBack: function(aBackURI) { return true; },
-  OnHistoryGoForward: function(aForwardURI) { return true; },
-  OnHistoryGotoIndex: function(aIndex, aGotoURI) { return true; },
-  OnHistoryPurge: function(aNumEntries) { return true; },
+// The following events (with the exception of OnHistoryPurge)
+// accompany either a "load" or a "pageshow" which will in turn cause
+// invalidations.
+  OnHistoryNewEntry: function(aNewURI) {
+
+  },
+  OnHistoryGoBack: function(aBackURI) {
+    return true;
+  },
+  OnHistoryGoForward: function(aForwardURI) {
+    return true;
+  },
+  OnHistoryGotoIndex: function(aIndex, aGotoURI) {
+    return true;
+  },
+  OnHistoryPurge: function(aNumEntries) {
+    TabStateCache.delete(this.tab);
+    return true;
+  },
   OnHistoryReload: function(aReloadURI, aReloadFlags) {
     // On reload, we want to make sure that session history loads the right
     // URI. In order to do that, we will juet call restoreTab. That will remove
     // the history listener and load the right URI.
     SessionStoreInternal.restoreTab(this.tab);
     // Returning false will stop the load that docshell is attempting.
     return false;
   }
@@ -4822,9 +4883,111 @@ String.prototype.hasRootDomain = functio
     return false;
 
   if (this == aDomain)
     return true;
 
   let prevChar = this[index - 1];
   return (index == (this.length - aDomain.length)) &&
          (prevChar == "." || prevChar == "/");
+};
+
+function TabData(obj = null) {
+  if (obj) {
+    if (obj instanceof TabData) {
+      // FIXME: Can we get rid of this?
+      return obj;
+    }
+    for (let [key, value] in Iterator(obj)) {
+      this[key] = value;
+    }
+  }
+  return this;
 }
+
+/**
+ * A cache for tabs data.
+ *
+ * This cache implements a weak map from tabs (as XUL elements)
+ * to tab data (as instances of TabData).
+ *
+ * Note that we should never cache private data, as:
+ * - that data is used very seldom by SessionStore;
+ * - caching private data in addition to public data is memory consuming.
+ */
+let TabStateCache = {
+  _data: new WeakMap(),
+
+  /**
+   * Add or replace an entry in the cache.
+   *
+   * @param {XULElement} aTab The key, which may be either a tab
+   * or the corresponding browser. The binding will disappear
+   * if the tab/browser is destroyed.
+   * @param {TabData} aValue The data associated to |aTab|.
+   */
+  set: function(aTab, aValue) {
+    let key = this._normalizeToBrowser(aTab);
+    if (!(aValue instanceof TabData)) {
+      throw new TypeError("Attempting to cache a non TabData");
+    }
+    this._data.set(key, aValue);
+  },
+
+  /**
+   * Return the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * @return {TabData|undefined} The data if available, |undefined|
+   * otherwise.
+   */
+  get: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    return this._data.get(key);
+  },
+
+  /**
+   * Delete the tab data associated with a tab.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   *
+   * Noop of there is no tab data associated with the tab.
+   */
+  delete: function(aKey) {
+    let key = this._normalizeToBrowser(aKey);
+    this._data.delete(key);
+  },
+
+  /**
+   * Delete all tab data.
+   */
+  clear: function() {
+    this._data.clear();
+  },
+
+  /**
+   * Update in place a piece of data.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   * If the tab/browser is not present, do nothing.
+   * @param {string} aField The field to update.
+   * @param {*} aValue The new value to place in the field.
+   */
+  update: function(aKey, aField, aValue) {
+    let key = this._normalizeToBrowser(aKey);
+    let data = this._data.get(key);
+    if (data) {
+      data[aField] = aValue;
+    }
+  },
+
+  _normalizeToBrowser: function(aKey) {
+    let nodeName = aKey.localName;
+    if (nodeName == "tab") {
+      return aKey.linkedBrowser;
+    }
+    if (nodeName == "browser") {
+      return aKey;
+    }
+    throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
+  }
+};
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -52,16 +52,22 @@ self.onmessage = function (msg) {
 let Agent = {
   // The initial session string as read from disk.
   initialState: null,
 
   // Boolean that tells whether we already wrote
   // the loadState to disk once after startup.
   hasWrittenLoadStateOnce: false,
 
+  // Boolean that tells whether we already made a
+  // call to write(). We will only attempt to move
+  // sessionstore.js to sessionstore.bak on the
+  // first write.
+  hasWrittenState: false,
+
   // The path to sessionstore.js
   path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
 
   // The path to sessionstore.bak
   backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
 
   /**
    * This method is only intended to be called by _SessionFile.syncRead() and
@@ -102,17 +108,29 @@ let Agent = {
 
     // No sessionstore data files found. Return an empty string.
     return "";
   },
 
   /**
    * Write the session to disk.
    */
-  write: function (stateString) {
+  write: function (stateString, options) {
+    if (!this.hasWrittenState) {
+      if (options && options.backupOnFirstWrite) {
+        try {
+          File.move(this.path, this.backupPath);
+        } catch (ex if isNoSuchFileEx(ex)) {
+          // Ignore exceptions about non-existent files.
+        }
+      }
+
+      this.hasWrittenState = true;
+    }
+
     let bytes = Encoder.encode(stateString);
     return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * Writes the session state to disk again but changes session.state to
    * 'running' before doing so. This is intended to be called only once, shortly
    * after startup so that we detect crashes on startup correctly.
@@ -135,29 +153,18 @@ let Agent = {
     try {
       state = JSON.parse(this.initialState);
     } finally {
       this.initialState = null;
     }
 
     state.session = state.session || {};
     state.session.state = loadState;
-    return this.write(JSON.stringify(state));
-  },
-
-  /**
-   * Moves sessionstore.js to sessionstore.bak.
-   */
-  moveToBackupPath: function () {
-    try {
-      return File.move(this.path, this.backupPath);
-    } catch (ex if isNoSuchFileEx(ex)) {
-      // Ignore exceptions about non-existent files.
-      return true;
-    }
+    let bytes = Encoder.encode(JSON.stringify(state));
+    return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"});
   },
 
   /**
    * Creates a copy of sessionstore.js.
    */
   createBackupCopy: function (ext) {
     try {
       return File.copy(this.path, this.backupPath + ext);
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -62,34 +62,28 @@ this._SessionFile = {
     Deprecated.warning(
       "syncRead is deprecated and will be removed in a future version",
       "https://bugzilla.mozilla.org/show_bug.cgi?id=532150")
     return SessionFileInternal.syncRead();
   },
   /**
    * Write the contents of the session file, asynchronously.
    */
-  write: function (aData) {
-    return SessionFileInternal.write(aData);
+  write: function (aData, aOptions = {}) {
+    return SessionFileInternal.write(aData, aOptions);
   },
   /**
    * Writes the initial state to disk again only to change the session's load
    * state. This must only be called once, it will throw an error otherwise.
    */
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
   },
   /**
    * Create a backup copy, asynchronously.
-   */
-  moveToBackupPath: function () {
-    return SessionFileInternal.moveToBackupPath();
-  },
-  /**
-   * Create a backup copy, asynchronously.
    * This is designed to perform backup on upgrade.
    */
   createBackupCopy: function (ext) {
     return SessionFileInternal.createBackupCopy(ext);
   },
   /**
    * Remove a backup copy, asynchronously.
    * This is designed to clean up a backup on upgrade.
@@ -207,24 +201,24 @@ let SessionFileInternal = {
     SessionWorker.post("setInitialState", [text]);
     return text;
   },
 
   read: function () {
     return SessionWorker.post("read").then(msg => msg.ok);
   },
 
-  write: function (aData) {
+  write: function (aData, aOptions) {
     let refObj = {};
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       try {
-        let promise = SessionWorker.post("write", [aData]);
+        let promise = SessionWorker.post("write", [aData, aOptions]);
         // At this point, we measure how long we stop the main thread
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
         // Now wait for the result and measure how long we had to wait for the result
         yield promise;
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       } catch (ex) {
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
@@ -234,20 +228,16 @@ let SessionFileInternal = {
       }
     }.bind(this));
   },
 
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]);
   },
 
-  moveToBackupPath: function () {
-    return SessionWorker.post("moveToBackupPath");
-  },
-
   createBackupCopy: function (ext) {
     return SessionWorker.post("createBackupCopy", [ext]);
   },
 
   removeBackupCopy: function (ext) {
     return SessionWorker.post("removeBackupCopy", [ext]);
   },
 
--- a/browser/components/sessionstore/test/Makefile.in
+++ b/browser/components/sessionstore/test/Makefile.in
@@ -22,16 +22,17 @@ MOCHITEST_BROWSER_FILES = \
 	browser_dying_cache.js \
 	browser_form_restore_events.js \
 	browser_form_restore_events_sample.html \
 	browser_formdata_format.js \
 	browser_formdata_format_sample.html \
 	browser_input.js \
 	browser_input_sample.html \
 	browser_pageshow.js \
+	browser_sessionStorage.js \
         browser_upgrade_backup.js \
 	browser_windowRestore_perwindowpb.js \
 	browser_248970_b_perwindowpb.js \
 	browser_248970_b_sample.html \
 	browser_339445.js \
 	browser_339445_sample.html \
 	browser_345898.js \
 	browser_346337.js \
--- a/browser/components/sessionstore/test/browser_625257.js
+++ b/browser/components/sessionstore/test/browser_625257.js
@@ -1,85 +1,86 @@
 /* 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/. */
 
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+
 // This tests that a tab which is closed while loading is not lost.
 // Specifically, that session store does not rely on an invalid cache when
 // constructing data for a tab which is loading.
 
-// The newly created tab which we load a URL into and try closing/undoing.
-let tab;
-
 // This test steps through the following parts:
 //  1. Tab has been created is loading URI_TO_LOAD.
 //  2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and
 //     tab is scheduled to be removed.
 //  3. After the tab has been closed, undoCloseTab() has been called and the tab
 //     should fully load.
 const URI_TO_LOAD = "about:mozilla";
 
+function waitForLoadStarted(aTab) {
+  let deferred = Promise.defer();
+  waitForContentMessage(aTab.linkedBrowser,
+    "SessionStore:loadStart",
+    1000,
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabLoaded(aTab) {
+  let deferred = Promise.defer();
+  whenBrowserLoaded(aTab.linkedBrowser, deferred.resolve);
+  return deferred.promise;
+}
+
+function waitForTabClosed() {
+  let deferred = Promise.defer();
+  let observer = function() {
+    gBrowser.tabContainer.removeEventListener("TabClose", observer, true);
+    deferred.resolve();
+  };
+  gBrowser.tabContainer.addEventListener("TabClose", observer, true);
+  return deferred.promise;
+}
+
 function test() {
   waitForExplicitFinish();
 
-  gBrowser.addTabsProgressListener(tabsListener);
-
-  tab = gBrowser.addTab();
-
-  tab.linkedBrowser.addEventListener("load", firstOnLoad, true);
-
-  gBrowser.tabContainer.addEventListener("TabClose", onTabClose, true);
-}
-
-function firstOnLoad(aEvent) {
-  tab.linkedBrowser.removeEventListener("load", firstOnLoad, true);
+  Task.spawn(function() {
+    try {
+      // Open a new tab
+      let tab = gBrowser.addTab("about:blank");
+      yield waitForTabLoaded(tab);
 
-  let uri = aEvent.target.location;
-  is(uri, "about:blank", "first load should be for about:blank");
-
-  // Trigger a save state.
-  ss.getBrowserState();
+      // Trigger a save state, to initialize any caches
+      ss.getBrowserState();
 
-  is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
-  ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
+      is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
+      ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
 
-  tab.linkedBrowser.loadURI(URI_TO_LOAD);
-}
+      // Start a load and interrupt it by closing the tab
+      tab.linkedBrowser.loadURI(URI_TO_LOAD);
+      let loaded = yield waitForLoadStarted(tab);
+      ok(loaded, "Load started");
 
-let tabsListener = {
-  onLocationChange: function onLocationChange(aBrowser) {
-    gBrowser.removeTabsProgressListener(tabsListener);
-
-    is(aBrowser.currentURI.spec, URI_TO_LOAD,
-       "should occur after about:blank load and be loading next page");
-
-    // Since we are running in the context of tabs listeners, we do not
-    // want to disrupt other tabs listeners.
-    executeSoon(function() {
+      let tabClosing = waitForTabClosed();
       gBrowser.removeTab(tab);
-    });
-  }
-};
+      info("Now waiting for TabClose to close");
+      yield tabClosing;
 
-function onTabClose(aEvent) {
-  gBrowser.tabContainer.removeEventListener("TabClose", onTabClose, true);
+      // Undo the tab, ensure that it proceeds with loading
+      tab = ss.undoCloseTab(window, 0);
+      yield waitForTabLoaded(tab);
+      is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD, "loading proceeded as expected");
 
-  is(tab.linkedBrowser.currentURI.spec, URI_TO_LOAD,
-     "should only remove when loading page");
+      gBrowser.removeTab(tab);
 
-  executeSoon(function() {
-    tab = ss.undoCloseTab(window, 0);
-    tab.linkedBrowser.addEventListener("load", secondOnLoad, true);
+      executeSoon(finish);
+    } catch (ex) {
+      ok(false, ex);
+      info(ex.stack);
+    }
   });
 }
-
-function secondOnLoad(aEvent) {
-  let uri = aEvent.target.location;
-  is(uri, URI_TO_LOAD, "should load page from undoCloseTab");
-  done();
-}
-
-function done() {
-  tab.linkedBrowser.removeEventListener("load", secondOnLoad, true);
-  gBrowser.removeTab(tab);
-
-  executeSoon(finish);
-}
--- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js
+++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // This tests are for a sessionstore.js atomic backup.
+// Each test will wait for a write to the Session Store
+// before executing.
 
 let tmp = {};
 Cu.import("resource://gre/modules/osfile.jsm", tmp);
 Cu.import("resource://gre/modules/Task.jsm", tmp);
 Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", tmp);
 
 const {OS, Task, _SessionFile} = tmp;
 
@@ -18,17 +20,17 @@ const backupPath = OS.Path.join(OS.Const
 
 // A text decoder.
 let gDecoder = new TextDecoder();
 // Global variables that contain sessionstore.js and sessionstore.bak data for
 // comparison between tests.
 let gSSData;
 let gSSBakData;
 
-// waitForSaveStateComplete waits for a state write completion.
+// Wait for a state write to complete and then execute a callback.
 function waitForSaveStateComplete(aSaveStateCallback) {
   let topic = "sessionstore-state-write-complete";
 
   function observer() {
     Services.prefs.clearUserPref(PREF_SS_INTERVAL);
     Services.obs.removeObserver(observer, topic);
     executeSoon(function taskCallback() {
       Task.spawn(aSaveStateCallback);
@@ -36,61 +38,56 @@ function waitForSaveStateComplete(aSaveS
   }
 
   Services.obs.addObserver(observer, topic, false);
 }
 
 // Register next test callback and trigger state saving change.
 function nextTest(testFunc) {
   waitForSaveStateComplete(testFunc);
+
+  // We set the interval for session store state saves to be zero
+  // to cause a save ASAP.
   Services.prefs.setIntPref(PREF_SS_INTERVAL, 0);
 }
 
 registerCleanupFunction(function() {
   // Cleaning up after the test: removing the sessionstore.bak file.
   Task.spawn(function cleanupTask() {
     yield OS.File.remove(backupPath);
   });
 });
 
 function test() {
   waitForExplicitFinish();
-  nextTest(testInitialWriteNoBackup);
+  nextTest(testAfterFirstWrite);
 }
 
-function testInitialWriteNoBackup() {
-  // Ensure that sessionstore.js is created, but not sessionstore.bak.
-  let ssExists = yield OS.File.exists(path);
-  let ssBackupExists = yield OS.File.exists(backupPath);
-  ok(ssExists, "sessionstore.js should be created.");
-  ok(!ssBackupExists, "sessionstore.bak should not have been created, yet.");
-
-  nextTest(testWriteNoBackup);
-}
-
-function testWriteNoBackup() {
-  // Ensure sessionstore.bak is not created.
+function testAfterFirstWrite() {
+  // Ensure sessionstore.bak is not created. We start with a clean
+  // profile so there was nothing to move to sessionstore.bak before
+  // initially writing sessionstore.js
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js should exist.");
   ok(!ssBackupExists, "sessionstore.bak should not have been created, yet");
 
   // Save sessionstore.js data to compare to the sessionstore.bak data in the
   // next test.
   let array = yield OS.File.read(path);
   gSSData = gDecoder.decode(array);
 
-  // Manually trigger _SessionFile.moveToBackupPath since the backup once
-  // promise is already resolved and backup would not be triggered again.
-  yield _SessionFile.moveToBackupPath();
+  // Manually move to the backup since the first write has already happened
+  // and a backup would not be triggered again.
+  yield OS.File.move(path, backupPath);
 
-  nextTest(testWriteBackup);
+  nextTest(testReadBackup);
 }
 
-function testWriteBackup() {
+function testReadBackup() {
   // Ensure sessionstore.bak is finally created.
   let ssExists = yield OS.File.exists(path);
   let ssBackupExists = yield OS.File.exists(backupPath);
   ok(ssExists, "sessionstore.js exists.");
   ok(ssBackupExists, "sessionstore.bak should now be created.");
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
@@ -122,20 +119,21 @@ function testWriteBackup() {
   ssDataRead = yield _SessionFile.read();
   is(ssDataRead, gSSBakData,
     "_SessionFile.read read sessionstore.bak correctly.");
 
   // Read sessionstore.bak with _SessionFile.syncRead.
   ssDataRead = _SessionFile.syncRead();
   is(ssDataRead, gSSBakData,
     "_SessionFile.syncRead read sessionstore.bak correctly.");
-  nextTest(testNoWriteBackup);
+
+  nextTest(testBackupUnchanged);
 }
 
-function testNoWriteBackup() {
+function testBackupUnchanged() {
   // Ensure sessionstore.bak is backed up only once.
 
   // Read sessionstore.bak data.
   let array = yield OS.File.read(backupPath);
   let ssBakData = gDecoder.decode(array);
   // Ensure the sessionstore.bak did not change.
   is(ssBakData, gSSBakData, "sessionstore.bak is unchanged.");
 
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -25,16 +25,21 @@ function runTests() {
   let state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed by default");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Flip a couple of allow* flags.
   docShell.allowImages = false;
   docShell.allowMetaRedirects = false;
 
+  // Now reload the document to ensure that these capabilities
+  // are taken into account
+  browser.reload();
+  yield whenBrowserLoaded(browser);
+
   // Check that we correctly save disallowed features.
   let disallowedState = JSON.parse(ss.getTabState(tab));
   let disallow = new Set(disallowedState.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Reuse the tab to restore a new, clean state into it.
@@ -47,17 +52,17 @@ function runTests() {
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Restore the state with disallowed features.
   ss.setTabState(tab, JSON.stringify(disallowedState));
   yield waitForLoad(browser);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
-  ok(!docShell.allowMetaRedirects, "meta redirects not allowed")
+  ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
 
   // Check that we correctly restored features as disabled.
   state = JSON.parse(ss.getTabState(tab));
   disallow = new Set(state.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   is(disallow.size, 2, "two capabilities disallowed");
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -0,0 +1,91 @@
+/* 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/. */
+
+let Scope = {};
+Cu.import("resource://gre/modules/Task.jsm", Scope);
+Cu.import("resource://gre/modules/Promise.jsm", Scope);
+let {Task, Promise} = Scope;
+
+function promiseBrowserLoaded(aBrowser) {
+  let deferred = Promise.defer();
+  whenBrowserLoaded(aBrowser, () => deferred.resolve());
+  return deferred.promise;
+}
+
+function forceWriteState() {
+  let deferred = Promise.defer();
+  const PREF = "browser.sessionstore.interval";
+  const TOPIC = "sessionstore-state-write";
+
+  Services.obs.addObserver(function observe() {
+    Services.obs.removeObserver(observe, TOPIC);
+    Services.prefs.clearUserPref(PREF);
+    deferred.resolve();
+  }, TOPIC, false);
+
+  Services.prefs.setIntPref(PREF, 0);
+  return deferred.promise;
+}
+
+function waitForStorageChange(aTab) {
+  let deferred = Promise.defer();
+  waitForContentMessage(aTab.linkedBrowser,
+    "SessionStore:MozStorageChanged",
+    1000,
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function test() {
+
+  waitForExplicitFinish();
+
+  let tab;
+  Task.spawn(function() {
+    try {
+      tab = gBrowser.addTab("http://example.com");
+      // about:home supports sessionStorage and localStorage
+
+      let win = tab.linkedBrowser.contentWindow;
+
+      // Flush loading and next save, call getBrowserState()
+      // a few times to ensure that everything is cached.
+      yield promiseBrowserLoaded(tab.linkedBrowser);
+      yield forceWriteState();
+      info("Calling getBrowserState() to populate cache");
+      ss.getBrowserState();
+
+      info("Change sessionStorage, ensure that state is saved");
+      win.sessionStorage["SESSION_STORAGE_KEY"] = "SESSION_STORAGE_VALUE";
+      let storageChanged = yield waitForStorageChange(tab);
+      ok(storageChanged, "Changing sessionStorage triggered the right message");
+      yield forceWriteState();
+
+      let state = ss.getBrowserState();
+      ok(state.indexOf("SESSION_STORAGE_KEY") != -1, "Key appears in state");
+      ok(state.indexOf("SESSION_STORAGE_VALUE") != -1, "Value appears in state");
+
+
+      info("Change localStorage, ensure that state is not saved");
+      win.localStorage["LOCAL_STORAGE_KEY"] = "LOCAL_STORAGE_VALUE";
+      storageChanged = yield waitForStorageChange(tab);
+      ok(!storageChanged, "Changing localStorage did not trigger a message");
+      yield forceWriteState();
+
+      state = ss.getBrowserState();
+      ok(state.indexOf("LOCAL_STORAGE_KEY") == -1, "Key does not appear in state");
+      ok(state.indexOf("LOCAL_STORAGE_VALUE") == -1, "Value does not appear in state");
+    } catch (ex) {
+      ok(false, ex);
+      info(ex.stack);
+    } finally {
+      // clean up
+      if (tab) {
+        gBrowser.removeTab(tab);
+      }
+
+      executeSoon(finish);
+    }
+  });
+}
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -151,53 +151,100 @@ function waitForTabState(aTab, aState, a
   registerCleanupFunction(function() {
     if (listening) {
       aTab.removeEventListener("SSTabRestored", onSSTabRestored, false);
     }
   });
   ss.setTabState(aTab, JSON.stringify(aState));
 }
 
-// waitForSaveState waits for a state write but not necessarily for the state to
-// turn dirty.
-function waitForSaveState(aSaveStateCallback) {
+/**
+ * Wait for a content -> chrome message.
+ */
+function waitForContentMessage(aBrowser, aTopic, aTimeout, aCallback) {
+  let mm = aBrowser.messageManager;
   let observing = false;
-  let topic = "sessionstore-state-write";
-
-  let sessionSaveTimeout = 1000 +
-    Services.prefs.getIntPref("browser.sessionstore.interval");
-
   function removeObserver() {
     if (!observing)
       return;
-    Services.obs.removeObserver(observer, topic);
+    mm.removeMessageListener(aTopic, observer);
     observing = false;
   }
 
   let timeout = setTimeout(function () {
     removeObserver();
-    aSaveStateCallback();
-  }, sessionSaveTimeout);
+    aCallback(false);
+  }, aTimeout);
 
   function observer(aSubject, aTopic, aData) {
     removeObserver();
     timeout = clearTimeout(timeout);
-    executeSoon(aSaveStateCallback);
+    executeSoon(() => aCallback(true));
   }
 
   registerCleanupFunction(function() {
     removeObserver();
     if (timeout) {
       clearTimeout(timeout);
     }
   });
 
   observing = true;
-  Services.obs.addObserver(observer, topic, false);
-};
+  mm.addMessageListener(aTopic, observer);
+}
+
+function waitForTopic(aTopic, aTimeout, aCallback) {
+  let observing = false;
+  function removeObserver() {
+    if (!observing)
+      return;
+    Services.obs.removeObserver(observer, aTopic);
+    observing = false;
+  }
+
+  let timeout = setTimeout(function () {
+    removeObserver();
+    aCallback(false);
+  }, aTimeout);
+
+  function observer(aSubject, aTopic, aData) {
+    removeObserver();
+    timeout = clearTimeout(timeout);
+    executeSoon(() => aCallback(true));
+  }
+
+  registerCleanupFunction(function() {
+    removeObserver();
+    if (timeout) {
+      clearTimeout(timeout);
+    }
+  });
+
+  observing = true;
+  Services.obs.addObserver(observer, aTopic, false);
+}
+
+/**
+ * Wait until session restore has finished collecting its data and is
+ * getting ready to write that data ("sessionstore-state-write").
+ *
+ * This function is meant to be called immediately after the code
+ * that will trigger the saving.
+ *
+ * Note that this does not wait for the disk write to be complete.
+ *
+ * @param {function} aCallback If sessionstore-state-write is sent
+ * within buffering interval + 100 ms, the callback is passed |true|,
+ * otherwise, it is passed |false|.
+ */
+function waitForSaveState(aCallback) {
+  let timeout = 100 +
+    Services.prefs.getIntPref("browser.sessionstore.interval");
+  return waitForTopic("sessionstore-state-write", timeout, aCallback);
+}
 
 function whenBrowserLoaded(aBrowser, aCallback = next) {
   aBrowser.addEventListener("load", function onLoad() {
     aBrowser.removeEventListener("load", onLoad, true);
     executeSoon(aCallback);
   }, true);
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_backup_once.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toplevel = this;
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  let profd = do_get_profile();
+  Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", toplevel);
+  decoder = new TextDecoder();
+  pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+  pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
+  let source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profd, "sessionstore.js");
+  run_next_test();
+}
+
+let pathStore;
+let pathBackup;
+let decoder;
+
+// Write to the store, and check that a backup is created first
+add_task(function test_first_write_backup() {
+  let content = "test_1";
+  let initial_content = decoder.decode(yield OS.File.read(pathStore));
+
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  do_check_true(yield OS.File.exists(pathBackup));
+
+  let backup_content = decoder.decode(yield OS.File.read(pathBackup));
+  do_check_eq(initial_content, backup_content);
+});
+
+// Write to the store again, and check that the backup is not updated
+add_task(function test_second_write_no_backup() {
+  let content = "test_2";
+  let initial_content = decoder.decode(yield OS.File.read(pathStore));
+  let initial_backup_content = decoder.decode(yield OS.File.read(pathBackup));
+
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+
+  let written_content = decoder.decode(yield OS.File.read(pathStore));
+  do_check_eq(content, written_content);
+
+  let backup_content = decoder.decode(yield OS.File.read(pathBackup));
+  do_check_eq(initial_backup_content, backup_content);
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_no_backup_first_write.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let toplevel = this;
+Cu.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+  let profd = do_get_profile();
+  Cu.import("resource:///modules/sessionstore/_SessionFile.jsm", toplevel);
+  pathStore = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js");
+  pathBackup = OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak");
+  let source = do_get_file("data/sessionstore_valid.js");
+  source.copyTo(profd, "sessionstore.js");
+  run_next_test();
+}
+
+let pathStore;
+let pathBackup;
+
+// Write to the store first with |backupOnFirstWrite: false|,
+// and make sure second write does not backup even with
+// |backupOnFirstWrite: true|
+add_task(function test_no_backup_on_second_write() {
+  let content = "test_1";
+
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+  yield _SessionFile.write(content, {backupOnFirstWrite: false});
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+
+  yield _SessionFile.write(content, {backupOnFirstWrite: true});
+  do_check_true(!(yield OS.File.exists(pathBackup)));
+});
--- a/browser/components/sessionstore/test/unit/xpcshell.ini
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 
 [test_backup.js]
+[test_backup_once.js]
+[test_no_backup_first_write.js]
 [test_startup_nosession_sync.js]
 [test_startup_nosession_async.js]
 [test_startup_session_sync.js]
 [test_startup_session_async.js]
--- a/testing/marionette/components/moz.build
+++ b/testing/marionette/components/moz.build
@@ -1,10 +1,13 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
+EXTRA_COMPONENTS += [
+    'MarionetteComponents.manifest'
+]
+
 EXTRA_PP_COMPONENTS += [
-    'MarionetteComponents.manifest',
-    'marionettecomponent.js',
+    'marionettecomponent.js'
 ]
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -591,18 +591,18 @@ var NodeListActor = exports.NodeListActo
       this.walker.ensurePathToRoot(item, newParents);
     }
     return {
       nodes: items,
       newParents: [node for (node of newParents)]
     }
   }, {
     request: {
-      start: Arg(0, "number", { optional: true }),
-      end: Arg(1, "number", { optional: true })
+      start: Arg(0, "nullable:number"),
+      end: Arg(1, "nullable:number")
     },
     response: RetVal("disconnectedNodeArray")
   }),
 
   release: method(function() {}, { release: true })
 });
 
 /**
@@ -659,17 +659,17 @@ let nodeArrayMethod = {
 };
 
 let traversalMethod = {
   request: {
     node: Arg(0, "domnode"),
     whatToShow: Option(1)
   },
   response: {
-    node: RetVal("domnode", {optional: true})
+    node: RetVal("nullable:domnode")
   }
 }
 
 /**
  * We need to know when a document is navigating away so that we can kill
  * the nodes underneath it.  We also need to know when a document is
  * navigated to so that we can send a mutation event for the iframe node.
  *
@@ -834,32 +834,32 @@ var WalkerActor = protocol.ActorClass({
    * @param NodeActor node
    *        The node whose document is needed, or null to
    *        return the root.
    */
   document: method(function(node) {
     let doc = node ? nodeDocument(node.rawNode) : this.rootDoc;
     return this._ref(doc);
   }, {
-    request: { node: Arg(0, "domnode", {optional: true}) },
+    request: { node: Arg(0, "nullable:domnode") },
     response: { node: RetVal("domnode") },
   }),
 
   /**
    * Return the documentElement for the document containing the
    * given node.
    * @param NodeActor node
    *        The node whose documentElement is requested, or null
    *        to use the root document.
    */
   documentElement: method(function(node) {
     let elt = node ? nodeDocument(node.rawNode).documentElement : this.rootDoc.documentElement;
     return this._ref(elt);
   }, {
-    request: { node: Arg(0, "domnode", {optional: true}) },
+    request: { node: Arg(0, "nullable:domnode") },
     response: { node: RetVal("domnode") },
   }),
 
   /**
    * Return all parents of the given node, ordered from immediate parent
    * to root.
    * @param NodeActor node
    *    The node whose parents are requested.
@@ -1352,17 +1352,17 @@ var WalkerActor = protocol.ActorClass({
       for (let locked of this._activePseudoClassLocks) {
         DOMUtils.clearPseudoClassLocks(locked.rawNode);
         this._activePseudoClassLocks.delete(locked);
         this._queuePseudoClassMutation(locked);
       }
     }
   }, {
     request: {
-      node: Arg(0, "domnode", { optional: true }),
+      node: Arg(0, "nullable:domnode")
     },
     response: {}
   }),
 
   /**
    * Get a node's innerHTML property.
    */
   innerHTML: method(function(node) {
@@ -1407,30 +1407,30 @@ var WalkerActor = protocol.ActorClass({
       // Mutation events will take care of the rest.
     }
     return nextSibling;
   }, {
     request: {
       node: Arg(0, "domnode")
     },
     response: {
-      nextSibling: RetVal("domnode", { optional: true })
+      nextSibling: RetVal("nullable:domnode")
     }
   }),
 
   /**
    * Insert a node into the DOM.
    */
   insertBefore: method(function(node, parent, sibling) {
     parent.rawNode.insertBefore(node.rawNode, sibling ? sibling.rawNode : null);
   }, {
     request: {
       node: Arg(0, "domnode"),
       parent: Arg(1, "domnode"),
-      sibling: Arg(2, "domnode", { optional: true })
+      sibling: Arg(2, "nullable:domnode")
     },
     response: {}
   }),
 
   /**
    * Get any pending mutation records.  Must be called by the client after
    * the `new-mutations` notification is received.  Returns an array of
    * mutation records.
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -76,16 +76,18 @@ types.getType = function(type) {
   // New type, see if it's a collection/lifetime type:
   let sep = type.indexOf(":");
   if (sep >= 0) {
     let collection = type.substring(0, sep);
     let subtype = types.getType(type.substring(sep + 1));
 
     if (collection === "array") {
       return types.addArrayType(subtype);
+    } else if (collection === "nullable") {
+      return types.addNullableType(subtype);
     }
 
     if (registeredLifetimes.has(collection)) {
       return types.addLifetimeType(collection, subtype);
     }
 
     throw Error("Unknown collection type: " + collection);
   }
@@ -101,16 +103,27 @@ types.getType = function(type) {
     require("devtools/server/actors/string");
     return registeredTypes.get("longstring");
   }
 
   throw Error("Unknown type: " + type);
 }
 
 /**
+ * Don't allow undefined when writing primitive types to packets.  If
+ * you want to allow undefined, use a nullable type.
+ */
+function identityWrite(v) {
+  if (v === undefined) {
+    throw Error("undefined passed where a value is required");
+  }
+  return v;
+}
+
+/**
  * Add a type to the type system.
  *
  * When registering a type, you can provide `read` and `write` methods.
  *
  * The `read` method will be passed a JS object value from the JSON
  * packet and must return a native representation.  The `write` method will
  * be passed a native representation and should provide a JSONable value.
  *
@@ -131,18 +144,18 @@ types.getType = function(type) {
 types.addType = function(name, typeObject={}, options={}) {
   if (registeredTypes.has(name)) {
     throw Error("Type '" + name + "' already exists.");
   }
 
   let type = object.merge({
     name: name,
     primitive: !(typeObject.read || typeObject.write),
-    read: v => v,
-    write: v => v
+    read: identityWrite,
+    write: identityWrite
   }, typeObject);
 
   registeredTypes.set(name, type);
 
   if (!options.thawed) {
     Object.freeze(type);
   }
 
@@ -236,22 +249,23 @@ types.addActorType = function(name) {
       // find the actor registered with this actorID.
       if (ctx instanceof Actor) {
         return ctx.conn.getActor(v);
       }
 
       // Reading a response on the client side, check for an
       // existing front on the connection, and create the front
       // if it isn't found.
-      let front = ctx.conn.getActor(v.actor);
+      let actorID = typeof(v) === "string" ? v : v.actor;
+      let front = ctx.conn.getActor(actorID);
       if (front) {
         front.form(v, detail, ctx);
       } else {
         front = new type.frontClass(ctx.conn, v, detail, ctx)
-        front.actorID = v.actor;
+        front.actorID = actorID;
         ctx.marshallPool().manage(front);
       }
       return front;
     },
     write: (v, ctx, detail) => {
       // If returning a response from the server side, make sure
       // the actor is added to a parent object and return its form.
       if (v instanceof Actor) {
@@ -267,16 +281,34 @@ types.addActorType = function(name) {
   }, {
     // We usually freeze types, but actor types are updated when clients are
     // created, so don't freeze yet.
     thawed: true
   });
   return type;
 }
 
+types.addNullableType = function(subtype) {
+  subtype = types.getType(subtype);
+  return types.addType("nullable:" + subtype.name, {
+    read: (value, ctx) => {
+      if (value == null) {
+        return value;
+      }
+      return subtype.read(value, ctx);
+    },
+    write: (value, ctx) => {
+      if (value == null) {
+        return value;
+      }
+      return subtype.write(value, ctx);
+    }
+  });
+}
+
 /**
  * Register an actor detail type.  This is just like an actor type, but
  * will pass a detail hint to the actor's form method during serialization/
  * deserialization.
  *
  * This is called by getType() when passed an 'actorType#detail' string.
  *
  * @param string name
@@ -359,42 +391,29 @@ types.JSON = types.addType("json");
 
 /**
  * Placeholder for simple arguments.
  *
  * @param number index
  *    The argument index to place at this position.
  * @param type type
  *    The argument should be marshalled as this type.
- * @param object options
- *    Argument options:
- *      optional: true if the argument can be undefined or null.
  * @constructor
  */
 let Arg = Class({
-  initialize: function(index, type, options={}) {
+  initialize: function(index, type) {
     this.index = index;
     this.type = types.getType(type);
-    this.optional = !!options.optional;
   },
 
   write: function(arg, ctx) {
-    if (arg === undefined || arg === null) {
-      if (!this.optional) throw Error("Required argument " + this.name + " not specified.");
-      return undefined;
-    }
     return this.type.write(arg, ctx);
   },
 
   read: function(v, ctx, outArgs) {
-    if (v === undefined || v === null) {
-      if (!this.optional) throw Error("Required argument " + this.name + " not specified.");
-      outArgs[this.index] = v;
-      return;
-    }
     outArgs[this.index] = this.type.read(v, ctx);
   }
 });
 exports.Arg = Arg;
 
 /**
  * Placeholder for an options argument value that should be hoisted
  * into the packet.
@@ -441,44 +460,28 @@ let Option = Class({
 
 exports.Option = Option;
 
 /**
  * Placeholder for return values in a response template.
  *
  * @param type type
  *    The return value should be marshalled as this type.
- * @param object options
- *    Argument options:
- *      optional: true if the argument can be undefined or null.
  */
 let RetVal = Class({
-  initialize: function(type, options={}) {
+  initialize: function(type) {
     this.type = types.getType(type);
-    this.optional = !!options.optional;
   },
 
   write: function(v, ctx) {
-    if (v !== undefined && v != null) {
-      return this.type.write(v, ctx);
-    }
-    if (!this.optional) {
-      throw Error("Return value not specified.");
-    }
-    return v;
+    return this.type.write(v, ctx);
   },
 
   read: function(v, ctx) {
-    if (v !== undefined && v != null) {
-      return this.type.read(v, ctx);
-    }
-    if (!this.optional) {
-      throw Error("Return value not specified.");
-    }
-    return v;
+    return this.type.read(v, ctx);
   }
 });
 
 exports.RetVal = RetVal;
 
 /* Template handling functions */
 
 /**
--- a/toolkit/devtools/server/tests/unit/test_protocol_children.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_children.js
@@ -38,16 +38,19 @@ let ChildActor = protocol.ActorClass({
   },
 
   destroy: function() {
     protocol.Actor.prototype.destroy.call(this);
     this.destroyed = true;
   },
 
   form: function(detail) {
+    if (detail === "actorid") {
+      return this.actorID;
+    }
     return {
       actor: this.actorID,
       childID: this.childID,
       detail: detail
     };
   },
 
   echo: method(function(str) {
@@ -67,16 +70,24 @@ let ChildActor = protocol.ActorClass({
 
   getDetail2: method(function() {
     return this;
   }, {
     // This also exercises return-value-as-packet.
     response: RetVal("childActor#detail2"),
   }),
 
+  getIDDetail: method(function() {
+    return this;
+  }, {
+    response: {
+      idDetail: RetVal("childActor#actorid")
+    }
+  }),
+
   getSibling: method(function(id) {
     return this.parent().getChild(id);
   }, {
     request: { id: Arg(0) },
     response: { sibling: RetVal("childActor") }
   }),
 
   emitEvents: method(function() {
@@ -122,17 +133,20 @@ let ChildFront = protocol.FrontClass(Chi
     this.destroyed = true;
     protocol.Front.prototype.destroy.call(this);
   },
 
   marshallPool: function() { return this.parent() },
 
   toString: function() "[child front " + this.childID + "]",
 
-  form: function(form) {
+  form: function(form, detail) {
+    if (detail === "actorid") {
+      return;
+    }
     this.childID = form.childID;
     this.detail = form.detail;
   },
 
   onEvent1: preEvent("event1", function(a, b, c) {
     this.event1arg3 = c;
   }),
 });
@@ -300,16 +314,22 @@ function run_test()
     }).then(() => {
       return childFront.getDetail2();
     }).then(ret => {
       trace.expectSend({"type":"getDetail2","to":"<actorid>"});
       trace.expectReceive({"actor":"<actorid>","childID":"child1","detail":"detail2","from":"<actorid>"});
       do_check_true(ret === childFront);
       do_check_eq(childFront.detail, "detail2");
     }).then(() => {
+      return childFront.getIDDetail();
+    }).then(ret => {
+      trace.expectSend({"type":"getIDDetail","to":"<actorid>"});
+      trace.expectReceive({"idDetail": childFront.actorID,"from":"<actorid>"});
+      do_check_true(ret === childFront);
+    }).then(() => {
       return childFront.getSibling("siblingID");
     }).then(ret => {
       trace.expectSend({"type":"getSibling","id":"siblingID","to":"<actorid>"});
       trace.expectReceive({"sibling":{"actor":"<actorid>","childID":"siblingID"},"from":"<actorid>"});
 
       expectRootChildren(2);
     }).then(ret => {
       return rootFront.getTemporaryChild("temp1").then(temp1 => {
--- a/toolkit/devtools/server/tests/unit/test_protocol_simple.js
+++ b/toolkit/devtools/server/tests/unit/test_protocol_simple.js
@@ -76,17 +76,17 @@ let RootActor = protocol.ActorClass({
     response: RetVal()
   }),
 
   optionalArgs: method(function(a, b=200) {
     return b;
   }, {
     request: {
       a: Arg(0),
-      b: Arg(1, "number", { optional: true })
+      b: Arg(1, "nullable:number")
     },
     response: {
       value: RetVal("number")
     },
   }),
 
   arrayArgs: method(function(a) {
     return a;
@@ -183,16 +183,21 @@ function run_test()
       do_check_eq(ret, 1);
     }).then(() => {
       return rootClient.promiseReturn();
     }).then(ret => {
       trace.expectSend({"type":"promiseReturn","to":"<actorid>"});
       trace.expectReceive({"value":1,"from":"<actorid>"});
       do_check_eq(ret, 1);
     }).then(() => {
+      // Missing argument should throw an exception
+      check_except(() => {
+        rootClient.simpleArgs(5);
+      });
+
       return rootClient.simpleArgs(5, 10)
     }).then(ret => {
       trace.expectSend({"type":"simpleArgs","firstArg":5,"secondArg":10,"to":"<actorid>"});
       trace.expectReceive({"firstResponse":6,"secondResponse":11,"from":"<actorid>"});
       do_check_eq(ret.firstResponse, 6);
       do_check_eq(ret.secondResponse, 11);
     }).then(() => {
       return rootClient.nestedArgs(1, 2, 3);