Bug 810981 - Part 1 - Record current scroll position in mobile session store. r=margaret
authorJan Henning <jh+bugzilla@buttercookie.de>
Wed, 18 May 2016 22:24:16 +0200
changeset 338518 5e6a99ab5e9a9ff893f7e12e19639847cae068f0
parent 338517 6bbf9b8467b1ed1a6b8b64a8f5ae0da3db3df239
child 338519 b92caaa61e809cc93e560c1fb9416e86446ed939
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmargaret
bugs810981
milestone49.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 810981 - Part 1 - Record current scroll position in mobile session store. r=margaret This copies the approach we've taken for form data saving and applies it to recording the current scroll position of the page, too. This means that after receiving a scroll event, we capture the scroll position for the top level document and all direct child frames and include it in the session store data. Because compared to the form data input events the scroll event can fire at a relatively high rate, we throttle the scroll position capturing using timeouts to run at most twice per second. MozReview-Commit-ID: C0lBxzHav7Z
mobile/android/components/SessionStore.js
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -10,16 +10,17 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormData", "resource://gre/modules/FormData.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ScrollPosition", "resource://gre/modules/ScrollPosition.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
 XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences", "resource://gre/modules/SharedPreferences.jsm");
 
 function dump(a) {
   Services.console.logStringMessage(a);
 }
 
@@ -56,16 +57,17 @@ SessionStore.prototype = {
                                          Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   _windows: {},
   _lastSaveTime: 0,
   _interval: 10000,
   _maxTabsUndo: 5,
   _pendingWrite: 0,
+  _scrollSavePending: null,
 
   // The index where the most recently closed tab was in the tabs array
   // when it was closed.
   _lastClosedTabIndex: -1,
 
   // Whether or not to send notifications for changes to the closed tabs.
   _notifyClosedTabs: false,
 
@@ -270,34 +272,60 @@ SessionStore.prototype = {
         // yet; pageshow happens too late, so we could lose session data if the
         // browser were killed.
         let browser = aEvent.currentTarget;
         log("DOMTitleChanged for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         this.onTabLoad(window, browser);
         break;
       }
       case "load": {
-        // Handle restoring the text data into the content and frames. We wait
-        // until the main content and all frames are loaded before trying to
-        // restore the text data.
         let browser = aEvent.currentTarget;
+
+        // Skip subframe loads.
+        if (browser.contentDocument !== aEvent.originalTarget) {
+          return;
+        }
+
+        // Handle restoring the scroll position and text data into the content
+        // and frames. We wait until the main content and all frames are loaded
+        // before trying to restore this data.
         log("load for tab " + window.BrowserApp.getTabForBrowser(browser).id);
-        if (browser.__SS_restore_text_data) {
+        if (browser.__SS_restoreDataOnLoad) {
+          delete browser.__SS_restoreDataOnLoad;
           this._restoreTextData(browser.__SS_data.formdata, browser);
+          this._restoreScrollPosition(browser.__SS_data.scrolldata, browser);
+        } else {
+          // We're not restoring, capture the initial scroll position on load.
+          this.onTabScroll(window, browser);
         }
         break;
       }
       case "change":
       case "input":
       case "DOMAutoComplete": {
         let browser = aEvent.currentTarget;
         log("TabInput for tab " + window.BrowserApp.getTabForBrowser(browser).id);
         this.onTabInput(window, browser);
         break;
       }
+      case "scroll": {
+        let browser = aEvent.currentTarget;
+        // Duplicated logging check to avoid calling getTabForBrowser on each scroll event.
+        if (loggingEnabled) {
+          log("scroll for tab " + window.BrowserApp.getTabForBrowser(browser).id);
+        }
+        if (!this._scrollSavePending) {
+          this._scrollSavePending =
+            window.setTimeout(() => {
+              this._scrollSavePending = null;
+              this.onTabScroll(window, browser);
+            }, 500);
+        }
+        break;
+      }
     }
   },
 
   onWindowOpen: function ss_onWindowOpen(aWindow) {
     // Return if window has already been initialized
     if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID]) {
       return;
     }
@@ -369,31 +397,35 @@ SessionStore.prototype = {
     // Use load to restore text data
     aBrowser.addEventListener("load", this, true);
 
     // Use a combination of events to watch for text data changes
     aBrowser.addEventListener("change", this, true);
     aBrowser.addEventListener("input", this, true);
     aBrowser.addEventListener("DOMAutoComplete", this, true);
 
+    // Record the current scroll position
+    aBrowser.addEventListener("scroll", this, true);
+
     log("onTabAdd() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id +
         ", aNoNotification = " + aNoNotification);
     if (!aNoNotification) {
       this.saveStateDelayed();
     }
     this._updateCrashReportURL(aWindow);
   },
 
   onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
     // Cleanup event listeners
     aBrowser.removeEventListener("DOMTitleChanged", this, true);
     aBrowser.removeEventListener("load", this, true);
     aBrowser.removeEventListener("change", this, true);
     aBrowser.removeEventListener("input", this, true);
     aBrowser.removeEventListener("DOMAutoComplete", this, true);
+    aBrowser.removeEventListener("scroll", this, true);
 
     let tabId = aWindow.BrowserApp.getTabForBrowser(aBrowser).id;
 
     // If this browser is being restored, skip any session save activity
     if (aBrowser.__SS_restore) {
       log("onTabRemove() ran for zombie tab " + tabId + ", aNoNotification = " + aNoNotification);
       return;
     }
