Bug 1216250 - Limit amount of DOM Storage data stored by Session Restore. r=ttaubert

DOM Storage is a pretty inefficient and memory-hungry storage mechanism. Session Store attempts to record DOM Storage for each tab, which leads to (possibly very large) objects being serialized once to be sent from frame/content to parent and once to be sent from the main thread to the I/O thread. This is a suspect behind a number of crashes (see bug 1106264 for a discussion on the topic).

This patch limits the amount of DOM Storage that Session Restore attempts to store. We perform a quick estimate on the amount of memory needed to serialize DOM Storage and prevent storage larger than ~10M chars being sent from frame/content to the parent. Once this patch has landed, we will need to watch FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS to find out whether our threshold is meaningful.

--HG--
extra : transplant_source : %26%07%ADzjT%A9%E3%B9%B9%EC%9D%97n%23%B5%F2%DAZ%CD
This commit is contained in:
David Rajchenbach-Teller 2015-10-20 14:15:17 +02:00
parent d083caf63c
commit d9df20222c
5 changed files with 126 additions and 16 deletions

View File

@ -30,8 +30,8 @@ this.SessionStorage = Object.freeze({
* @param frameTree
* The docShell's FrameTree instance.
* @return Returns a nested object that will have hosts as keys and per-host
* session storage data as values. For example:
* {"example.com": {"key": "value", "my_number": 123}}
* session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
collect: function (docShell, frameTree) {
return SessionStorageInternal.collect(docShell, frameTree);
@ -43,12 +43,12 @@ this.SessionStorage = Object.freeze({
* A tab's docshell (containing the sessionStorage)
* @param aStorageData
* A nested object with storage data to be restored that has hosts as
* keys and per-host session storage data as values. For example:
* {"example.com": {"key": "value", "my_number": 123}}
* keys and per-host session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
restore: function (aDocShell, aStorageData) {
SessionStorageInternal.restore(aDocShell, aStorageData);
}
},
});
var SessionStorageInternal = {
@ -59,8 +59,8 @@ var SessionStorageInternal = {
* @param frameTree
* The docShell's FrameTree instance.
* @return Returns a nested object that will have hosts as keys and per-host
* session storage data as values. For example:
* {"example.com": {"key": "value", "my_number": 123}}
* session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
collect: function (docShell, frameTree) {
let data = {};
@ -98,8 +98,8 @@ var SessionStorageInternal = {
* A tab's docshell (containing the sessionStorage)
* @param aStorageData
* A nested object with storage data to be restored that has hosts as
* keys and per-host session storage data as values. For example:
* {"example.com": {"key": "value", "my_number": 123}}
* keys and per-host session storage data as strings. For example:
* {"example.com": {"key": "value", "my_number": "123"}}
*/
restore: function (aDocShell, aStorageData) {
for (let origin of Object.keys(aStorageData)) {

View File

@ -16,10 +16,13 @@ var Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Timer.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormData",
"resource://gre/modules/FormData.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
"resource:///modules/sessionstore/DocShellCapabilities.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
"resource:///modules/sessionstore/PageStyle.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition",
@ -39,6 +42,9 @@ XPCOMUtils.defineLazyGetter(this, 'gContentRestore',
// The current epoch.
var gCurrentEpoch = 0;
// A bound to the size of data to store for DOM Storage.
const DOM_STORAGE_MAX_CHARS = 10000000; // 10M characters
/**
* Returns a lazy function that will evaluate the given
* function |fn| only once and cache its return value.
@ -523,9 +529,58 @@ var SessionStorageListener = {
setTimeout(() => this.collect(), 0);
},
// Before DOM Storage can be written to disk, it needs to be serialized
// for sending across frames/processes, then again to be sent across
// threads, then again to be put in a buffer for the disk. Each of these
// serializations is an opportunity to OOM and (depending on the site of
// the OOM), either crash, lose all data for the frame or lose all data
// for the application.
//
// In order to avoid this, compute an estimate of the size of the
// object, and block SessionStorage items that are too large. As
// we also don't want to cause an OOM here, we use a quick and memory-
// efficient approximation: we compute the total sum of string lengths
// involved in this object.
estimateStorageSize: function(collected) {
if (!collected) {
return 0;
}
let size = 0;
for (let host of Object.keys(collected)) {
size += host.length;
let perHost = collected[host];
for (let key of Object.keys(perHost)) {
size += key.length;
let perKey = perHost[key];
size += perKey.length;
}
}
return size;
},
collect: function () {
if (docShell) {
MessageQueue.push("storage", () => SessionStorage.collect(docShell, gFrameTree));
MessageQueue.push("storage", () => {
let collected = SessionStorage.collect(docShell, gFrameTree);
if (collected == null) {
return collected;
}
let size = this.estimateStorageSize(collected);
MessageQueue.push("telemetry", () => ({ FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS: size }));
if (size > Preferences.get("browser.sessionstore.dom_storage_limit", DOM_STORAGE_MAX_CHARS)) {
// Rather than keeping the old storage, which wouldn't match the rest
// of the state of the page, empty the storage. DOM storage will be
// recollected the next time and stored if it is now small enough.
return {};
}
return collected;
});
}
},
@ -665,6 +720,7 @@ var MessageQueue = {
let durationMs = Date.now();
let data = {};
let telemetry = {};
for (let [key, id] of this._lastUpdated) {
// There is no data for the given key anymore because
// the parent process already marked it as received.
@ -680,13 +736,18 @@ var MessageQueue = {
continue;
}
data[key] = this._data.get(key)();
let value = this._data.get(key)();
if (key == "telemetry") {
for (let histogramId of Object.keys(value)) {
telemetry[histogramId] = value[histogramId];
}
} else {
data[key] = value;
}
}
durationMs = Date.now() - durationMs;
let telemetry = {
FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS: durationMs
}
telemetry.FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_LONGEST_OP_MS = durationMs;
try {
// Send all data to the parent process.

View File

@ -20,6 +20,7 @@
if (sessionStorage.length === 0) {
sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
document.title = sessionStorage.test;
}
</script>
</body>

View File

@ -183,6 +183,46 @@ add_task(function respect_privacy_level() {
"https sessionStorage data has been saved");
});
// Test that we record the size of messages.
add_task(function* test_telemetry() {
Services.telemetry.canRecordExtended = true;
let histogram = Services.telemetry.getHistogramById("FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS");
let snap1 = histogram.snapshot();
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Flush to make sure chrome received all data.
yield TabStateFlusher.flush(browser);
let snap2 = histogram.snapshot();
Assert.ok(snap2.counts[5] > snap1.counts[5]);
yield promiseRemoveTab(tab);
Services.telemetry.canRecordExtended = false;
});
// Lower the size limit for DOM Storage content. Check that DOM Storage
// is not updated, but that other things remain updated.
add_task(function* test_large_content() {
Services.prefs.setIntPref("browser.sessionstore.dom_storage_limit", 5);
let tab = gBrowser.addTab(URL);
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Flush to make sure chrome received all data.
yield TabStateFlusher.flush(browser);
let state = JSON.parse(ss.getTabState(tab));
info(JSON.stringify(state, null, "\t"));
Assert.equal(state.storage, null, "We have no storage for the tab");
Assert.equal(state.entries[0].title, OUTER_VALUE);
yield promiseRemoveTab(tab);
Services.prefs.clearUserPref("browser.sessionstore.dom_storage_limit");
});
function purgeDomainData(browser, domain) {
return sendMessage(browser, "ss-test:purgeDomainData", domain);
}

View File

@ -4358,6 +4358,14 @@
"kind": "count",
"description": "Count of messages sent by SessionRestore from child frames to the parent and that cannot be transmitted as they eat up too much memory."
},
"FX_SESSION_RESTORE_DOM_STORAGE_SIZE_ESTIMATE_CHARS": {
"expires_in_version": "default",
"kind": "exponential",
"high": "30000000",
"n_buckets": 20,
"extended_statistics_ok": true,
"description": "Session restore: Number of characters in DOM Storage for a tab. Pages without DOM Storage or with an empty DOM Storage are ignored."
},
"FX_TABLETMODE_PAGE_LOAD": {
"expires_in_version": "47",
"kind": "exponential",