mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-17 07:15:46 +00:00
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:
parent
d083caf63c
commit
d9df20222c
@ -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)) {
|
||||
|
@ -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.
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
if (sessionStorage.length === 0) {
|
||||
sessionStorage.test = (isOuter ? "outer" : "inner") + "-value-" + rand;
|
||||
document.title = sessionStorage.test;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user