Bug 1034036 - Part 4: move away from keeping state on the living objects, like windows, tabs and browsers, but keep it truly privately stored in WeakMaps. r?dao draft
authorMike de Boer <mdeboer@mozilla.com>
Tue, 10 Apr 2018 13:04:40 +0200
changeset 779641 d4fa2b4ed430d2c1a2d7ca3bd61b206794ae8e42
parent 779640 93c372f0e72c2ef9c134fddd4c5bbd72682b8820
child 779642 2a712e43cf4f2de12030968286df165e5b48f696
push id105829
push usermdeboer@mozilla.com
push dateTue, 10 Apr 2018 11:05:38 +0000
reviewersdao
bugs1034036
milestone61.0a1
Bug 1034036 - Part 4: move away from keeping state on the living objects, like windows, tabs and browsers, but keep it truly privately stored in WeakMaps. r?dao NOTE: The '__SSi' and '__SS_lastSessionWindowID' properties on windows are kept, because they are expected to stick around longer during application shutdown. The benefits is are: 1. Cleaner code - Sessionstore implementation details are not leaked outside its module. 2. Observing the lifetime of objects becomes unnecessary, because the WeakMaps are cleaned up when objects are GC'd, making leakage of their references impossible and Sessionstore can't hold objects hostage anymore. 3. Simplification - all state is now maintained in SessionStore.jsm, which allows for additional refactoring later on to simplify the implementation further. MozReview-Commit-ID: I1lCeKe8fy4
browser/components/sessionstore/SessionStore.jsm
browser/components/sessionstore/TabState.jsm
browser/components/sessionstore/test/browser_595601-restore_hidden.js
browser/components/sessionstore/test/browser_636279.js
browser/components/sessionstore/test/browser_739805.js
browser/components/sessionstore/test/browser_pending_tabs.js
browser/components/sessionstore/test/browser_restore_redirect.js
browser/components/sessionstore/test/head.js
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -4,19 +4,23 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["SessionStore"];
 
 // Current version of the format used by Session Restore.
 const FORMAT_VERSION = 1;
 
+const TAB_CUSTOM_VALUES = new WeakMap();
+const TAB_LAZY_STATES = new WeakMap();
 const TAB_STATE_NEEDS_RESTORE = 1;
 const TAB_STATE_RESTORING = 2;
 const TAB_STATE_WILL_RESTORE = 3;
+const TAB_STATE_FOR_BROWSER = new WeakMap();
+const WINDOW_RESTORE_IDS = new WeakMap();
 
 // A new window has just been restored. At this stage, tabs are generally
 // not restored.
 const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
 const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
 const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
@@ -248,16 +252,20 @@ var SessionStore = {
   getTabState: function ss_getTabState(aTab) {
     return SessionStoreInternal.getTabState(aTab);
   },
 
   setTabState: function ss_setTabState(aTab, aState) {
     SessionStoreInternal.setTabState(aTab, aState);
   },
 
+  getInternalObjectState(obj) {
+    return SessionStoreInternal.getInternalObjectState(obj);
+  },
+
   duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) {
     return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta, aRestoreImmediately);
   },
 
   getClosedTabCount: function ss_getClosedTabCount(aWindow) {
     return SessionStoreInternal.getClosedTabCount(aWindow);
   },
 
