Bug 1523562 [wpt PR 14802] - [Animation Worklet] Upstream worklet animation with...
[gecko.git] / browser / modules / AsyncTabSwitcher.jsm
blobc869badcaeb5d4e78bdc3e134ff688a4be8a5751
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 var EXPORTED_SYMBOLS = ["AsyncTabSwitcher"];
10 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
11 XPCOMUtils.defineLazyModuleGetters(this, {
12   AppConstants: "resource://gre/modules/AppConstants.jsm",
13   Services: "resource://gre/modules/Services.jsm",
14 });
16 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingEnabled",
17   "browser.tabs.remote.warmup.enabled");
18 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingMax",
19   "browser.tabs.remote.warmup.maxTabs");
20 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingUnloadDelayMs",
21   "browser.tabs.remote.warmup.unloadDelayMs");
22 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabCacheSize",
23   "browser.tabs.remote.tabCacheSize");
25 /**
26  * The tab switcher is responsible for asynchronously switching
27  * tabs in e10s. It waits until the new tab is ready (i.e., the
28  * layer tree is available) before switching to it. Then it
29  * unloads the layer tree for the old tab.
30  *
31  * The tab switcher is a state machine. For each tab, it
32  * maintains state about whether the layer tree for the tab is
33  * available, being loaded, being unloaded, or unavailable. It
34  * also keeps track of the tab currently being displayed, the tab
35  * it's trying to load, and the tab the user has asked to switch
36  * to. The switcher object is created upon tab switch. It is
37  * released when there are no pending tabs to load or unload.
38  *
39  * The following general principles have guided the design:
40  *
41  * 1. We only request one layer tree at a time. If the user
42  * switches to a different tab while waiting, we don't request
43  * the new layer tree until the old tab has loaded or timed out.
44  *
45  * 2. If loading the layers for a tab times out, we show the
46  * spinner and possibly request the layer tree for another tab if
47  * the user has requested one.
48  *
49  * 3. We discard layer trees on a delay. This way, if the user is
50  * switching among the same tabs frequently, we don't continually
51  * load the same tabs.
52  *
53  * It's important that we always show either the spinner or a tab
54  * whose layers are available. Otherwise the compositor will draw
55  * an entirely black frame, which is very jarring. To ensure this
56  * never happens when switching away from a tab, we assume the
57  * old tab might still be drawn until a MozAfterPaint event
58  * occurs. Because layout and compositing happen asynchronously,
59  * we don't have any other way of knowing when the switch
60  * actually takes place. Therefore, we don't unload the old tab
61  * until the next MozAfterPaint event.
62  */
63 class AsyncTabSwitcher {
64   constructor(tabbrowser) {
65     this.log("START");
67     // How long to wait for a tab's layers to load. After this
68     // time elapses, we're free to put up the spinner and start
69     // trying to load a different tab.
70     this.TAB_SWITCH_TIMEOUT = 400; // ms
72     // When the user hasn't switched tabs for this long, we unload
73     // layers for all tabs that aren't in use.
74     this.UNLOAD_DELAY = 300; // ms
76     // The next three tabs form the principal state variables.
77     // See the assertions in postActions for their invariants.
79     // Tab the user requested most recently.
80     this.requestedTab = tabbrowser.selectedTab;
82     // Tab we're currently trying to load.
83     this.loadingTab = null;
85     // We show this tab in case the requestedTab hasn't loaded yet.
86     this.lastVisibleTab = tabbrowser.selectedTab;
88     // Auxilliary state variables:
90     this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
91     this.spinnerTab = null; // Tab showing a spinner.
92     this.blankTab = null; // Tab showing blank.
93     this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
95     this.tabbrowser = tabbrowser;
96     this.window = tabbrowser.ownerGlobal;
97     this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
98     this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
100     // Map from tabs to STATE_* (below).
101     this.tabState = new Map();
103     // True if we're in the midst of switching tabs.
104     this.switchInProgress = false;
106     // Transaction id for the composite that will show the requested
107     // tab for the first tab after a tab switch.
108     // Set to -1 when we're not waiting for notification of a
109     // completed switch.
110     this.switchPaintId = -1;
112     // Set of tabs that might be visible right now. We maintain
113     // this set because we can't be sure when a tab is actually
114     // drawn. A tab is added to this set when we ask to make it
115     // visible. All tabs but the most recently shown tab are
116     // removed from the set upon MozAfterPaint.
117     this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
119     // This holds onto the set of tabs that we've been asked to warm up,
120     // and tabs are evicted once they're done loading or are unloaded.
121     this.warmingTabs = new WeakSet();
123     this.STATE_UNLOADED = 0;
124     this.STATE_LOADING = 1;
125     this.STATE_LOADED = 2;
126     this.STATE_UNLOADING = 3;
128     // re-entrancy guard:
129     this._processing = false;
131     // For telemetry, keeps track of what most recently cleared
132     // the loadTimer, which can tell us something about the cause
133     // of tab switch spinners.
134     this._loadTimerClearedBy = "none";
136     this._useDumpForLogging = false;
137     this._logInit = false;
139     this.window.addEventListener("MozAfterPaint", this);
140     this.window.addEventListener("MozLayerTreeReady", this);
141     this.window.addEventListener("MozLayerTreeCleared", this);
142     this.window.addEventListener("TabRemotenessChange", this);
143     this.window.addEventListener("sizemodechange", this);
144     this.window.addEventListener("occlusionstatechange", this);
145     this.window.addEventListener("SwapDocShells", this, true);
146     this.window.addEventListener("EndSwapDocShells", this, true);
148     let initialTab = this.requestedTab;
149     let initialBrowser = initialTab.linkedBrowser;
151     let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
152       initialBrowser.frameLoader.tabParent.hasLayers;
154     // If we minimized the window before the switcher was activated,
155     // we might have set  the preserveLayers flag for the current
156     // browser. Let's clear it.
157     initialBrowser.preserveLayers(false);
159     if (!this.minimizedOrFullyOccluded) {
160       this.log("Initial tab is loaded?: " + tabIsLoaded);
161       this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
162                                                : this.STATE_LOADING);
163     }
165     for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
166       let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
167       let state = ppBrowser.hasLayers ? this.STATE_LOADED
168                                       : this.STATE_LOADING;
169       this.setTabState(ppTab, state);
170     }
171   }
173   destroy() {
174     if (this.unloadTimer) {
175       this.clearTimer(this.unloadTimer);
176       this.unloadTimer = null;
177     }
178     if (this.loadTimer) {
179       this.clearTimer(this.loadTimer);
180       this.loadTimer = null;
181     }
183     this.window.removeEventListener("MozAfterPaint", this);
184     this.window.removeEventListener("MozLayerTreeReady", this);
185     this.window.removeEventListener("MozLayerTreeCleared", this);
186     this.window.removeEventListener("TabRemotenessChange", this);
187     this.window.removeEventListener("sizemodechange", this);
188     this.window.removeEventListener("occlusionstatechange", this);
189     this.window.removeEventListener("SwapDocShells", this, true);
190     this.window.removeEventListener("EndSwapDocShells", this, true);
192     this.tabbrowser._switcher = null;
193   }
195   // Wraps nsITimer. Must not use the vanilla setTimeout and
196   // clearTimeout, because they will be blocked by nsIPromptService
197   // dialogs.
198   setTimer(callback, timeout) {
199     let event = {
200       notify: callback,
201     };
203     var timer = Cc["@mozilla.org/timer;1"]
204       .createInstance(Ci.nsITimer);
205     timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
206     return timer;
207   }
209   clearTimer(timer) {
210     timer.cancel();
211   }
213   getTabState(tab) {
214     let state = this.tabState.get(tab);
216     // As an optimization, we lazily evaluate the state of tabs
217     // that we've never seen before. Once we've figured it out,
218     // we stash it in our state map.
219     if (state === undefined) {
220       state = this.STATE_UNLOADED;
222       if (tab && tab.linkedPanel) {
223         let b = tab.linkedBrowser;
224         if (b.renderLayers && b.hasLayers) {
225           state = this.STATE_LOADED;
226         } else if (b.renderLayers && !b.hasLayers) {
227           state = this.STATE_LOADING;
228         } else if (!b.renderLayers && b.hasLayers) {
229           state = this.STATE_UNLOADING;
230         }
231       }
233       this.setTabStateNoAction(tab, state);
234     }
236     return state;
237   }
239   setTabStateNoAction(tab, state) {
240     if (state == this.STATE_UNLOADED) {
241       this.tabState.delete(tab);
242     } else {
243       this.tabState.set(tab, state);
244     }
245   }
247   setTabState(tab, state) {
248     if (state == this.getTabState(tab)) {
249       return;
250     }
252     this.setTabStateNoAction(tab, state);
254     let browser = tab.linkedBrowser;
255     let { tabParent } = browser.frameLoader;
256     if (state == this.STATE_LOADING) {
257       this.assert(!this.minimizedOrFullyOccluded);
259       // If we're not in the process of warming this tab, we
260       // don't need to delay activating its DocShell.
261       if (!this.warmingTabs.has(tab)) {
262         browser.docShellIsActive = true;
263       }
265       if (tabParent) {
266         browser.renderLayers = true;
267       } else {
268         this.onLayersReady(browser);
269       }
270     } else if (state == this.STATE_UNLOADING) {
271       this.unwarmTab(tab);
272       // Setting the docShell to be inactive will also cause it
273       // to stop rendering layers.
274       browser.docShellIsActive = false;
275       if (!tabParent) {
276         this.onLayersCleared(browser);
277       }
278     } else if (state == this.STATE_LOADED) {
279       this.maybeActivateDocShell(tab);
280     }
282     if (!tab.linkedBrowser.isRemoteBrowser) {
283       // setTabState is potentially re-entrant in the non-remote case,
284       // so we must re-get the state for this assertion.
285       let nonRemoteState = this.getTabState(tab);
286       // Non-remote tabs can never stay in the STATE_LOADING
287       // or STATE_UNLOADING states. By the time this function
288       // exits, a non-remote tab must be in STATE_LOADED or
289       // STATE_UNLOADED, since the painting and the layer
290       // upload happen synchronously.
291       this.assert(nonRemoteState == this.STATE_UNLOADED ||
292         nonRemoteState == this.STATE_LOADED);
293     }
294   }
296   get minimizedOrFullyOccluded() {
297     return this.window.windowState == this.window.STATE_MINIMIZED ||
298            this.window.isFullyOccluded;
299   }
301   get tabLayerCache() {
302     return this.tabbrowser._tabLayerCache;
303   }
305   finish() {
306     this.log("FINISH");
308     this.assert(this.tabbrowser._switcher);
309     this.assert(this.tabbrowser._switcher === this);
310     this.assert(!this.spinnerTab);
311     this.assert(!this.blankTab);
312     this.assert(!this.loadTimer);
313     this.assert(!this.loadingTab);
314     this.assert(this.lastVisibleTab === this.requestedTab);
315     this.assert(this.minimizedOrFullyOccluded ||
316       this.getTabState(this.requestedTab) == this.STATE_LOADED);
318     this.destroy();
320     this.window.document.commandDispatcher.unlock();
322     let event = new this.window.CustomEvent("TabSwitchDone", {
323       bubbles: true,
324       cancelable: true,
325     });
326     this.tabbrowser.dispatchEvent(event);
327   }
329   // This function is called after all the main state changes to
330   // make sure we display the right tab.
331   updateDisplay() {
332     let requestedTabState = this.getTabState(this.requestedTab);
333     let requestedBrowser = this.requestedTab.linkedBrowser;
335     // It is often more desirable to show a blank tab when appropriate than
336     // the tab switch spinner - especially since the spinner is usually
337     // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
338     // tab switch. We can hide this lag, and hide the time being spent
339     // constructing TabChild's, layer trees, etc, by showing a blank
340     // tab instead and focusing it immediately.
341     let shouldBeBlank = false;
342     if (requestedBrowser.isRemoteBrowser) {
343       // If a tab is remote and the window is not minimized, we can show a
344       // blank tab instead of a spinner in the following cases:
345       //
346       // 1. The tab has just crashed, and we haven't started showing the
347       //    tab crashed page yet (in this case, the TabParent is null)
348       // 2. The tab has never presented, and has not finished loading
349       //    a non-local-about: page.
350       //
351       // For (2), "finished loading a non-local-about: page" is
352       // determined by the busy state on the tab element and checking
353       // if the loaded URI is local.
354       let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
355         !this.tabbrowser.isLocalAboutURI(requestedBrowser.currentURI);
357       let fl = requestedBrowser.frameLoader;
358       shouldBeBlank = !this.minimizedOrFullyOccluded &&
359         (!fl.tabParent ||
360           (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
361     }
363     this.log("Tab should be blank: " + shouldBeBlank);
364     this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
366     // Figure out which tab we actually want visible right now.
367     let showTab = null;
368     if (requestedTabState != this.STATE_LOADED &&
369         this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
370       // If we can't show the requestedTab, and lastVisibleTab is
371       // available, show it.
372       showTab = this.lastVisibleTab;
373     } else {
374       // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
375       showTab = this.requestedTab;
376     }
378     // First, let's deal with blank tabs, which we show instead
379     // of the spinner when the tab is not currently set up
380     // properly in the content process.
381     if (!shouldBeBlank && this.blankTab) {
382       this.blankTab.linkedBrowser.removeAttribute("blank");
383       this.blankTab = null;
384     } else if (shouldBeBlank && this.blankTab !== showTab) {
385       if (this.blankTab) {
386         this.blankTab.linkedBrowser.removeAttribute("blank");
387       }
388       this.blankTab = showTab;
389       this.blankTab.linkedBrowser.setAttribute("blank", "true");
390     }
392     // Show or hide the spinner as needed.
393     let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
394                       !this.minimizedOrFullyOccluded &&
395                       !shouldBeBlank;
397     if (!needSpinner && this.spinnerTab) {
398       this.spinnerHidden();
399       this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
400       this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
401       this.spinnerTab = null;
402     } else if (needSpinner && this.spinnerTab !== showTab) {
403       if (this.spinnerTab) {
404         this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
405       } else {
406         this.spinnerDisplayed();
407       }
408       this.spinnerTab = showTab;
409       this.tabbrowser.tabpanels.setAttribute("pendingpaint", "true");
410       this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
411     }
413     // Switch to the tab we've decided to make visible.
414     if (this.visibleTab !== showTab) {
415       this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
416       this.visibleTab = showTab;
418       this.maybeVisibleTabs.add(showTab);
420       let tabpanels = this.tabbrowser.tabpanels;
421       let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
422       let index = Array.indexOf(tabpanels.children, showPanel);
423       if (index != -1) {
424         this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
425         tabpanels.setAttribute("selectedIndex", index);
426         if (showTab === this.requestedTab) {
427           if (requestedTabState == this.STATE_LOADED) {
428             // The new tab will be made visible in the next paint, record the expected
429             // transaction id for that, and we'll mark when we get notified of its
430             // completion.
431             this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
432           } else {
433             // We're making the tab visible even though we haven't yet got layers for it.
434             // It's hard to know which composite the layers will first be available in (and
435             // the parent process might not even get MozAfterPaint delivered for it), so just
436             // give up measuring this for now. :(
437             TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
438           }
440           this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
441           this.maybeActivateDocShell(this.requestedTab);
442         }
443       }
445       // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
446       if (this.lastVisibleTab)
447         this.lastVisibleTab._visuallySelected = false;
449       this.visibleTab._visuallySelected = true;
450       this.tabbrowser.tabContainer._setPositionalAttributes();
451     }
453     this.lastVisibleTab = this.visibleTab;
454   }
456   assert(cond) {
457     if (!cond) {
458       dump("Assertion failure\n" + Error().stack);
460       // Don't break a user's browser if an assertion fails.
461       if (AppConstants.DEBUG) {
462         throw new Error("Assertion failure");
463       }
464     }
465   }
467   maybeClearLoadTimer(caller) {
468     if (this.loadingTab) {
469       this._loadTimerClearedBy = caller;
470       this.loadingTab = null;
471       if (this.loadTimer) {
472         this.clearTimer(this.loadTimer);
473         this.loadTimer = null;
474       }
475     }
476   }
479   // We've decided to try to load requestedTab.
480   loadRequestedTab() {
481     this.assert(!this.loadTimer);
482     this.assert(!this.minimizedOrFullyOccluded);
484     // loadingTab can be non-null here if we timed out loading the current tab.
485     // In that case we just overwrite it with a different tab; it's had its chance.
486     this.loadingTab = this.requestedTab;
487     this.log("Loading tab " + this.tinfo(this.loadingTab));
489     this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
490     this.setTabState(this.requestedTab, this.STATE_LOADING);
491   }
493   maybeActivateDocShell(tab) {
494     // If we've reached the point where the requested tab has entered
495     // the loaded state, but the DocShell is still not yet active, we
496     // should activate it.
497     let browser = tab.linkedBrowser;
498     let state = this.getTabState(tab);
499     let canCheckDocShellState = !browser.mDestroyed &&
500       (browser.docShell || browser.frameLoader.tabParent);
501     if (tab == this.requestedTab &&
502         canCheckDocShellState &&
503         state == this.STATE_LOADED &&
504         !browser.docShellIsActive &&
505         !this.minimizedOrFullyOccluded) {
506       browser.docShellIsActive = true;
507       this.logState("Set requested tab docshell to active and preserveLayers to false");
508       // If we minimized the window before the switcher was activated,
509       // we might have set the preserveLayers flag for the current
510       // browser. Let's clear it.
511       browser.preserveLayers(false);
512     }
513   }
515   // This function runs before every event. It fixes up the state
516   // to account for closed tabs.
517   preActions() {
518     this.assert(this.tabbrowser._switcher);
519     this.assert(this.tabbrowser._switcher === this);
521     for (let i = 0; i < this.tabLayerCache.length; i++) {
522       let tab = this.tabLayerCache[i];
523       if (!tab.linkedBrowser) {
524         this.tabState.delete(tab);
525         this.tabLayerCache.splice(i, 1);
526         i--;
527       }
528     }
530     for (let [tab ] of this.tabState) {
531       if (!tab.linkedBrowser) {
532         this.tabState.delete(tab);
533         this.unwarmTab(tab);
534       }
535     }
537     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
538       this.lastVisibleTab = null;
539     }
540     if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
541       this.lastPrimaryTab = null;
542     }
543     if (this.blankTab && !this.blankTab.linkedBrowser) {
544       this.blankTab = null;
545     }
546     if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
547       this.spinnerHidden();
548       this.spinnerTab = null;
549     }
550     if (this.loadingTab && !this.loadingTab.linkedBrowser) {
551       this.maybeClearLoadTimer("preActions");
552     }
553   }
555   // This code runs after we've responded to an event or requested a new
556   // tab. It's expected that we've already updated all the principal
557   // state variables. This function takes care of updating any auxilliary
558   // state.
559   postActions() {
560     // Once we finish loading loadingTab, we null it out. So the state should
561     // always be LOADING.
562     this.assert(!this.loadingTab ||
563       this.getTabState(this.loadingTab) == this.STATE_LOADING);
565     // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
566     // the timer is set only when we're loading something.
567     this.assert(!this.loadTimer || this.loadingTab);
568     this.assert(!this.loadingTab || this.loadTimer);
570     // If we're switching to a non-remote tab, there's no need to wait
571     // for it to send layers to the compositor, as this will happen
572     // synchronously. Clearing this here means that in the next step,
573     // we can load the non-remote browser immediately.
574     if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
575       this.maybeClearLoadTimer("postActions");
576     }
578     // If we're not loading anything, try loading the requested tab.
579     let stateOfRequestedTab = this.getTabState(this.requestedTab);
580     if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
581         (stateOfRequestedTab == this.STATE_UNLOADED ||
582         stateOfRequestedTab == this.STATE_UNLOADING ||
583         this.warmingTabs.has(this.requestedTab))) {
584       this.assert(stateOfRequestedTab != this.STATE_LOADED);
585       this.loadRequestedTab();
586     }
588     let numBackgroundCached = 0;
589     for (let tab of this.tabLayerCache) {
590       if (tab !== this.requestedTab) {
591         numBackgroundCached++;
592       }
593     }
595     // See how many tabs still have work to do.
596     let numPending = 0;
597     let numWarming = 0;
598     for (let [tab, state] of this.tabState) {
599       // Skip print preview browsers since they shouldn't affect tab switching.
600       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
601         continue;
602       }
604       if (state == this.STATE_LOADED &&
605           tab !== this.requestedTab &&
606           !this.tabLayerCache.includes(tab)) {
607         numPending++;
609         if (tab !== this.visibleTab) {
610           numWarming++;
611         }
612       }
613       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
614         numPending++;
615       }
616     }
618     this.updateDisplay();
620     // It's possible for updateDisplay to trigger one of our own event
621     // handlers, which might cause finish() to already have been called.
622     // Check for that before calling finish() again.
623     if (!this.tabbrowser._switcher) {
624       return;
625     }
627     this.maybeFinishTabSwitch();
629     if (numBackgroundCached > 0) {
630       this.deactivateCachedBackgroundTabs();
631     }
633     if (numWarming > gTabWarmingMax) {
634       this.logState("Hit tabWarmingMax");
635       if (this.unloadTimer) {
636         this.clearTimer(this.unloadTimer);
637       }
638       this.unloadNonRequiredTabs();
639     }
641     if (numPending == 0) {
642       this.finish();
643     }
645     this.logState("done");
646   }
648   // Fires when we're ready to unload unused tabs.
649   onUnloadTimeout() {
650     this.logState("onUnloadTimeout");
651     this.preActions();
652     this.unloadTimer = null;
654     this.unloadNonRequiredTabs();
656     this.postActions();
657   }
659   deactivateCachedBackgroundTabs() {
660     for (let tab of this.tabLayerCache) {
661       if (tab !== this.requestedTab) {
662         let browser = tab.linkedBrowser;
663         browser.preserveLayers(true);
664         browser.docShellIsActive = false;
665       }
666     }
667   }
669   // If there are any non-visible and non-requested tabs in
670   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
671   // up the unloadTimer to run onUnloadTimeout if there are still
672   // tabs in the process of unloading.
673   unloadNonRequiredTabs() {
674     this.warmingTabs = new WeakSet();
675     let numPending = 0;
677     // Unload any tabs that can be unloaded.
678     for (let [tab, state] of this.tabState) {
679       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
680         continue;
681       }
683       let isInLayerCache = this.tabLayerCache.includes(tab);
685       if (state == this.STATE_LOADED &&
686           !this.maybeVisibleTabs.has(tab) &&
687           tab !== this.lastVisibleTab &&
688           tab !== this.loadingTab &&
689           tab !== this.requestedTab &&
690           !isInLayerCache) {
691         this.setTabState(tab, this.STATE_UNLOADING);
692       }
694       if (state != this.STATE_UNLOADED &&
695           tab !== this.requestedTab &&
696           !isInLayerCache) {
697         numPending++;
698       }
699     }
701     if (numPending) {
702       // Keep the timer going since there may be more tabs to unload.
703       this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
704     }
705   }
707   // Fires when an ongoing load has taken too long.
708   onLoadTimeout() {
709     this.logState("onLoadTimeout");
710     this.preActions();
711     this.maybeClearLoadTimer("onLoadTimeout");
712     this.postActions();
713   }
715   // Fires when the layers become available for a tab.
716   onLayersReady(browser) {
717     let tab = this.tabbrowser.getTabForBrowser(browser);
718     if (!tab) {
719       // We probably got a layer update from a tab that got before
720       // the switcher was created, or for browser that's not being
721       // tracked by the async tab switcher (like the preloaded about:newtab).
722       return;
723     }
725     this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
727     this.assert(this.getTabState(tab) == this.STATE_LOADING ||
728       this.getTabState(tab) == this.STATE_LOADED);
729     this.setTabState(tab, this.STATE_LOADED);
730     this.unwarmTab(tab);
732     if (this.loadingTab === tab) {
733       this.maybeClearLoadTimer("onLayersReady");
734     }
735   }
737   // Fires when we paint the screen. Any tab switches we initiated
738   // previously are done, so there's no need to keep the old layers
739   // around.
740   onPaint(event) {
741     if (this.switchPaintId != -1 &&
742         event.transactionId >= this.switchPaintId) {
743       if (TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)) {
744         let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
745         if (time != -1) {
746           TelemetryStopwatch.finish("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
747           this.log("DEBUG: tab switch time including compositing = " + time);
748         }
749       }
750       this.addMarker("AsyncTabSwitch:Composited");
751       this.switchPaintId = -1;
752     }
754     this.maybeVisibleTabs.clear();
755   }
757   // Called when we're done clearing the layers for a tab.
758   onLayersCleared(browser) {
759     let tab = this.tabbrowser.getTabForBrowser(browser);
760     if (tab) {
761       this.logState(`onLayersCleared(${tab._tPos})`);
762       this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
763         this.getTabState(tab) == this.STATE_UNLOADED);
764       this.setTabState(tab, this.STATE_UNLOADED);
765     }
766   }
768   // Called when a tab switches from remote to non-remote. In this case
769   // a MozLayerTreeReady notification that we requested may never fire,
770   // so we need to simulate it.
771   onRemotenessChange(tab) {
772     this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
773     if (!tab.linkedBrowser.isRemoteBrowser) {
774       if (this.getTabState(tab) == this.STATE_LOADING) {
775         this.onLayersReady(tab.linkedBrowser);
776       } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
777         this.onLayersCleared(tab.linkedBrowser);
778       }
779     } else if (this.getTabState(tab) == this.STATE_LOADED) {
780       // A tab just changed from non-remote to remote, which means
781       // that it's gone back into the STATE_LOADING state until
782       // it sends up a layer tree.
783       this.setTabState(tab, this.STATE_LOADING);
784     }
785   }
787   // Called when a tab has been removed, and the browser node is
788   // about to be removed from the DOM.
789   onTabRemoved(tab) {
790     if (this.lastVisibleTab == tab) {
791       // The browser that was being presented to the user is
792       // going to be removed during this tick of the event loop.
793       // This will cause us to show a tab spinner instead.
794       this.preActions();
795       this.lastVisibleTab = null;
796       this.postActions();
797     }
798   }
800   onSizeModeOrOcclusionStateChange() {
801     if (this.minimizedOrFullyOccluded) {
802       for (let [tab, state] of this.tabState) {
803         // Skip print preview browsers since they shouldn't affect tab switching.
804         if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
805           continue;
806         }
808         if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
809           this.setTabState(tab, this.STATE_UNLOADING);
810         }
811       }
812       this.maybeClearLoadTimer("onSizeModeOrOcc");
813     } else {
814       // We're no longer minimized or occluded. This means we might want
815       // to activate the current tab's docShell.
816       this.maybeActivateDocShell(this.tabbrowser.selectedTab);
817     }
818   }
820   onSwapDocShells(ourBrowser, otherBrowser) {
821     // This event fires before the swap. ourBrowser is from
822     // our window. We save the state of otherBrowser since ourBrowser
823     // needs to take on that state at the end of the swap.
825     let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
826     let otherState;
827     if (otherTabbrowser && otherTabbrowser._switcher) {
828       let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
829       let otherSwitcher = otherTabbrowser._switcher;
830       otherState = otherSwitcher.getTabState(otherTab);
831     } else {
832       otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
833     }
834     if (!this.swapMap) {
835       this.swapMap = new WeakMap();
836     }
837     this.swapMap.set(otherBrowser, {
838       state: otherState,
839     });
840   }
842   onEndSwapDocShells(ourBrowser, otherBrowser) {
843     // The swap has happened. We reset the loadingTab in
844     // case it has been swapped. We also set ourBrowser's state
845     // to whatever otherBrowser's state was before the swap.
847     // Clearing the load timer means that we will
848     // immediately display a spinner if ourBrowser isn't
849     // ready yet. Typically it will already be ready
850     // though. If it's not, we're probably in a new window,
851     // in which case we have no other tabs to display anyway.
852     this.maybeClearLoadTimer("onEndSwapDocShells");
854     let { state: otherState } = this.swapMap.get(otherBrowser);
856     this.swapMap.delete(otherBrowser);
858     let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
859     if (ourTab) {
860       this.setTabStateNoAction(ourTab, otherState);
861     }
862   }
864   shouldActivateDocShell(browser) {
865     let tab = this.tabbrowser.getTabForBrowser(browser);
866     let state = this.getTabState(tab);
867     return state == this.STATE_LOADING || state == this.STATE_LOADED;
868   }
870   activateBrowserForPrintPreview(browser) {
871     let tab = this.tabbrowser.getTabForBrowser(browser);
872     let state = this.getTabState(tab);
873     if (state != this.STATE_LOADING &&
874         state != this.STATE_LOADED) {
875       this.setTabState(tab, this.STATE_LOADING);
876       this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
877     }
878   }
880   canWarmTab(tab) {
881     if (!gTabWarmingEnabled) {
882       return false;
883     }
885     if (!tab) {
886       return false;
887     }
889     // If the tab is not yet inserted, closing, not remote,
890     // crashed, already visible, or already requested, warming
891     // up the tab makes no sense.
892     if (this.minimizedOrFullyOccluded ||
893         !tab.linkedPanel ||
894         tab.closing ||
895         !tab.linkedBrowser.isRemoteBrowser ||
896         !tab.linkedBrowser.frameLoader.tabParent) {
897       return false;
898     }
900     return true;
901   }
903   shouldWarmTab(tab) {
904     if (this.canWarmTab(tab)) {
905       // Tabs that are already in STATE_LOADING or STATE_LOADED
906       // have no need to be warmed up.
907       let state = this.getTabState(tab);
908       if (state === this.STATE_UNLOADING ||
909           state === this.STATE_UNLOADED) {
910         return true;
911       }
912     }
914     return false;
915   }
917   unwarmTab(tab) {
918     this.warmingTabs.delete(tab);
919   }
921   warmupTab(tab) {
922     if (!this.shouldWarmTab(tab)) {
923       return;
924     }
926     this.logState("warmupTab " + this.tinfo(tab));
928     this.warmingTabs.add(tab);
929     this.setTabState(tab, this.STATE_LOADING);
930     this.queueUnload(gTabWarmingUnloadDelayMs);
931   }
933   cleanUpTabAfterEviction(tab) {
934     this.assert(tab !== this.requestedTab);
935     let browser = tab.linkedBrowser;
936     if (browser) {
937       browser.preserveLayers(false);
938     }
939     this.setTabState(tab, this.STATE_UNLOADING);
940   }
942   evictOldestTabFromCache() {
943     let tab = this.tabLayerCache.shift();
944     this.cleanUpTabAfterEviction(tab);
945   }
947   maybePromoteTabInLayerCache(tab) {
948     if (gTabCacheSize > 1 &&
949         tab.linkedBrowser.isRemoteBrowser &&
950         tab.linkedBrowser.currentURI.spec != "about:blank") {
951       let tabIndex = this.tabLayerCache.indexOf(tab);
953       if (tabIndex != -1) {
954         this.tabLayerCache.splice(tabIndex, 1);
955       }
957       this.tabLayerCache.push(tab);
959       if (this.tabLayerCache.length > gTabCacheSize) {
960         this.evictOldestTabFromCache();
961       }
962     }
963   }
965   // Called when the user asks to switch to a given tab.
966   requestTab(tab) {
967     if (tab === this.requestedTab) {
968       return;
969     }
971     let tabState = this.getTabState(tab);
972     if (gTabWarmingEnabled) {
973       let warmingState = "disqualified";
975       if (this.canWarmTab(tab)) {
976         if (tabState == this.STATE_LOADING) {
977           warmingState = "stillLoading";
978         } else if (tabState == this.STATE_LOADED) {
979           warmingState = "loaded";
980         } else if (tabState == this.STATE_UNLOADING ||
981                    tabState == this.STATE_UNLOADED) {
982           // At this point, if the tab's browser was being inserted
983           // lazily, we never had a chance to warm it up, and unfortunately
984           // there's no great way to detect that case. Those cases will
985           // end up in the "notWarmed" bucket, along with legitimate cases
986           // where tabs could have been warmed but weren't.
987           warmingState = "notWarmed";
988         }
989       }
991       Services.telemetry
992         .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
993         .add(warmingState);
994     }
996     this.logState("requestTab " + this.tinfo(tab));
997     this.startTabSwitch();
999     let oldBrowser = this.requestedTab.linkedBrowser;
1000     oldBrowser.deprioritize();
1001     this.requestedTab = tab;
1002     if (tabState == this.STATE_LOADED) {
1003       this.maybeVisibleTabs.clear();
1004       if (tab.linkedBrowser.isRemoteBrowser) {
1005         tab.linkedBrowser.forceRepaint();
1006       }
1007     }
1009     tab.linkedBrowser.setAttribute("primary", "true");
1010     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1011       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1012     }
1013     this.lastPrimaryTab = tab;
1015     this.queueUnload(this.UNLOAD_DELAY);
1016   }
1018   queueUnload(unloadTimeout) {
1019     this.preActions();
1021     if (this.unloadTimer) {
1022       this.clearTimer(this.unloadTimer);
1023     }
1024     this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
1026     this.postActions();
1027   }
1029   handleEvent(event, delayed = false) {
1030     if (this._processing) {
1031       this.setTimer(() => this.handleEvent(event, true), 0);
1032       return;
1033     }
1034     if (delayed && this.tabbrowser._switcher != this) {
1035       // if we delayed processing this event, we might be out of date, in which
1036       // case we drop the delayed events
1037       return;
1038     }
1039     this._processing = true;
1040     this.preActions();
1042     switch (event.type) {
1043       case "MozLayerTreeReady":
1044         this.onLayersReady(event.originalTarget);
1045         break;
1046       case "MozAfterPaint":
1047         this.onPaint(event);
1048         break;
1049       case "MozLayerTreeCleared":
1050         this.onLayersCleared(event.originalTarget);
1051         break;
1052       case "TabRemotenessChange":
1053         this.onRemotenessChange(event.target);
1054         break;
1055       case "sizemodechange":
1056       case "occlusionstatechange":
1057         this.onSizeModeOrOcclusionStateChange();
1058         break;
1059       case "SwapDocShells":
1060         this.onSwapDocShells(event.originalTarget, event.detail);
1061         break;
1062       case "EndSwapDocShells":
1063         this.onEndSwapDocShells(event.originalTarget, event.detail);
1064         break;
1065     }
1067     this.postActions();
1068     this._processing = false;
1069   }
1071   /*
1072    * Telemetry and Profiler related helpers for recording tab switch
1073    * timing.
1074    */
1076   startTabSwitch() {
1077     TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1078     TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1080     if (TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)) {
1081       TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1082     }
1083     TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1084     this.addMarker("AsyncTabSwitch:Start");
1085     this.switchInProgress = true;
1086   }
1088   /**
1089    * Something has occurred that might mean that we've completed
1090    * the tab switch (layers are ready, paints are done, spinners
1091    * are hidden). This checks to make sure all conditions are
1092    * satisfied, and then records the tab switch as finished.
1093    */
1094   maybeFinishTabSwitch() {
1095     if (this.switchInProgress && this.requestedTab &&
1096         (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1097           this.requestedTab === this.blankTab)) {
1098       if (this.requestedTab !== this.blankTab) {
1099         this.maybePromoteTabInLayerCache(this.requestedTab);
1100       }
1102       // After this point the tab has switched from the content thread's point of view.
1103       // The changes will be visible after the next refresh driver tick + composite.
1104       let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1105       if (time != -1) {
1106         TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1107         this.log("DEBUG: tab switch time = " + time);
1108         this.addMarker("AsyncTabSwitch:Finish");
1109       }
1110       this.switchInProgress = false;
1111     }
1112   }
1114   spinnerDisplayed() {
1115     this.assert(!this.spinnerTab);
1116     let browser = this.requestedTab.linkedBrowser;
1117     this.assert(browser.isRemoteBrowser);
1118     TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1119     // We have a second, similar probe for capturing recordings of
1120     // when the spinner is displayed for very long periods.
1121     TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
1122     this.addMarker("AsyncTabSwitch:SpinnerShown");
1123     Services.telemetry
1124       .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1125       .add(this._loadTimerClearedBy);
1126     if (AppConstants.NIGHTLY_BUILD) {
1127       Services.obs.notifyObservers(null, "tabswitch-spinner");
1128     }
1129   }
1131   spinnerHidden() {
1132     this.assert(this.spinnerTab);
1133     this.log("DEBUG: spinner time = " +
1134       TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window));
1135     TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1136     TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
1137     this.addMarker("AsyncTabSwitch:SpinnerHidden");
1138     // we do not get a onPaint after displaying the spinner
1139     this._loadTimerClearedBy = "none";
1140   }
1142   addMarker(marker) {
1143     if (Services.profiler) {
1144       Services.profiler.AddMarker(marker);
1145     }
1146   }
1148   /*
1149    * Debug related logging for switcher.
1150    */
1151   logging() {
1152     if (this._useDumpForLogging)
1153       return true;
1154     if (this._logInit)
1155       return this._shouldLog;
1156     let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
1157     this._shouldLog = result;
1158     this._logInit = true;
1159     return this._shouldLog;
1160   }
1162   tinfo(tab) {
1163     if (tab) {
1164       return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1165     }
1166     return "null";
1167   }
1169   log(s) {
1170     if (!this.logging())
1171       return;
1172     if (this._useDumpForLogging) {
1173       dump(s + "\n");
1174     } else {
1175       Services.console.logStringMessage(s);
1176     }
1177   }
1179   logState(prefix) {
1180     if (!this.logging())
1181       return;
1183     let accum = prefix + " ";
1184     for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
1185       let tab = this.tabbrowser.tabs[i];
1186       let state = this.getTabState(tab);
1187       let isWarming = this.warmingTabs.has(tab);
1188       let isCached = this.tabLayerCache.includes(tab);
1189       let isClosing = tab.closing;
1190       let linkedBrowser = tab.linkedBrowser;
1191       let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1192       let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1194       accum += i + ":";
1195       if (tab === this.lastVisibleTab) accum += "V";
1196       if (tab === this.loadingTab) accum += "L";
1197       if (tab === this.requestedTab) accum += "R";
1198       if (tab === this.blankTab) accum += "B";
1200       let extraStates = "";
1201       if (isWarming) extraStates += "W";
1202       if (isCached) extraStates += "C";
1203       if (isClosing) extraStates += "X";
1204       if (isActive) extraStates += "A";
1205       if (isRendered) extraStates += "R";
1206       if (extraStates != "") {
1207         accum += `(${extraStates})`;
1208       }
1210       if (state == this.STATE_LOADED) accum += "(+)";
1211       if (state == this.STATE_LOADING) accum += "(+?)";
1212       if (state == this.STATE_UNLOADED) accum += "(-)";
1213       if (state == this.STATE_UNLOADING) accum += "(-?)";
1214       accum += " ";
1215     }
1217     accum += "cached: " + this.tabLayerCache.length;
1219     if (this._useDumpForLogging) {
1220       dump(accum + "\n");
1221     } else {
1222       Services.console.logStringMessage(accum);
1223     }
1224   }