Bug 1286798 - Part 32: Add a test for snapshotting verification in multi-e10s setup; r=asuth
authorJan Varga <jan.varga@gmail.com>
Thu, 29 Nov 2018 21:48:57 +0100
changeset 505250 1309fe77cfaa7237aeb193db5b3544f7c2a0f6f0
parent 505249 bc288ab2655c2cf6d2404fd204b7afa1d67262d2
child 505251 eba8447d393532c4f429c84a14c9a3a3acb5c447
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersasuth
bugs1286798
milestone65.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 1286798 - Part 32: Add a test for snapshotting verification in multi-e10s setup; r=asuth
dom/tests/browser/browser.ini
dom/tests/browser/browser_localStorage_e10s.js
dom/tests/browser/browser_localStorage_snapshotting_e10s.js
dom/tests/browser/helper_localStorage_e10s.js
dom/tests/browser/page_localstorage_snapshotting_e10s.html
--- a/dom/tests/browser/browser.ini
+++ b/dom/tests/browser/browser.ini
@@ -1,26 +1,28 @@
 [DEFAULT]
 prefs = dom.performance.enable_scheduler_timing=false
 support-files =
   browser_frame_elements.html
   page_privatestorageevent.html
   page_localstorage_e10s.html
+  page_localstorage_snapshotting_e10s.html
   position.html
   test-console-api.html
   test_bug1004814.html
   worker_bug1004814.js
   geo_leak_test.html
   dummy.html
   test_largeAllocation.html
   test_largeAllocation.html^headers^
   test_largeAllocation2.html
   test_largeAllocation2.html^headers^
   test_largeAllocationFormSubmit.sjs
   helper_largeAllocation.js
+  helper_localStorage_e10s.js
   !/dom/tests/mochitest/geolocation/network_geolocation.sjs
 
 [browser_allocateGigabyte.js]
 disabled = Does not reliably pass on 32-bit systems - bug 1314098
 skip-if = !e10s
 [browser_autofocus_background.js]
 [browser_autofocus_preference.js]
 [browser_beforeunload_between_chrome_content.js]
@@ -51,16 +53,18 @@ support-files =
 run-if = e10s
 [browser_largeAllocation_win32.js]
 skip-if = !e10s || os != "win" || processor != "x86" # Large-Allocation requires e10s
 [browser_largeAllocation_non_win32.js]
 skip-if = !e10s || (os == "win" && processor == "x86") || (verify && debug && (os == 'linux')) || (os == 'linux') || (os == 'mac' && debug)  # Large-Allocation requires e10s # Bug 1336075
 [browser_localStorage_e10s.js]
 skip-if = !e10s || verify # This is a test of e10s functionality.
 [browser_localStorage_privatestorageevent.js]
+[browser_localStorage_snapshotting_e10s.js]
+skip-if = !e10s # This is a test of e10s functionality.
 [browser_persist_cookies.js]
 support-files =
   set-samesite-cookies-and-redirect.sjs
   mimeme.sjs
 [browser_persist_mixed_content_image.js]
 support-files =
   test_mixed_content_image.html
   dummy.png
--- a/dom/tests/browser/browser_localStorage_e10s.js
+++ b/dom/tests/browser/browser_localStorage_e10s.js
@@ -1,80 +1,15 @@
 const HELPER_PAGE_URL =
   "http://example.com/browser/dom/tests/browser/page_localstorage_e10s.html";
 const HELPER_PAGE_ORIGIN = "http://example.com/";
 