@@ -911,17 +919,17 @@ var SessionStoreInternal = {
               // longer consider its data interesting enough to keep around.
               this.removeClosedTabData(closedTabs, index);
             }
           }
         }
         break;
       case "SessionStore:restoreHistoryComplete": {
         // Notify the tabbrowser that the tab chrome has been restored.
-        let tabData = TabState.collect(tab);
+        let tabData = TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
 
         // wall-paper fix for bug 439675: make sure that the URL to be loaded
         // is always visible in the address bar if no other value is present
         let activePageData = tabData.entries[tabData.index - 1] || null;
         let uri = activePageData ? activePageData.url || null : null;
         // NB: we won't set initial URIs (about:home, about:newtab, etc.) here
         // because their load will not normally trigger a location bar clearing
         // when they finish loading (to avoid race conditions where we then
@@ -939,30 +947,30 @@ var SessionStoreInternal = {
         this.updateTabLabelAndIcon(tab, tabData);
 
         let event = win.document.createEvent("Events");
         event.initEvent("SSTabRestoring", true, false);
         tab.dispatchEvent(event);
         break;
       }
       case "SessionStore:restoreTabContentStarted":
-        if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+        if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) {
           // If a load not initiated by sessionstore was started in a
           // previously pending tab. Mark the tab as no longer pending.
           this.markTabAsRestoring(tab);
         } else if (data.reason != RESTORE_TAB_CONTENT_REASON.NAVIGATE_AND_RESTORE) {
           // If the user was typing into the URL bar when we crashed, but hadn't hit
           // enter yet, then we just need to write that value to the URL bar without
           // loading anything. This must happen after the load, as the load will clear
           // userTypedValue.
           //
           // Note that we only want to do that if we're restoring state for reasons
           // _other_ than a navigateAndRestore remoteness-flip, as such a flip
           // implies that the user was navigating.
-          let tabData = TabState.collect(tab);
+          let tabData = TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
           if (tabData.userTypedValue && !tabData.userTypedClear && !browser.userTypedValue) {
             browser.userTypedValue = tabData.userTypedValue;
             win.URLBarSetURI();
           }
 
           // Remove state we don't need any longer.
           TabStateCache.update(browser, {
             userTypedValue: null, userTypedClear: null
@@ -1180,17 +1188,17 @@ var SessionStoreInternal = {
       } else {
         // Nothing to restore, notify observers things are complete.
         Services.obs.notifyObservers(null, NOTIFY_WINDOWS_RESTORED);
         Services.obs.notifyObservers(null, "sessionstore-one-or-no-tab-restored");
         this._deferredAllWindowsRestored.resolve();
       }
     // this window was opened by _openWindowWithState
     } else if (!this._isWindowLoaded(aWindow)) {
-      let state = this._statesToRestore[aWindow.__SS_restoreID];
+      let state = this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
       let options = {overwriteTabs: true, isFollowUp: state.windows.length == 1};
       this.restoreWindow(aWindow, state.windows[0], options);
     // The user opened another, non-private window after starting up with
     // a single private one. Let's restore the session we actually wanted to
     // restore at startup.
     } else if (this._deferredInitialState && !isPrivateWindow &&
              aWindow.toolbar.visible) {
 
@@ -1348,19 +1356,20 @@ var SessionStoreInternal = {
     let completionPromise = Promise.resolve();
     // this window was about to be restored - conserve its original data, if any
     let isFullyLoaded = this._isWindowLoaded(aWindow);
     if (!isFullyLoaded) {
       if (!aWindow.__SSi) {
         aWindow.__SSi = this._generateWindowID();
       }
 
-      this._windows[aWindow.__SSi] = this._statesToRestore[aWindow.__SS_restoreID];
-      delete this._statesToRestore[aWindow.__SS_restoreID];
-      delete aWindow.__SS_restoreID;
+      let restoreID = WINDOW_RESTORE_IDS.get(aWindow);
+      this._windows[aWindow.__SSi] = this._statesToRestore[restoreID];
+      delete this._statesToRestore[restoreID];
+      WINDOW_RESTORE_IDS.delete(aWindow);
     }
 
     // ignore windows not tracked by SessionStore
     if (!aWindow.__SSi || !this._windows[aWindow.__SSi]) {
       return completionPromise;
     }
 
     // notify that the session store will stop tracking this window so that
@@ -1894,23 +1903,23 @@ var SessionStoreInternal = {
     browser.addEventListener("SwapDocShells", this);
     browser.addEventListener("oop-browser-crashed", this);
 
     if (browser.frameLoader) {
       this._lastKnownFrameLoader.set(browser.permanentKey, browser.frameLoader);
     }
 
     // Only restore if browser has been lazy.
-    if (aTab.__SS_lazyData && !browser.__SS_restoreState && TabStateCache.get(browser)) {
-      let tabState = TabState.clone(aTab);
+    if (TAB_LAZY_STATES.has(aTab) && !TAB_STATE_FOR_BROWSER.has(browser) && TabStateCache.has(browser)) {
+      let tabState = TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
       this.restoreTab(aTab, tabState);
     }
 
     // The browser has been inserted now, so lazy data is no longer relevant.
-    delete aTab.__SS_lazyData;
+    TAB_LAZY_STATES.delete(aTab);
   },
 
   /**
    * remove listeners for a tab
    * @param aWindow
    *        Window reference
    * @param aTab
    *        Tab reference
@@ -1940,17 +1949,17 @@ var SessionStoreInternal = {
     aTab.dispatchEvent(event);
 
     // don't update our internal state if we don't have to
     if (this._max_tabs_undo == 0) {
       return;
     }
 
     // Get the latest data for this tab (generally, from the cache)
-    let tabState = TabState.collect(aTab);
+    let tabState = TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
 
     // Don't save private tabs
     let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
     if (!isPrivateWindow && tabState.isPrivate) {
       return;
     }
 
     // Store closed-tab data for undo.
@@ -2003,39 +2012,39 @@ var SessionStoreInternal = {
     this.cleanUpRemovedBrowser(aTab);
 
     aTab.setAttribute("pending", "true");
 
     this._lastKnownFrameLoader.delete(browser.permanentKey);
     this._crashedBrowsers.delete(browser.permanentKey);
     aTab.removeAttribute("crashed");
 
-    aTab.__SS_lazyData = {
+    TAB_LAZY_STATES.set(aTab, {
       url: browser.currentURI.spec,
       title: aTab.label,
       userTypedValue: browser.userTypedValue || "",
       userTypedClear: browser.userTypedClear || 0
-    };
+    });
   },
 
   /**
    * When a tab is removed or suspended, remove listeners and reset restoring state.
    * @param aBrowser
    *        Browser reference
    */
   cleanUpRemovedBrowser(aTab) {
     let browser = aTab.linkedBrowser;
 
     browser.removeEventListener("SwapDocShells", this);
     browser.removeEventListener("oop-browser-crashed", this);
 
     // If this tab was in the middle of restoring or still needs to be restored,
     // we need to reset that state. If the tab was restoring, we will attempt to
     // restore the next tab.
-    let previousState = browser.__SS_restoreState;
+    let previousState = TAB_STATE_FOR_BROWSER.get(browser);
     if (previousState) {
       this._resetTabRestoringState(aTab);
       if (previousState == TAB_STATE_RESTORING)
         this.restoreNextTab();
     }
   },
 
   /**
@@ -2110,18 +2119,18 @@ var SessionStoreInternal = {
    */
   onTabSelect: function ssi_onTabSelect(aWindow) {
     if (RunState.isRunning) {
       this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
 
       let tab = aWindow.gBrowser.selectedTab;
       let browser = tab.linkedBrowser;
 
-      if (browser.__SS_restoreState &&
-          browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
+      if (TAB_STATE_FOR_BROWSER.get(browser) == TAB_STATE_NEEDS_RESTORE) {
+        // If BROWSER_STATE is still available for the browser and it is
         // If __SS_restoreState is still on the browser and it is
         // TAB_STATE_NEEDS_RESTORE, then then we haven't restored
         // this tab yet.
         //
         // It's possible that this tab was recently revived, and that
         // we've deferred showing the tab crashed page for it (if the
         // tab crashed in the background). If so, we need to re-enter
         // the crashed state, since we'll be showing the tab crashed
@@ -2132,35 +2141,33 @@ var SessionStoreInternal = {
           this.restoreTabContent(tab);
         }
       }
     }
   },
 
   onTabShow: function ssi_onTabShow(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) {
+    if (TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == 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();
     }
 
     // Default delay of 2 seconds gives enough time to catch multiple TabShow
     // events. This used to be due to changing groups in 'tab groups'. We
     // might be able to get rid of this now?
     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) {
+    if (TAB_STATE_FOR_BROWSER.get(aTab.linkedBrowser) == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.visibleToHidden(aTab);
     }
 
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events. This used to be due to changing groups in 'tab groups'. We
     // might be able to get rid of this now?
     this.saveStateDelayed(aWindow);
   },
@@ -2192,17 +2199,17 @@ var SessionStoreInternal = {
     this._crashedBrowsers.add(browser.permanentKey);
 
     let win = browser.ownerGlobal;
 
     // If we hadn't yet restored, or were still in the midst of
     // restoring this browser at the time of the crash, we need
     // to reset its state so that we can try to restore it again
     // when the user revives the tab from the crash.
-    if (browser.__SS_restoreState) {
+    if (TAB_STATE_FOR_BROWSER.has(browser)) {
       let tab = win.gBrowser.getTabForBrowser(browser);
       this._resetLocalTabRestoringState(tab);
     }
   },
 
   // Clean up data that has been closed a long time ago.
   // Do not reschedule a save. This will wait for the next regular
   // save.
@@ -2336,17 +2343,17 @@ var SessionStoreInternal = {
   getTabState: function ssi_getTabState(aTab) {
     if (!aTab || !aTab.ownerGlobal) {
       throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!aTab.ownerGlobal.__SSi) {
       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    let tabState = TabState.collect(aTab);
+    let tabState = TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
 
     return JSON.stringify(tabState);
   },
 
   setTabState(aTab, aState) {
     // 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
@@ -2362,26 +2369,33 @@ var SessionStoreInternal = {
       throw Components.Exception("Invalid state object: no entries", Cr.NS_ERROR_INVALID_ARG);
     }
 
     let window = aTab.ownerGlobal;
     if (!window || !("__SSi" in window)) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
-    if (aTab.linkedBrowser.__SS_restoreState) {
+    if (TAB_STATE_FOR_BROWSER.has(aTab.linkedBrowser)) {
       this._resetTabRestoringState(aTab);
     }
 
     this.restoreTab(aTab, tabState);
 
     // Notify of changes to closed objects.
     this._notifyOfClosedObjectsChange();
   },
 
+  getInternalObjectState(obj) {
+    if (obj.__SSi) {
+      return this._windows[obj.__SSi];
+    }
+    return obj.loadURI ? TAB_STATE_FOR_BROWSER.get(obj) : TAB_CUSTOM_VALUES.get(obj);
+  },
+
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0, aRestoreImmediately = true) {
     if (!aTab || !aTab.ownerGlobal) {
       throw Components.Exception("Need a valid tab", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!aTab.ownerGlobal.__SSi) {
       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!aWindow.gBrowser) {
@@ -2394,17 +2408,17 @@ var SessionStoreInternal = {
       aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab, userContextId}) :
       aWindow.gBrowser.addTab(null, {userContextId});
 
     // Start the throbber to pretend we're doing something while actually
     // waiting for data from the frame script.
     newTab.setAttribute("busy", "true");
 
     // Collect state before flushing.
-    let tabState = TabState.clone(aTab);
+    let tabState = TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
 
     // Flush to get the latest tab state to duplicate.
     let browser = aTab.linkedBrowser;
     TabStateFlusher.flush(browser).then(() => {
       // The new tab might have been closed in the meantime.
       if (newTab.closing || !newTab.linkedBrowser) {
         return;
       }
@@ -2591,52 +2605,53 @@ var SessionStoreInternal = {
   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);
   },
 
   getCustomTabValue(aTab, aKey) {
-    return (aTab.__SS_extdata || {})[aKey] || "";
+    return (TAB_CUSTOM_VALUES.get(aTab) || {})[aKey] || "";
   },
 
   setCustomTabValue(aTab, aKey, aStringValue) {
     if (typeof aStringValue != "string") {
       throw new TypeError("setCustomTabValue only accepts string values");
     }
 
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
-    if (!aTab.__SS_extdata) {
-      aTab.__SS_extdata = {};
-    }
-
-    aTab.__SS_extdata[aKey] = aStringValue;
+    if (!TAB_CUSTOM_VALUES.has(aTab)) {
+      TAB_CUSTOM_VALUES.set(aTab, {});
+    }
+
+    TAB_CUSTOM_VALUES.get(aTab)[aKey] = aStringValue;
     this.saveStateDelayed(aTab.ownerGlobal);
   },
 
   deleteCustomTabValue(aTab, aKey) {
-    if (aTab.__SS_extdata && aKey in aTab.__SS_extdata) {
-      delete aTab.__SS_extdata[aKey];
+    let state = TAB_CUSTOM_VALUES.get(aTab);
+    if (state && aKey in state) {
+      delete state[aKey];
       this.saveStateDelayed(aTab.ownerGlobal);
     }
   },
 
   /**
    * Retrieves data specific to lazy-browser tabs.  If tab is not lazy,
    * will return undefined.
    *
    * @param aTab (xul:tab)
    *        The tabbrowser-tab the data is for.
    * @param aKey (string)
    *        The key which maps to the desired data.
    */
   getLazyTabValue(aTab, aKey) {
-    return (aTab.__SS_lazyData || {})[aKey];
+    return (TAB_LAZY_STATES.get(aTab) || {})[aKey];
   },
 
   getGlobalValue: function ssi_getGlobalValue(aKey) {
     return this._globalState.get(aKey);
   },
 
   setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
     if (typeof aStringValue != "string") {
@@ -2708,17 +2723,17 @@ var SessionStoreInternal = {
     if (tab.hasAttribute("customizemode")) {
       return;
     }
 
     let browser = tab.linkedBrowser;
     let win = browser.ownerGlobal;
 
     if (!tabData) {
-      tabData = TabState.collect(tab);
+      tabData = TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
       if (!tabData) {
         throw new Error("tabData not found for given tab");
       }
     }
 
     let activePageData = tabData.entries[tabData.index - 1] || null;
 
     // If the page has a title, set it.
@@ -2904,17 +2919,17 @@ var SessionStoreInternal = {
 
     // We put the browser at about:blank in case the user is
     // restoring tabs on demand. This way, the user won't see
     // a flash of the about:tabcrashed page after selecting
     // the revived tab.
     aTab.removeAttribute("crashed");
     browser.loadURI("about:blank");
 
-    let data = TabState.collect(aTab);
+    let data = TabState.collect(aTab, TAB_CUSTOM_VALUES.get(aTab));
     this.restoreTab(aTab, data, {
       forceOnDemand: true,
     });
   },
 
   /**
    * Revive all crashed tabs and reset the crashed tabs count to 0.
    */
@@ -2985,17 +3000,17 @@ var SessionStoreInternal = {
 
       let refreshedWindow = tab.ownerGlobal;
 
       // The tab or its window might be gone.
       if (!refreshedWindow || !refreshedWindow.__SSi || refreshedWindow.closed) {
         return;
       }
 
-      let tabState = TabState.clone(tab);
+      let tabState = TabState.clone(tab, TAB_CUSTOM_VALUES.get(tab));
       let options = {
         restoreImmediately: true,
         // We want to make sure that this information is passed to restoreTab
         // whether or not a historyIndex is passed in. Thus, we extract it from
         // the loadArguments.
         newFrameloader: recentLoadArguments.newFrameloader,
         remoteType: recentLoadArguments.remoteType,
         // Make sure that SessionStore knows that this restoration is due
@@ -3006,25 +3021,25 @@ var SessionStoreInternal = {
       if (historyIndex >= 0) {
         tabState.index = historyIndex + 1;
         tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
       } else {
         options.loadArguments = recentLoadArguments;
       }
 
       // Need to reset restoring tabs.
-      if (tab.linkedBrowser.__SS_restoreState) {
+      if (TAB_STATE_FOR_BROWSER.has(tab.linkedBrowser)) {
         this._resetLocalTabRestoringState(tab);
       }
 
       // Restore the state into the tab.
       this.restoreTab(tab, tabState, options);
     });
 
-    tab.linkedBrowser.__SS_restoreState = TAB_STATE_WILL_RESTORE;
+    TAB_STATE_FOR_BROWSER.set(tab.linkedBrowser, TAB_STATE_WILL_RESTORE);
 
     // Notify of changes to closed objects.
     this._notifyOfClosedObjectsChange();
   },
 
   /**
    * Retrieves the latest session history information for a tab. The cached data
    * is returned immediately, but a callback may be provided that supplies
@@ -3043,17 +3058,17 @@ var SessionStoreInternal = {
         if (sessionHistory) {
           updatedCallback(sessionHistory);
         }
       });
     }
 
     // Don't continue if the tab was closed before TabStateFlusher.flush resolves.
     if (tab.linkedBrowser) {
-      let tabState = TabState.collect(tab);
+      let tabState = TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
       return { index: tabState.index - 1, entries: tabState.entries };
     }
     return null;
   },
 
   /**
    * See if aWindow is usable for use when restoring a previous session via
    * restoreLastSession. If usable, prepare it for use.
@@ -3255,17 +3270,17 @@ var SessionStoreInternal = {
   /**
    * serialize session data for a window
    * @param aWindow
    *        Window reference
    * @returns string
    */
   _getWindowState: function ssi_getWindowState(aWindow) {
     if (!this._isWindowLoaded(aWindow))
-      return this._statesToRestore[aWindow.__SS_restoreID];
+      return this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
 
     if (RunState.isRunning) {
       this._collectWindowData(aWindow);
     }
 
     return { windows: [this._windows[aWindow.__SSi]] };
   },
 
@@ -3286,17 +3301,17 @@ var SessionStoreInternal = {
 
     let tabbrowser = aWindow.gBrowser;
     let tabs = tabbrowser.tabs;
     let winData = this._windows[aWindow.__SSi];
     let tabsData = winData.tabs = [];
 
     // update the internal state data for this window
     for (let tab of tabs) {
-      let tabData = TabState.collect(tab);
+      let tabData = TabState.collect(tab, TAB_CUSTOM_VALUES.get(tab));
       tabMap.set(tab, tabData);
       tabsData.push(tabData);
     }
     winData.selected = tabbrowser.tabbox.selectedIndex + 1;
 
     this._updateWindowFeatures(aWindow);
 
     // Make sure we keep __SS_lastSessionWindowID around for cases like entering
@@ -3645,18 +3660,18 @@ var SessionStoreInternal = {
    *        indicates the first tab should be selected, and "0" indicates that
    *        the currently selected tab will not be changed.
    */
   restoreTabs(aWindow, aTabs, aTabData, aSelectTab) {
     var tabbrowser = aWindow.gBrowser;
 
     if (!this._isWindowLoaded(aWindow)) {
       // from now on, the data will come from the actual window
-      delete this._statesToRestore[aWindow.__SS_restoreID];
-      delete aWindow.__SS_restoreID;
+      delete this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)];
+      WINDOW_RESTORE_IDS.delete(aWindow);
       delete this._windows[aWindow.__SSi]._restoring;
     }
 
     let numTabsToRestore = aTabs.length;
     let numTabsInWindow = tabbrowser.tabs.length;
     let tabsDataArray = this._windows[aWindow.__SSi].tabs;
 
     // Update the window state in case we shut down without being notified.
@@ -3690,17 +3705,17 @@ var SessionStoreInternal = {
       }
     }
   },
 
   // Restores the given tab state for a given tab.
   restoreTab(tab, tabData, options = {}) {
     let browser = tab.linkedBrowser;
 
-    if (browser.__SS_restoreState) {
+    if (TAB_STATE_FOR_BROWSER.has(browser)) {
       Cu.reportError("Must reset tab before calling restoreTab.");
       return;
     }
 
     let loadArguments = options.loadArguments;
     let window = tab.ownerGlobal;
     let tabbrowser = window.gBrowser;
     let forceOnDemand = options.forceOnDemand;
@@ -3759,19 +3774,19 @@ var SessionStoreInternal = {
       // Ensure that we persist tab attributes restored from previous sessions.
       Object.keys(tabData.attributes).forEach(a => TabAttributes.persist(a));
     }
 
     if (!tabData.entries) {
       tabData.entries = [];
     }
     if (tabData.extData) {
-      tab.__SS_extdata = Cu.cloneInto(tabData.extData, {});
+      TAB_CUSTOM_VALUES.set(tab, Cu.cloneInto(tabData.extData, {}));
     } else {
-      delete tab.__SS_extdata;
+      TAB_CUSTOM_VALUES.delete(tab);
     }
 
     // Tab is now open.
     delete tabData.closedAt;
 
     // Ensure the index is in bounds.
     let activeIndex = (tabData.index || tabData.entries.length) - 1;
     activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
@@ -3815,17 +3830,17 @@ var SessionStoreInternal = {
       // Start a new epoch to discard all frame script messages relating to a
       // previous epoch. All async messages that are still on their way to chrome
       // will be ignored and don't override any tab data set when restoring.
       let epoch = this.startNextEpoch(browser);
 
       // Ensure that the tab will get properly restored in the event the tab
       // crashes while restoring.  But don't set this on lazy browsers as
       // restoreTab will get called again when the browser is instantiated.
-      browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
+      TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_NEEDS_RESTORE);
 
       this._sendRestoreHistory(browser, {tabData, epoch, loadArguments});
 
       // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
       // it ensures each window will have its selected tab loaded.
       if (willRestoreImmediately) {
         this.restoreTabContent(tab, options);
       } else if (!forceOnDemand) {
@@ -3841,33 +3856,33 @@ var SessionStoreInternal = {
               tab.__test_connection_prepared = prepared;
               tab.__test_connection_url = url;
             }
           }
         }
         this.restoreNextTab();
       }
     } else {
-      // __SS_lazyData holds data for lazy-browser tabs to proxy for
+      // TAB_LAZY_STATES holds data for lazy-browser tabs to proxy for
       // data unobtainable from the unbound browser.  This only applies to lazy
       // browsers and will be removed once the browser is inserted in the document.
       // This must preceed `updateTabLabelAndIcon` call for required data to be present.
       let url = "about:blank";
       let title = "";
 
       if (activeIndex in tabData.entries) {
         url = tabData.entries[activeIndex].url;
         title = tabData.entries[activeIndex].title || url;
       }
-      tab.__SS_lazyData = {
+      TAB_LAZY_STATES.set(tab, {
         url,
         title,
         userTypedValue: tabData.userTypedValue || "",
         userTypedClear: tabData.userTypedClear || 0
-      };
+      });
     }
 
     if (tab.hasAttribute("customizemode")) {
       window.gCustomizeMode.setTab(tab);
     }
 
     // Update tab label and icon to show something
     // while we wait for the messages to be processed.
@@ -3889,17 +3904,17 @@ var SessionStoreInternal = {
     let loadArguments = aOptions.loadArguments;
     if (aTab.hasAttribute("customizemode") && !loadArguments) {
       return;
     }
 
     let browser = aTab.linkedBrowser;
     let window = aTab.ownerGlobal;
     let tabbrowser = window.gBrowser;
-    let tabData = TabState.clone(aTab);
+    let tabData = TabState.clone(aTab, TAB_CUSTOM_VALUES.get(aTab));
     let activeIndex = tabData.index - 1;
     let activePageData = tabData.entries[activeIndex] || null;
     let uri = activePageData ? activePageData.url || null : null;
     if (loadArguments) {
       uri = loadArguments.uri;
       if (loadArguments.userContextId) {
         browser.setAttribute("usercontextid", loadArguments.userContextId);
       }
@@ -3955,28 +3970,28 @@ var SessionStoreInternal = {
   /**
    * Marks a given pending tab as restoring.
    *
    * @param aTab
    *        the pending tab to mark as restoring
    */
   markTabAsRestoring(aTab) {
     let browser = aTab.linkedBrowser;
-    if (browser.__SS_restoreState != TAB_STATE_NEEDS_RESTORE) {
+    if (TAB_STATE_FOR_BROWSER.get(browser) != TAB_STATE_NEEDS_RESTORE) {
       throw new Error("Given tab is not pending.");
     }
 
     // Make sure that this tab is removed from the priority queue.
     TabRestoreQueue.remove(aTab);
 
     // Increase our internal count.
     this._tabsRestoringCount++;
 
     // Set this tab's state to restoring
-    browser.__SS_restoreState = TAB_STATE_RESTORING;
+    TAB_STATE_FOR_BROWSER.set(browser, TAB_STATE_RESTORING);
     aTab.removeAttribute("pending");
   },
 
   /**
    * This _attempts_ to restore the next available tab. If the restore fails,
    * then we will attempt the next one.
    * There are conditions where this won't do anything:
    *   if we're in the process of quitting
@@ -4278,17 +4293,18 @@ var SessionStoreInternal = {
 
     var window =
       Services.ww.openWindow(null, this._prefBranch.getCharPref("chromeURL"),
                              "_blank", features, argString);
 
     do {
       var ID = "window" + Math.random();
     } while (ID in this._statesToRestore);
-    this._statesToRestore[(window.__SS_restoreID = ID)] = aState;
+    WINDOW_RESTORE_IDS.set(window, ID);
+    this._statesToRestore[ID] = aState;
 
     return window;
   },
 
   /**
    * Whether or not to resume session, if not recovering from a crash.
    * @returns bool
    */
@@ -4603,17 +4619,17 @@ var SessionStoreInternal = {
   _setWindowStateBusyValue:
     function ssi_changeWindowStateBusyValue(aWindow, aValue) {
 
     this._windows[aWindow.__SSi].busy = aValue;
 
     // Keep the to-be-restored state in sync because that is returned by
     // getWindowState() as long as the window isn't loaded, yet.
     if (!this._isWindowLoaded(aWindow)) {
-      let stateToRestore = this._statesToRestore[aWindow.__SS_restoreID].windows[0];
+      let stateToRestore = this._statesToRestore[WINDOW_RESTORE_IDS.get(aWindow)].windows[0];
       stateToRestore.busy = aValue;
     }
   },
 
   /**
    * Set the given window's state to 'not busy'.
    * @param aWindow the window
    */
@@ -4684,17 +4700,17 @@ var SessionStoreInternal = {
 
   /**
    * @param aWindow
    *        Window reference
    * @returns whether this window's data is still cached in _statesToRestore
    *          because it's not fully loaded yet
    */
   _isWindowLoaded: function ssi_isWindowLoaded(aWindow) {
-    return !aWindow.__SS_restoreID;
+    return !WINDOW_RESTORE_IDS.has(aWindow);
   },
 
   /**
    * Resize this._closedWindows to the value of the pref, except in the case
    * where we don't have any non-popup windows on Windows and Linux. Then we must
    * resize such that we have at least one non-popup window.
    */
   _capClosedWindows: function ssi_capClosedWindows() {
@@ -4745,25 +4761,25 @@ var SessionStoreInternal = {
    *
    * @param aTab
    *        The tab that will be "reset"
    */
   _resetLocalTabRestoringState(aTab) {
     let browser = aTab.linkedBrowser;
 
     // Keep the tab's previous state for later in this method
-    let previousState = browser.__SS_restoreState;
+    let previousState = TAB_STATE_FOR_BROWSER.get(browser);
 
     if (!previousState) {
       Cu.reportError("Given tab is not restoring.");
       return;
     }
 
     // The browser is no longer in any sort of restoring state.
-    delete browser.__SS_restoreState;
+    TAB_STATE_FOR_BROWSER.delete(browser);
 
     aTab.removeAttribute("pending");
 
     if (previousState == TAB_STATE_RESTORING) {
       if (this._tabsRestoringCount)
         this._tabsRestoringCount--;
     } else if (previousState == TAB_STATE_NEEDS_RESTORE) {
       // Make sure that the tab is removed from the list of tabs to restore.
@@ -4771,17 +4787,17 @@ var SessionStoreInternal = {
       // for this tab.
       TabRestoreQueue.remove(aTab);
     }
   },
 
   _resetTabRestoringState(tab) {
     let browser = tab.linkedBrowser;
 
-    if (!browser.__SS_restoreState) {
+    if (!TAB_STATE_FOR_BROWSER.has(browser)) {
       Cu.reportError("Given tab is not restoring.");
       return;
     }
 
     browser.messageManager.sendAsyncMessage("SessionStore:resetRestore", {});
     this._resetLocalTabRestoringState(tab);
   },
 
--- a/browser/components/sessionstore/TabState.jsm
+++ b/browser/components/sessionstore/TabState.jsm
@@ -20,22 +20,22 @@ ChromeUtils.defineModuleGetter(this, "Ut
 /**
  * Module that contains tab state collection methods.
  */
 var TabState = Object.freeze({
   update(browser, data) {
     TabStateInternal.update(browser, data);
   },
 
-  collect(tab) {
-    return TabStateInternal.collect(tab);
+  collect(tab, extData) {
+    return TabStateInternal.collect(tab, extData);
   },
 
-  clone(tab) {
-    return TabStateInternal.clone(tab);
+  clone(tab, extData) {
+    return TabStateInternal.clone(tab, extData);
   },
 
   copyFromCache(browser, tabData, options) {
     TabStateInternal.copyFromCache(browser, tabData, options);
   },
 });
 
 var TabStateInternal = {
@@ -46,46 +46,51 @@ var TabStateInternal = {
     TabStateCache.update(browser, data);
   },
 
   /**
    * Collect data related to a single tab, synchronously.
    *
    * @param tab
    *        tabbrowser tab
+   * @param [extData]
+   *        optional dictionary object, containing custom tab values.
    *
    * @returns {TabData} An object with the data for this tab.  If the
    * tab has not been invalidated since the last call to
    * collect(aTab), the same object is returned.
    */
-  collect(tab) {
-    return this._collectBaseTabData(tab);
+  collect(tab, extData) {
+    return this._collectBaseTabData(tab, {extData});
   },
 
   /**
    * Collect data related to a single tab, including private data.
    * Use with caution.
    *
    * @param tab
    *        tabbrowser tab
+   * @param [extData]
+   *        optional dictionary object, containing custom tab values.
    *
    * @returns {object} An object with the data for this tab. This data is never
    *                   cached, it will always be read from the tab and thus be
    *                   up-to-date.
    */
-  clone(tab) {
-    return this._collectBaseTabData(tab, {includePrivateData: true});
+  clone(tab, extData) {
+    return this._collectBaseTabData(tab, {extData, includePrivateData: true});
   },
 
   /**
    * Collects basic tab data for a given tab.
    *
    * @param tab
    *        tabbrowser tab
    * @param options (object)
+   *        {extData: object} optional dictionary object, containing custom tab values
    *        {includePrivateData: true} to always include private data
    *
    * @returns {object} An object with the basic data for this tab.
    */
   _collectBaseTabData(tab, options) {
     let tabData = { entries: [], lastAccessed: tab.lastAccessed };
     let browser = tab.linkedBrowser;
 
@@ -100,18 +105,18 @@ var TabStateInternal = {
       tabData.muteReason = tab.muteReason;
     }
 
     tabData.mediaBlocked = browser.mediaBlocked;
 
     // Save tab attributes.
     tabData.attributes = TabAttributes.get(tab);
 
-    if (tab.__SS_extdata) {
-      tabData.extData = tab.__SS_extdata;
+    if (options.extData) {
+      tabData.extData = options.extData;
     }
 
     // Copy data from the tab state cache only if the tab has fully finished
     // restoring. We don't want to overwrite data contained in __SS_data.
     this.copyFromCache(browser, tabData, options);
 
     // After copyFromCache() was called we check for properties that are kept
     // in the cache only while the tab is pending or restoring. Once that
--- a/browser/components/sessionstore/test/browser_595601-restore_hidden.js
+++ b/browser/components/sessionstore/test/browser_595601-restore_hidden.js
@@ -72,28 +72,28 @@ var TabsProgressListener = {
     this.callback = callback;
   },
 
   observe(browser) {
     TabsProgressListener.onRestored(browser);
   },
 
   onRestored(browser) {
-    if (this.callback && browser.__SS_restoreState == TAB_STATE_RESTORING)
+    if (this.callback && ss.getInternalObjectState(browser) == TAB_STATE_RESTORING)
       this.callback.apply(null, [this.window].concat(this.countTabs()));
   },
 
   countTabs() {
     let needsRestore = 0, isRestoring = 0;
 
     for (let i = 0; i < this.window.gBrowser.tabs.length; i++) {
-      let browser = this.window.gBrowser.tabs[i].linkedBrowser;
-      if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+      let state = ss.getInternalObjectState(this.window.gBrowser.tabs[i].linkedBrowser);
+      if (state == TAB_STATE_RESTORING)
         isRestoring++;
-      else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+      else if (state == TAB_STATE_NEEDS_RESTORE)
         needsRestore++;
     }
 
     return [needsRestore, isRestoring];
   }
 };
 
 // ----------
--- a/browser/components/sessionstore/test/browser_636279.js
+++ b/browser/components/sessionstore/test/browser_636279.js
@@ -53,20 +53,20 @@ function countTabs() {
   let windowsEnum = Services.wm.getEnumerator("navigator:browser");
 
   while (windowsEnum.hasMoreElements()) {
     let window = windowsEnum.getNext();
     if (window.closed)
       continue;
 
     for (let i = 0; i < window.gBrowser.tabs.length; i++) {
-      let browser = window.gBrowser.tabs[i].linkedBrowser;
-      if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+      let browserState = ss.getInternalObjectState(window.gBrowser.tabs[i].linkedBrowser);
+      if (browserState == TAB_STATE_RESTORING)
         isRestoring++;
-      else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE)
+      else if (browserState == TAB_STATE_NEEDS_RESTORE)
         needsRestore++;
     }
   }
 
   return [needsRestore, isRestoring];
 }
 
 var TabsProgressListener = {
@@ -87,13 +87,13 @@ var TabsProgressListener = {
     delete this.callback;
   },
 
   observe(browser, topic, data) {
     TabsProgressListener.onRestored(browser);
   },
 
   onRestored(browser) {
-    if (this.callback && browser.__SS_restoreState == TAB_STATE_RESTORING) {
+    if (this.callback && ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) {
       this.callback.apply(null, countTabs());
     }
   }
 };
--- a/browser/components/sessionstore/test/browser_739805.js
+++ b/browser/components/sessionstore/test/browser_739805.js
@@ -18,17 +18,17 @@ function test() {
 
   let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
   let browser = tab.linkedBrowser;
 
   promiseBrowserLoaded(browser).then(() => {
     isnot(gBrowser.selectedTab, tab, "newly created tab is not selected");
 
     ss.setTabState(tab, JSON.stringify(tabState));
-    is(browser.__SS_restoreState, TAB_STATE_NEEDS_RESTORE, "tab needs restoring");
+    is(ss.getInternalObjectState(browser), TAB_STATE_NEEDS_RESTORE, "tab needs restoring");
 
     let {formdata} = JSON.parse(ss.getTabState(tab));
     is(formdata && formdata.id.foo, "bar", "tab state's formdata is valid");
 
     promiseTabRestored(tab).then(() => {
       ContentTask.spawn(browser, null, function() {
         let input = content.document.getElementById("foo");
         is(input.value, "bar", "formdata has been restored correctly");
--- a/browser/components/sessionstore/test/browser_pending_tabs.js
+++ b/browser/components/sessionstore/test/browser_pending_tabs.js
@@ -21,17 +21,17 @@ add_task(async function() {
   ss.setTabState(tab, JSON.stringify(TAB_STATE));
   ok(tab.hasAttribute("pending"), "tab is pending");
   await promise;
 
   // Flush to ensure the parent has all data.
   await TabStateFlusher.flush(browser);
 
   // Check that the shistory index is the one we restored.
-  let tabState = TabState.collect(tab);
+  let tabState = TabState.collect(tab, ss.getInternalObjectState(tab));
   is(tabState.index, TAB_STATE.index, "correct shistory index");
 
   // Check we don't collect userTypedValue when we shouldn't.
   ok(!tabState.userTypedValue, "tab didn't have a userTypedValue");
 
   // Cleanup.
   gBrowser.removeTab(tab);
 });
--- a/browser/components/sessionstore/test/browser_restore_redirect.js
+++ b/browser/components/sessionstore/test/browser_restore_redirect.js
@@ -18,17 +18,17 @@ add_task(async function check_http_redir
 
   info("Restored tab");
 
   await TabStateFlusher.flush(browser);
   let data = TabState.collect(tab);
   is(data.entries.length, 1, "Should be one entry in session history");
   is(data.entries[0].url, TARGET, "Should be the right session history entry");
 
-  ok(!("__SS_data" in browser), "Temporary restore data should have been cleared");
+  ok(!ss.getInternalObjectState(browser), "Temporary restore data should have been cleared");
 
   // Cleanup.
   BrowserTestUtils.removeTab(tab);
 });
 
 /**
  * Ensure that a js redirect leaves a working tab.
  */
@@ -57,13 +57,13 @@ add_task(async function check_js_redirec
 
   await loadPromise;
 
   await TabStateFlusher.flush(browser);
   let data = TabState.collect(tab);
   is(data.entries.length, 1, "Should be one entry in session history");
   is(data.entries[0].url, TARGET, "Should be the right session history entry");
 
-  ok(!("__SS_data" in browser), "Temporary restore data should have been cleared");
+  ok(!ss.getInternalObjectState(browser), "Temporary restore data should have been cleared");
 
   // Cleanup.
   BrowserTestUtils.removeTab(tab);
 });
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -359,33 +359,34 @@ var gProgressListener = {
     }
   },
 
   observe(browser, topic, data) {
     gProgressListener.onRestored(browser);
   },
 
   onRestored(browser) {
-    if (browser.__SS_restoreState == TAB_STATE_RESTORING) {
+    if (ss.getInternalObjectState(browser) == TAB_STATE_RESTORING) {
       let args = [browser].concat(gProgressListener._countTabs());
       gProgressListener._callback.apply(gProgressListener, args);
     }
   },
 
   _countTabs() {
     let needsRestore = 0, isRestoring = 0, wasRestored = 0;
 
     for (let win of BrowserWindowIterator()) {
       for (let i = 0; i < win.gBrowser.tabs.length; i++) {
         let browser = win.gBrowser.tabs[i].linkedBrowser;
-        if (browser.isConnected && !browser.__SS_restoreState)
+        let state = ss.getInternalObjectState(browser);
+        if (browser.isConnected && !state)
           wasRestored++;
-        else if (browser.__SS_restoreState == TAB_STATE_RESTORING)
+        else if (state == TAB_STATE_RESTORING)
           isRestoring++;
-        else if (browser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE || !browser.isConnected)
+        else if (state == TAB_STATE_NEEDS_RESTORE || !browser.isConnected)
           needsRestore++;
       }
     }
     return [needsRestore, isRestoring, wasRestored];
   }
 };
 
 registerCleanupFunction(function() {