Bug 867143 - Cache Session Restore state aggressively. r=ttaubert
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Fri, 26 Jul 2013 12:15:00 -0400
changeset 140247 6f8cc9245141fb1cdf7131981d87ab8426ec973c
parent 140104 8da2f00eb92ea5dc7f8b0e2f312153f6c03fd607
child 140248 6a62ce6a092bd57a28f0139b4b7cf5bd7863fb27
push id25021
push userryanvm@gmail.com
push dateSun, 28 Jul 2013 01:51:56 +0000
treeherdermozilla-central@015135250e06 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttaubert
bugs867143
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 867143 - Cache Session Restore state aggressively. r=ttaubert
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/src/SessionStore.jsm
--- 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() {
@@ -532,19 +547,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 +677,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 +708,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 +732,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 +1111,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 +1335,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 +1357,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 +1396,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 +1434,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 +1527,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 +1743,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 +1778,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 +1971,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 +2050,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 +2085,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 +2134,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 +2207,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 +2277,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 +2497,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 +2733,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 +2889,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 +3057,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 +3209,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 +3773,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();
       }
     }
@@ -3973,16 +4055,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 +4878,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 +4919,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);
+  }
+};