Bug 942340 - Extracting many Telemetry statistics from sessionstore.js;r=ttaubert,froydnj
authorDavid Rajchenbach-Teller <dteller@mozilla.com>
Sat, 11 Jan 2014 09:51:24 +0100
changeset 162983 bd20b6aae4a68ffb769b5e9b722f6f52de1101e4
parent 162982 ee80588ead207d49d962c890b7a8ce8b6571ce23
child 162984 211c36cf894a131fe172960714eaa25183690d47
push id25977
push userttaubert@mozilla.com
push dateSun, 12 Jan 2014 09:26:17 +0000
treeherdermozilla-central@b029e8d3e130 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersttaubert, froydnj
bugs942340
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 942340 - Extracting many Telemetry statistics from sessionstore.js;r=ttaubert,froydnj
browser/components/sessionstore/src/SessionFile.jsm
browser/components/sessionstore/src/SessionStore.jsm
browser/components/sessionstore/src/SessionWorker.js
browser/components/sessionstore/test/browser.ini
browser/components/sessionstore/test/browser_broadcast.js
browser/components/sessionstore/test/browser_privatetabs.js
browser/components/sessionstore/test/browser_telemetry.js
browser/components/sessionstore/test/content.js
toolkit/components/telemetry/Histograms.json
--- a/browser/components/sessionstore/src/SessionFile.jsm
+++ b/browser/components/sessionstore/src/SessionFile.jsm
@@ -54,16 +54,32 @@ this.SessionFile = {
   },
   /**
    * Write the contents of the session file, asynchronously.
    */
   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.
    */
   writeLoadStateOnceAfterStartup: function (aLoadState) {
     SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
   },
   /**
    * Create a backup copy, asynchronously.
@@ -117,16 +133,24 @@ let SessionFileInternal = {
 
   read: function () {
     return SessionWorker.post("read").then(msg => {
       this._recordTelemetry(msg.telemetry);
       return msg.ok;
     });
   },
 
+  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"));
     }
     let refObj = {};
 
     let isFinalWrite = false;
     if (Services.startup.shuttingDown) {
@@ -172,18 +196,28 @@ let SessionFileInternal = {
     return SessionWorker.post("removeBackupCopy", [ext]);
   },
 
   wipe: function () {
     SessionWorker.post("wipe");
   },
 
   _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);
+      }
     }
   }
 };
 
 // Interface to a dedicated thread handling I/O
 let SessionWorker = (function () {
   let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
     OS.Shared.LOG.bind("SessionWorker"));
@@ -191,19 +225,22 @@ let SessionWorker = (function () {
     post: function post(...args) {
       let promise = worker.post.apply(worker, args);
       return promise.then(
         null,
         function onError(error) {
           // 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;
         }
       );
     }
   };
 })();
 
 // Ensure that we can write sessionstore.js cleanly before the profile
 // becomes unaccessible.
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -30,17 +30,18 @@ const NOTIFY_LAST_SESSION_CLEARED = "ses
 const MAX_CONCURRENT_TAB_RESTORES = 3;
 
 // global notifications observed
 const OBSERVING = [
   "domwindowopened", "domwindowclosed",
   "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
 // Restored in restoreDimensions()
 const WINDOW_ATTRIBUTES = ["width", "height", "screenX", "screenY", "sizemode"];
 
 // Hideable window features to (re)store
 // Restored in restoreWindowFeatures()
@@ -582,16 +583,19 @@ let SessionStoreInternal = {
         this.onPurgeSessionHistory();
         break;
       case "browser:purge-domain-data":
         this.onPurgeDomainData(aData);
         break;
       case "nsPref:changed": // catch pref changes
         this.onPrefChange(aData);
         break;
+      case "gather-telemetry":
+        this.onGatherTelemetry();
+        break;
     }
   },
 
   /**
    * This method handles incoming messages sent by the session store content
    * script and thus enables communication with OOP tabs.
    */
   receiveMessage: function ssi_receiveMessage(aMessage) {
@@ -1452,16 +1456,26 @@ let SessionStoreInternal = {
     // If possible, update cached data without having to invalidate it
     TabStateCache.updateField(aTab, "hidden", true);
 
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events due to changing groups in Panorama.
     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() {
     let state = this.getCurrentState();
 
     // Don't include the last session state in getBrowserState().
     delete state.lastSessionState;
 
--- a/browser/components/sessionstore/src/SessionWorker.js
+++ b/browser/components/sessionstore/src/SessionWorker.js
@@ -126,16 +126,26 @@ let Agent = {
     if (exn) {
       throw exn;
     }
 
     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
    * after startup so that we detect crashes on startup correctly.
    */
   writeLoadStateOnceAfterStartup: function (loadState) {
     if (this.hasWrittenLoadStateOnce) {
       throw new Error("writeLoadStateOnceAfterStartup() must only be called once.");
     }
@@ -231,8 +241,149 @@ let Agent = {
 
     return {result: true};
   }
 };
 
 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;
+  },
+
+};
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -65,16 +65,17 @@ support-files =
 [browser_merge_closed_tabs.js]
 [browser_pageshow.js]
 [browser_pageStyle.js]
 [browser_privatetabs.js]
 [browser_scrollPositions.js]
 [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]
 # Disabled because of leaks.
 # Re-enabling and rewriting this test is tracked in bug 936919.
 skip-if = true
 [browser_339445.js]
 [browser_345898.js]
--- a/browser/components/sessionstore/test/browser_broadcast.js
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "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
  * closing a tab.
  */
 add_task(function flush_on_tabclose() {
   let tab = yield createTabWithStorageData(["http://example.com"]);
   let browser = tab.linkedBrowser;
--- a/browser/components/sessionstore/test/browser_privatetabs.js
+++ b/browser/components/sessionstore/test/browser_privatetabs.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 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");
   while (ss.getClosedTabCount(window)) {
     ss.forgetClosedTab(window, 0);
   }
 });
 
new file mode 100644
--- /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,<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");
+}
+
--- a/browser/components/sessionstore/test/content.js
+++ b/browser/components/sessionstore/test/content.js
@@ -34,16 +34,23 @@ addMessageListener("ss-test:modifySessio
 });
 
 addMessageListener("ss-test:modifySessionStorage2", function (msg) {
   for (let key of Object.keys(msg.data)) {
     content.frames[0].sessionStorage[key] = msg.data[key];
   }
 });
 
+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"));
 });
 
 addMessageListener("ss-test:getStyleSheets", function (msg) {
   let sheets = content.document.styleSheets;
   let titles = Array.map(sheets, ss => [ss.title, ss.disabled]);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3346,16 +3346,136 @@
     "description": "Session restore: Percentage of tab state cache hits in all tab state cache accesses"
   },
   "FX_SESSION_RESTORE_TABSTATECACHE_CLEAR_RATIO": {
     "expires_in_version": "never",
     "kind": "enumerated",
     "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",
     "description": "Deleted or to-be-reused innerwindow which has had mutation event listeners."
   },
   "CHARSET_OVERRIDE_SITUATION": {
     "expires_in_version": "never",
     "kind": "enumerated",