Bug 942340 - Extracting many Telemetry statistics from sessionstore.js;r=ttaubert,froydnj

This commit is contained in:
David Rajchenbach-Teller 2014-01-11 09:51:24 +01:00
parent 378388f5ab
commit f4ae8eb1e3
9 changed files with 603 additions and 7 deletions

View File

@ -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;
}
);
}

View File

@ -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() {

View File

@ -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;
},
};

View File

@ -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]

View File

@ -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

View File

@ -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");

View File

@ -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,<input%20id='input'>");
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");
}

View File

@ -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"));

View File

@ -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",