Bug 1067648 - Introduce restoreTab() and use it from restoreTabs() r=billm
authorTim Taubert <ttaubert@mozilla.com>
Tue, 16 Sep 2014 12:04:34 +0200
changeset 206236 8b9340f3a185570285d26f69fb016cf99fb93919
parent 206161 f6c42abb5457af7e430404e558db58794ae7f06a
child 206237 86a707d5ffdba458281e6dbfb833b57956c39b9a
push id27517
push userryanvm@gmail.com
push dateFri, 19 Sep 2014 18:13:39 +0000
treeherdermozilla-central@a084c4cfd8a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1067648
milestone35.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 1067648 - Introduce restoreTab() and use it from restoreTabs() r=billm
browser/components/sessionstore/SessionStore.jsm
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -341,16 +341,19 @@ let SessionStoreInternal = {
 
   // Whether session has been initialized
   _sessionInitialized: false,
 
   // Promise that is resolved when we're ready to initialize
   // and restore the session.
   _promiseReadyForInitialization: null,
 
+  // Keep busy state counters per window.
+  _windowBusyStates: new WeakMap(),
+
   /**
    * A promise fulfilled once initialization is complete.
    */
   get promiseInitialized() {
     return this._deferredInitialized.promise;
   },
 
   get canRestoreLastSession() {
@@ -1551,18 +1554,17 @@ let SessionStoreInternal = {
     if (!("__SSi" in window)) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
 
     if (aTab.linkedBrowser.__SS_restoreState) {
       this._resetTabRestoringState(aTab);
     }
 
-    this._setWindowStateBusy(window);
-    this.restoreTabs(window, [aTab], [tabState], 0);
+    this.restoreTab(aTab, tabState);
   },
 
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) {
     if (!aTab.ownerDocument.defaultView.__SSi) {
       throw Components.Exception("Default view is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!aWindow.getBrowser) {
       throw Components.Exception("Invalid window object: no getBrowser", Cr.NS_ERROR_INVALID_ARG);
@@ -1574,24 +1576,21 @@ let SessionStoreInternal = {
 
     // Duplicate the tab state
     let tabState = TabState.clone(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();
 
-    this.restoreTabs(aWindow, [newTab], [tabState], 0,
-                     true /* Load this tab right away. */);
-
+    this.restoreTab(newTab, tabState, true /* Load this tab right away. */);
     return newTab;
   },
 
   getClosedTabCount: function ssi_getClosedTabCount(aWindow) {
     if ("__SSi" in aWindow) {
       return this._windows[aWindow.__SSi]._closedTabs.length;
     }
 
@@ -1627,23 +1626,22 @@ let SessionStoreInternal = {
     if (!(aIndex in closedTabs)) {
       throw Components.Exception("Invalid index: not in the closed tabs", Cr.NS_ERROR_INVALID_ARG);
     }
 
     // fetch the data of closed tab, while removing it from the array
     let closedTab = closedTabs.splice(aIndex, 1).shift();
     let closedTabState = closedTab.state;
 
-    this._setWindowStateBusy(aWindow);
     // create a new tab
     let tabbrowser = aWindow.gBrowser;
-    let tab = tabbrowser.addTab();
+    let tab = tabbrowser.selectedTab = tabbrowser.addTab();
 
     // restore tab content
-    this.restoreTabs(aWindow, [tab], [closedTabState], 1);
+    this.restoreTab(tab, closedTabState);
 
     // restore the tab's position
     tabbrowser.moveTabTo(tab, closedTab.pos);
 
     // focus the tab's content area (bug 342432)
     tab.linkedBrowser.focus();
 
     return tab;
@@ -2378,184 +2376,202 @@ let SessionStoreInternal = {
         newClosedTabsData.concat(this._windows[aWindow.__SSi]._closedTabs);
 
       // ... and make sure that we don't exceed the max number of closed tabs
       // we can restore.
       this._windows[aWindow.__SSi]._closedTabs =
         newClosedTabsData.slice(0, this._max_tabs_undo);
     }
 
-    this.restoreTabs(aWindow, tabs, winData.tabs,
-      (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
+    // Restore tabs, if any.
+    if (winData.tabs.length) {
+      this.restoreTabs(aWindow, tabs, winData.tabs,
+        (overwriteTabs ? (parseInt(winData.selected || "1")) : 0));
+    }
 
     if (aState.scratchpads) {
       ScratchpadManager.restoreSession(aState.scratchpads);
     }
 
     // set smoothScroll back to the original value
     tabstrip.smoothScroll = smoothScroll;
 
     TelemetryStopwatch.finish("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
 
+    this._setWindowStateReady(aWindow);
     this._sendRestoreCompletedNotifications();
   },
 
   /**
    * Manage history restoration for a window
    * @param aWindow
    *        Window to restore the tabs into
    * @param aTabs
    *        Array of tab references
    * @param aTabData
    *        Array of tab data
    * @param aSelectTab
    *        Index of the tab to select. This is a 1-based index where "1"
    *        indicates the first tab should be selected, and "0" indicates that
    *        the currently selected tab will not be changed.
-   * @param aRestoreImmediately
-   *        Flag to indicate whether the given set of tabs aTabs should be
-   *        restored/loaded immediately even if restore_on_demand = true
    */
-  restoreTabs: function (aWindow, aTabs, aTabData, aSelectTab,
-                         aRestoreImmediately = false)
-  {
-
+  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._windows[aWindow.__SSi]._restoring;
     }
 
-    // It's important to set the window state to dirty so that
-    // we collect their data for the first time when saving state.
-    DirtyWindows.add(aWindow);
-
-    // Set the state to restore as the window's current state. Normally, this
-    // will just be overridden the next time we collect state but we need this
-    // as a fallback should Firefox be shutdown early without notifying us
-    // beforehand.
-    this._windows[aWindow.__SSi].tabs = aTabData.slice();
-    this._windows[aWindow.__SSi].selected = aSelectTab;
-
-    if (aTabs.length == 0) {
-      // This is normally done later, but as we're returning early
-      // here we need to take care of it.
-      this._setWindowStateReady(aWindow);
-      return;
+    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.
+    // Individual tab states will be taken care of by restoreTab() below.
+    if (numTabsInWindow == numTabsToRestore) {
+      // Remove all previous tab data.
+      tabsDataArray.length = 0;
+    } else {
+      // Remove all previous tab data except tabs that should not be overriden.
+      tabsDataArray.splice(numTabsInWindow - numTabsToRestore);
     }
 
+    // Let the tab data array have the right number of slots.
+    tabsDataArray.length = numTabsInWindow;
+
     // If provided, set the selected tab.
     if (aSelectTab > 0 && aSelectTab <= aTabs.length) {
       tabbrowser.selectedTab = aTabs[aSelectTab - 1];
+
+      // Update the window state in case we shut down without being notified.
+      this._windows[aWindow.__SSi].selected = aSelectTab;
     }
 
-    // Prepare the tabs so that they can be properly restored. We'll pin/unpin
-    // and show/hide tabs as necessary. We'll also set the labels, user typed
-    // value, and attach a copy of the tab's data in case we close it before
-    // it's been restored.
+    // Restore all tabs.
     for (let t = 0; t < aTabs.length; t++) {
-      let tab = aTabs[t];
-      let browser = tabbrowser.getBrowserForTab(tab);
-      let tabData = aTabData[t];
-
-      if (tabData.pinned)
-        tabbrowser.pinTab(tab);
-      else
-        tabbrowser.unpinTab(tab);
-
-      if (tabData.hidden)
-        tabbrowser.hideTab(tab);
-      else
-        tabbrowser.showTab(tab);
-
-      if (tabData.lastAccessed) {
-        tab.lastAccessed = tabData.lastAccessed;
-      }
-
-      if ("attributes" in tabData) {
-        // 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 = {};
-        for (let key in tabData.extData)
-         tab.__SS_extdata[key] = tabData.extData[key];
-      } else {
-        delete tab.__SS_extdata;
-      }
-      delete tabData.closedAt; // Tab is now open.
-
-      // Flush all data from the content script synchronously. This is done so
-      // that all async messages that are still on their way to chrome will
-      // be ignored and don't override any tab data set when restoring.
-      TabState.flush(tab.linkedBrowser);
-
-      // Ensure the index is in bounds.
-      let activeIndex = (tabData.index || tabData.entries.length) - 1;
-      activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
-      activeIndex = Math.max(activeIndex, 0);
-
-      // Save the index in case we updated it above.
-      tabData.index = activeIndex + 1;
-
-      // In electrolysis, we may need to change the browser's remote
-      // attribute so that it runs in a content process.
-      let activePageData = tabData.entries[activeIndex] || null;
-      let uri = activePageData ? activePageData.url || null : null;
-      tabbrowser.updateBrowserRemotenessByURL(browser, uri);
-
-      // Start a new epoch and include the epoch in the restoreHistory
-      // message. If a message is received that relates to a previous epoch, we
-      // discard it.
-      let epoch = this._nextRestoreEpoch++;
-      this._browserEpochs.set(browser.permanentKey, epoch);
-
-      // keep the data around to prevent dataloss in case
-      // a tab gets closed before it's been properly restored
-      browser.__SS_data = tabData;
-      browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
-      browser.setAttribute("pending", "true");
-      tab.setAttribute("pending", "true");
-
-      // Update the persistent tab state cache with |tabData| information.
-      TabStateCache.update(browser, {
-        history: {entries: tabData.entries, index: tabData.index},
-        scroll: tabData.scroll || null,
-        storage: tabData.storage || null,
-        formdata: tabData.formdata || null,
-        disallow: tabData.disallow || null,
-        pageStyle: tabData.pageStyle || null
-      });
-
-      browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
-                                              {tabData: tabData, epoch: epoch});
-
-      // Restore tab attributes.
-      if ("attributes" in tabData) {
-        TabAttributes.set(tab, tabData.attributes);
-      }
-
-      // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
-      // it ensures each window will have its selected tab loaded.
-      if (aRestoreImmediately || tabbrowser.selectedBrowser == browser) {
-        this.restoreTabContent(tab);
-      } else {
-        TabRestoreQueue.add(tab);
-        this.restoreNextTab();
-      }
+      this.restoreTab(aTabs[t], aTabData[t]);
+    }
+  },
+
+  // Restores the given tab state for a given tab.
+  restoreTab(tab, tabData, restoreImmediately = false) {
+    let browser = tab.linkedBrowser;
+    let window = tab.ownerDocument.defaultView;
+    let tabbrowser = window.gBrowser;
+
+    // Increase the busy state counter before modifying the tab.
+    this._setWindowStateBusy(window);
+
+    // It's important to set the window state to dirty so that
+    // we collect their data for the first time when saving state.
+    DirtyWindows.add(window);
+
+    // Update the tab state in case we shut down without being notified.
+    this._windows[window.__SSi].tabs[tab._tPos] = tabData;
+
+    // Prepare the tab so that it can be properly restored. We'll pin/unpin
+    // and show/hide tabs as necessary. We'll also attach a copy of the tab's
+    // data in case we close it before it's been restored.
+    if (tabData.pinned) {
+      tabbrowser.pinTab(tab);
+    } else {
+      tabbrowser.unpinTab(tab);
+    }
+
+    if (tabData.hidden) {
+      tabbrowser.hideTab(tab);
+    } else {
+      tabbrowser.showTab(tab);
+    }
+
+    if (tabData.lastAccessed) {
+      tab.lastAccessed = tabData.lastAccessed;
+    }
+
+    if ("attributes" in tabData) {
+      // 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, {});
+    } else {
+      delete tab.__SS_extdata;
     }
 
-    this._setWindowStateReady(aWindow);
+    // Tab is now open.
+    delete tabData.closedAt;
+
+    // Flush all data from the content script synchronously. This is done so
+    // that all async messages that are still on their way to chrome will
+    // be ignored and don't override any tab data set when restoring.
+    TabState.flush(browser);
+
+    // Ensure the index is in bounds.
+    let activeIndex = (tabData.index || tabData.entries.length) - 1;
+    activeIndex = Math.min(activeIndex, tabData.entries.length - 1);
+    activeIndex = Math.max(activeIndex, 0);
+
+    // Save the index in case we updated it above.
+    tabData.index = activeIndex + 1;
+
+    // In electrolysis, we may need to change the browser's remote
+    // attribute so that it runs in a content process.
+    let activePageData = tabData.entries[activeIndex] || null;
+    let uri = activePageData ? activePageData.url || null : null;
+    tabbrowser.updateBrowserRemotenessByURL(browser, uri);
+
+    // Start a new epoch and include the epoch in the restoreHistory
+    // message. If a message is received that relates to a previous epoch, we
+    // discard it.
+    let epoch = this._nextRestoreEpoch++;
+    this._browserEpochs.set(browser.permanentKey, epoch);
+
+    // keep the data around to prevent dataloss in case
+    // a tab gets closed before it's been properly restored
+    browser.__SS_data = tabData;
+    browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
+    browser.setAttribute("pending", "true");
+    tab.setAttribute("pending", "true");
+
+    // Update the persistent tab state cache with |tabData| information.
+    TabStateCache.update(browser, {
+      history: {entries: tabData.entries, index: tabData.index},
+      scroll: tabData.scroll || null,
+      storage: tabData.storage || null,
+      formdata: tabData.formdata || null,
+      disallow: tabData.disallow || null,
+      pageStyle: tabData.pageStyle || null
+    });
+
+    browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
+                                            {tabData: tabData, epoch: epoch});
+
+    // Restore tab attributes.
+    if ("attributes" in tabData) {
+      TabAttributes.set(tab, tabData.attributes);
+    }
+
+    // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
+    // it ensures each window will have its selected tab loaded.
+    if (restoreImmediately || tabbrowser.selectedBrowser == browser) {
+      this.restoreTabContent(tab);
+    } else {
+      TabRestoreQueue.add(tab);
+      this.restoreNextTab();
+    }
+
+    // Decrease the busy state counter after we're done.
+    this._setWindowStateReady(window);
   },
 
   /**
    * Restores the specified tab. If the tab can't be restored (eg, no history or
    * calling gotoIndex fails), then state changes will be rolled back.
    * This method will check if gTabsProgressListener is attached to the tab's
    * window, ensuring that we don't get caught without one.
    * This method removes the session history listener right before starting to
@@ -3235,27 +3251,40 @@ let SessionStoreInternal = {
     }
   },
 
   /**
    * Set the given window's state to 'not busy'.
    * @param aWindow the window
    */
   _setWindowStateReady: function ssi_setWindowStateReady(aWindow) {
-    this._setWindowStateBusyValue(aWindow, false);
-    this._sendWindowStateEvent(aWindow, "Ready");
+    let newCount = (this._windowBusyStates.get(aWindow) || 0) - 1;
+    if (newCount < 0) {
+      throw new Error("Invalid window busy state (less than zero).");
+    }
+    this._windowBusyStates.set(aWindow, newCount);
+
+    if (newCount == 0) {
+      this._setWindowStateBusyValue(aWindow, false);
+      this._sendWindowStateEvent(aWindow, "Ready");
+    }
   },
 
   /**
    * Set the given window's state to 'busy'.
    * @param aWindow the window
    */
   _setWindowStateBusy: function ssi_setWindowStateBusy(aWindow) {
-    this._setWindowStateBusyValue(aWindow, true);
-    this._sendWindowStateEvent(aWindow, "Busy");
+    let newCount = (this._windowBusyStates.get(aWindow) || 0) + 1;
+    this._windowBusyStates.set(aWindow, newCount);
+
+    if (newCount == 1) {
+      this._setWindowStateBusyValue(aWindow, true);
+      this._sendWindowStateEvent(aWindow, "Busy");
+    }
   },
 
   /**
    * Dispatch an SSWindowState_____ event for the given window.
    * @param aWindow the window
    * @param aType the type of event, SSWindowState will be prepended to this string
    */
   _sendWindowStateEvent: function ssi_sendWindowStateEvent(aWindow, aType) {