diff --git a/browser/components/sessionstore/src/SessionFile.jsm b/browser/components/sessionstore/src/SessionFile.jsm index dc7d9db4aa0..ab1c49d16e5 100644 --- a/browser/components/sessionstore/src/SessionFile.jsm +++ b/browser/components/sessionstore/src/SessionFile.jsm @@ -58,6 +58,22 @@ this.SessionFile = { write: function (aData) { return SessionFileInternal.write(aData); }, + /** + * Gather telemetry statistics. + * + * + * Most of the work is done off the main thread but there is a main + * thread cost involved to send data to the worker thread. This method + * should therefore be called only when we know that it will not disrupt + * the user's experience, e.g. on idle-daily. + * + * @return {Promise} + * @promise {object} An object holding all the information to be submitted + * to Telemetry. + */ + gatherTelemetry: function(aData) { + return SessionFileInternal.gatherTelemetry(aData); + }, /** * Writes the initial state to disk again only to change the session's load * state. This must only be called once, it will throw an error otherwise. @@ -122,6 +138,14 @@ let SessionFileInternal = { }); }, + gatherTelemetry: function(aStateString) { + return Task.spawn(function() { + let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]); + this._recordTelemetry(msg.telemetry); + throw new Task.Result(msg.telemetry); + }.bind(this)); + }, + write: function (aData) { if (this._isClosed) { return Promise.reject(new Error("SessionFile is closed")); @@ -177,8 +201,18 @@ let SessionFileInternal = { }, _recordTelemetry: function(telemetry) { - for (let histogramId in telemetry){ - Telemetry.getHistogramById(histogramId).add(telemetry[histogramId]); + for (let id of Object.keys(telemetry)){ + let value = telemetry[id]; + let samples = []; + if (Array.isArray(value)) { + samples.push(...value); + } else { + samples.push(value); + } + let histogram = Telemetry.getHistogramById(id); + for (let sample of samples) { + histogram.add(sample); + } } } }; @@ -196,9 +230,12 @@ let SessionWorker = (function () { // Decode any serialized error if (error instanceof PromiseWorker.WorkerError) { throw OS.File.Error.fromMsg(error.data); - } else { - throw error; } + // Extract something meaningful from ErrorEvent + if (error instanceof ErrorEvent) { + throw new Error(error.message, error.filename, error.lineno); + } + throw error; } ); } diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 56303996ed4..3b5770b7a1f 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -35,7 +35,8 @@ const OBSERVING = [ "quit-application-requested", "quit-application-granted", "browser-lastwindow-close-granted", "quit-application", "browser:purge-session-history", - "browser:purge-domain-data" + "browser:purge-domain-data", + "gather-telemetry", ]; // XUL Window properties to (re)store @@ -587,6 +588,9 @@ let SessionStoreInternal = { case "nsPref:changed": // catch pref changes this.onPrefChange(aData); break; + case "gather-telemetry": + this.onGatherTelemetry(); + break; } }, @@ -1457,6 +1461,16 @@ let SessionStoreInternal = { this.saveStateDelayed(aWindow); }, + onGatherTelemetry: function() { + // On the first gather-telemetry notification of the session, + // gather telemetry data. + Services.obs.removeObserver(this, "gather-telemetry"); + this.fillTabCachesAsynchronously().then(function() { + let stateString = SessionStore.getBrowserState(); + return SessionFile.gatherTelemetry(stateString); + }); + }, + /* ........ nsISessionStore API .............. */ getBrowserState: function ssi_getBrowserState() { diff --git a/browser/components/sessionstore/src/SessionWorker.js b/browser/components/sessionstore/src/SessionWorker.js index 276d8d7aa51..c83d5116f43 100644 --- a/browser/components/sessionstore/src/SessionWorker.js +++ b/browser/components/sessionstore/src/SessionWorker.js @@ -130,6 +130,16 @@ let Agent = { return ret; }, + /** + * Extract all sorts of useful statistics from a state string, + * for use with Telemetry. + * + * @return {object} + */ + gatherTelemetry: function (stateString) { + return Statistics.collect(stateString); + }, + /** * Writes the session state to disk again but changes session.state to * 'running' before doing so. This is intended to be called only once, shortly @@ -236,3 +246,144 @@ let Agent = { function isNoSuchFileEx(aReason) { return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; } + +/** + * Estimate the number of bytes that a data structure will use on disk + * once serialized. + */ +function getByteLength(str) { + return Encoder.encode(JSON.stringify(str)).byteLength; +} + +/** + * Tools for gathering statistics on a state string. + */ +let Statistics = { + collect: function(stateString) { + let start = Date.now(); + let TOTAL_PREFIX = "FX_SESSION_RESTORE_TOTAL_"; + let INDIVIDUAL_PREFIX = "FX_SESSION_RESTORE_INDIVIDUAL_"; + let SIZE_SUFFIX = "_SIZE_BYTES"; + + let state = JSON.parse(stateString); + + // Gather all data + let subsets = {}; + this.gatherSimpleData(state, subsets); + this.gatherComplexData(state, subsets); + + // Extract telemetry + let telemetry = {}; + for (let k of Object.keys(subsets)) { + let obj = subsets[k]; + telemetry[TOTAL_PREFIX + k + SIZE_SUFFIX] = getByteLength(obj); + + if (Array.isArray(obj)) { + let size = obj.map(getByteLength); + telemetry[INDIVIDUAL_PREFIX + k + SIZE_SUFFIX] = size; + } + } + + let stop = Date.now(); + telemetry["FX_SESSION_RESTORE_EXTRACTING_STATISTICS_DURATION_MS"] = stop - start; + return { + telemetry: telemetry + }; + }, + + /** + * Collect data that doesn't require a recursive walk through the + * data structure. + */ + gatherSimpleData: function(state, subsets) { + // The subset of sessionstore.js dealing with open windows + subsets.OPEN_WINDOWS = state.windows; + + // The subset of sessionstore.js dealing with closed windows + subsets.CLOSED_WINDOWS = state._closedWindows; + + // The subset of sessionstore.js dealing with closed tabs + // in open windows + subsets.CLOSED_TABS_IN_OPEN_WINDOWS = []; + + // The subset of sessionstore.js dealing with cookies + // in both open and closed windows + subsets.COOKIES = []; + + for (let winData of state.windows) { + let closedTabs = winData._closedTabs || []; + subsets.CLOSED_TABS_IN_OPEN_WINDOWS.push(...closedTabs); + + let cookies = winData.cookies || []; + subsets.COOKIES.push(...cookies); + } + + for (let winData of state._closedWindows) { + let cookies = winData.cookies || []; + subsets.COOKIES.push(...cookies); + } + }, + + /** + * Walk through a data structure, recursively. + * + * @param {object} root The object from which to start walking. + * @param {function(key, value)} cb Callback, called for each + * item except the root. Returns |true| to walk the subtree rooted + * at |value|, |false| otherwise */ + walk: function(root, cb) { + if (!root || typeof root !== "object") { + return; + } + for (let k of Object.keys(root)) { + let obj = root[k]; + let stepIn = cb(k, obj); + if (stepIn) { + this.walk(obj, cb); + } + } + }, + + /** + * Collect data that requires walking through the data structure + */ + gatherComplexData: function(state, subsets) { + // The subset of sessionstore.js dealing with DOM storage + subsets.DOM_STORAGE = []; + // The subset of sessionstore.js storing form data + subsets.FORMDATA = []; + // The subset of sessionstore.js storing POST data in history + subsets.POSTDATA = []; + // The subset of sessionstore.js storing history + subsets.HISTORY = []; + + + this.walk(state, function(k, value) { + let dest; + switch (k) { + case "entries": + subsets.HISTORY.push(value); + return true; + case "storage": + subsets.DOM_STORAGE.push(value); + // Never visit storage, it's full of weird stuff + return false; + case "formdata": + subsets.FORMDATA.push(value); + // Never visit formdata, it's full of weird stuff + return false; + case "postdata_b64": + subsets.POSTDATA.push(value); + return false; // Nothing to visit anyway + case "cookies": // Don't visit these places, they are full of weird stuff + case "extData": + return false; + default: + return true; + } + }); + + return subsets; + }, + +}; diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini index 8c94ff96030..eb43b1fcf9b 100644 --- a/browser/components/sessionstore/test/browser.ini +++ b/browser/components/sessionstore/test/browser.ini @@ -70,6 +70,7 @@ support-files = [browser_sessionStorage.js] [browser_swapDocShells.js] [browser_tabStateCache.js] +[browser_telemetry.js] [browser_upgrade_backup.js] [browser_windowRestore_perwindowpb.js] [browser_248970_b_perwindowpb.js] diff --git a/browser/components/sessionstore/test/browser_broadcast.js b/browser/components/sessionstore/test/browser_broadcast.js index 8e3bda45045..987703a2791 100644 --- a/browser/components/sessionstore/test/browser_broadcast.js +++ b/browser/components/sessionstore/test/browser_broadcast.js @@ -3,7 +3,7 @@ "use strict"; -const INITIAL_VALUE = "initial-value-" + Date.now(); +const INITIAL_VALUE = "browser_broadcast.js-initial-value-" + Date.now(); /** * This test ensures we won't lose tab data queued in the content script when diff --git a/browser/components/sessionstore/test/browser_privatetabs.js b/browser/components/sessionstore/test/browser_privatetabs.js index ec43bd7f8d0..94a0d14aa3d 100644 --- a/browser/components/sessionstore/test/browser_privatetabs.js +++ b/browser/components/sessionstore/test/browser_privatetabs.js @@ -3,7 +3,7 @@ let Imports = {}; Cu.import("resource:///modules/sessionstore/SessionSaver.jsm", Imports); -let {SessionSaver} = Imports; +let {Task, SessionSaver} = Imports; add_task(function cleanup() { info("Forgetting closed tabs"); diff --git a/browser/components/sessionstore/test/browser_telemetry.js b/browser/components/sessionstore/test/browser_telemetry.js new file mode 100644 index 00000000000..25ac2f95270 --- /dev/null +++ b/browser/components/sessionstore/test/browser_telemetry.js @@ -0,0 +1,266 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + + +let tmp = {}; +Cu.import("resource:///modules/sessionstore/SessionFile.jsm", tmp); +Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", tmp); +let {SessionFile, TabStateCache} = tmp; + +// Shortcuts for histogram names +let Keys = {}; +for (let k of ["HISTORY", "FORMDATA", "OPEN_WINDOWS", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS", "DOM_STORAGE", "POSTDATA"]) { + Keys[k] = "FX_SESSION_RESTORE_TOTAL_" + k + "_SIZE_BYTES"; +} + +function lt(a, b, message) { + isnot(a, undefined, message + " (sanity check)"); + isnot(b, undefined, message + " (sanity check)"); + ok(a < b, message + " ( " + a + " < " + b + ")"); +} +function gt(a, b, message) { + isnot(a, undefined, message + " (sanity check)"); + isnot(b, undefined, message + " (sanity check)"); + ok(a > b, message + " ( " + a + " > " + b + ")"); +} + +add_task(function init() { + for (let i = ss.getClosedWindowCount() - 1; i >= 0; --i) { + ss.forgetClosedWindow(i); + } + for (let i = ss.getClosedTabCount(window) - 1; i >= 0; --i) { + ss.forgetClosedTab(window, i); + } +}); + +/** + * Test that Telemetry collection doesn't cause any error. + */ +add_task(function() { + info("Checking a little bit of consistency"); + let statistics = yield promiseStats(); + + for (let k of Object.keys(statistics)) { + let data = statistics[k]; + info("Data for " + k + ": " + data); + if (Array.isArray(data)) { + ok(data.every(x => x >= 0), "Data for " + k + " is >= 0"); + } else { + ok(data >= 0, "Data for " + k + " is >= 0"); + } + } +}); + +/** + * Test HISTORY key. + */ +add_task(function history() { + let KEY = Keys.HISTORY; + let tab = gBrowser.addTab("http://example.org:80/?"); + yield promiseBrowserLoaded(tab.linkedBrowser); + try { + SyncHandlers.get(tab.linkedBrowser).flush(); + let statistics = yield promiseStats(); + + info("Now changing history"); + tab.linkedBrowser.contentWindow.history.pushState({foo:1}, "ref"); + SyncHandlers.get(tab.linkedBrowser).flush(); + let statistics2 = yield promiseStats(); + + // We have changed history, so it must have increased + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of HISTORY has increased"); + +// Almost nothing else should + for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + } finally { + if (tab) { + gBrowser.removeTab(tab); + } + } +}); + +/** + * Test CLOSED_TABS_IN_OPEN_WINDOWS key. + */ +add_task(function close_tab() { + let KEY = Keys.CLOSED_TABS_IN_OPEN_WINDOWS; + let tab = gBrowser.addTab("http://example.org:80/?close_tab"); + yield promiseBrowserLoaded(tab.linkedBrowser); + try { + SyncHandlers.get(tab.linkedBrowser).flush(); + let statistics = yield promiseStats(); + + info("Now closing a tab"); + gBrowser.removeTab(tab); + tab = null; + let statistics2 = yield promiseStats(); + + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of CLOSED_TABS_IN_OPEN_WINDOWS has increased"); + + // Almost nothing else should change + for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + + } finally { + if (tab) { + gBrowser.removeTab(tab); + } + } +}); + +/** + * Test OPEN_WINDOWS key. + */ +add_task(function open_window() { + let KEY = Keys.OPEN_WINDOWS; + let win; + try { + let statistics = yield promiseStats(); + win = yield promiseNewWindowLoaded("http://example.org:80/?open_window"); + let statistics2 = yield promiseStats(); + + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of OPEN_WINDOWS has increased"); + + // Almost nothing else should change + for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + + } finally { + if (win) { + yield promiseWindowClosed(win); + } + } +}); + +/** + * Test CLOSED_WINDOWS key. + */ +add_task(function close_window() { + let KEY = Keys.CLOSED_WINDOWS; + let win = yield promiseNewWindowLoaded("http://example.org:80/?close_window"); + + // We need to add something to the window, otherwise it won't be saved + let tab = win.gBrowser.addTab("http://example.org:80/?close_tab"); + yield promiseBrowserLoaded(tab.linkedBrowser); + try { + let statistics = yield promiseStats(); + yield promiseWindowClosed(win); + win = null; + let statistics2 = yield promiseStats(); + + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of CLOSED_WINDOWS has increased"); + lt(statistics2[Keys.OPEN_WINDOWS], statistics[Keys.OPEN_WINDOWS], "The total size of OPEN_WINDOWS has decreased"); + + // Almost nothing else should change + for (let k of ["FORMDATA", "DOM_STORAGE", "CLOSED_TABS_IN_OPEN_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + + } finally { + if (win) { + yield promiseWindowClosed(win); + } + } +}); + + +/** + * Test DOM_STORAGE key. + */ +add_task(function dom_storage() { + let KEY = Keys.DOM_STORAGE; + let tab = gBrowser.addTab("http://example.org:80/?dom_storage"); + yield promiseBrowserLoaded(tab.linkedBrowser); + try { + SyncHandlers.get(tab.linkedBrowser).flush(); + let statistics = yield promiseStats(); + + info("Now adding some storage"); + yield modifySessionStorage(tab.linkedBrowser, {foo: "bar"}); + SyncHandlers.get(tab.linkedBrowser).flush(); + + let statistics2 = yield promiseStats(); + + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of DOM_STORAGE has increased"); + + // Almost nothing else should change + for (let k of ["CLOSED_TABS_IN_OPEN_WINDOWS", "FORMDATA", "CLOSED_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + + } finally { + if (tab) { + gBrowser.removeTab(tab); + } + } +}); + +/** + * Test FORMDATA key. + */ +add_task(function formdata() { + let KEY = Keys.FORMDATA; + let tab = gBrowser.addTab("data:text/html;charset=utf-8,"); + yield promiseBrowserLoaded(tab.linkedBrowser); + try { + SyncHandlers.get(tab.linkedBrowser).flush(); + let statistics = yield promiseStats(); + + info("Now changing form data"); + + yield modifyFormData(tab.linkedBrowser, {input: "This is some form data "}); + SyncHandlers.get(tab.linkedBrowser).flush(); + TabStateCache.delete(tab.linkedBrowser); + + let statistics2 = yield promiseStats(); + + isnot(statistics[KEY], undefined, "Key was defined"); + isnot(statistics2[KEY], undefined, "Key is still defined"); + gt(statistics2[KEY], statistics[KEY], "The total size of FORMDATA has increased"); + + // Almost nothing else should + for (let k of ["DOM_STORAGE", "CLOSED_WINDOWS", "CLOSED_TABS_IN_OPEN_WINDOWS"]) { + is(statistics2[Keys[k]], statistics[Keys[k]], "The total size of " + k + " has not increased"); + } + } finally { + if (tab) { + gBrowser.removeTab(tab); + } + } +}); + + +/** + * Get the latest statistics. + */ +function promiseStats() { + let state = ss.getBrowserState(); + info("Stats: " + state); + return SessionFile.gatherTelemetry(state); +} + + +function modifySessionStorage(browser, data) { + browser.messageManager.sendAsyncMessage("ss-test:modifySessionStorage", data); + return promiseContentMessage(browser, "ss-test:MozStorageChanged"); +} + +function modifyFormData(browser, data) { + browser.messageManager.sendAsyncMessage("ss-test:modifyFormData", data); + return promiseContentMessage(browser, "ss-test:modifyFormData:done"); +} + diff --git a/browser/components/sessionstore/test/content.js b/browser/components/sessionstore/test/content.js index dab506aeb1e..8af04a086e1 100644 --- a/browser/components/sessionstore/test/content.js +++ b/browser/components/sessionstore/test/content.js @@ -39,6 +39,13 @@ addMessageListener("ss-test:modifySessionStorage2", function (msg) { } }); +addMessageListener("ss-test:modifyFormData", function (msg) { + for (let id of Object.keys(msg.data)) { + content.document.getElementById(id).value = msg.data[id]; + } + sendSyncMessage("ss-test:modifyFormData:done"); +}); + addMessageListener("ss-test:purgeDomainData", function ({data: domain}) { Services.obs.notifyObservers(null, "browser:purge-domain-data", domain); content.setTimeout(() => sendAsyncMessage("ss-test:purgeDomainData")); diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index ac42d6d02d8..a8dedd4621b 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -3351,6 +3351,126 @@ "n_values": 101, "description": "Session restore: Number of times the tab state cache has been cleared during a session divided by number of total accesses during the session (percentage)" }, + "FX_SESSION_RESTORE_EXTRACTING_STATISTICS_DURATION_MS": { + "expires_in_version": "never", + "kind": "exponential", + "high": "3000", + "n_buckets": 10, + "extended_statistics_ok": true, + "description": "Session restore: Duration of the off main thread statistics extraction mechanism (ms)" + }, + "FX_SESSION_RESTORE_TOTAL_OPEN_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "Session restore: The subset of sessionrestore.js representing open windows (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_CLOSED_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "Session restore: The subset of sessionrestore.js representing closed windows (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_CLOSED_TABS_IN_OPEN_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "Sessionrestore: The subset of sesionstore.js representing closed tabs in open windows (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_COOKIES_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with cookies (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_DOM_STORAGE_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with DOM storage (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_FORMDATA_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing form data (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_HISTORY_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing history (total size, in bytes)" + }, + "FX_SESSION_RESTORE_TOTAL_POSTDATA_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing POST data (total size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_OPEN_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "Session restore: The subset of sessionrestore.js representing open windows (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_CLOSED_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "Session restore: The subset of sessionrestore.js representing closed windows (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_CLOSED_TABS_IN_OPEN_WINDOWS_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "Sessionrestore: The subset of sesionstore.js representing closed tabs in open windows (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_COOKIES_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "50000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with cookies (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_DOM_STORAGE_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with DOM storage (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_FORMDATA_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing form data (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_HISTORY_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing history (item size, in bytes)" + }, + "FX_SESSION_RESTORE_INDIVIDUAL_POSTDATA_SIZE_BYTES": { + "expires_in_version": "never", + "kind": "exponential", + "high": "5000000", + "n_buckets": 30, + "description": "The subset of sessionstore.js dealing with storing history POST data (item size, in bytes)" + }, "INNERWINDOWS_WITH_MUTATION_LISTENERS": { "expires_in_version": "never", "kind": "boolean",