Bug 1286798 - Part 32: Add a test for snapshotting verification in multi-e10s setup; r=asuth draft
authorJan Varga <jan.varga@gmail.com>
Wed, 24 Oct 2018 06:59:05 +0200
changeset 481710 9cf3caa326f1838e44c30ba8a2a157682c50b242
parent 481709 43686ee468e4bf6bfa64daa87d22d38cd9e4696e
child 481711 799ad1be5831839831c6f6d32984312e959fa8ab
push id10
push userbugmail@asutherland.org
push dateSun, 18 Nov 2018 18:57:42 +0000
reviewersasuth
bugs1286798
milestone65.0a1
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_test_focus_after_modal_state.js]
 skip-if = verify
 support-files =
   focus_after_prompt.html
--- 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>