From d9a89091ddb3e6a1dcfa493fef358e0f6ac7e1c8 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Sat, 7 Jul 2018 16:30:56 -0700 Subject: [PATCH] Bug 1474139: Part 1 - Refactor content-sessionSstore to not depend on running in a frame script global. r=mikedeboer MozReview-Commit-ID: 2uiKk9B8KWy --HG-- extra : rebase_source : b36defff266f448473bd5029416ee5bfea507765 --- .../content/content-sessionStore.js | 849 ++++++++++-------- 1 file changed, 456 insertions(+), 393 deletions(-) diff --git a/browser/components/sessionstore/content/content-sessionStore.js b/browser/components/sessionstore/content/content-sessionStore.js index 98f6c805fd7f..d84656f0d796 100644 --- a/browser/components/sessionstore/content/content-sessionStore.js +++ b/browser/components/sessionstore/content/content-sessionStore.js @@ -31,22 +31,11 @@ ChromeUtils.defineModuleGetter(this, "SessionHistory", ChromeUtils.defineModuleGetter(this, "SessionStorage", "resource:///modules/sessionstore/SessionStorage.jsm"); -var contentRestoreInitialized = false; - -XPCOMUtils.defineLazyGetter(this, "gContentRestore", - () => { - contentRestoreInitialized = true; - return new ContentRestore(this); - }); - ChromeUtils.defineModuleGetter(this, "Utils", "resource://gre/modules/sessionstore/Utils.jsm"); const ssu = Cc["@mozilla.org/browser/sessionstore/utils;1"] .getService(Ci.nsISessionStoreUtils); -// The current epoch. -var gCurrentEpoch = 0; - // A bound to the size of data to store for DOM Storage. const DOM_STORAGE_LIMIT_PREF = "browser.sessionstore.dom_storage_limit"; @@ -59,30 +48,54 @@ const PREF_INTERVAL = "browser.sessionstore.interval"; const kNoIndex = Number.MAX_SAFE_INTEGER; const kLastIndex = Number.MAX_SAFE_INTEGER - 1; -// Grab our global so we can access it in functions below. -const global = this; - /** * A function that will recursively call |cb| to collect data for all * non-dynamic frames in the current frame/docShell tree. */ -function mapFrameTree(callback) { - let [data] = Utils.mapFrameTree(content, callback); +function mapFrameTree(mm, callback) { + let [data] = Utils.mapFrameTree(mm.content, callback); return data; } +class Handler { + constructor(store) { + this.store = store; + } + + get contentRestore() { + return this.store.contentRestore; + } + + get contentRestoreInitialized() { + return this.store.contentRestoreInitialized; + } + + get mm() { + return this.store.mm; + } + + get messageQueue() { + return this.store.messageQueue; + } + + get stateChangeNotifier() { + return this.store.stateChangeNotifier; + } +} + /** * Listens for state change notifcations from webProgress and notifies each * registered observer for either the start of a page load, or its completion. */ -var StateChangeNotifier = { +class StateChangeNotifier extends Handler { + constructor(store) { + super(store); - init() { this._observers = new Set(); - let ifreq = docShell.QueryInterface(Ci.nsIInterfaceRequestor); + let ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor); let webProgress = ifreq.getInterface(Ci.nsIWebProgress); webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT); - }, + } /** * Adds a given observer |obs| to the set of observers that will be notified @@ -92,7 +105,7 @@ var StateChangeNotifier = { */ addObserver(obs) { this._observers.add(obs); - }, + } /** * Notifies all observers that implement the given |method|. @@ -101,11 +114,11 @@ var StateChangeNotifier = { */ notifyObservers(method) { for (let obs of this._observers) { - if (obs.hasOwnProperty(method)) { + if (typeof obs[method] == "function") { obs[method](); } } - }, + } /** * @see nsIWebProgressListener.onStateChange @@ -113,7 +126,7 @@ var StateChangeNotifier = { onStateChange(webProgress, request, stateFlags, status) { // Ignore state changes for subframes because we're only interested in the // top-document starting or stopping its load. - if (!webProgress.isTopLevel || webProgress.DOMWindow != content) { + if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { return; } @@ -122,7 +135,7 @@ var StateChangeNotifier = { // the case of unrestored background tabs, where the content has not yet // been restored: we don't want to accidentally send any updates to the // parent when the about:blank placeholder page has loaded. - if (!docShell.hasLoadedNonBlankURI) { + if (!this.mm.docShell.hasLoadedNonBlankURI) { return; } @@ -131,23 +144,26 @@ var StateChangeNotifier = { } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { this.notifyObservers("onPageLoadCompleted"); } - }, - - QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, - Ci.nsISupportsWeakReference]) -}; + } +} +StateChangeNotifier.prototype.QueryInterface = + ChromeUtils.generateQI([Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference]); /** * Listens for and handles content events that we need for the * session store service to be notified of state changes in content. */ -var EventListener = { +class EventListener extends Handler { + constructor(store) { + super(store); - init() { - ssu.addDynamicFrameFilteredListener(global, "load", this, true); - }, + ssu.addDynamicFrameFilteredListener(this.mm, "load", this, true); + } handleEvent(event) { + let {content} = this.mm; + // Ignore load events from subframes. if (event.target != content.document) { return; @@ -165,134 +181,13 @@ var EventListener = { content.removeEventListener("AboutReaderContentReady", this); } - if (contentRestoreInitialized) { + if (this.contentRestoreInitialized) { // Restore the form data and scroll position. If we're not currently // restoring a tab state then this call will simply be a noop. - gContentRestore.restoreDocument(); + this.contentRestore.restoreDocument(); } } -}; - -/** - * Listens for and handles messages sent by the session store service. - */ -var MessageListener = { - - MESSAGES: [ - "SessionStore:restoreHistory", - "SessionStore:restoreTabContent", - "SessionStore:resetRestore", - "SessionStore:flush", - "SessionStore:becomeActiveProcess", - ], - - init() { - this.MESSAGES.forEach(m => addMessageListener(m, this)); - }, - - receiveMessage({name, data}) { - // The docShell might be gone. Don't process messages, - // that will just lead to errors anyway. - if (!docShell) { - return; - } - - // A fresh tab always starts with epoch=0. The parent has the ability to - // override that to signal a new era in this tab's life. This enables it - // to ignore async messages that were already sent but not yet received - // and would otherwise confuse the internal tab state. - if (data.epoch && data.epoch != gCurrentEpoch) { - gCurrentEpoch = data.epoch; - } - - switch (name) { - case "SessionStore:restoreHistory": - this.restoreHistory(data); - break; - case "SessionStore:restoreTabContent": - if (data.isRemotenessUpdate) { - let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS"); - histogram.add("SessionStore:restoreTabContent", - Services.telemetry.msSystemNow() - data.requestTime); - } - this.restoreTabContent(data); - break; - case "SessionStore:resetRestore": - gContentRestore.resetRestore(); - break; - case "SessionStore:flush": - this.flush(data); - break; - case "SessionStore:becomeActiveProcess": - SessionHistoryListener.collect(); - break; - default: - debug("received unknown message '" + name + "'"); - break; - } - }, - - restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) { - gContentRestore.restoreHistory(tabData, loadArguments, { - // Note: The callbacks passed here will only be used when a load starts - // that was not initiated by sessionstore itself. This can happen when - // some code calls browser.loadURI() or browser.reload() on a pending - // browser/tab. - - onLoadStarted() { - // Notify the parent that the tab is no longer pending. - sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch}); - }, - - onLoadFinished() { - // Tell SessionStore.jsm that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch}); - } - }); - - if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { - // For non-remote tabs, when restoreHistory finishes, we send a synchronous - // message to SessionStore.jsm so that it can run SSTabRestoring. Users of - // SSTabRestoring seem to get confused if chrome and content are out of - // sync about the state of the restore (particularly regarding - // docShell.currentURI). Using a synchronous message is the easiest way - // to temporarily synchronize them. - // - // For remote tabs, because all nsIWebProgress notifications are sent - // asynchronously using messages, we get the same-order guarantees of the - // message manager, and can use an async message. - sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate}); - } else { - sendAsyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate}); - } - }, - - restoreTabContent({loadArguments, isRemotenessUpdate, reason}) { - let epoch = gCurrentEpoch; - - // We need to pass the value of didStartLoad back to SessionStore.jsm. - let didStartLoad = gContentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => { - // Tell SessionStore.jsm that it may want to restore some more tabs, - // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); - }); - - sendAsyncMessage("SessionStore:restoreTabContentStarted", { - epoch, isRemotenessUpdate, reason, - }); - - if (!didStartLoad) { - // Pretend that the load succeeded so that event handlers fire correctly. - sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); - } - }, - - flush({id}) { - // Flush the message queue, send the latest updates. - MessageQueue.send({flushID: id}); - } -}; +} /** * Listens for changes to the session history. Whenever the user navigates @@ -304,23 +199,28 @@ var MessageListener = { * Example: * {entries: [{url: "about:mozilla", ...}, ...], index: 1} */ -var SessionHistoryListener = { - init() { +class SessionHistoryListener extends Handler { + constructor(store) { + super(store); + + this._fromIdx = kNoIndex; + + // The state change observer is needed to handle initial subframe loads. // It will redundantly invalidate with the SHistoryListener in some cases // but these invalidations are very cheap. - StateChangeNotifier.addObserver(this); + this.stateChangeNotifier.addObserver(this); // By adding the SHistoryListener immediately, we will unfortunately be // notified of every history entry as the tab is restored. We don't bother // waiting to add the listener later because these notifications are cheap. // We will likely only collect once since we are batching collection on // a delay. - docShell.QueryInterface(Ci.nsIWebNavigation). - sessionHistory.legacySHistory.addSHistoryListener(this); + this.mm.docShell.QueryInterface(Ci.nsIWebNavigation) + .sessionHistory.legacySHistory.addSHistoryListener(this); // Collect data if we start with a non-empty shistory. - if (!SessionHistory.isEmpty(docShell)) { + if (!SessionHistory.isEmpty(this.mm.docShell)) { this.collect(); // When a tab is detached from the window, for the new window there is a // new SessionHistoryListener created. Normally it is empty at this point @@ -328,31 +228,29 @@ var SessionHistoryListener = { // case we fire off a history message here with about:blank in it. If we // don't do it ASAP then there is going to be a browser swap and the parent // will be all confused by that message. - MessageQueue.send(); + this.messageQueue.send(); } // Listen for page title changes. - addEventListener("DOMTitleChanged", this); - }, + this.mm.addEventListener("DOMTitleChanged", this); + } uninit() { - let sessionHistory = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; + let sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; if (sessionHistory) { sessionHistory.legacySHistory.removeSHistoryListener(this); } - }, + } collect() { // We want to send down a historychange even for full collects in case our // session history is a partial session history, in which case we don't have // enough information for a full update. collectFrom(-1) tells the collect // function to collect all data avaliable in this process. - if (docShell) { + if (this.mm.docShell) { this.collectFrom(-1); } - }, - - _fromIdx: kNoIndex, + } // History can grow relatively big with the nested elements, so if we don't have to, we // don't want to send the entire history all the time. For a simple optimization @@ -372,79 +270,77 @@ var SessionHistoryListener = { } this._fromIdx = idx; - MessageQueue.push("historychange", () => { + this.messageQueue.push("historychange", () => { if (this._fromIdx === kNoIndex) { return null; } - let history = SessionHistory.collect(docShell, this._fromIdx); + let history = SessionHistory.collect(this.mm.docShell, this._fromIdx); this._fromIdx = kNoIndex; return history; }); - }, + } handleEvent(event) { this.collect(); - }, + } onPageLoadCompleted() { this.collect(); - }, + } onPageLoadStarted() { this.collect(); - }, + } OnHistoryNewEntry(newURI, oldIndex) { // We ought to collect the previously current entry as well, see bug 1350567. this.collectFrom(oldIndex); - }, + } OnHistoryGoBack(backURI) { // We ought to collect the previously current entry as well, see bug 1350567. this.collectFrom(kLastIndex); return true; - }, + } OnHistoryGoForward(forwardURI) { // We ought to collect the previously current entry as well, see bug 1350567. this.collectFrom(kLastIndex); return true; - }, + } OnHistoryGotoIndex(index, gotoURI) { // We ought to collect the previously current entry as well, see bug 1350567. this.collectFrom(kLastIndex); return true; - }, + } OnHistoryPurge(numEntries) { this.collect(); return true; - }, + } OnHistoryReload(reloadURI, reloadFlags) { this.collect(); return true; - }, + } OnHistoryReplaceEntry(index) { this.collect(); - }, + } OnLengthChanged(aCount) { // Ignore, the method is implemented so that XPConnect doesn't throw! - }, + } OnIndexChanged(aIndex) { // Ignore, the method is implemented so that XPConnect doesn't throw! - }, - - QueryInterface: ChromeUtils.generateQI([ - Ci.nsISHistoryListener, - Ci.nsISupportsWeakReference - ]) -}; + } +} +SessionHistoryListener.prototype.QueryInterface = + ChromeUtils.generateQI([Ci.nsISHistoryListener, + Ci.nsISupportsWeakReference]); /** * Listens for scroll position changes. Whenever the user scrolls the top-most @@ -458,28 +354,30 @@ var SessionHistoryListener = { * Example: * {scroll: "100,100", children: [null, null, {scroll: "200,200"}]} */ -var ScrollPositionListener = { - init() { - ssu.addDynamicFrameFilteredListener(global, "scroll", this, false); - StateChangeNotifier.addObserver(this); - }, +class ScrollPositionListener extends Handler { + constructor(store) { + super(store); + + ssu.addDynamicFrameFilteredListener(this.mm, "scroll", this, false); + this.stateChangeNotifier.addObserver(this); + } handleEvent() { - MessageQueue.push("scroll", () => this.collect()); - }, + this.messageQueue.push("scroll", () => this.collect()); + } onPageLoadCompleted() { - MessageQueue.push("scroll", () => this.collect()); - }, + this.messageQueue.push("scroll", () => this.collect()); + } onPageLoadStarted() { - MessageQueue.push("scroll", () => null); - }, + this.messageQueue.push("scroll", () => null); + } collect() { - return mapFrameTree(ScrollPosition.collect); + return mapFrameTree(this.mm, ScrollPosition.collect); } -}; +} /** * Listens for changes to input elements. Whenever the value of an input @@ -498,24 +396,26 @@ var ScrollPositionListener = { * ] * } */ -var FormDataListener = { - init() { - ssu.addDynamicFrameFilteredListener(global, "input", this, true); - StateChangeNotifier.addObserver(this); - }, +class FormDataListener extends Handler { + constructor(store) { + super(store); + + ssu.addDynamicFrameFilteredListener(this.mm, "input", this, true); + this.stateChangeNotifier.addObserver(this); + } handleEvent() { - MessageQueue.push("formdata", () => this.collect()); - }, + this.messageQueue.push("formdata", () => this.collect()); + } onPageLoadStarted() { - MessageQueue.push("formdata", () => null); - }, + this.messageQueue.push("formdata", () => null); + } collect() { - return mapFrameTree(FormData.collect); + return mapFrameTree(this.mm, FormData.collect); } -}; +} /** * Listens for changes to docShell capabilities. Whenever a new load is started @@ -526,29 +426,31 @@ var FormDataListener = { * disabled docShell capabilities (all nsIDocShell.allow* properties set to * false) as a string - i.e. capability names separate by commas. */ -var DocShellCapabilitiesListener = { - /** - * This field is used to compare the last docShell capabilities to the ones - * that have just been collected. If nothing changed we won't send a message. - */ - _latestCapabilities: "", +class DocShellCapabilitiesListener extends Handler { + constructor(store) { + super(store); - init() { - StateChangeNotifier.addObserver(this); - }, + /** + * This field is used to compare the last docShell capabilities to the ones + * that have just been collected. If nothing changed we won't send a message. + */ + this._latestCapabilities = ""; + + this.stateChangeNotifier.addObserver(this); + } onPageLoadStarted() { // The order of docShell capabilities cannot change while we're running // so calling join() without sorting before is totally sufficient. - let caps = DocShellCapabilities.collect(docShell).join(","); + let caps = DocShellCapabilities.collect(this.mm.docShell).join(","); // Send new data only when the capability list changes. if (caps != this._latestCapabilities) { this._latestCapabilities = caps; - MessageQueue.push("disallow", () => caps || null); + this.messageQueue.push("disallow", () => caps || null); } } -}; +} /** * Listens for changes to the DOMSessionStorage. Whenever new keys are added, @@ -559,56 +461,60 @@ var DocShellCapabilitiesListener = { * DOMSessionStorage contents. The data is a nested object using host names * as keys and per-host DOMSessionStorage data as values. */ -var SessionStorageListener = { - init() { +class SessionStorageListener extends Handler { + constructor(store) { + super(store); + + // We don't want to send all the session storage data for all the frames + // for every change. So if only a few value changed we send them over as + // a "storagechange" event. If however for some reason before we send these + // changes we have to send over the entire sessions storage data, we just + // reset these changes. + this._changes = undefined; + + // The event listener waiting for MozSessionStorageChanged events. + this._listener = null; + Services.obs.addObserver(this, "browser:purge-domain-data"); - StateChangeNotifier.addObserver(this); + this.stateChangeNotifier.addObserver(this); this.resetEventListener(); - }, + } uninit() { Services.obs.removeObserver(this, "browser:purge-domain-data"); - }, + } observe() { // Collect data on the next tick so that any other observer // that needs to purge data can do its work first. - setTimeoutWithTarget(() => this.collect(), 0, tabEventTarget); - }, - - // We don't want to send all the session storage data for all the frames - // for every change. So if only a few value changed we send them over as - // a "storagechange" event. If however for some reason before we send these - // changes we have to send over the entire sessions storage data, we just - // reset these changes. - _changes: undefined, + setTimeoutWithTarget(() => this.collect(), 0, this.mm.tabEventTarget); + } resetChanges() { this._changes = undefined; - }, - - // The event listener waiting for MozSessionStorageChanged events. - _listener: null, + } resetEventListener() { if (!this._listener) { this._listener = - ssu.addDynamicFrameFilteredListener(global, "MozSessionStorageChanged", + ssu.addDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged", this, true); } - }, + } removeEventListener() { - ssu.removeDynamicFrameFilteredListener(global, "MozSessionStorageChanged", + ssu.removeDynamicFrameFilteredListener(this.mm, "MozSessionStorageChanged", this._listener, true); this._listener = null; - }, + } handleEvent(event) { - if (!docShell) { + if (!this.mm.docShell) { return; } + let {content} = this.mm; + // How much data does DOMSessionStorage contain? let usage = content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) @@ -617,7 +523,7 @@ var SessionStorageListener = { // Don't store any data if we exceed the limit. Wipe any data we previously // collected so that we don't confuse websites with partial state. if (usage > Services.prefs.getIntPref(DOM_STORAGE_LIMIT_PREF)) { - MessageQueue.push("storage", () => null); + this.messageQueue.push("storage", () => null); this.removeEventListener(); this.resetChanges(); return; @@ -634,7 +540,7 @@ var SessionStorageListener = { } this._changes[domain][key] = newValue; - MessageQueue.push("storagechange", () => { + this.messageQueue.push("storagechange", () => { let tmp = this._changes; // If there were multiple changes we send them merged. // First one will collect all the changes the rest of @@ -642,29 +548,31 @@ var SessionStorageListener = { this.resetChanges(); return tmp; }); - }, + } collect() { - if (!docShell) { + if (!this.mm.docShell) { return; } + let {content} = this.mm; + // We need the entire session storage, let's reset the pending individual change // messages. this.resetChanges(); - MessageQueue.push("storage", () => SessionStorage.collect(content)); - }, + this.messageQueue.push("storage", () => SessionStorage.collect(content)); + } onPageLoadCompleted() { this.collect(); - }, + } onPageLoadStarted() { this.resetEventListener(); this.collect(); } -}; +} /** * Listen for changes to the privacy status of the tab. @@ -676,25 +584,27 @@ var SessionStorageListener = { * |null| if the tab is now public - the field is therefore * not saved. */ -var PrivacyListener = { - init() { - docShell.addWeakPrivacyTransitionObserver(this); +class PrivacyListener extends Handler { + constructor(store) { + super(store); + + this.mm.docShell.addWeakPrivacyTransitionObserver(this); // Check that value at startup as it might have // been set before the frame script was loaded. - if (docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { - MessageQueue.push("isPrivate", () => true); + if (this.mm.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing) { + this.messageQueue.push("isPrivate", () => true); } - }, + } // Ci.nsIPrivacyTransitionObserver privateModeChanged(enabled) { - MessageQueue.push("isPrivate", () => enabled || null); - }, - - QueryInterface: ChromeUtils.generateQI([Ci.nsIPrivacyTransitionObserver, - Ci.nsISupportsWeakReference]) -}; + this.messageQueue.push("isPrivate", () => enabled || null); + } +} +PrivacyListener.prototype.QueryInterface = + ChromeUtils.generateQI([Ci.nsIPrivacyTransitionObserver, + Ci.nsISupportsWeakReference]); /** * A message queue that takes collected data and will take care of sending it @@ -703,49 +613,62 @@ var PrivacyListener = { * will be batched if they're pushed in quick succession to avoid a message * flood. */ -var MessageQueue = { - /** - * A map (string -> lazy fn) holding lazy closures of all queued data - * collection routines. These functions will return data collected from the - * docShell. - */ - _data: new Map(), +class MessageQueue extends Handler { + constructor(store) { + super(store); - /** - * The delay (in ms) used to delay sending changes after data has been - * invalidated. - */ - BATCH_DELAY_MS: 1000, + /** + * A map (string -> lazy fn) holding lazy closures of all queued data + * collection routines. These functions will return data collected from the + * docShell. + */ + this._data = new Map(); - /** - * The minimum idle period (in ms) we need for sending data to chrome process. - */ - NEEDED_IDLE_PERIOD_MS: 5, + /** + * The delay (in ms) used to delay sending changes after data has been + * invalidated. + */ + this.BATCH_DELAY_MS = 1000; - /** - * Timeout for waiting an idle period to send data. We will set this from - * the pref "browser.sessionstore.interval". - */ - _timeoutWaitIdlePeriodMs: null, + /** + * The minimum idle period (in ms) we need for sending data to chrome process. + */ + this.NEEDED_IDLE_PERIOD_MS = 5; - /** - * 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, + /** + * Timeout for waiting an idle period to send data. We will set this from + * the pref "browser.sessionstore.interval". + */ + this._timeoutWaitIdlePeriodMs = 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, + /** + * 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. + */ + this._timeout = null; - /** - * True if there is already a send pending idle dispatch, set to prevent - * scheduling more than one. If false there may or may not be one scheduled. - */ - _idleScheduled: false, + /** + * 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. + */ + this._timeoutDisabled = false; + + /** + * True if there is already a send pending idle dispatch, set to prevent + * scheduling more than one. If false there may or may not be one scheduled. + */ + this._idleScheduled = false; + + + this.timeoutDisabled = + Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); + this._timeoutWaitIdlePeriodMs = + Services.prefs.getIntPref(PREF_INTERVAL); + + Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this); + Services.prefs.addObserver(PREF_INTERVAL, this); + } /** * True if batched messages are not being fired on a timer. This should only @@ -753,7 +676,7 @@ var MessageQueue = { */ get timeoutDisabled() { return this._timeoutDisabled; - }, + } /** * Disables sending batched messages on a timer. Also cancels any pending @@ -768,23 +691,13 @@ var MessageQueue = { } return val; - }, - - init() { - this.timeoutDisabled = - Services.prefs.getBoolPref(TIMEOUT_DISABLED_PREF); - this._timeoutWaitIdlePeriodMs = - Services.prefs.getIntPref(PREF_INTERVAL); - - Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this); - Services.prefs.addObserver(PREF_INTERVAL, this); - }, + } uninit() { Services.prefs.removeObserver(TIMEOUT_DISABLED_PREF, this); Services.prefs.removeObserver(PREF_INTERVAL, this); this.cleanupTimers(); - }, + } /** * Cleanup pending idle callback and timer. @@ -795,7 +708,7 @@ var MessageQueue = { clearTimeout(this._timeout); this._timeout = null; } - }, + } observe(subject, topic, data) { if (topic == "nsPref:changed") { @@ -813,7 +726,7 @@ var MessageQueue = { break; } } - }, + } /** * Pushes a given |value| onto the queue. The given |key| represents the type @@ -832,9 +745,9 @@ var MessageQueue = { if (!this._timeout && !this._timeoutDisabled) { // Wait a little before sending the message to batch multiple changes. this._timeout = setTimeoutWithTarget( - () => this.sendWhenIdle(), this.BATCH_DELAY_MS, tabEventTarget); + () => this.sendWhenIdle(), this.BATCH_DELAY_MS, this.mm.tabEventTarget); } - }, + } /** * Sends queued data when the remaining idle time is enough or waiting too @@ -845,23 +758,24 @@ var MessageQueue = { * An IdleDeadline object passed by idleDispatch(). */ sendWhenIdle(deadline) { - if (!content) { + if (!this.mm.content) { // The frameloader is being torn down. Nothing more to do. return; } if (deadline) { - if (deadline.didTimeout || deadline.timeRemaining() > MessageQueue.NEEDED_IDLE_PERIOD_MS) { - MessageQueue.send(); + if (deadline.didTimeout || deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS) { + this.send(); return; } - } else if (MessageQueue._idleScheduled) { + } else if (this._idleScheduled) { // Bail out if there's a pending run. return; } - ChromeUtils.idleDispatch(MessageQueue.sendWhenIdle, {timeout: MessageQueue._timeoutWaitIdlePeriodMs}); - MessageQueue._idleScheduled = true; - }, + ChromeUtils.idleDispatch((deadline_) => this.sendWhenIdle(deadline_), + {timeout: this._timeoutWaitIdlePeriodMs}); + this._idleScheduled = true; + } /** * Sends queued data to the chrome process. @@ -874,7 +788,7 @@ var MessageQueue = { // Looks like we have been called off a timeout after the tab has been // closed. The docShell is gone now and we can just return here as there // is nothing to do. - if (!docShell) { + if (!this.mm.docShell) { return; } @@ -904,78 +818,227 @@ var MessageQueue = { try { // Send all data to the parent process. - sendAsyncMessage("SessionStore:update", { + this.mm.sendAsyncMessage("SessionStore:update", { data, flushID, isFinal: options.isFinal || false, - epoch: gCurrentEpoch + epoch: this.store.epoch, }); } catch (ex) { if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) { Services.telemetry.getHistogramById("FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM").add(1); - sendAsyncMessage("SessionStore:error"); + this.mm.sendAsyncMessage("SessionStore:error"); } } - }, -}; - -StateChangeNotifier.init(); -EventListener.init(); -MessageListener.init(); -FormDataListener.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")) { - if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { - // Sanity check - we'd better be loading this in a non-remote browser. - throw new Error("We seem to be navigating away from about:tabcrashed in " + - "a non-remote browser. This should really never happen."); - } - - removeEventListener("pagehide", handleRevivedTab); - - // Notify the parent. - sendAsyncMessage("SessionStore:crashedTabRevived"); } } -// If we're browsing from the tab crashed UI to a blacklisted URI that keeps -// this browser non-remote, we'll handle that in a pagehide event. -addEventListener("pagehide", handleRevivedTab); +/** + * Listens for and handles messages sent by the session store service. + */ +const MESSAGES = [ + "SessionStore:restoreHistory", + "SessionStore:restoreTabContent", + "SessionStore:resetRestore", + "SessionStore:flush", + "SessionStore:becomeActiveProcess", +]; -addEventListener("unload", () => { - // Upon frameLoader destruction, send a final update message to - // the parent and flush all data currently held in the child. - MessageQueue.send({isFinal: true}); +class ContentSessionStore { + constructor(mm) { + this.mm = mm; + this.messageQueue = new MessageQueue(this); + this.stateChangeNotifier = new StateChangeNotifier(this); - // If we're browsing from the tab crashed UI to a URI that causes the tab - // to go remote again, we catch this in the unload event handler, because - // swapping out the non-remote browser for a remote one in - // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide - // event to be fired. - handleRevivedTab(); + this.epoch = 0; - // Remove all registered nsIObservers. - SessionStorageListener.uninit(); - SessionHistoryListener.uninit(); - MessageQueue.uninit(); + this.contentRestoreInitialized = false; - if (contentRestoreInitialized) { - // Remove progress listeners. - gContentRestore.resetRestore(); + XPCOMUtils.defineLazyGetter(this, "contentRestore", + () => { + this.contentRestoreInitialized = true; + return new ContentRestore(mm); + }); + + this.handlers = [ + new EventListener(this), + new FormDataListener(this), + new SessionHistoryListener(this), + new SessionStorageListener(this), + new ScrollPositionListener(this), + new DocShellCapabilitiesListener(this), + new PrivacyListener(this), + this.stateChangeNotifier, + this.messageQueue, + ]; + + MESSAGES.forEach(m => mm.addMessageListener(m, this)); + + // If we're browsing from the tab crashed UI to a blacklisted URI that keeps + // this browser non-remote, we'll handle that in a pagehide event. + mm.addEventListener("pagehide", this); + mm.addEventListener("unload", this); } - // We don't need to take care of any StateChangeNotifier observers as they - // 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. -}); + receiveMessage({name, data}) { + // The docShell might be gone. Don't process messages, + // that will just lead to errors anyway. + if (!this.mm.docShell) { + return; + } + + // A fresh tab always starts with epoch=0. The parent has the ability to + // override that to signal a new era in this tab's life. This enables it + // to ignore async messages that were already sent but not yet received + // and would otherwise confuse the internal tab state. + if (data.epoch && data.epoch != this.epoch) { + this.epoch = data.epoch; + } + + switch (name) { + case "SessionStore:restoreHistory": + this.restoreHistory(data); + break; + case "SessionStore:restoreTabContent": + if (data.isRemotenessUpdate) { + let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS"); + histogram.add("SessionStore:restoreTabContent", + Services.telemetry.msSystemNow() - data.requestTime); + } + this.restoreTabContent(data); + break; + case "SessionStore:resetRestore": + this.contentRestore.resetRestore(); + break; + case "SessionStore:flush": + this.flush(data); + break; + case "SessionStore:becomeActiveProcess": + SessionHistoryListener.collect(); + break; + default: + debug("received unknown message '" + name + "'"); + break; + } + } + + restoreHistory({epoch, tabData, loadArguments, isRemotenessUpdate}) { + this.contentRestore.restoreHistory(tabData, loadArguments, { + // Note: The callbacks passed here will only be used when a load starts + // that was not initiated by sessionstore itself. This can happen when + // some code calls browser.loadURI() or browser.reload() on a pending + // browser/tab. + + onLoadStarted: () => { + // Notify the parent that the tab is no longer pending. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch}); + }, + + onLoadFinished: () => { + // Tell SessionStore.jsm that it may want to restore some more tabs, + // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch}); + } + }); + + if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) { + // For non-remote tabs, when restoreHistory finishes, we send a synchronous + // message to SessionStore.jsm so that it can run SSTabRestoring. Users of + // SSTabRestoring seem to get confused if chrome and content are out of + // sync about the state of the restore (particularly regarding + // docShell.currentURI). Using a synchronous message is the easiest way + // to temporarily synchronize them. + // + // For remote tabs, because all nsIWebProgress notifications are sent + // asynchronously using messages, we get the same-order guarantees of the + // message manager, and can use an async message. + this.mm.sendSyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate}); + } else { + this.mm.sendAsyncMessage("SessionStore:restoreHistoryComplete", {epoch, isRemotenessUpdate}); + } + } + + restoreTabContent({loadArguments, isRemotenessUpdate, reason}) { + let epoch = this.epoch; + + // We need to pass the value of didStartLoad back to SessionStore.jsm. + let didStartLoad = this.contentRestore.restoreTabContent(loadArguments, isRemotenessUpdate, () => { + // Tell SessionStore.jsm that it may want to restore some more tabs, + // since it restores a max of MAX_CONCURRENT_TAB_RESTORES at a time. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); + }); + + this.mm.sendAsyncMessage("SessionStore:restoreTabContentStarted", { + epoch, isRemotenessUpdate, reason, + }); + + if (!didStartLoad) { + // Pretend that the load succeeded so that event handlers fire correctly. + this.mm.sendAsyncMessage("SessionStore:restoreTabContentComplete", {epoch, isRemotenessUpdate}); + } + } + + flush({id}) { + // Flush the message queue, send the latest updates. + this.messageQueue.send({flushID: id}); + } + + handleEvent(event) { + if (event.type == "pagehide") { + this.handleRevivedTab(); + } else if (event.type == "unload") { + this.onUnload(); + } + } + + onUnload() { + // Upon frameLoader destruction, send a final update message to + // the parent and flush all data currently held in the child. + this.messageQueue.send({isFinal: true}); + + // If we're browsing from the tab crashed UI to a URI that causes the tab + // to go remote again, we catch this in the unload event handler, because + // swapping out the non-remote browser for a remote one in + // tabbrowser.xml's updateBrowserRemoteness doesn't cause the pagehide + // event to be fired. + this.handleRevivedTab(); + + for (let handler of this.handlers) { + if (handler.uninit) { + handler.uninit(); + } + } + + if (this.contentRestoreInitialized) { + // Remove progress listeners. + this.contentRestore.resetRestore(); + } + + // We don't need to take care of any StateChangeNotifier observers as they + // 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. + } + + handleRevivedTab() { + let {content} = this.mm; + + if (!content) { + this.mm.removeEventListener("pagehide", this); + return; + } + + if (content.document.documentURI.startsWith("about:tabcrashed")) { + if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) { + // Sanity check - we'd better be loading this in a non-remote browser. + throw new Error("We seem to be navigating away from about:tabcrashed in " + + "a non-remote browser. This should really never happen."); + } + + this.mm.removeEventListener("pagehide", this); + + // Notify the parent. + this.mm.sendAsyncMessage("SessionStore:crashedTabRevived"); + } + } +} + +void new ContentSessionStore(this);