-// Simple tab wrapper abstracting our messaging mechanism;
-class KnownTab {
-  constructor(name, tab) {
-    this.name = name;
-    this.tab = tab;
-  }
-
-  cleanup() {
-    this.tab = null;
-  }
-}
-
-// Simple data structure class to help us track opened tabs and their pids.
-class KnownTabs {
-  constructor() {
-    this.byPid = new Map();
-    this.byName = new Map();
-  }
-
-  cleanup() {
-    this.byPid = null;
-    this.byName = null;
-  }
-}
-
-/**
- * Open our helper page in a tab in its own content process, asserting that it
- * really is in its own process.  We initially load and wait for about:blank to
- * load, and only then loadURI to our actual page.  This is to ensure that
- * LocalStorageManager has had an opportunity to be created and populate
- * mOriginsHavingData.
- *
- * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of
- * the unconditional call to nsGlobalWindow::PreloadLocalStorage.  This will
- * reliably create the StorageDBChild instance, and its corresponding
- * StorageDBParent will send the set of origins when it is constructed.)
- */
-async function openTestTabInOwnProcess(name, knownTabs) {
-  let realUrl = HELPER_PAGE_URL + '?' + encodeURIComponent(name);
-  // Load and wait for about:blank.
-  let tab = await BrowserTestUtils.openNewForegroundTab({
-    gBrowser, opening: 'about:blank', forceNewProcess: true
-  });
-  let pid = tab.linkedBrowser.frameLoader.tabParent.osPid;
-  ok(!knownTabs.byName.has(name), "tab needs its own name: " + name);
-  ok(!knownTabs.byPid.has(pid), "tab needs to be in its own process: " + pid);
-
-  let knownTab = new KnownTab(name, tab);
-  knownTabs.byPid.set(pid, knownTab);
-  knownTabs.byName.set(name, knownTab);
-
-  // Now trigger the actual load of our page.
-  BrowserTestUtils.loadURI(tab.linkedBrowser, realUrl);
-  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
-  is(tab.linkedBrowser.frameLoader.tabParent.osPid, pid, "still same pid");
-  return knownTab;
-}
-
-/**
- * Close all the tabs we opened.
- */
-async function cleanupTabs(knownTabs) {
-  for (let knownTab of knownTabs.byName.values()) {
-    BrowserTestUtils.removeTab(knownTab.tab);
-    knownTab.cleanup();
-  }
-  knownTabs.cleanup();
-}
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "/helper_localStorage_e10s.js",
+                                    this);
 
 /**
  * Wait for a LocalStorage flush to occur.  This notification can occur as a
  * result of any of:
  * - The normal, hardcoded 5-second flush timer.
  * - InsertDBOp seeing a preload op for an origin with outstanding changes.
  * - Us generating a "domstorage-test-flush-force" observer notification.
  */
@@ -335,20 +270,23 @@ add_task(async function() {
   // start of our test.
   await clearOriginStorageEnsuringNoPreload();
 
   // Make sure mOriginsHavingData gets updated.
   await triggerAndWaitForLocalStorageFlush();
 
   // - Open tabs.  Don't configure any of them yet.
   const knownTabs = new KnownTabs();
-  const writerTab = await openTestTabInOwnProcess("writer", knownTabs);
-  const listenerTab = await openTestTabInOwnProcess("listener", knownTabs);
-  const readerTab = await openTestTabInOwnProcess("reader", knownTabs);
-  const lateWriteThenListenTab = await openTestTabInOwnProcess(
+  const writerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer",
+    knownTabs);
+  const listenerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "listener",
+    knownTabs);
+  const readerTab = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader",
+    knownTabs);
+  const lateWriteThenListenTab = await openTestTabInOwnProcess(HELPER_PAGE_URL,
     "lateWriteThenListen", knownTabs);
 
   // Sanity check that preloading did not occur in the tabs.
   await verifyTabPreload(writerTab, false);
   await verifyTabPreload(listenerTab, false);
   await verifyTabPreload(readerTab, false);
 
   // - Configure the tabs.
