Bug 532150 - Reading and writing session file off the main thread; r=felipe

--HG--
extra : rebase_source : f9ceb62680c932621b876a39f007e74a4f4e4c40
This commit is contained in:
David Rajchenbach-Teller 2012-12-15 10:44:07 -05:00
parent aa24841938
commit f3770967d9
6 changed files with 479 additions and 219 deletions

View File

@ -10,9 +10,15 @@
* - and allows to restore everything into one window.
*/
[scriptable, uuid(170c6857-7f71-46ce-bc9b-185723b1c3a8)]
[scriptable, uuid(35235b39-7098-4b3b-8e28-cd004a88b06f)]
interface nsISessionStartup: nsISupports
{
/**
* Return a promise that is resolved once initialization
* is complete.
*/
readonly attribute jsval onceInitialized;
// Get session state
readonly attribute jsval state;

View File

@ -12,8 +12,8 @@ include $(topsrcdir)/config/config.mk
EXTRA_COMPONENTS = \
nsSessionStore.manifest \
nsSessionStore.js \
nsSessionStartup.js \
nsSessionStore.js \
nsSessionStartup.js \
$(NULL)
JS_MODULES_PATH := $(FINAL_TARGET)/modules/sessionstore
@ -22,6 +22,7 @@ EXTRA_JS_MODULES := \
DocumentUtils.jsm \
SessionStorage.jsm \
XPathGenerator.jsm \
_SessionFile.jsm \
$(NULL)
EXTRA_PP_JS_MODULES := \

View File

@ -75,14 +75,18 @@ const TAB_EVENTS = [
#define BROKEN_WM_Z_ORDER
#endif
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
// debug.js adds NS_ASSERT. cf. bug 669196
Cu.import("resource://gre/modules/debug.js");
Cu.import("resource:///modules/TelemetryTimestamps.jsm");
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/debug.js", this);
Cu.import("resource:///modules/TelemetryTimestamps.jsm", this);
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm", this);
Cu.import("resource://gre/modules/commonjs/promise/core.js", this);
XPCOMUtils.defineLazyServiceGetter(this, "gSessionStartup",
"@mozilla.org/browser/sessionstartup;1", "nsISessionStartup");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
@ -92,6 +96,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DocumentUtils",
"resource:///modules/sessionstore/DocumentUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
"resource:///modules/sessionstore/SessionStorage.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
"resource:///modules/sessionstore/_SessionFile.jsm");
#ifdef MOZ_CRASHREPORTER
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
@ -290,8 +296,11 @@ let SessionStoreInternal = {
// session
_lastSessionState: null,
// Whether we've been initialized
_initialized: false,
// A promise resolved once initialization is complete
_promiseInitialization: Promise.defer(),
// Whether session has been initialized
_sessionInitialized: false,
// The original "sessionstore.resume_session_once" preference value before it
// was modified by saveState. saveState will set the
@ -326,6 +335,9 @@ let SessionStoreInternal = {
* Initialize the component
*/
initService: function ssi_initService() {
if (this._sessionInitialized) {
return;
}
TelemetryTimestamps.add("sessionRestoreInitialized");
OBSERVING.forEach(function(aTopic) {
Services.obs.addObserver(this, aTopic, true);
@ -358,15 +370,13 @@ let SessionStoreInternal = {
this._prefBranch.getBoolPref("sessionstore.restore_pinned_tabs_on_demand");
this._prefBranch.addObserver("sessionstore.restore_pinned_tabs_on_demand", this, true);
// get file references
this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
this._sessionFileBackup = this._sessionFile.clone();
this._sessionFile.append("sessionstore.js");
this._sessionFileBackup.append("sessionstore.bak");
gSessionStartup.onceInitialized.then(
this.initSession.bind(this)
);
},
// get string containing session state
var ss = Cc["@mozilla.org/browser/sessionstartup;1"].
getService(Ci.nsISessionStartup);
initSession: function ssi_initSession() {
let ss = gSessionStartup;
try {
if (ss.doRestore() ||
ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION)
@ -437,14 +447,11 @@ let SessionStoreInternal = {
}
if (this._resume_from_crash) {
// create a backup if the session data file exists
try {
if (this._sessionFileBackup.exists())
this._sessionFileBackup.remove(false);
if (this._sessionFile.exists())
this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
}
catch (ex) { Cu.reportError(ex); } // file was write-locked?
// Launch background copy of the session file. Note that we do
// not have race conditions here as _SessionFile guarantees
// that any I/O operation is completed before proceeding to
// the next I/O operation.
_SessionFile.createBackupCopy();
}
// at this point, we've as good as resumed the session, so we can
@ -455,7 +462,9 @@ let SessionStoreInternal = {
this._initEncoding();
this._initialized = true;
// Session is ready.
this._sessionInitialized = true;
this._promiseInitialization.resolve();
},
_initEncoding : function ssi_initEncoding() {
@ -495,16 +504,7 @@ let SessionStoreInternal = {
});
},
/**
* Start tracking a window.
* This function also initializes the component if it's not already
* initialized.
*/
init: function ssi_init(aWindow) {
// Initialize the service if needed.
if (!this._initialized)
this.initService();
_initWindow: function ssi_initWindow(aWindow) {
if (!aWindow || this._loadState == STATE_RUNNING) {
// make sure that all browser windows which try to initialize
// SessionStore are really tracked by it
@ -524,13 +524,30 @@ let SessionStoreInternal = {
this.onLoad(aWindow);
},
/**
* Start tracking a window.
*
* This function also initializes the component if it is not
* initialized yet.
*/
init: function ssi_init(aWindow) {
let self = this;
this.initService();
this._promiseInitialization.promise.then(
function onSuccess() {
self._initWindow(aWindow);
}
);
},
/**
* Called on application shutdown, after notifications:
* quit-application-granted, quit-application
*/
_uninit: function ssi_uninit() {
// save all data for session resuming
this.saveState(true);
if (this._sessionInitialized)
this.saveState(true);
// clear out _tabsToRestore in case it's still holding refs
this._tabsToRestore.priority = null;
@ -998,7 +1015,7 @@ let SessionStoreInternal = {
*/
onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
var _this = this;
this._clearDisk();
_SessionFile.wipe();
// If the browser is shutting down, simply return after clearing the
// session data on disk as this notification fires after the
// quit-application notification so the browser is about to exit.
@ -1139,7 +1156,7 @@ let SessionStoreInternal = {
// either create the file with crash recovery information or remove it
// (when _loadState is not STATE_RUNNING, that file is used for session resuming instead)
if (!this._resume_from_crash)
this._clearDisk();
_SessionFile.wipe();
this.saveState(true);
break;
case "sessionstore.restore_on_demand":
@ -3767,40 +3784,37 @@ let SessionStoreInternal = {
*/
_saveStateObject: function ssi_saveStateObject(aStateObj) {
TelemetryStopwatch.start("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
var stateString = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
stateString.data = this._toJSONString(aStateObj);
let data = this._toJSONString(aStateObj);
TelemetryStopwatch.finish("FX_SESSION_RESTORE_SERIALIZE_DATA_MS");
let stateString = this._createSupportsString(data);
Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
data = stateString.data;
// don't touch the file if an observer has deleted all state data
if (stateString.data)
this._writeFile(this._sessionFile, stateString.data);
this._lastSaveTime = Date.now();
},
/**
* delete session datafile and backup
*/
_clearDisk: function ssi_clearDisk() {
if (this._sessionFile.exists()) {
try {
this._sessionFile.remove(false);
}
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
// Don't touch the file if an observer has deleted all state data.
if (!data) {
return;
}
if (this._sessionFileBackup.exists()) {
try {
this._sessionFileBackup.remove(false);
let self = this;
_SessionFile.write(data).then(
function onSuccess() {
self._lastSaveTime = Date.now();
Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
}
catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
}
);
},
/* ........ Auxiliary Functions .............. */
// Wrap a string as a nsISupports
_createSupportsString: function ssi_createSupportsString(aData) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = aData;
return string;
},
/**
* call a callback for all currently opened browser windows
* (might miss the most recent one)
@ -4494,33 +4508,6 @@ let SessionStoreInternal = {
removeSHistoryListener(browser.__SS_shistoryListener);
delete browser.__SS_shistoryListener;
}
},
/**
* write file to disk
* @param aFile
* nsIFile
* @param aData
* String data
*/
_writeFile: function ssi_writeFile(aFile, aData) {
let refObj = {};
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
let path = aFile.path;
let encoded = this._writeFileEncoder.encode(aData);
let promise = OS.File.writeAtomic(path, encoded, {tmpPath: path + ".tmp"});
promise.then(
function onSuccess() {
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
Services.obs.notifyObservers(null,
"sessionstore-state-write-complete",
"");
},
function onFailure(reason) {
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
Components.reportError("ssi_writeFile failure " + reason);
});
}
};

View File

@ -0,0 +1,255 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["_SessionFile"];
/**
* Implementation of all the disk I/O required by the session store.
* This is a private API, meant to be used only by the session store.
* It will change. Do not use it for any other purpose.
*
* Note that this module implicitly depends on one of two things:
* 1. either the asynchronous file I/O system enqueues its requests
* and never attempts to simultaneously execute two I/O requests on
* the files used by this module from two distinct threads; or
* 2. the clients of this API are well-behaved and do not place
* concurrent requests to the files used by this module.
*
* Otherwise, we could encounter bugs, especially under Windows,
* e.g. if a request attempts to write sessionstore.js while
* another attempts to copy that file.
*
* This implementation uses OS.File, which guarantees property 1.
*/
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/commonjs/promise/core.js");
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
// An encoder to UTF-8.
XPCOMUtils.defineLazyGetter(this, "gEncoder", function () {
return new TextEncoder();
});
this._SessionFile = {
/**
* A promise fulfilled once initialization (either synchronous or
* asynchronous) is complete.
*/
promiseInitialized: function SessionFile_initialized() {
return SessionFileInternal.promiseInitialized;
},
/**
* Read the contents of the session file, asynchronously.
*/
read: function SessionFile_read() {
return SessionFileInternal.read();
},
/**
* Read the contents of the session file, synchronously.
*/
syncRead: function SessionFile_syncRead() {
return SessionFileInternal.syncRead();
},
/**
* Write the contents of the session file, asynchronously.
*/
write: function SessionFile_write(aData) {
return SessionFileInternal.write(aData);
},
/**
* Create a backup copy, asynchronously.
*/
createBackupCopy: function SessionFile_createBackupCopy() {
return SessionFileInternal.createBackupCopy();
},
/**
* Wipe the contents of the session file, asynchronously.
*/
wipe: function SessionFile_wipe() {
return SessionFileInternal.wipe();
}
};
Object.freeze(_SessionFile);
/**
* Utilities for dealing with promises and Task.jsm
*/
const TaskUtils = {
/**
* Add logging to a promise.
*
* @param {Promise} promise
* @return {Promise} A promise behaving as |promise|, but with additional
* logging in case of uncaught error.
*/
captureErrors: function captureErrors(promise) {
return promise.then(
null,
function onError(reason) {
Cu.reportError("Uncaught asynchronous error: " + reason + " at\n" + reason.stack);
throw reason;
}
);
},
/**
* Spawn a new Task from a generator.
*
* This function behaves as |Task.spawn|, with the exception that it
* adds logging in case of uncaught error. For more information, see
* the documentation of |Task.jsm|.
*
* @param {generator} gen Some generator.
* @return {Promise} A promise built from |gen|, with the same semantics
* as |Task.spawn(gen)|.
*/
spawn: function spawn(gen) {
return this.captureErrors(Task.spawn(gen));
}
};
let SessionFileInternal = {
/**
* A promise fulfilled once initialization is complete
*/
promiseInitialized: Promise.defer(),
/**
* The path to sessionstore.js
*/
path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
/**
* The path to sessionstore.bak
*/
backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
/**
* Read the sessionstore file synchronously.
*
* This function is meant to serve as a fallback in case of race
* between a synchronous usage of the API and asynchronous
* initialization.
*/
syncRead: function ssfi_syncRead() {
let text;
let exn;
TelemetryStopwatch.start("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
try {
let file = new FileUtils.File(this.path);
if (!file.exists()) {
return null;
}
let chan = NetUtil.newChannel(file);
let stream = chan.open();
text = NetUtil.readInputStreamToString(stream, stream.available(), {charset: "utf-8"});
} catch(ex) {
exn = ex;
} finally {
TelemetryStopwatch.finish("FX_SESSION_RESTORE_SYNC_READ_FILE_MS");
}
if (exn) {
Cu.reportError(exn);
return "";
}
return text;
},
read: function ssfi_read() {
let refObj = {};
let self = this;
return TaskUtils.spawn(function task() {
TelemetryStopwatch.start("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
let text;
try {
let bytes = yield OS.File.read(self.path);
text = new TextDecoder().decode(bytes);
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
} catch (ex) {
if (self._isNoSuchFile(ex)) {
// The file does not exist, this is perfectly valid
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
} else {
// Any other error: let's not measure with telemetry
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_READ_FILE_MS", refObj);
Cu.reportError(ex);
}
text = "";
}
throw new Task.Result(text);
});
},
write: function ssfi_write(aData) {
let refObj = {};
let self = this;
return TaskUtils.spawn(function task() {
TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
let bytes = gEncoder.encode(aData);
try {
yield OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
} catch (ex) {
TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
Cu.reportError("Could not write session state file " + self.path
+ ": " + aReason);
}
});
},
createBackupCopy: function ssfi_createBackupCopy() {
let self = this;
return TaskUtils.spawn(function task() {
try {
yield OS.File.copy(self.path, self.backupPath);
} catch (ex) {
if (!self._isNoSuchFile(ex)) {
Cu.reportError("Could not backup session state file: " + ex);
throw ex;
}
}
});
},
wipe: function ssfi_wipe() {
let self = this;
return TaskUtils.spawn(function task() {
try {
yield OS.File.remove(self.path);
} catch (ex) {
Cu.reportError("Could not remove session state file: " + ex);
throw ex;
}
try {
yield OS.File.remove(self.backupPath);
} catch (ex) {
Cu.reportError("Could not remove session state backup file: " + ex);
throw ex;
}
});
},
_isNoSuchFile: function ssfi_isNoSuchFile(aReason) {
return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile;
}
};

View File

@ -39,15 +39,20 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/commonjs/promise/core.js");
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
"resource:///modules/sessionstore/_SessionFile.jsm");
const STATE_RUNNING_STR = "running";
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 megabytes
function debug(aMsg) {
aMsg = ("SessionStartup: " + aMsg).replace(/\S{80}/g, "$&\n");
Services.console.logStringMessage(aMsg);
}
let gOnceInitializedDeferred = Promise.defer();
/* :::::::: The Service ::::::::::::::: */
function SessionStartup() {
@ -58,6 +63,7 @@ SessionStartup.prototype = {
// the state to restore at startup
_initialState: null,
_sessionType: Ci.nsISessionStartup.NO_SESSION,
_initialized: false,
/* ........ Global Event Handlers .............. */
@ -65,95 +71,121 @@ SessionStartup.prototype = {
* Initialize the component
*/
init: function sss_init() {
debug("init starting");
// do not need to initialize anything in auto-started private browsing sessions
let pbs = Cc["@mozilla.org/privatebrowsing;1"].
getService(Ci.nsIPrivateBrowsingService);
if (PrivateBrowsingUtils.permanentPrivateBrowsing ||
pbs.lastChangedByCommandLine)
return;
// Session state is unknown until we read the file.
this._sessionType = null;
_SessionFile.read().then(
this._onSessionFileRead.bind(this)
);
debug("init launched");
},
let prefBranch = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService).getBranch("browser.");
// Wrap a string as a nsISupports
_createSupportsString: function ssfi_createSupportsString(aData) {
let string = Cc["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
string.data = aData;
return string;
},
// get file references
var dirService = Cc["@mozilla.org/file/directory_service;1"].
getService(Ci.nsIProperties);
let sessionFile = dirService.get("ProfD", Ci.nsILocalFile);
sessionFile.append("sessionstore.js");
let doResumeSessionOnce = prefBranch.getBoolPref("sessionstore.resume_session_once");
let doResumeSession = doResumeSessionOnce ||
prefBranch.getIntPref("startup.page") == 3;
// only continue if the session file exists
if (!sessionFile.exists())
_onSessionFileRead: function sss_onSessionFileRead(aStateString) {
debug("onSessionFileRead ");
if (this._initialized) {
debug("onSessionFileRead: Initialization is already complete");
// Initialization is complete, nothing else to do
return;
// get string containing session state
let iniString = this._readStateFile(sessionFile);
if (!iniString)
return;
// parse the session state into a JS object
// remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
if (iniString.charAt(0) == '(')
iniString = iniString.slice(1, -1);
let corruptFile = false;
}
try {
this._initialState = JSON.parse(iniString);
}
catch (ex) {
debug("The session file contained un-parse-able JSON: " + ex);
// Try to eval.
// evalInSandbox will throw if iniString is not parse-able.
try {
var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
this._initialState = Cu.evalInSandbox("(" + iniString + ")", s);
} catch(ex) {
debug("The session file contained un-eval-able JSON: " + ex);
corruptFile = true;
this._initialized = true;
// Let observers modify the state before it is used
let supportsStateString = this._createSupportsString(aStateString);
Services.obs.notifyObservers(supportsStateString, "sessionstore-state-read", "");
aStateString = supportsStateString.data;
// No valid session found.
if (!aStateString) {
this._sessionType = Ci.nsISessionStartup.NO_SESSION;
return;
}
// parse the session state into a JS object
// remove unneeded braces (added for compatibility with Firefox 2.0 and 3.0)
if (aStateString.charAt(0) == '(')
aStateString = aStateString.slice(1, -1);
let corruptFile = false;
try {
this._initialState = JSON.parse(aStateString);
}
catch (ex) {
debug("The session file contained un-parse-able JSON: " + ex);
// This is not valid JSON, but this might still be valid JavaScript,
// as used in FF2/FF3, so we need to eval.
// evalInSandbox will throw if aStateString is not parse-able.
try {
var s = new Cu.Sandbox("about:blank", {sandboxName: 'nsSessionStartup'});
this._initialState = Cu.evalInSandbox("(" + aStateString + ")", s);
} catch(ex) {
debug("The session file contained un-eval-able JSON: " + ex);
corruptFile = true;
}
}
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
let doResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
let doResumeSession = doResumeSessionOnce ||
Services.prefs.getIntPref("browser.startup.page") == 3;
// If this is a normal restore then throw away any previous session
if (!doResumeSessionOnce)
delete this._initialState.lastSessionState;
let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
let lastSessionCrashed =
this._initialState && this._initialState.session &&
this._initialState.session.state &&
this._initialState.session.state == STATE_RUNNING_STR;
// Report shutdown success via telemetry. Shortcoming here are
// being-killed-by-OS-shutdown-logic, shutdown freezing after
// session restore was written, etc.
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!lastSessionCrashed);
// set the startup type
if (lastSessionCrashed && resumeFromCrash)
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
else if (!lastSessionCrashed && doResumeSession)
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
else if (this._initialState)
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
else
this._initialState = null; // reset the state
// wait for the first browser window to open
// Don't reset the initial window's default args (i.e. the home page(s))
// if all stored tabs are pinned.
if (this.doRestore() &&
(!this._initialState.windows ||
!this._initialState.windows.every(function (win)
win.tabs.every(function (tab) tab.pinned))))
Services.obs.addObserver(this, "domwindowopened", true);
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
Services.obs.addObserver(this, "browser:purge-session-history", true);
} finally {
// We're ready. Notify everyone else.
Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
gOnceInitializedDeferred.resolve();
}
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_CORRUPT_FILE").add(corruptFile);
// If this is a normal restore then throw away any previous session
if (!doResumeSessionOnce)
delete this._initialState.lastSessionState;
let resumeFromCrash = prefBranch.getBoolPref("sessionstore.resume_from_crash");
let lastSessionCrashed =
this._initialState && this._initialState.session &&
this._initialState.session.state &&
this._initialState.session.state == STATE_RUNNING_STR;
// Report shutdown success via telemetry. Shortcoming here are
// being-killed-by-OS-shutdown-logic, shutdown freezing after
// session restore was written, etc.
Services.telemetry.getHistogramById("SHUTDOWN_OK").add(!lastSessionCrashed);
// set the startup type
if (lastSessionCrashed && resumeFromCrash)
this._sessionType = Ci.nsISessionStartup.RECOVER_SESSION;
else if (!lastSessionCrashed && doResumeSession)
this._sessionType = Ci.nsISessionStartup.RESUME_SESSION;
else if (this._initialState)
this._sessionType = Ci.nsISessionStartup.DEFER_SESSION;
else
this._initialState = null; // reset the state
// wait for the first browser window to open
// Don't reset the initial window's default args (i.e. the home page(s))
// if all stored tabs are pinned.
if (this.doRestore() &&
(!this._initialState.windows ||
!this._initialState.windows.every(function (win)
win.tabs.every(function (tab) tab.pinned))))
Services.obs.addObserver(this, "domwindowopened", true);
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
if (this._sessionType != Ci.nsISessionStartup.NO_SESSION)
Services.obs.addObserver(this, "browser:purge-session-history", true);
},
/**
@ -237,10 +269,15 @@ SessionStartup.prototype = {
/* ........ Public API ................*/
get onceInitialized() {
return gOnceInitializedDeferred.promise;
},
/**
* Get the session state as a jsval
*/
get state() {
this._ensureInitialized();
return this._initialState;
},
@ -249,6 +286,7 @@ SessionStartup.prototype = {
* @returns bool
*/
doRestore: function sss_doRestore() {
this._ensureInitialized();
return this._sessionType == Ci.nsISessionStartup.RECOVER_SESSION ||
this._sessionType == Ci.nsISessionStartup.RESUME_SESSION;
},
@ -257,59 +295,26 @@ SessionStartup.prototype = {
* Get the type of pending session store, if any.
*/
get sessionType() {
this._ensureInitialized();
return this._sessionType;
},
/* ........ Storage API .............. */
/**
* Reads a session state file into a string and lets
* observers modify the state before it's being used
*
* @param aFile is any nsIFile
* @returns a session state string
*/
_readStateFile: function sss_readStateFile(aFile) {
TelemetryStopwatch.start("FX_SESSION_RESTORE_READ_FILE_MS");
var stateString = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
stateString.data = this._readFile(aFile) || "";
TelemetryStopwatch.finish("FX_SESSION_RESTORE_READ_FILE_MS");
Services.obs.notifyObservers(stateString, "sessionstore-state-read", "");
return stateString.data;
},
/**
* reads a file into a string
* @param aFile
* nsIFile
* @returns string
*/
_readFile: function sss_readFile(aFile) {
// Ensure that initialization is complete.
// If initialization is not complete yet, fall back to a synchronous
// initialization and kill ongoing asynchronous initialization
_ensureInitialized: function sss__ensureInitialized() {
try {
var stream = Cc["@mozilla.org/network/file-input-stream;1"].
createInstance(Ci.nsIFileInputStream);
stream.init(aFile, 0x01, 0, 0);
var cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"].
createInstance(Ci.nsIConverterInputStream);
var fileSize = stream.available();
if (fileSize > MAX_FILE_SIZE)
throw "SessionStartup: sessionstore.js was not processed because it was too large.";
cvstream.init(stream, "UTF-8", fileSize, Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
var data = {};
cvstream.readString(fileSize, data);
var content = data.value;
cvstream.close();
return content.replace(/\r\n?/g, "\n");
debug("_ensureInitialized: " + this._initialState);
if (this._initialized) {
// Initialization is complete, nothing else to do
return;
}
let contents = _SessionFile.syncRead();
this._onSessionFileRead(contents);
} catch(ex) {
debug("ensureInitialized: could not read session " + ex + ", " + ex.stack);
throw ex;
}
catch (ex) { Cu.reportError(ex); }
return null;
},
/* ........ QueryInterface .............. */

View File

@ -2113,6 +2113,12 @@
"n_buckets": 10,
"description": "Session restore: Time to read the session data from the file on disk (ms)"
},
"FX_SESSION_RESTORE_SYNC_READ_FILE_MS": {
"kind": "exponential",
"high": "3000",
"n_buckets": 10,
"description": "Session restore: Time to read the session data from the file on disk, using the synchronous fallback (ms)"
},
"FX_SESSION_RESTORE_WRITE_FILE_MS": {
"kind": "exponential",
"high": "3000",