From cabc97dd9e57fdd52965407b97741921b6daf608 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Thu, 29 Aug 2013 16:02:42 +0200 Subject: [PATCH] Bug 894595 - part 3 - Use asynchronous data collection for delayed save state calls; r=yoric --- browser/app/profile/firefox.js | 2 + .../sessionstore/src/SessionSaver.jsm | 32 ++++++++- .../sessionstore/src/SessionStore.jsm | 66 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index b7a756bb2ee8..ea5c03a3a5ad 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -860,6 +860,8 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false); pref("browser.sessionstore.upgradeBackup.latestBuildID", ""); // End-users should not run sessionstore in debug mode pref("browser.sessionstore.debug", false); +// Enable asynchronous data collection by default. +pref("browser.sessionstore.async", true); // allow META refresh by default pref("accessibility.blockautorefresh", false); diff --git a/browser/components/sessionstore/src/SessionSaver.jsm b/browser/components/sessionstore/src/SessionSaver.jsm index b1977d9727c3..c8682303e42d 100644 --- a/browser/components/sessionstore/src/SessionSaver.jsm +++ b/browser/components/sessionstore/src/SessionSaver.jsm @@ -151,7 +151,7 @@ let SessionSaverInternal = { delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0); // Schedule a state save. - this._timeoutID = setTimeout(() => this._saveState(), delay); + this._timeoutID = setTimeout(() => this._saveStateAsync(), delay); }, /** @@ -186,8 +186,7 @@ let SessionSaverInternal = { * update the corresponding caches. */ _saveState: function (forceUpdateAllWindows = false) { - // Cancel any pending timeouts or just clear - // the timeout if this is why we've been called. + // Cancel any pending timeouts. this.cancel(); stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); @@ -246,6 +245,33 @@ let SessionSaverInternal = { this._writeState(state); }, + /** + * Saves the current session state. Collects data asynchronously and calls + * _saveState() to collect data again (with a cache hit rate of hopefully + * 100%) and write to disk afterwards. + */ + _saveStateAsync: function () { + // Allow scheduling delayed saves again. + this._timeoutID = null; + + // Check whether asynchronous data collection is disabled. + if (!Services.prefs.getBoolPref("browser.sessionstore.async")) { + this._saveState(); + return; + } + + // Update the last save time to make sure we wait at least another interval + // length until we call _saveStateAsync() again. + this.updateLastSaveTime(); + + // Save state synchronously after all tab caches have been filled. The data + // for the tab caches is collected asynchronously. We will reuse this + // cached data if the tab hasn't been invalidated in the meantime. In that + // case we will just fall back to synchronous data collection for single + // tabs. + SessionStore.fillTabCachesAsynchronously().then(() => this._saveState()); + }, + /** * Write the given state object to disk. */ diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 5360555e5d89..46c3f06304a5 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -277,6 +277,10 @@ this.SessionStore = { return SessionStoreInternal.getCurrentState(aUpdateAll); }, + fillTabCachesAsynchronously: function () { + return SessionStoreInternal.fillTabCachesAsynchronously(); + }, + /** * Backstage pass to implementation details, used for testing purpose. * Controlled by preference "browser.sessionstore.testmode". @@ -1883,6 +1887,68 @@ let SessionStoreInternal = { return [true, canOverwriteTabs]; }, + /* ........ Async Data Collection .............. */ + + /** + * Kicks off asynchronous data collection for all tabs that do not have any + * cached data. The returned promise will only notify that the tab collection + * has been finished without resolving to any data. The tab collection for a + * a few or all tabs might have failed or timed out. By calling + * fillTabCachesAsynchronously() and waiting for the promise to be resolved + * before calling getCurrentState(), callers ensure that most of the data + * should have been collected asynchronously, without blocking the main + * thread. + * + * @return {Promise} the promise that is fulfilled when the tab data is ready + */ + fillTabCachesAsynchronously: function () { + let countdown = 0; + let deferred = Promise.defer(); + let activeWindow = this._getMostRecentBrowserWindow(); + + // The callback that will be called when a promise has been resolved + // successfully, i.e. the tab data has been collected. + function done() { + if (--countdown === 0) { + deferred.resolve(); + } + } + + // The callback that will be called when a promise is rejected, i.e. we + // we couldn't collect the tab data because of a script error or a timeout. + function fail(reason) { + debug("Failed collecting tab data asynchronously: " + reason); + done(); + } + + this._forEachBrowserWindow(win => { + if (!this._isWindowLoaded(win)) { + // Bail out if the window hasn't even loaded, yet. + return; + } + + if (!DirtyWindows.has(win) && win != activeWindow) { + // Bail out if the window is not dirty and inactive. + return; + } + + for (let tab of win.gBrowser.tabs) { + if (!TabStateCache.has(tab)) { + countdown++; + TabState.collect(tab).then(done, fail); + } + } + }); + + // If no dirty tabs were found, return a resolved + // promise because there is nothing to do here. + if (countdown == 0) { + return Promise.resolve(); + } + + return deferred.promise; + }, + /* ........ Saving Functionality .............. */ /**