@@ -462,27 +494,30 @@ SessionStore.prototype = {
         continue;
       }
       let entry = this._serializeHistoryEntry(historyEntry);
       entries.push(entry);
     }
     let data = { entries: entries, index: index };
 
     let formdata;
+    let scrolldata;
     if (aBrowser.__SS_data) {
       formdata = aBrowser.__SS_data.formdata;
+      scrolldata = aBrowser.__SS_data.scrolldata;
     }
     delete aBrowser.__SS_data;
 
     this._collectTabData(aWindow, aBrowser, data);
-    if (aBrowser.__SS_restore_text_data) {
+    if (aBrowser.__SS_restoreDataOnLoad) {
       // If the tab has been freshly restored and the "load" event
-      // hasn't yet fired, we need to restore any form data that
-      // might have been present.
+      // hasn't yet fired, we need to restore any form data and
+      // scroll positions that might have been present.
       aBrowser.__SS_data.formdata = formdata;
+      aBrowser.__SS_data.scrolldata = scrolldata;
     } else {
       // When navigating via the forward/back buttons, Gecko restores
       // the form data all by itself and doesn't invoke any input events.
       // As _collectTabData() doesn't save any form data, we need to manually
       // capture it to bridge the time until the next input event arrives.
       this.onTabInput(aWindow, aBrowser);
     }
 
@@ -574,16 +609,70 @@ SessionStore.prototype = {
     // If we found any form data, main content or frames, let's save it
     if (Object.keys(formdata).length) {
       data.formdata = formdata;
       log("onTabInput() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
       this.saveStateDelayed();
     }
   },
 
+  onTabScroll: function ss_onTabScroll(aWindow, aBrowser) {
+    // If we've been called directly, cancel any pending timeouts.
+    if (this._scrollSavePending) {
+      aWindow.clearTimeout(this._scrollSavePending);
+      this._scrollSavePending = null;
+      log("onTabScroll() clearing pending timeout");
+    }
+
+    // If this browser is being restored, skip any session save activity.
+    if (aBrowser.__SS_restore) {
+      return;
+    }
+
+    // Don't bother trying to save scroll positions if we don't have history yet.
+    let data = aBrowser.__SS_data;
+    if (!data || data.entries.length == 0) {
+      return;
+    }
+
+    // Neither bother if we're yet to restore the previous scroll position.
+    if (aBrowser.__SS_restoreDataOnLoad) {
+      return;
+    }
+
+    // Start with storing the main content.
+    let content = aBrowser.contentWindow;
+
+    // Store the main content.
+    let scrolldata = ScrollPosition.collect(content) || {};
+
+    // Loop over direct child frames, and store the scroll positions.
+    let children = [];
+    for (let i = 0; i < content.frames.length; i++) {
+      let frame = content.frames[i];
+
+      let result = ScrollPosition.collect(frame);
+      if (result && Object.keys(result).length) {
+        children[i] = result;
+      }
+    }
+
+    // If any frame had scroll positions, add them to the main scroll data.
+    if (children.length) {
+      scrolldata.children = children;
+    }
+
+    // If we found any scroll positions, main content or frames, let's save them.
+    if (Object.keys(scrolldata).length) {
+      data.scrolldata = scrolldata;
+      log("onTabScroll() ran for tab " + aWindow.BrowserApp.getTabForBrowser(aBrowser).id);
+      this.saveStateDelayed();
+    }
+  },
+
   saveStateDelayed: function ss_saveStateDelayed() {
     if (!this._saveTimer) {
       // Interval until the next disk operation is allowed
       let minimalDelay = this._lastSaveTime + this._interval - Date.now();
 
       // If we have to wait, set a timer, otherwise saveState directly
       let delay = Math.max(minimalDelay, 2000);
       if (delay > 0) {
@@ -1115,19 +1204,19 @@ SessionStore.prototype = {
     // aTabData shouldn't be empty here, but if it is,
     // _restoreHistory() will crash otherwise.
     if (!aTabData || aTabData.entries.length == 0) {
       Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
       return;
     }
     this._restoreHistory(aTabData, aBrowser.sessionHistory);
 
-    // Restoring the text data requires waiting for the content to load. So
-    // we set a flag and delay this until the "load" event.
-    aBrowser.__SS_restore_text_data = true;
+    // Restoring text data and scroll position requires waiting for the content
+    // to load. So we set a flag and delay this until the appropriate event.
+    aBrowser.__SS_restoreDataOnLoad = true;
   },
 
   /**
   * Takes serialized history data and create news entries into the given
   * nsISessionHistory object.
   */
   _restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
     if (aHistory.count > 0) {
@@ -1162,17 +1251,26 @@ SessionStore.prototype = {
   /**
   * Takes serialized form text data and restores it into the given browser.
   */
   _restoreTextData: function ss_restoreTextData(aFormData, aBrowser) {
     if (aFormData) {
       log("_restoreTextData()");
       FormData.restoreTree(aBrowser.contentWindow, aFormData);
     }
-    delete aBrowser.__SS_restore_text_data;
+  },
+
+  /**
+  * Takes serialized scroll positions and restores them into the given browser.
+  */
+  _restoreScrollPosition: function ss_restoreScrollPosition(aScrollData, aBrowser) {
+    if (aScrollData) {
+      log("_restoreScrollPosition()");
+      ScrollPosition.restoreTree(aBrowser.contentWindow, aScrollData);
+    }
   },
 
   getBrowserState: function ss_getBrowserState() {
     return this._getCurrentState();
   },
 
   _restoreWindow: function ss_restoreWindow(aData) {
     let state;