Bug 1190627 - Part 2 - Do regular session data backups. r=sebastian
We now do a backup copy of the session store data at regular intervals to guard against interrupted write operations damaging the main session data file.
We don't use writeAtomic()'s backupTo option, because that one works by first moving the old data to the backup file before attempting to write the new data, which might still leave us vulnerable against data loss if Firefox crashes or is otherwise forcibly terminated at precisely that moment.
MozReview-Commit-ID: Cv52rmlfmfh
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -136,16 +136,17 @@ pref("browser.sessionhistory.max_total_v
pref("browser.sessionhistory.max_entries", 50);
pref("browser.sessionhistory.contentViewerTimeout", 360);
pref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", false);
/* session store */
pref("browser.sessionstore.resume_session_once", false);
pref("browser.sessionstore.resume_from_crash", true);
pref("browser.sessionstore.interval", 10000); // milliseconds
+pref("browser.sessionstore.backupInterval", 120000); // milliseconds -> 2 minutes
pref("browser.sessionstore.max_tabs_undo", 10);
pref("browser.sessionstore.max_resumed_crashes", 1);
pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
pref("browser.sessionstore.debug_logging", false);
/* these should help performance */
pref("mozilla.widget.force-24bpp", true);
pref("mozilla.widget.use-buffer-pixmap", true);
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -61,21 +61,28 @@ SessionStore.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
Ci.nsIDOMEventListener,
Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
_windows: {},
_lastSaveTime: 0,
+ _lastBackupTime: 0,
_interval: 10000,
+ _backupInterval: 120000, // 2 minutes
_minSaveDelay: MINIMUM_SAVE_DELAY,
_maxTabsUndo: 5,
_pendingWrite: 0,
_scrollSavePending: null,
+ _writeInProgress: false,
+
+ // We only want to start doing backups if we've successfully
+ // written the session data at least once.
+ _sessionDataIsGood: false,
// The index where the most recently closed tab was in the tabs array
// when it was closed.
_lastClosedTabIndex: -1,
// Whether or not to send notifications for changes to the closed tabs.
_notifyClosedTabs: false,
@@ -91,31 +98,34 @@ SessionStore.prototype = {
this._sessionFileBackup.append("sessionstore.bak"); // A backup copy to guard against interrupted writes.
this._sessionFilePrevious.append("sessionstore.old"); // The previous session's file, used for what used to be the "Tabs from last time".
this._sessionFileTemp.append(this._sessionFile.leafName + ".tmp"); // Temporary file for writing changes to disk.
this._loadState = STATE_STOPPED;
this._startupRestoreFinished = false;
this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._backupInterval = Services.prefs.getIntPref("browser.sessionstore.backupInterval");
this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
// Copy changes in Gecko settings to their Java counterparts,
// so the startup code can access them
Services.prefs.addObserver(PREFS_RESTORE_FROM_CRASH, function() {
SharedPreferences.forApp().setBoolPref(PREFS_RESTORE_FROM_CRASH,
Services.prefs.getBoolPref(PREFS_RESTORE_FROM_CRASH));
}, false);
Services.prefs.addObserver(PREFS_MAX_CRASH_RESUMES, function() {
SharedPreferences.forApp().setIntPref(PREFS_MAX_CRASH_RESUMES,
Services.prefs.getIntPref(PREFS_MAX_CRASH_RESUMES));
}, false);
},
_clearDisk: function ss_clearDisk() {
+ this._sessionDataIsGood = false;
+
OS.File.remove(this._sessionFile.path);
OS.File.remove(this._sessionFileBackup.path);
OS.File.remove(this._sessionFilePrevious.path);
OS.File.remove(this._sessionFileTemp.path);
},
observe: function ss_observe(aSubject, aTopic, aData) {
let self = this;
@@ -840,16 +850,28 @@ SessionStore.prototype = {
log("_saveState(aAsync = " + aAsync + ")");
// Kill any queued timer and save immediately
if (this._saveTimer) {
this._saveTimer.cancel();
this._saveTimer = null;
log("_saveState() killed queued timer");
}
+ // Periodically save a "known good" copy of the session store data.
+ if (!this._writeInProgress && Date.now() - this._lastBackupTime > this._backupInterval &&
+ this._sessionDataIsGood && this._sessionFile.exists()) {
+ if (this._sessionFileBackup.exists()) {
+ this._sessionFileBackup.remove(false);
+ }
+
+ log("_saveState() backing up session data");
+ this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+ this._lastBackupTime = Date.now();
+ }
+
let data = this._getCurrentState();
let normalData = { windows: [] };
let privateData = { windows: [] };
log("_saveState() current state collected");
for (let winIndex = 0; winIndex < data.windows.length; ++winIndex) {
let win = data.windows[winIndex];
let normalWin = {};
@@ -980,33 +1002,36 @@ SessionStore.prototype = {
// Convert data string to a utf-8 encoded array buffer
let buffer = new TextEncoder().encode(state);
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_FILE_SIZE_BYTES").add(buffer.byteLength);
Services.obs.notifyObservers(null, "sessionstore-state-write", "");
let startWriteMs = Cu.now();
log("_writeFile(aAsync = " + aAsync + "), _pendingWrite = " + this._pendingWrite);
+ this._writeInProgress = true;
let pendingWrite = this._pendingWrite;
this._write(aFile, aFileTemp, buffer, aAsync).then(() => {
let stopWriteMs = Cu.now();
// Make sure this._pendingWrite is the same value it was before we
// fired off the async write. If the count is different, another write
// is pending, so we shouldn't reset this._pendingWrite yet.
if (pendingWrite === this._pendingWrite) {
this._pendingWrite = 0;
+ this._writeInProgress = false;
}
log("_writeFile() _write() returned, _pendingWrite = " + this._pendingWrite);
// We don't use a stopwatch here since the calls are async and stopwatches can only manage
// a single timer per histogram.
Services.telemetry.getHistogramById("FX_SESSION_RESTORE_WRITE_FILE_MS").add(Math.round(stopWriteMs - startWriteMs));
Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
+ this._sessionDataIsGood = true;
});
},
/**
* Writes the session state to a disk file, using async or sync methods
* @param aFile nsIFile used for saving the session
* @param aFileTemp nsIFile used as a temporary file in writing the data
* @param aBuffer UTF-8 encoded ArrayBuffer of the session state