@@ -479,17 +417,17 @@ add_task(async function() {
   // that a flush has occurred before moving on to the next step,
   // mOriginsHavingData may not include our origin when it's sent down to the
   // child process.
   info("flush to make preload check work");
   await triggerAndWaitForLocalStorageFlush();
 
   // - Open a fresh tab and make sure it sees the precache/preload
   info("late open preload check");
-  const lateOpenSeesPreload =
-    await openTestTabInOwnProcess("lateOpenSeesPreload", knownTabs);
+  const lateOpenSeesPreload = await openTestTabInOwnProcess(HELPER_PAGE_URL,
+    "lateOpenSeesPreload", knownTabs);
   await verifyTabPreload(lateOpenSeesPreload, true);
 
   // - Clean up.
   await cleanupTabs(knownTabs);
 
   clearOriginStorageEnsuringNoPreload();
 });
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/browser_localStorage_snapshotting_e10s.js
@@ -0,0 +1,371 @@
+const HELPER_PAGE_URL =
+  "http://example.com/browser/dom/tests/browser/page_localstorage_snapshotting_e10s.html";
+const HELPER_PAGE_ORIGIN = "http://example.com/";
+
+let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+Services.scriptloader.loadSubScript(testDir + "/helper_localStorage_e10s.js",
+                                    this);
+
+function clearOrigin() {
+  let principal =
+    Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(
+      HELPER_PAGE_ORIGIN);
+  let request =
+    Services.qms.clearStoragesForPrincipal(principal, "default", "ls");
+  let promise = new Promise(resolve => {
+    request.callback = () => {
+      resolve();
+    };
+  });
+  return promise;
+}
+
+async function applyMutations(knownTab, mutations) {
+  await ContentTask.spawn(
+    knownTab.tab.linkedBrowser,
+    mutations,
+    function(mutations) {
+      return content.wrappedJSObject.applyMutations(Cu.cloneInto(mutations,
+                                                                 content));
+    });
+}
+
+async function verifyState(knownTab, expectedState) {
+  let actualState = await ContentTask.spawn(
+    knownTab.tab.linkedBrowser,
+    {},
+    function() {
+      return content.wrappedJSObject.getState();
+    });
+
+  for (let [expectedKey, expectedValue] of Object.entries(expectedState)) {
+    ok(actualState.hasOwnProperty(expectedKey), "key present: " + expectedKey);
+    is(actualState[expectedKey], expectedValue, "value correct");
+  }
+  for (let actualKey of Object.keys(actualState)) {
+    if (!expectedState.hasOwnProperty(actualKey)) {
+      ok(false, "actual state has key it shouldn't have: " + actualKey);
+    }
+  }
+}
+
+async function getKeys(knownTab) {
+  let keys = await ContentTask.spawn(
+    knownTab.tab.linkedBrowser,
+    null,
+    function() {
+      return content.wrappedJSObject.getKeys();
+    });
+  return keys;
+}
+
+async function beginExplicitSnapshot(knownTab) {
+  await ContentTask.spawn(
+    knownTab.tab.linkedBrowser,
+    null,
+    function() {
+      return content.wrappedJSObject.beginExplicitSnapshot();
+    });
+}
+
+async function endExplicitSnapshot(knownTab) {
+  await ContentTask.spawn(
+    knownTab.tab.linkedBrowser,
+    null,
+    function() {
+      return content.wrappedJSObject.endExplicitSnapshot();
+    });
+}
+
+// We spin up a ton of child processes.
+requestLongerTimeout(4);
+
+/**
+ * Verify snapshotting of our localStorage implementation in multi-e10s setup.
+ */
+add_task(async function() {
+  if (!Services.lsm.nextGenLocalStorageEnabled) {
+    ok(true, "Test ignored when the next gen local storage is not enabled.");
+    return;
+  }
+
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      // Enable LocalStorage's testing API so we can explicitly create
+      // snapshots when needed.
+      ["dom.storage.testing", true],
+    ]
+  });
+
+  // Ensure that there is no localstorage data by forcing the origin to be
+  // cleared prior to the start of our test..
+  await clearOrigin();
+
+  // - Open tabs.  Don't configure any of them yet.
+  const knownTabs = new KnownTabs();
+  const writerTab1 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer1",
+    knownTabs);
+  const writerTab2 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "writer2",
+    knownTabs);
+  const readerTab1 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader1",
+    knownTabs);
+  const readerTab2 = await openTestTabInOwnProcess(HELPER_PAGE_URL, "reader2",
+    knownTabs);
+
+  const initialMutations = [
+    [null, null],
+    ["key1", "initial1"],
+    ["key2", "initial2"],
+    ["key3", "initial3"],
+    ["key5", "initial5"],
+    ["key6", "initial6"],
+    ["key7", "initial7"],
+    ["key8", "initial8"]
+  ];
+
+  const initialState = {
+    key1: "initial1",
+    key2: "initial2",
+    key3: "initial3",
+    key5: "initial5",
+    key6: "initial6",
+    key7: "initial7",
+    key8: "initial8"
+  };
+
+  function getPartialPrefill()
+  {
+    let size = 0;
+    let entries = Object.entries(initialState);
+    for (let i = 0; i < entries.length / 2; i++) {
+      let entry = entries[i];
+      size += entry[0].length + entry[1].length;
+    }
+    return size;
+  }
+
+  const prefillValues = [
+    0,                   // no prefill
+    getPartialPrefill(), // partial prefill
+    -1                   // full prefill
+  ];
+
+  for (let prefillValue of prefillValues) {
+    info("Setting prefill value");
+
+    await SpecialPowers.pushPrefEnv({
+      set: [
+        ["dom.storage.snapshot_prefill", prefillValue]
+      ]
+    });
+
+    info("Stage 1");
+
+    const setRemoveMutations1 = [
+      ["key0", "setRemove10"],
+      ["key1", "setRemove11"],
+      ["key2", null],
+      ["key3", "setRemove13"],
+      ["key4", "setRemove14"],
+      ["key5", "setRemove15"],
+      ["key6", "setRemove16"],
+      ["key7", "setRemove17"],
+      ["key8", null],
+      ["key9", "setRemove19"]
+    ];
+
+    const setRemoveState1 = {
+      key0: "setRemove10",
+      key1: "setRemove11",
+      key3: "setRemove13",
+      key4: "setRemove14",
+      key5: "setRemove15",
+      key6: "setRemove16",
+      key7: "setRemove17",
+      key9: "setRemove19"
+    };
+
+    const setRemoveMutations2 = [
+      ["key0", "setRemove20"],
+      ["key1", null],
+      ["key2", "setRemove22"],
+      ["key3", "setRemove23"],
+      ["key4", "setRemove24"],
+      ["key5", "setRemove25"],
+      ["key6", "setRemove26"],
+      ["key7", null],
+      ["key8", "setRemove28"],
+      ["key9", "setRemove29"]
+    ];
+
+    const setRemoveState2 = {
+      key0: "setRemove20",
+      key2: "setRemove22",
+      key3: "setRemove23",
+      key4: "setRemove24",
+      key5: "setRemove25",
+      key6: "setRemove26",
+      key8: "setRemove28",
+      key9: "setRemove29"
+    };
+
+    // Apply initial mutations using an explicit snapshot. The explicit
+    // snapshot here ensures that the parent process have received the changes.
+    await beginExplicitSnapshot(writerTab1);
+    await applyMutations(writerTab1, initialMutations);
+    await endExplicitSnapshot(writerTab1);
+
+    // Begin explicit snapshots in all tabs except readerTab2. All these tabs
+    // should see the initial state regardless what other tabs are doing.
+    await beginExplicitSnapshot(writerTab1);
+    await beginExplicitSnapshot(writerTab2);
+    await beginExplicitSnapshot(readerTab1);
+
+    // Apply first array of set/remove mutations in writerTab1 and end the
+    // explicit snapshot. This will trigger saving of values in other active
+    // snapshots.
+    await applyMutations(writerTab1, setRemoveMutations1);
+    await endExplicitSnapshot(writerTab1);
+
+    // Begin an explicit snapshot in readerTab2. writerTab1 already ended its
+    // explicit snapshot, so readerTab2 should see mutations done by
+    // writerTab1.
+    await beginExplicitSnapshot(readerTab2);
+
+    // Apply second array of set/remove mutations in writerTab2 and end the
+    // explicit snapshot. This will trigger saving of values in other active
+    // snapshots, but only if they haven't been saved already.
+    await applyMutations(writerTab2, setRemoveMutations2);
+    await endExplicitSnapshot(writerTab2);
+
+    // Verify state in readerTab1, it should match the initial state.
+    await verifyState(readerTab1, initialState);
+    await endExplicitSnapshot(readerTab1);
+
+    // Verify state in readerTab2, it should match the state after the first
+    // array of set/remove mutatations have been applied and "commited".
+    await verifyState(readerTab2, setRemoveState1);
+    await endExplicitSnapshot(readerTab2);
+
+    // Verify final state, it should match the state after the second array of
+    // set/remove mutation have been applied and "commited". An explicit
+    // snapshot is used.
+    await beginExplicitSnapshot(readerTab1);
+    await verifyState(readerTab1, setRemoveState2);
+    await endExplicitSnapshot(readerTab1);
+
+    info("Stage 2");
+
+    const setRemoveClearMutations1 = [
+      ["key0", "setRemoveClear10"],
+      ["key1", null],
+      [null, null]
+    ];
+
+    const setRemoveClearState1 = {
+    };
+
+    const setRemoveClearMutations2 = [
+      ["key8", null],
+      ["key9", "setRemoveClear29"],
+      [null, null]
+    ];
+
+    const setRemoveClearState2 = {
+    };
+
+    // This is very similar to previous stage except that in addition to
+    // set/remove, the clear operation is involved too.
+    await beginExplicitSnapshot(writerTab1);
+    await applyMutations(writerTab1, initialMutations);
+    await endExplicitSnapshot(writerTab1);
+
+    await beginExplicitSnapshot(writerTab1);
+    await beginExplicitSnapshot(writerTab2);
+    await beginExplicitSnapshot(readerTab1);
+
+    await applyMutations(writerTab1, setRemoveClearMutations1);
+    await endExplicitSnapshot(writerTab1);
+
+    await beginExplicitSnapshot(readerTab2);
+
+    await applyMutations(writerTab2, setRemoveClearMutations2);
+    await endExplicitSnapshot(writerTab2);
+
+    await verifyState(readerTab1, initialState);
+    await endExplicitSnapshot(readerTab1);
+
+    await verifyState(readerTab2, setRemoveClearState1);
+    await endExplicitSnapshot(readerTab2);
+
+    await beginExplicitSnapshot(readerTab1);
+    await verifyState(readerTab1, setRemoveClearState2);
+    await endExplicitSnapshot(readerTab1);
+
+    info("Stage 3");
+
+    const changeOrderMutations = [
+      ["key1", null],
+      ["key2", null],
+      ["key3", null],
+      ["key5", null],
+      ["key6", null],
+      ["key7", null],
+      ["key8", null],
+      ["key8", "initial8"],
+      ["key7", "initial7"],
+      ["key6", "initial6"],
+      ["key5", "initial5"],
+      ["key3", "initial3"],
+      ["key2", "initial2"],
+      ["key1", "initial1"]
+    ];
+
+    // Apply initial mutations using an explicit snapshot. The explicit
+    // snapshot here ensures that the parent process have received the changes.
+    await beginExplicitSnapshot(writerTab1);
+    await applyMutations(writerTab1, initialMutations);
+    await endExplicitSnapshot(writerTab1);
+
+    // Begin explicit snapshots in all tabs except writerTab2 which is not used
+    // in this stage. All these tabs should see the initial order regardless
+    // what other tabs are doing.
+    await beginExplicitSnapshot(readerTab1);
+    await beginExplicitSnapshot(writerTab1);
+    await beginExplicitSnapshot(readerTab2);
+
+    // Get all keys in readerTab1 and end the explicit snapshot. No mutations
+    // have been applied yet.
+    let tab1Keys = await getKeys(readerTab1);
+    await endExplicitSnapshot(readerTab1);
+
+    // Apply mutations that change the order of keys and end the explicit
+    // snapshot. The state is unchanged. This will trigger saving of key order
+    // in other active snapshots, but only if the order hasn't been saved
+    // already.
+    await applyMutations(writerTab1, changeOrderMutations);
+    await endExplicitSnapshot(writerTab1);
+
+    // Get all keys in readerTab2 and end the explicit snapshot. Change order
+    // mutations have been applied, but the order should stay unchanged.
+    let tab2Keys = await getKeys(readerTab2);
+    await endExplicitSnapshot(readerTab2);
+
+    // Verify the key order is the same.
+    is(tab2Keys.length, tab1Keys.length, "Correct keys length");
+    for (let i = 0; i < tab2Keys.length; i++) {
+      is(tab2Keys[i], tab1Keys[i], "Correct key");
+    }
+
+    // Verify final state, it should match the initial state since applied
+    // mutations only changed the key order. An explicit snapshot is used.
+    await beginExplicitSnapshot(readerTab1);
+    await verifyState(readerTab1, initialState);
+    await endExplicitSnapshot(readerTab1);
+  }
+
+  // - Clean up.
+  await cleanupTabs(knownTabs);
+
+  clearOrigin();
+});
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/helper_localStorage_e10s.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Simple tab wrapper abstracting our messaging mechanism;
+class KnownTab {
+  constructor(name, tab) {
+    this.name = name;
+    this.tab = tab;
+  }
+
+  cleanup() {
+    this.tab = null;
+  }
+}
+
+// Simple data structure class to help us track opened tabs and their pids.
+class KnownTabs {
+  constructor() {
+    this.byPid = new Map();
+    this.byName = new Map();
+  }
+
+  cleanup() {
+    this.byPid = null;
+    this.byName = null;
+  }
+}
+
+/**
+ * Open our helper page in a tab in its own content process, asserting that it
+ * really is in its own process.  We initially load and wait for about:blank to
+ * load, and only then loadURI to our actual page.  This is to ensure that
+ * LocalStorageManager has had an opportunity to be created and populate
+ * mOriginsHavingData.
+ *
+ * (nsGlobalWindow will reliably create LocalStorageManager as a side-effect of
+ * the unconditional call to nsGlobalWindow::PreloadLocalStorage.  This will
+ * reliably create the StorageDBChild instance, and its corresponding
+ * StorageDBParent will send the set of origins when it is constructed.)
+ */
+async function openTestTabInOwnProcess(helperPageUrl, name, knownTabs) {
+  let realUrl = helperPageUrl + '?' + encodeURIComponent(name);
+  // Load and wait for about:blank.
+  let tab = await BrowserTestUtils.openNewForegroundTab({
+    gBrowser, opening: 'about:blank', forceNewProcess: true
+  });
+  let pid = tab.linkedBrowser.frameLoader.tabParent.osPid;
+  ok(!knownTabs.byName.has(name), "tab needs its own name: " + name);
+  ok(!knownTabs.byPid.has(pid), "tab needs to be in its own process: " + pid);
+
+  let knownTab = new KnownTab(name, tab);
+  knownTabs.byPid.set(pid, knownTab);
+  knownTabs.byName.set(name, knownTab);
+
+  // Now trigger the actual load of our page.
+  BrowserTestUtils.loadURI(tab.linkedBrowser, realUrl);
+  await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  is(tab.linkedBrowser.frameLoader.tabParent.osPid, pid, "still same pid");
+  return knownTab;
+}
+
+/**
+ * Close all the tabs we opened.
+ */
+async function cleanupTabs(knownTabs) {
+  for (let knownTab of knownTabs.byName.values()) {
+    BrowserTestUtils.removeTab(knownTab.tab);
+    knownTab.cleanup();
+  }
+  knownTabs.cleanup();
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/browser/page_localstorage_snapshotting_e10s.html
@@ -0,0 +1,55 @@
+<!doctype html>
+<html>
+<head>
+  <meta charset="utf-8">
+<script>
+/**
+ * Helper page used by browser_localStorage_snapshotting.js.
+ *
+ * We expose methods to be invoked by ContentTask.spawn() calls.
+ *
+ **/
+var pageName = document.location.search.substring(1);
+window.addEventListener(
+  "load",
+  () => { document.getElementById("pageNameH").textContent = pageName; });
+
+function applyMutations(mutations) {
+  mutations.forEach(function([key, value]) {
+    if (key !== null) {
+      if (value === null) {
+        localStorage.removeItem(key);
+      } else {
+        localStorage.setItem(key, value);
+      }
+    } else {
+      localStorage.clear();
+    }
+  });
+}
+
+function getState() {
+  let state = {};
+  let length = localStorage.length;
+  for (let index = 0; index < length; index++) {
+    let key = localStorage.key(index);
+    state[key] = localStorage.getItem(key);
+  }
+  return state;
+}
+
+function getKeys() {
+  return Object.keys(localStorage);
+}
+
+function beginExplicitSnapshot() {
+  localStorage.beginExplicitSnapshot();
+}
+
+function endExplicitSnapshot() {
+  localStorage.endExplicitSnapshot();
+}
+</script>
+</head>
+<body><h2 id="pageNameH"></h2></body>
+</html>