Bug 1226333 - Add tests for async window flushing. r=billm.
authorMike Conley <mconley@mozilla.com>
Wed, 02 Dec 2015 09:46:06 -0500
changeset 275546 1a8e8ab6ee3dacc69e865879a4eb6739904d57b1
parent 275545 dfa767dc0c86d161b0d2c938358e7954a5f3bfbe
child 275547 a618b79155185a63ef94710be7d80c4f571e92bb
push id16558
push usermconley@mozilla.com
push dateFri, 04 Dec 2015 02:17:18 +0000
treeherderfx-team@1a8e8ab6ee3d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1226333
milestone45.0a1
Bug 1226333 - Add tests for async window flushing. r=billm.
browser/app/profile/firefox.js
browser/components/sessionstore/content/content-sessionStore.js
browser/components/sessionstore/test/browser_async_window_flushing.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1038,16 +1038,20 @@ pref("browser.sessionstore.restore_hidde
 // be restored when they are focused.
 pref("browser.sessionstore.restore_pinned_tabs_on_demand", false);
 // The version at which we performed the latest upgrade backup
 pref("browser.sessionstore.upgradeBackup.latestBuildID", "");
 // How many upgrade backups should be kept
 pref("browser.sessionstore.upgradeBackup.maxUpgradeBackups", 3);
 // End-users should not run sessionstore in debug mode
 pref("browser.sessionstore.debug", false);
+// Causes SessionStore to ignore non-final update messages from
+// browser tabs that were not caused by a flush from the parent.
+// This is a testing flag and should not be used by end-users.
+pref("browser.sessionstore.debug.no_auto_updates", false);
 // Forget closed windows/tabs after two weeks
 pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000);
 
 // allow META refresh by default
 pref("accessibility.blockautorefresh", false);
 
 // Whether history is enabled or not.
 pref("places.history.enabled", true);
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -40,16 +40,20 @@ XPCOMUtils.defineLazyGetter(this, 'gCont
                             () => { return new ContentRestore(this) });
 
 // The current epoch.
 var gCurrentEpoch = 0;
 
 // A bound to the size of data to store for DOM Storage.
 const DOM_STORAGE_MAX_CHARS = 10000000; // 10M characters
 
