Bug 586068 - Cascade page loads when restoring [r=dietrich, a=blocking]

This commit is contained in:
Paul O’Shannessy 2010-09-17 15:02:28 -07:00
parent 6455f29ae1
commit 8ee118773b
4 changed files with 499 additions and 43 deletions

View File

@ -800,6 +800,8 @@ pref("browser.sessionstore.max_windows_undo", 3);
// number of crashes that can occur before the about:sessionrestore page is displayed
// (this pref has no effect if more than 6 hours have passed since the last crash)
pref("browser.sessionstore.max_resumed_crashes", 1);
// number of tabs to restore concurrently
pref("browser.sessionstore.max_concurrent_tabs", 3);
// allow META refresh by default
pref("accessibility.blockautorefresh", false);

View File

@ -114,6 +114,9 @@ const CAPABILITIES = [
// that gets saved to disk nor part of the strings returned by the API.
const INTERNAL_KEYS = ["_tabStillLoading", "_hosts", "_formDataSaved"];
// These are tab events that we listen to.
const TAB_EVENTS = ["TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide"];
#ifndef XP_WIN
#define BROKEN_WM_Z_ORDER
#endif
@ -201,6 +204,13 @@ SessionStoreService.prototype = {
// whether the last window was closed and should be restored
_restoreLastWindow: false,
// tabs to restore in order
_tabsToRestore: { visible: [], hidden: [] },
_tabsRestoringCount: 0,
// number of tabs to restore concurrently, pref controlled.
_maxConcurrentTabRestores: null,
// The state from the previous session (after restoring pinned tabs)
_lastSessionState: null,
@ -259,6 +269,14 @@ SessionStoreService.prototype = {
this._sessionhistory_max_entries =
this._prefBranch.getIntPref("sessionhistory.max_entries");
this._maxConcurrentTabRestores =
this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs");
this._prefBranch.addObserver("sessionstore.max_concurrent_tabs", this, true);
// Make sure gRestoreTabsProgressListener has a reference to sessionstore
// so that it can make calls back in
gRestoreTabsProgressListener.ss = this;
// get file references
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
this._sessionFileBackup = this._sessionFile.clone();
@ -348,6 +366,13 @@ SessionStoreService.prototype = {
// save all data for session resuming
this.saveState(true);
// clear out _tabsToRestore in case it's still holding refs
this._tabsToRestore.visible = null;
this._tabsToRestore.hidden = null;
// remove the ref to us from the progress listener
gRestoreTabsProgressListener.ss = null;
// Make sure to break our cycle with the save timer
if (this._saveTimer) {
this._saveTimer.cancel();
@ -403,6 +428,7 @@ SessionStoreService.prototype = {
this._forEachBrowserWindow(function(aWindow) {
Array.forEach(aWindow.gBrowser.browsers, function(aBrowser) {
delete aBrowser.__SS_data;
delete aBrowser.__SS_needsRestore;
});
openWindows[aWindow.__SSi] = true;
});
@ -507,6 +533,10 @@ SessionStoreService.prototype = {
this._clearDisk();
this.saveState(true);
break;
case "sessionstore.max_concurrent_tabs":
this._maxConcurrentTabRestores =
this._prefBranch.getIntPref("sessionstore.max_concurrent_tabs");
break;
}
break;
case "timer-callback": // timer call back for delayed saving
@ -547,6 +577,9 @@ SessionStoreService.prototype = {
// to objects that can change (specifically this._windows[x]).
this._stateBackup = JSON.parse(this._toJSONString(this._getCurrentState(true)));
}
// Make sure _tabsToRestore is cleared. It will be repopulated when
// entering/exiting private browsing (by calls to setBrowserState).
this._resetRestoringState();
break;
}
},
@ -586,6 +619,12 @@ SessionStoreService.prototype = {
case "TabSelect":
this.onTabSelect(win);
break;
case "TabShow":
this.onTabShow(aEvent.originalTarget);
break;
case "TabHide":
this.onTabHide(aEvent.originalTarget);
break;
}
},
@ -691,10 +730,10 @@ SessionStoreService.prototype = {
for (let i = 0; i < tabbrowser.tabs.length; i++) {
this.onTabAdd(aWindow, tabbrowser.tabs[i], true);
}
// notification of tab add/remove/selection
tabbrowser.tabContainer.addEventListener("TabOpen", this, true);
tabbrowser.tabContainer.addEventListener("TabClose", this, true);
tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
// notification of tab add/remove/selection/show/hide
TAB_EVENTS.forEach(function(aEvent) {
tabbrowser.tabContainer.addEventListener(aEvent, this, true);
}, this);
},
/**
@ -726,10 +765,13 @@ SessionStoreService.prototype = {
var tabbrowser = aWindow.gBrowser;
tabbrowser.tabContainer.removeEventListener("TabOpen", this, true);
tabbrowser.tabContainer.removeEventListener("TabClose", this, true);
tabbrowser.tabContainer.removeEventListener("TabSelect", this, true);
TAB_EVENTS.forEach(function(aEvent) {
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
}, this);
// remove the progress listener for this window
tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener);
let winData = this._windows[aWindow.__SSi];
if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down
// update all window data for a last time
@ -809,6 +851,18 @@ SessionStoreService.prototype = {
delete browser.__SS_data;
// If this tab was in the middle of restoring, we want to restore the next
// tab. If the tab hasn't been restored, we want to remove it from the array.
if (browser.__SS_restoring) {
this.restoreNextTab(true);
}
else if (browser.__SS_needsRestore) {
if (aTab.hidden)
this._tabsToRestore.hidden.splice(this._tabsToRestore.hidden.indexOf(aTab));
else
this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(aTab));
}
if (!aNoNotification) {
this.saveStateDelayed(aWindow);
}
@ -902,11 +956,37 @@ SessionStoreService.prototype = {
if (this._loadState == STATE_RUNNING) {
this._windows[aWindow.__SSi].selected = aWindow.gBrowser.tabContainer.selectedIndex;
let tab = aWindow.gBrowser.selectedTab;
// If __SS_needsRestore is still on the browser, then we haven't restored
// this tab yet. Explicitly call restoreTab to kick off the restore.
if (tab.linkedBrowser.__SS_needsRestore) {
this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(tab));
this.restoreTab(tab);
}
// attempt to update the current URL we send in a crash report
this._updateCrashReportURL(aWindow);
}
},
onTabShow: function sss_onTabShow(aTab) {
// If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket
if (aTab.linkedBrowser.__SS_needsRestore) {
this._tabsToRestore.hidden.splice(this._tabsToRestore.hidden.indexOf(aTab));
// Just put it at the end of the list of visible tabs;
this._tabsToRestore.visible.push(aTab);
}
},
onTabHide: function sss_onTabHide(aTab) {
// If the tab hasn't been restored yet, move it into the right _tabsToRestore bucket
if (aTab.linkedBrowser.__SS_needsRestore) {
this._tabsToRestore.visible.splice(this._tabsToRestore.visible.indexOf(aTab));
// Just put it at the end of the list of hidden tabs;
this._tabsToRestore.hidden.push(aTab);
}
},
/* ........ nsISessionStore API .............. */
getBrowserState: function sss_getBrowserState() {
@ -925,6 +1005,9 @@ SessionStoreService.prototype = {
this._browserSetState = true;
// Make sure _tabsToRestore is emptied out
this._resetRestoringState();
var window = this._getMostRecentBrowserWindow();
if (!window) {
this._restoreCount = 1;
@ -2173,7 +2256,14 @@ SessionStoreService.prototype = {
restoreHistoryPrecursor:
function sss_restoreHistoryPrecursor(aWindow, aTabs, aTabData, aSelectTab, aIx, aCount) {
var tabbrowser = aWindow.gBrowser;
// In order for setTabState to add gTabsProgressListener, we want to add it
// here. We'll only attempt to do it when aCount == 0 though so we don't add
// it multiple times.
if (aCount == 0 &&
tabbrowser.mTabsProgressListeners.indexOf(gRestoreTabsProgressListener) == -1)
tabbrowser.addTabsProgressListener(gRestoreTabsProgressListener);
// 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++) {
@ -2206,6 +2296,12 @@ SessionStoreService.prototype = {
tab.hidden = tabData.hidden;
tabData._tabStillLoading = true;
// 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_needsRestore = true;
if (!tabData.entries || tabData.entries.length == 0) {
// make sure to blank out this tab's content
// (just purging the tab's history won't be enough)
@ -2217,17 +2313,16 @@ SessionStoreService.prototype = {
tab.setAttribute("busy", "true");
tabbrowser.updateIcon(tab);
tabbrowser.setTabTitleLoading(tab);
// wall-paper fix for bug 439675: make sure that the URL to be loaded
// is always visible in the address bar
let activeIndex = (tabData.index || tabData.entries.length) - 1;
let activePageData = tabData.entries[activeIndex] || null;
browser.userTypedValue = activePageData ? activePageData.url || null : null;
// keep the data around to prevent dataloss in case
// a tab gets closed before it's been properly restored
browser.__SS_data = tabData;
// If the page has a title, set it.
if (activePageData && activePageData.title)
tab.label = activePageData.title;
}
if (aTabs.length > 0) {
@ -2356,39 +2451,124 @@ SessionStoreService.prototype = {
var event = aWindow.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
tab.dispatchEvent(event);
let activeIndex = (tabData.index || tabData.entries.length) - 1;
if (activeIndex >= tabData.entries.length)
activeIndex = tabData.entries.length - 1;
try {
if (activeIndex >= 0)
browser.webNavigation.gotoIndex(activeIndex);
}
catch (ex) {
// ignore page load errors
tab.removeAttribute("busy");
}
if (tabData.entries.length > 0) {
// restore those aspects of the currently active documents
// which are not preserved in the plain history entries
// (mainly scroll state and text data)
browser.__SS_restore_data = tabData.entries[activeIndex] || {};
browser.__SS_restore_pageStyle = tabData.pageStyle || "";
browser.__SS_restore_tab = tab;
}
// Handle userTypedValue. Setting userTypedValue seems to update gURLbar
// as needed. Calling loadURI will cancel form filling in restoreDocument_proxy
if (tabData.userTypedValue) {
browser.userTypedValue = tabData.userTypedValue;
if (tabData.userTypedClear)
browser.loadURI(tabData.userTypedValue, null, null, true);
}
// Restore the history in the next tab
aWindow.setTimeout(function(){
_this.restoreHistory(aWindow, aTabs, aTabData, aIdMap, aDocIdentMap);
}, 0);
// This could cause us to ignore the max_concurrent_tabs pref a bit, but
// it ensures each window will have its selected tab loaded.
if (aWindow.gBrowser.selectedBrowser == browser) {
this.restoreTab(tab);
}
else {
// Put the tab into the right bucket
if (tabData.hidden)
this._tabsToRestore.hidden.push(tab);
else
this._tabsToRestore.visible.push(tab);
this.restoreNextTab();
}
},
restoreTab: function(aTab) {
let browser = aTab.linkedBrowser;
let tabData = browser.__SS_data;
// There are cases within where we haven't actually started a load and so we
// should call restoreNextTab. We don't want to do it immediately though
// since we might not set userTypedValue in a timely fashion.
let shouldRestoreNextTab = false;
// Increase our internal count.
this._tabsRestoringCount++;
delete browser.__SS_needsRestore;
let activeIndex = (tabData.index || tabData.entries.length) - 1;
if (activeIndex >= tabData.entries.length)
activeIndex = tabData.entries.length - 1;
// Attach data that will be restored on "load" event, after tab is restored.
if (activeIndex > -1) {
// restore those aspects of the currently active documents which are not
// preserved in the plain history entries (mainly scroll state and text data)
browser.__SS_restore_data = tabData.entries[activeIndex] || {};
browser.__SS_restore_pageStyle = tabData.pageStyle || "";
browser.__SS_restore_tab = aTab;
try {
// Get the tab title (set in restoreHistoryPrecursor) for later
let label = aTab.label;
browser.__SS_restoring = true;
browser.webNavigation.gotoIndex(activeIndex);
// gotoIndex will force the "loading" string, so set the title
aTab.label = label;
}
catch (ex) {
// ignore page load errors
aTab.removeAttribute("busy");
shouldRestoreNextTab = true;
}
}
else {
// If there is no active index, then we'll never set __SS_restore_data
// and even if there is a load event, we won't kick of the next tab
// (restoreDocument won't get called), so do it ourselves.
shouldRestoreNextTab = true;
}
// Handle userTypedValue. Setting userTypedValue seems to update gURLbar
// as needed. Calling loadURI will cancel form filling in restoreDocument
if (tabData.userTypedValue) {
browser.userTypedValue = tabData.userTypedValue;
if (tabData.userTypedClear) {
browser.__SS_restoring = true;
browser.loadURI(tabData.userTypedValue, null, null, true);
}
else {
shouldRestoreNextTab = true;
}
}
if (shouldRestoreNextTab)
this.restoreNextTab(true);
},
restoreNextTab: function sss_restoreNextTab(aFromTabFinished) {
// If we call in here while quitting, we don't actually want to do anything
if (this._loadState == STATE_QUITTING)
return;
if (aFromTabFinished)
this._tabsRestoringCount--;
// If it's not possible to restore anything, then just bail out.
if (this._maxConcurrentTabRestores >= 0 &&
this._tabsRestoringCount >= this._maxConcurrentTabRestores)
return;
// Look in visible, then hidden
let nextTabArray;
if (this._tabsToRestore.visible.length) {
nextTabArray = this._tabsToRestore.visible;
}
else if (this._tabsToRestore.hidden.length) {
nextTabArray = this._tabsToRestore.hidden;
}
if (nextTabArray) {
let tab = nextTabArray.shift();
this.restoreTab(tab);
}
else {
// Remove the progress listener from windows. It will get re-added as needed.
this._forEachBrowserWindow(function(aWindow) {
// This won't fail since removeTabsProgressListener just filters. It
// doesn't attempt to splice.
aWindow.gBrowser.removeTabsProgressListener(gRestoreTabsProgressListener)
});
}
},
/**
@ -3302,6 +3482,15 @@ SessionStoreService.prototype = {
this._closedWindows.splice(spliceTo);
},
/**
* Reset state to prepare for a new session state to be restored.
*/
_resetRestoringState: function sss__initRestoringState() {
//
this._tasToRestore = { visible: [], hidden: [] };
this._tabsRestoringCount = 0;
},
/* ........ Storage API .............. */
/**
@ -3427,6 +3616,25 @@ let XPathHelper = {
}
};
// This is used to help meter the number of restoring tabs. This is the control
// point for telling the next tab to restore. It gets attached to each gBrowser
// via gBrowser.addTabsProgressListener
let gRestoreTabsProgressListener = {
ss: null,
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
// Ignore state changes on browsers that we've already restored
if (!aBrowser.__SS_restoring)
return;
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
delete aBrowser.__SS_restoring;
this.ss.restoreNextTab(true);
}
}
}
// see nsPrivateBrowsingService.js
String.prototype.hasRootDomain = function hasRootDomain(aDomain)
{

View File

@ -117,6 +117,7 @@ _BROWSER_TEST_FILES = \
browser_579879.js \
browser_580512.js \
browser_586147.js \
browser_586068-cascaded_restore.js \
$(NULL)
libs:: $(_BROWSER_TEST_FILES)

View File

@ -0,0 +1,245 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is sessionstore test code.
*
* The Initial Developer of the Original Code is
* Mozilla Foundation.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Paul OShannessy <paul@oshannessy.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
Cu.import("resource://gre/modules/Services.jsm");
let ss = Cc["@mozilla.org/browser/sessionstore;1"].
getService(Ci.nsISessionStore);
let stateBackup = ss.getBrowserState();
function test() {
/** Test for Bug 586068 - Cascade page loads when restoring **/
waitForExplicitFinish();
runNextTest();
}
let tests = [test_cascade, test_select];
function runNextTest() {
if (tests.length) {
ss.setWindowState(window,
JSON.stringify({ windows: [{ tabs: [{ url: 'about:blank' }], }] }),
true);
executeSoon(tests.shift());
}
else {
ss.setBrowserState(stateBackup);
executeSoon(finish);
}
}
function test_cascade() {
// Set the pref to 1 so we know exactly how many tabs should be restoring at any given time
Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 1);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_cascade_progressCallback();
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.com" }], extData: { "uniq": r() } }
] }] };
let loadCount = 0;
// Since our progress listener is fired before the one in sessionstore, our
// expected counts look a little weird. This is because we inspect the state
// before sessionstore has marked the tab as finished restoring and before it
// starts restoring the next tab
let expectedCounts = [
[5, 1, 0],
[4, 1, 1],
[3, 1, 2],
[2, 1, 3],
[1, 1, 4],
[0, 1, 5]
];
function test_cascade_progressCallback() {
loadCount++;
// We'll get the first <length of windows[0].tabs> load events here, even
// though they are ignored by sessionstore. Those are due to explicit
// "stop" events before any restoring action takes place. We can safely
// ignore these events.
if (loadCount <= state.windows[0].tabs.length)
return;
let counts = countTabs();
let expected = expectedCounts[loadCount - state.windows[0].tabs.length - 1];
is(counts[0], expected[0], "test_cascade: load " + loadCount + " - # tabs that need to be restored");
is(counts[1], expected[1], "test_cascade: load " + loadCount + " - # tabs that are restoring");
is(counts[2], expected[2], "test_cascade: load " + loadCount + " - # tabs that has been restored");
if (loadCount == state.windows[0].tabs.length * 2) {
window.gBrowser.removeTabsProgressListener(progressListener);
// Reset the pref
try {
Services.prefs.clearUserPref("browser.sessionstore.max_concurrent_tabs");
} catch (e) {}
runNextTest();
}
}
// This progress listener will get attached before the listener in session store.
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function test_select() {
// Set the pref to 0 so we know exactly how many tabs should be restoring at
// any given time. This guarantees that a finishing load won't start another.
Services.prefs.setIntPref("browser.sessionstore.max_concurrent_tabs", 0);
// We have our own progress listener for this test, which we'll attach before our state is set
let progressListener = {
onStateChange: function (aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW)
test_select_progressCallback(aBrowser);
}
}
let state = { windows: [{ tabs: [
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } },
{ entries: [{ url: "http://example.org" }], extData: { "uniq": r() } }
], selectedIndex: 1 }] };
let loadCount = 0;
// expectedCounts looks a little wierd for the test case, but it works. See
// comment in test_cascade for an explanation
let expectedCounts = [
[5, 1, 0],
[4, 1, 1],
[3, 1, 2],
[2, 1, 3],
[1, 1, 4],
[0, 1, 5]
];
let tabOrder = [0, 5, 1, 4, 3, 2];
function test_select_progressCallback(aBrowser) {
loadCount++;
// We'll get the first <length of windows[0].tabs> load events here, even
// though they are ignored by sessionstore. Those are due to explicit
// "stop" events before any restoring action takes place. We can safely
// ignore these events.
if (loadCount <= state.windows[0].tabs.length)
return;
let loadIndex = loadCount - state.windows[0].tabs.length - 1;
let counts = countTabs();
let expected = expectedCounts[loadIndex];
is(counts[0], expected[0], "test_select: load " + loadCount + " - # tabs that need to be restored");
is(counts[1], expected[1], "test_select: load " + loadCount + " - # tabs that are restoring");
is(counts[2], expected[2], "test_select: load " + loadCount + " - # tabs that has been restored");
if (loadCount == state.windows[0].tabs.length * 2) {
window.gBrowser.removeTabsProgressListener(progressListener);
// Reset the pref
try {
Services.prefs.clearUserPref("browser.sessionstore.max_concurrent_tabs");
} catch (e) {}
runNextTest();
}
else {
// double check that this tab was the right one
let expectedData = state.windows[0].tabs[tabOrder[loadIndex]].extData.uniq;
let tab;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
if (!tab && window.gBrowser.tabs[i].linkedBrowser == aBrowser)
tab = window.gBrowser.tabs[i];
}
is(ss.getTabValue(tab, "uniq"), expectedData, "test_select: load " + loadCount + " - correct tab was restored");
// select the next tab
window.gBrowser.selectTabAtIndex(tabOrder[loadIndex + 1]);
}
}
window.gBrowser.addTabsProgressListener(progressListener);
ss.setBrowserState(JSON.stringify(state));
}
function countTabs() {
let needsRestore = 0,
isRestoring = 0,
wasRestored = 0;
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_restoring)
isRestoring++;
else if (browser.__SS_needsRestore)
needsRestore++;
else
wasRestored++;
}
}
return [needsRestore, isRestoring, wasRestored];
}
function r() {
return "" + Date.now() + Math.random();
}