+// This pref controls whether or not we send updates to the parent on a timeout
+// or not, and should only be used for tests or debugging.
+const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates";
+
 /**
  * Returns a lazy function that will evaluate the given
  * function |fn| only once and cache its return value.
  */
 function createLazy(fn) {
   let cached = false;
   let cachedValue = null;
 
@@ -660,31 +664,79 @@ var MessageQueue = {
 
   /**
    * The current timeout ID, null if there is no queue data. We use timeouts
    * to damp a flood of data changes and send lots of changes as one batch.
    */
   _timeout: null,
 
   /**
+   * Whether or not sending batched messages on a timer is disabled. This should
+   * only be used for debugging or testing. If you need to access this value,
+   * you should probably use the timeoutDisabled getter.
+   */
+  _timeoutDisabled: false,
+
+  /**
+   * True if batched messages are not being fired on a timer. This should only
+   * ever be true when debugging or during tests.
+   */
+  get timeoutDisabled() {
+    return this._timeoutDisabled;
+  },
+
+  /**
+   * Disables sending batched messages on a timer. Also cancels any pending
+   * timers.
+   */
+  set timeoutDisabled(val) {
+    this._timeoutDisabled = val;
+
+    if (!val && this._timeout) {
+      clearTimeout(this._timeout);
+      this._timeout = null;
+    }
+
+    return val;
+  },
+
+  init() {
+    this.timeoutDisabled =
+      Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
+
+    Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this, false);
+  },
+
+  uninit() {
+    Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this);
+  },
+
+  observe(subject, topic, data) {
+    if (topic == TIMEOUT_DISABLED_PREF) {
+      this.timeoutDisabled =
+        Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF);
+    }
+  },
+
+  /**
    * Pushes a given |value| onto the queue. The given |key| represents the type
    * of data that is stored and can override data that has been queued before
    * but has not been sent to the parent process, yet.
    *
    * @param key (string)
    *        A unique identifier specific to the type of data this is passed.
    * @param fn (function)
    *        A function that returns the value that will be sent to the parent
    *        process.
    */
   push: function (key, fn) {
     this._data.set(key, createLazy(fn));
     this._lastUpdated.set(key, this._id);
 
-    if (!this._timeout) {
+    if (!this._timeout && !this._timeoutDisabled) {
       // Wait a little before sending the message to batch multiple changes.
       this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS);
     }
   },
 
   /**
    * Sends queued data to the chrome process.
    *
@@ -794,16 +846,17 @@ MessageListener.init();
 FormDataListener.init();
 SyncHandler.init();
 PageStyleListener.init();
 SessionHistoryListener.init();
 SessionStorageListener.init();
 ScrollPositionListener.init();
 DocShellCapabilitiesListener.init();
 PrivacyListener.init();
+MessageQueue.init();
 
 function handleRevivedTab() {
   if (!content) {
     removeEventListener("pagehide", handleRevivedTab);
     return;
   }
 
   if (content.document.documentURI.startsWith("about:tabcrashed")) {
@@ -835,16 +888,17 @@ addEventListener("unload", () => {
   // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide
   // event to be fired.
   handleRevivedTab();
 
   // Remove all registered nsIObservers.
   PageStyleListener.uninit();
   SessionStorageListener.uninit();
   SessionHistoryListener.uninit();
+  MessageQueue.uninit();
 
   // Remove progress listeners.
   gContentRestore.resetRestore();
 
   // We don't need to take care of any gFrameTree observers as the gFrameTree
   // will die with the content script. The same goes for the privacy transition
   // observer that will die with the docShell when the tab is closed.
 });
--- a/browser/components/sessionstore/test/browser_async_window_flushing.js
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -1,26 +1,175 @@
+"use strict";
+
+const PAGE = "http://example.com/";
+
+/**
+ * Tests that if we initially discard a window as not interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed.
+ */
+add_task(function* test_add_interesting_window() {
+  // We want to suppress all non-final updates from the browser tabs
+  // so as to eliminate any racy-ness with this test.
+  yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+  // Depending on previous tests, we might already have some closed
+  // windows stored. We'll use its length to determine whether or not
+  // the window was added or not.
+  let initialClosedWindows = ss.getClosedWindowCount();
+
+  // Make sure we can actually store another closed window
+  yield pushPrefs(["browser.sessionstore.max_windows_undo",
+                   initialClosedWindows + 1]);
+
+  // Create a new browser window. Since the default window will start
+  // at about:blank, SessionStore should find this tab (and therefore the
+  // whole window) uninteresting, and should not initially put it into
+  // the closed windows array.
+  let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+
+  let browser = newWin.gBrowser.selectedBrowser;
+
+  // Send a message that will cause the content to change its location
+  // to someplace more interesting. We've disabled auto updates from
+  // the browser, so the parent won't know about this
+  yield ContentTask.spawn(browser, PAGE, function*(PAGE) {
+    content.location = PAGE;
+  });
+
+  // for e10s, this will cause a remoteness switch, since the
+  // initial browser in a newly opened window will not be remote.
+  // We need to wait for that remoteness change before we attach
+  // our OnHistoryReplaceEntry listener.
+  if (gMultiProcessBrowser) {
+    yield BrowserTestUtils.waitForEvent(newWin.gBrowser.selectedTab,
+                                        "TabRemotenessChange");
+  }
+
+  yield promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
+
+  // Clear out the userTypedValue so that the new window looks like
+  // it's really not worth restoring.
+  browser.userTypedValue = null;
+
+  // Once the domWindowClosed Promise resolves, the window should
+  // have closed, and SessionStore's onClose handler should have just
+  // run.
+  let domWindowClosed = BrowserTestUtils.domWindowClosed(newWin);
+
+  // Once this windowClosed Promise resolves, we should have finished
+  // the flush and revisited our decision to put this window into
+  // the closed windows array.
+  let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+  // Ok, let's close the window.
+  newWin.close();
+
+  yield domWindowClosed;
+  // OnClose has just finished running.
+  let currentClosedWindows = ss.getClosedWindowCount();
+  is(currentClosedWindows, initialClosedWindows,
+     "We should not have added the window to the closed windows array");
+
+  yield windowClosed;
+  // The window flush has finished
+  currentClosedWindows = ss.getClosedWindowCount();
+  is(currentClosedWindows,
+     initialClosedWindows + 1,
+     "We should have added the window to the closed windows array");
+});
+
+/**
+ * Tests that if we initially store a closed window as interesting
+ * to save in the closed windows array, that we revisit that decision
+ * after a window flush has completed, and stop storing a window that
+ * we've deemed no longer interesting.
+ */
+add_task(function* test_remove_uninteresting_window() {
+  // We want to suppress all non-final updates from the browser tabs
+  // so as to eliminate any racy-ness with this test.
+  yield pushPrefs(["browser.sessionstore.debug.no_auto_updates", true]);
+
+  // Depending on previous tests, we might already have some closed
+  // windows stored. We'll use its length to determine whether or not
+  // the window was added or not.
+  let initialClosedWindows = ss.getClosedWindowCount();
+
+  // Make sure we can actually store another closed window
+  yield pushPrefs(["browser.sessionstore.max_windows_undo",
+                   initialClosedWindows + 1]);
+
+  let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+
+  // Now browse the initial tab of that window to an interesting
+  // site.
+  let tab = newWin.gBrowser.selectedTab;
+  let browser = tab.linkedBrowser;
+  browser.loadURI(PAGE);
+
+  yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
+  yield TabStateFlusher.flush(browser);
+
+  // Send a message that will cause the content to purge its
+  // history entries and make itself seem uninteresting.
+  yield ContentTask.spawn(browser, null, function*() {
+    // Epic hackery to make this browser seem suddenly boring.
+    Components.utils.import("resource://gre/modules/BrowserUtils.jsm");
+    docShell.setCurrentURI(BrowserUtils.makeURI("about:blank"));
+
+    let {sessionHistory} = docShell.QueryInterface(Ci.nsIWebNavigation);
+    sessionHistory.PurgeHistory(sessionHistory.count);
+  });
+
+  // Once the domWindowClosed Promise resolves, the window should
+  // have closed, and SessionStore's onClose handler should have just
+  // run.
+  let domWindowClosed = BrowserTestUtils.domWindowClosed(newWin);
+
+  // Once this windowClosed Promise resolves, we should have finished
+  // the flush and revisited our decision to put this window into
+  // the closed windows array.
+  let windowClosed = BrowserTestUtils.windowClosed(newWin);
+
+  // Ok, let's close the window.
+  newWin.close();
+
+  yield domWindowClosed;
+  // OnClose has just finished running.
+  let currentClosedWindows = ss.getClosedWindowCount();
+  is(currentClosedWindows, initialClosedWindows + 1,
+     "We should have added the window to the closed windows array");
+
+  yield windowClosed;
+  // The window flush has finished
+  currentClosedWindows = ss.getClosedWindowCount();
+  is(currentClosedWindows,
+     initialClosedWindows,
+     "We should have removed the window from the closed windows array");
+});
+
 /**
  * Tests that when we close a window, it is immediately removed from the
  * _windows array.
  */
 add_task(function* test_synchronously_remove_window_state() {
   // Depending on previous tests, we might already have some closed
   // windows stored. We'll use its length to determine whether or not
   // the window was added or not.
   let state = JSON.parse(ss.getBrowserState());
   ok(state, "Make sure we can get the state");
   let initialWindows = state.windows.length;
 
   // Open a new window and send the first tab somewhere
   // interesting.
   let newWin = yield BrowserTestUtils.openNewBrowserWindow();
   let browser = newWin.gBrowser.selectedBrowser;
-  browser.loadURI("http://example.com");
-  yield BrowserTestUtils.browserLoaded(browser);
+  browser.loadURI(PAGE);
+  yield BrowserTestUtils.browserLoaded(browser, false, PAGE);
   yield TabStateFlusher.flush(browser);
 
   state = JSON.parse(ss.getBrowserState());
   is(state.windows.length, initialWindows + 1,
      "The new window to be in the state");
 
   // Now close the window, and make sure that the window was removed
   // from the windows list from the SessionState. We're specifically