Bug 1885580 - Add a MenuGroup component for the menu redesign r=android-reviewers,007
[gecko.git] / browser / modules / AsyncTabSwitcher.sys.mjs
blob9f4aa535e07adab496788165f4089be6732b1444
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 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
9 const lazy = {};
11 ChromeUtils.defineESModuleGetters(lazy, {
12   PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
13 });
15 XPCOMUtils.defineLazyPreferenceGetter(
16   lazy,
17   "gTabWarmingEnabled",
18   "browser.tabs.remote.warmup.enabled"
20 XPCOMUtils.defineLazyPreferenceGetter(
21   lazy,
22   "gTabWarmingMax",
23   "browser.tabs.remote.warmup.maxTabs"
25 XPCOMUtils.defineLazyPreferenceGetter(
26   lazy,
27   "gTabWarmingUnloadDelayMs",
28   "browser.tabs.remote.warmup.unloadDelayMs"
30 XPCOMUtils.defineLazyPreferenceGetter(
31   lazy,
32   "gTabCacheSize",
33   "browser.tabs.remote.tabCacheSize"
35 XPCOMUtils.defineLazyPreferenceGetter(
36   lazy,
37   "gTabUnloadDelay",
38   "browser.tabs.remote.unloadDelayMs",
39   300
42 /**
43  * The tab switcher is responsible for asynchronously switching
44  * tabs in e10s. It waits until the new tab is ready (i.e., the
45  * layer tree is available) before switching to it. Then it
46  * unloads the layer tree for the old tab.
47  *
48  * The tab switcher is a state machine. For each tab, it
49  * maintains state about whether the layer tree for the tab is
50  * available, being loaded, being unloaded, or unavailable. It
51  * also keeps track of the tab currently being displayed, the tab
52  * it's trying to load, and the tab the user has asked to switch
53  * to. The switcher object is created upon tab switch. It is
54  * released when there are no pending tabs to load or unload.
55  *
56  * The following general principles have guided the design:
57  *
58  * 1. We only request one layer tree at a time. If the user
59  * switches to a different tab while waiting, we don't request
60  * the new layer tree until the old tab has loaded or timed out.
61  *
62  * 2. If loading the layers for a tab times out, we show the
63  * spinner and possibly request the layer tree for another tab if
64  * the user has requested one.
65  *
66  * 3. We discard layer trees on a delay. This way, if the user is
67  * switching among the same tabs frequently, we don't continually
68  * load the same tabs.
69  *
70  * It's important that we always show either the spinner or a tab
71  * whose layers are available. Otherwise the compositor will draw
72  * an entirely black frame, which is very jarring. To ensure this
73  * never happens when switching away from a tab, we assume the
74  * old tab might still be drawn until a MozAfterPaint event
75  * occurs. Because layout and compositing happen asynchronously,
76  * we don't have any other way of knowing when the switch
77  * actually takes place. Therefore, we don't unload the old tab
78  * until the next MozAfterPaint event.
79  */
80 export class AsyncTabSwitcher {
81   constructor(tabbrowser) {
82     this.log("START");
84     // How long to wait for a tab's layers to load. After this
85     // time elapses, we're free to put up the spinner and start
86     // trying to load a different tab.
87     this.TAB_SWITCH_TIMEOUT = 400; // ms
89     // When the user hasn't switched tabs for this long, we unload
90     // layers for all tabs that aren't in use.
91     this.UNLOAD_DELAY = lazy.gTabUnloadDelay; // ms
93     // The next three tabs form the principal state variables.
94     // See the assertions in postActions for their invariants.
96     // Tab the user requested most recently.
97     this.requestedTab = tabbrowser.selectedTab;
99     // Tab we're currently trying to load.
100     this.loadingTab = null;
102     // We show this tab in case the requestedTab hasn't loaded yet.
103     this.lastVisibleTab = tabbrowser.selectedTab;
105     // Auxilliary state variables:
107     this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
108     this.spinnerTab = null; // Tab showing a spinner.
109     this.blankTab = null; // Tab showing blank.
110     this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
112     this.tabbrowser = tabbrowser;
113     this.window = tabbrowser.ownerGlobal;
114     this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
115     this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
117     // Map from tabs to STATE_* (below).
118     this.tabState = new Map();
120     // True if we're in the midst of switching tabs.
121     this.switchInProgress = false;
123     // Transaction id for the composite that will show the requested
124     // tab for the first tab after a tab switch.
125     // Set to -1 when we're not waiting for notification of a
126     // completed switch.
127     this.switchPaintId = -1;
129     // Set of tabs that might be visible right now. We maintain
130     // this set because we can't be sure when a tab is actually
131     // drawn. A tab is added to this set when we ask to make it
132     // visible. All tabs but the most recently shown tab are
133     // removed from the set upon MozAfterPaint.
134     this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
136     // This holds onto the set of tabs that we've been asked to warm up,
137     // and tabs are evicted once they're done loading or are unloaded.
138     this.warmingTabs = new WeakSet();
140     this.STATE_UNLOADED = 0;
141     this.STATE_LOADING = 1;
142     this.STATE_LOADED = 2;
143     this.STATE_UNLOADING = 3;
145     // re-entrancy guard:
146     this._processing = false;
148     // For telemetry, keeps track of what most recently cleared
149     // the loadTimer, which can tell us something about the cause
150     // of tab switch spinners.
151     this._loadTimerClearedBy = "none";
153     this._useDumpForLogging = false;
154     this._logInit = false;
155     this._logFlags = [];
157     this.window.addEventListener("MozAfterPaint", this);
158     this.window.addEventListener("MozLayerTreeReady", this);
159     this.window.addEventListener("MozLayerTreeCleared", this);
160     this.window.addEventListener("TabRemotenessChange", this);
161     this.window.addEventListener("SwapDocShells", this, true);
162     this.window.addEventListener("EndSwapDocShells", this, true);
163     this.window.document.addEventListener("visibilitychange", this);
165     let initialTab = this.requestedTab;
166     let initialBrowser = initialTab.linkedBrowser;
168     let tabIsLoaded =
169       !initialBrowser.isRemoteBrowser ||
170       initialBrowser.frameLoader.remoteTab?.hasLayers;
172     // If we minimized the window before the switcher was activated,
173     // we might have set  the preserveLayers flag for the current
174     // browser. Let's clear it.
175     initialBrowser.preserveLayers(false);
177     if (!this.windowHidden) {
178       this.log("Initial tab is loaded?: " + tabIsLoaded);
179       this.setTabState(
180         initialTab,
181         tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING
182       );
183     }
185     for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
186       let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
187       let state = ppBrowser.hasLayers ? this.STATE_LOADED : this.STATE_LOADING;
188       this.setTabState(ppTab, state);
189     }
190   }
192   destroy() {
193     if (this.unloadTimer) {
194       this.clearTimer(this.unloadTimer);
195       this.unloadTimer = null;
196     }
197     if (this.loadTimer) {
198       this.clearTimer(this.loadTimer);
199       this.loadTimer = null;
200     }
202     this.window.removeEventListener("MozAfterPaint", this);
203     this.window.removeEventListener("MozLayerTreeReady", this);
204     this.window.removeEventListener("MozLayerTreeCleared", this);
205     this.window.removeEventListener("TabRemotenessChange", this);
206     this.window.removeEventListener("SwapDocShells", this, true);
207     this.window.removeEventListener("EndSwapDocShells", this, true);
208     this.window.document.removeEventListener("visibilitychange", this);
210     this.tabbrowser._switcher = null;
211   }
213   // Wraps nsITimer. Must not use the vanilla setTimeout and
214   // clearTimeout, because they will be blocked by nsIPromptService
215   // dialogs.
216   setTimer(callback, timeout) {
217     let event = {
218       notify: callback,
219     };
221     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
222     timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
223     return timer;
224   }
226   clearTimer(timer) {
227     timer.cancel();
228   }
230   getTabState(tab) {
231     let state = this.tabState.get(tab);
233     // As an optimization, we lazily evaluate the state of tabs
234     // that we've never seen before. Once we've figured it out,
235     // we stash it in our state map.
236     if (state === undefined) {
237       state = this.STATE_UNLOADED;
239       if (tab && tab.linkedPanel) {
240         let b = tab.linkedBrowser;
241         if (b.renderLayers && b.hasLayers) {
242           state = this.STATE_LOADED;
243         } else if (b.renderLayers && !b.hasLayers) {
244           state = this.STATE_LOADING;
245         } else if (!b.renderLayers && b.hasLayers) {
246           state = this.STATE_UNLOADING;
247         }
248       }
250       this.setTabStateNoAction(tab, state);
251     }
253     return state;
254   }
256   setTabStateNoAction(tab, state) {
257     if (state == this.STATE_UNLOADED) {
258       this.tabState.delete(tab);
259     } else {
260       this.tabState.set(tab, state);
261     }
262   }
264   setTabState(tab, state) {
265     if (state == this.getTabState(tab)) {
266       return;
267     }
269     this.setTabStateNoAction(tab, state);
271     let browser = tab.linkedBrowser;
272     let { remoteTab } = browser.frameLoader;
273     if (state == this.STATE_LOADING) {
274       this.assert(!this.windowHidden);
276       // If we're not in the process of warming this tab, we
277       // don't need to delay activating its DocShell.
278       if (!this.warmingTabs.has(tab)) {
279         browser.docShellIsActive = true;
280       }
282       if (remoteTab) {
283         browser.renderLayers = true;
284         remoteTab.priorityHint = true;
285       }
286       if (browser.hasLayers) {
287         this.onLayersReady(browser);
288       }
289     } else if (state == this.STATE_UNLOADING) {
290       this.unwarmTab(tab);
291       // Setting the docShell to be inactive will also cause it
292       // to stop rendering layers.
293       browser.docShellIsActive = false;
294       if (remoteTab) {
295         remoteTab.priorityHint = false;
296       }
297       if (!browser.hasLayers) {
298         this.onLayersCleared(browser);
299       }
300     } else if (state == this.STATE_LOADED) {
301       this.maybeActivateDocShell(tab);
302     }
304     if (!tab.linkedBrowser.isRemoteBrowser) {
305       // setTabState is potentially re-entrant, so we must re-get the state for
306       // this assertion.
307       let nonRemoteState = this.getTabState(tab);
308       // Non-remote tabs can never stay in the STATE_LOADING
309       // or STATE_UNLOADING states. By the time this function
310       // exits, a non-remote tab must be in STATE_LOADED or
311       // STATE_UNLOADED, since the painting and the layer
312       // upload happen synchronously.
313       this.assert(
314         nonRemoteState == this.STATE_UNLOADED ||
315           nonRemoteState == this.STATE_LOADED
316       );
317     }
318   }
320   get windowHidden() {
321     return this.window.document.hidden;
322   }
324   get tabLayerCache() {
325     return this.tabbrowser._tabLayerCache;
326   }
328   finish() {
329     this.log("FINISH");
331     this.assert(this.tabbrowser._switcher);
332     this.assert(this.tabbrowser._switcher === this);
333     this.assert(!this.spinnerTab);
334     this.assert(!this.blankTab);
335     this.assert(!this.loadTimer);
336     this.assert(!this.loadingTab);
337     this.assert(this.lastVisibleTab === this.requestedTab);
338     this.assert(
339       this.windowHidden ||
340         this.getTabState(this.requestedTab) == this.STATE_LOADED
341     );
343     this.destroy();
345     this.window.document.commandDispatcher.unlock();
347     let event = new this.window.CustomEvent("TabSwitchDone", {
348       bubbles: true,
349       cancelable: true,
350     });
351     this.tabbrowser.dispatchEvent(event);
352   }
354   // This function is called after all the main state changes to
355   // make sure we display the right tab.
356   updateDisplay() {
357     let requestedTabState = this.getTabState(this.requestedTab);
358     let requestedBrowser = this.requestedTab.linkedBrowser;
360     // It is often more desirable to show a blank tab when appropriate than
361     // the tab switch spinner - especially since the spinner is usually
362     // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
363     // tab switch. We can hide this lag, and hide the time being spent
364     // constructing BrowserChild's, layer trees, etc, by showing a blank
365     // tab instead and focusing it immediately.
366     let shouldBeBlank = false;
367     if (requestedBrowser.isRemoteBrowser) {
368       // If a tab is remote and the window is not minimized, we can show a
369       // blank tab instead of a spinner in the following cases:
370       //
371       // 1. The tab has just crashed, and we haven't started showing the
372       //    tab crashed page yet (in this case, the RemoteTab is null)
373       // 2. The tab has never presented, and has not finished loading
374       //    a non-local-about: page.
375       //
376       // For (2), "finished loading a non-local-about: page" is
377       // determined by the busy state on the tab element and checking
378       // if the loaded URI is local.
379       let isBusy = this.requestedTab.hasAttribute("busy");
380       let isLocalAbout = requestedBrowser.currentURI.schemeIs("about");
381       let hasSufficientlyLoaded = !isBusy && !isLocalAbout;
383       let fl = requestedBrowser.frameLoader;
384       shouldBeBlank =
385         !this.windowHidden &&
386         (!fl.remoteTab ||
387           (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
389       if (this.logging()) {
390         let flag = shouldBeBlank ? "blank" : "nonblank";
391         this.addLogFlag(
392           flag,
393           this.windowHidden,
394           fl.remoteTab,
395           isBusy,
396           isLocalAbout,
397           fl.remoteTab ? fl.remoteTab.hasPresented : 0
398         );
399       }
400     }
402     if (requestedBrowser.isRemoteBrowser) {
403       this.addLogFlag("isRemote");
404     }
406     // Figure out which tab we actually want visible right now.
407     let showTab = null;
408     if (
409       requestedTabState != this.STATE_LOADED &&
410       this.lastVisibleTab &&
411       this.loadTimer &&
412       !shouldBeBlank
413     ) {
414       // If we can't show the requestedTab, and lastVisibleTab is
415       // available, show it.
416       showTab = this.lastVisibleTab;
417     } else {
418       // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
419       showTab = this.requestedTab;
420     }
422     // First, let's deal with blank tabs, which we show instead
423     // of the spinner when the tab is not currently set up
424     // properly in the content process.
425     if (!shouldBeBlank && this.blankTab) {
426       this.blankTab.linkedBrowser.removeAttribute("blank");
427       this.blankTab = null;
428     } else if (shouldBeBlank && this.blankTab !== showTab) {
429       if (this.blankTab) {
430         this.blankTab.linkedBrowser.removeAttribute("blank");
431       }
432       this.blankTab = showTab;
433       this.blankTab.linkedBrowser.setAttribute("blank", "true");
434     }
436     // Show or hide the spinner as needed.
437     let needSpinner =
438       this.getTabState(showTab) != this.STATE_LOADED &&
439       !this.windowHidden &&
440       !shouldBeBlank &&
441       !this.loadTimer;
443     if (!needSpinner && this.spinnerTab) {
444       this.noteSpinnerHidden();
445       this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
446       this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
447       this.spinnerTab = null;
448     } else if (needSpinner && this.spinnerTab !== showTab) {
449       if (this.spinnerTab) {
450         this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
451       } else {
452         this.noteSpinnerDisplayed();
453       }
454       this.spinnerTab = showTab;
455       this.tabbrowser.tabpanels.toggleAttribute("pendingpaint", true);
456       this.spinnerTab.linkedBrowser.toggleAttribute("pendingpaint", true);
457     }
459     // Switch to the tab we've decided to make visible.
460     if (this.visibleTab !== showTab) {
461       this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
462       this.visibleTab = showTab;
464       this.maybeVisibleTabs.add(showTab);
466       let tabpanels = this.tabbrowser.tabpanels;
467       let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
468       let index = Array.prototype.indexOf.call(tabpanels.children, showPanel);
469       if (index != -1) {
470         this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
471         tabpanels.updateSelectedIndex(index);
472         if (showTab === this.requestedTab) {
473           if (requestedTabState == this.STATE_LOADED) {
474             // The new tab will be made visible in the next paint, record the expected
475             // transaction id for that, and we'll mark when we get notified of its
476             // completion.
477             this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
478           } else {
479             this.noteMakingTabVisibleWithoutLayers();
480           }
482           this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
483           this.window.gURLBar.afterTabSwitchFocusChange();
484           this.maybeActivateDocShell(this.requestedTab);
485         }
486       }
488       // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
489       if (this.lastVisibleTab) {
490         this.lastVisibleTab._visuallySelected = false;
491       }
493       this.visibleTab._visuallySelected = true;
494     }
496     this.lastVisibleTab = this.visibleTab;
497   }
499   assert(cond) {
500     if (!cond) {
501       dump("Assertion failure\n" + Error().stack);
503       // Don't break a user's browser if an assertion fails.
504       if (AppConstants.DEBUG) {
505         throw new Error("Assertion failure");
506       }
507     }
508   }
510   maybeClearLoadTimer(caller) {
511     if (this.loadingTab) {
512       this._loadTimerClearedBy = caller;
513       this.loadingTab = null;
514       if (this.loadTimer) {
515         this.clearTimer(this.loadTimer);
516         this.loadTimer = null;
517       }
518     }
519   }
521   // We've decided to try to load requestedTab.
522   loadRequestedTab() {
523     this.assert(!this.loadTimer);
524     this.assert(!this.windowHidden);
526     // loadingTab can be non-null here if we timed out loading the current tab.
527     // In that case we just overwrite it with a different tab; it's had its chance.
528     this.loadingTab = this.requestedTab;
529     this.log("Loading tab " + this.tinfo(this.loadingTab));
531     this.loadTimer = this.setTimer(
532       () => this.handleEvent({ type: "loadTimeout" }),
533       this.TAB_SWITCH_TIMEOUT
534     );
535     this.setTabState(this.requestedTab, this.STATE_LOADING);
536   }
538   maybeActivateDocShell(tab) {
539     // If we've reached the point where the requested tab has entered
540     // the loaded state, but the DocShell is still not yet active, we
541     // should activate it.
542     let browser = tab.linkedBrowser;
543     let state = this.getTabState(tab);
544     let canCheckDocShellState =
545       !browser.mDestroyed &&
546       (browser.docShell || browser.frameLoader.remoteTab);
547     if (
548       tab == this.requestedTab &&
549       canCheckDocShellState &&
550       state == this.STATE_LOADED &&
551       !browser.docShellIsActive &&
552       !this.windowHidden
553     ) {
554       browser.docShellIsActive = true;
555       this.logState(
556         "Set requested tab docshell to active and preserveLayers to false"
557       );
558       // If we minimized the window before the switcher was activated,
559       // we might have set the preserveLayers flag for the current
560       // browser. Let's clear it.
561       browser.preserveLayers(false);
562     }
563   }
565   // This function runs before every event. It fixes up the state
566   // to account for closed tabs.
567   preActions() {
568     this.assert(this.tabbrowser._switcher);
569     this.assert(this.tabbrowser._switcher === this);
571     for (let i = 0; i < this.tabLayerCache.length; i++) {
572       let tab = this.tabLayerCache[i];
573       if (!tab.linkedBrowser) {
574         this.tabState.delete(tab);
575         this.tabLayerCache.splice(i, 1);
576         i--;
577       }
578     }
580     for (let [tab] of this.tabState) {
581       if (!tab.linkedBrowser) {
582         this.tabState.delete(tab);
583         this.unwarmTab(tab);
584       }
585     }
587     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
588       this.lastVisibleTab = null;
589     }
590     if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
591       this.lastPrimaryTab = null;
592     }
593     if (this.blankTab && !this.blankTab.linkedBrowser) {
594       this.blankTab = null;
595     }
596     if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
597       this.noteSpinnerHidden();
598       this.spinnerTab = null;
599     }
600     if (this.loadingTab && !this.loadingTab.linkedBrowser) {
601       this.maybeClearLoadTimer("preActions");
602     }
603   }
605   // This code runs after we've responded to an event or requested a new
606   // tab. It's expected that we've already updated all the principal
607   // state variables. This function takes care of updating any auxilliary
608   // state.
609   postActions(eventString) {
610     // Once we finish loading loadingTab, we null it out. So the state should
611     // always be LOADING.
612     this.assert(
613       !this.loadingTab ||
614         this.getTabState(this.loadingTab) == this.STATE_LOADING
615     );
617     // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
618     // the timer is set only when we're loading something.
619     this.assert(!this.loadTimer || this.loadingTab);
620     this.assert(!this.loadingTab || this.loadTimer);
622     // If we're switching to a non-remote tab, there's no need to wait
623     // for it to send layers to the compositor, as this will happen
624     // synchronously. Clearing this here means that in the next step,
625     // we can load the non-remote browser immediately.
626     if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
627       this.maybeClearLoadTimer("postActions");
628     }
630     // If we're not loading anything, try loading the requested tab.
631     let stateOfRequestedTab = this.getTabState(this.requestedTab);
632     if (
633       !this.loadTimer &&
634       !this.windowHidden &&
635       (stateOfRequestedTab == this.STATE_UNLOADED ||
636         stateOfRequestedTab == this.STATE_UNLOADING ||
637         this.warmingTabs.has(this.requestedTab))
638     ) {
639       this.assert(stateOfRequestedTab != this.STATE_LOADED);
640       this.loadRequestedTab();
641     }
643     let numBackgroundCached = 0;
644     for (let tab of this.tabLayerCache) {
645       if (tab !== this.requestedTab) {
646         numBackgroundCached++;
647       }
648     }
650     // See how many tabs still have work to do.
651     let numPending = 0;
652     let numWarming = 0;
653     for (let [tab, state] of this.tabState) {
654       if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
655         continue;
656       }
658       if (
659         state == this.STATE_LOADED &&
660         tab !== this.requestedTab &&
661         !this.tabLayerCache.includes(tab)
662       ) {
663         numPending++;
665         if (tab !== this.visibleTab) {
666           numWarming++;
667         }
668       }
669       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
670         numPending++;
671       }
672     }
674     this.updateDisplay();
676     // It's possible for updateDisplay to trigger one of our own event
677     // handlers, which might cause finish() to already have been called.
678     // Check for that before calling finish() again.
679     if (!this.tabbrowser._switcher) {
680       return;
681     }
683     this.maybeFinishTabSwitch();
685     if (numBackgroundCached > 0) {
686       this.deactivateCachedBackgroundTabs();
687     }
689     if (numWarming > lazy.gTabWarmingMax) {
690       this.logState("Hit tabWarmingMax");
691       if (this.unloadTimer) {
692         this.clearTimer(this.unloadTimer);
693       }
694       this.unloadNonRequiredTabs();
695     }
697     if (numPending == 0) {
698       this.finish();
699     }
701     this.logState("/" + eventString);
702   }
704   // Fires when we're ready to unload unused tabs.
705   onUnloadTimeout() {
706     this.unloadTimer = null;
707     this.unloadNonRequiredTabs();
708   }
710   deactivateCachedBackgroundTabs() {
711     for (let tab of this.tabLayerCache) {
712       if (tab !== this.requestedTab) {
713         let browser = tab.linkedBrowser;
714         browser.preserveLayers(true);
715         browser.docShellIsActive = false;
716       }
717     }
718   }
720   // If there are any non-visible and non-requested tabs in
721   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
722   // up the unloadTimer to run onUnloadTimeout if there are still
723   // tabs in the process of unloading.
724   unloadNonRequiredTabs() {
725     this.warmingTabs = new WeakSet();
726     let numPending = 0;
728     // Unload any tabs that can be unloaded.
729     for (let [tab, state] of this.tabState) {
730       if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
731         continue;
732       }
734       let isInLayerCache = this.tabLayerCache.includes(tab);
736       if (
737         state == this.STATE_LOADED &&
738         !this.maybeVisibleTabs.has(tab) &&
739         tab !== this.lastVisibleTab &&
740         tab !== this.loadingTab &&
741         tab !== this.requestedTab &&
742         !isInLayerCache
743       ) {
744         this.setTabState(tab, this.STATE_UNLOADING);
745       }
747       if (
748         state != this.STATE_UNLOADED &&
749         tab !== this.requestedTab &&
750         !isInLayerCache
751       ) {
752         numPending++;
753       }
754     }
756     if (numPending) {
757       // Keep the timer going since there may be more tabs to unload.
758       this.unloadTimer = this.setTimer(
759         () => this.handleEvent({ type: "unloadTimeout" }),
760         this.UNLOAD_DELAY
761       );
762     }
763   }
765   // Fires when an ongoing load has taken too long.
766   onLoadTimeout() {
767     this.maybeClearLoadTimer("onLoadTimeout");
768   }
770   // Fires when the layers become available for a tab.
771   onLayersReady(browser) {
772     let tab = this.tabbrowser.getTabForBrowser(browser);
773     if (!tab) {
774       // We probably got a layer update from a tab that got before
775       // the switcher was created, or for browser that's not being
776       // tracked by the async tab switcher (like the preloaded about:newtab).
777       return;
778     }
780     this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
781     this.assert(
782       this.getTabState(tab) == this.STATE_LOADING ||
783         this.getTabState(tab) == this.STATE_LOADED
784     );
785     this.setTabState(tab, this.STATE_LOADED);
786     this.unwarmTab(tab);
788     if (this.loadingTab === tab) {
789       this.maybeClearLoadTimer("onLayersReady");
790     }
791   }
793   // Fires when we paint the screen. Any tab switches we initiated
794   // previously are done, so there's no need to keep the old layers
795   // around.
796   onPaint(event) {
797     this.addLogFlag(
798       "onPaint",
799       this.switchPaintId != -1,
800       event.transactionId >= this.switchPaintId
801     );
802     this.notePaint(event);
803     this.maybeVisibleTabs.clear();
804   }
806   // Called when we're done clearing the layers for a tab.
807   onLayersCleared(browser) {
808     let tab = this.tabbrowser.getTabForBrowser(browser);
809     if (!tab) {
810       return;
811     }
812     this.logState(`onLayersCleared(${tab._tPos})`);
813     this.assert(
814       this.getTabState(tab) == this.STATE_UNLOADING ||
815         this.getTabState(tab) == this.STATE_UNLOADED
816     );
817     this.setTabState(tab, this.STATE_UNLOADED);
818   }
820   // Called when a tab switches from remote to non-remote. In this case
821   // a MozLayerTreeReady notification that we requested may never fire,
822   // so we need to simulate it.
823   onRemotenessChange(tab) {
824     this.logState(
825       `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`
826     );
827     if (!tab.linkedBrowser.isRemoteBrowser) {
828       if (this.getTabState(tab) == this.STATE_LOADING) {
829         this.onLayersReady(tab.linkedBrowser);
830       } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
831         this.onLayersCleared(tab.linkedBrowser);
832       }
833     } else if (this.getTabState(tab) == this.STATE_LOADED) {
834       // A tab just changed from non-remote to remote, which means
835       // that it's gone back into the STATE_LOADING state until
836       // it sends up a layer tree.
837       this.setTabState(tab, this.STATE_LOADING);
838     }
839   }
841   onTabRemoved(tab) {
842     if (this.lastVisibleTab == tab) {
843       this.handleEvent({ type: "tabRemoved", tab });
844     }
845   }
847   // Called when a tab has been removed, and the browser node is
848   // about to be removed from the DOM.
849   onTabRemovedImpl() {
850     this.lastVisibleTab = null;
851   }
853   onVisibilityChange() {
854     if (this.windowHidden) {
855       for (let [tab, state] of this.tabState) {
856         if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
857           continue;
858         }
860         if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
861           this.setTabState(tab, this.STATE_UNLOADING);
862         }
863       }
864       this.maybeClearLoadTimer("onSizeModeOrOcc");
865     } else {
866       // We're no longer minimized or occluded. This means we might want
867       // to activate the current tab's docShell.
868       this.maybeActivateDocShell(this.tabbrowser.selectedTab);
869     }
870   }
872   onSwapDocShells(ourBrowser, otherBrowser) {
873     // This event fires before the swap. ourBrowser is from
874     // our window. We save the state of otherBrowser since ourBrowser
875     // needs to take on that state at the end of the swap.
877     let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
878     let otherState;
879     if (otherTabbrowser && otherTabbrowser._switcher) {
880       let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
881       let otherSwitcher = otherTabbrowser._switcher;
882       otherState = otherSwitcher.getTabState(otherTab);
883     } else {
884       otherState = otherBrowser.docShellIsActive
885         ? this.STATE_LOADED
886         : this.STATE_UNLOADED;
887     }
888     if (!this.swapMap) {
889       this.swapMap = new WeakMap();
890     }
891     this.swapMap.set(otherBrowser, {
892       state: otherState,
893     });
894   }
896   onEndSwapDocShells(ourBrowser, otherBrowser) {
897     // The swap has happened. We reset the loadingTab in
898     // case it has been swapped. We also set ourBrowser's state
899     // to whatever otherBrowser's state was before the swap.
901     // Clearing the load timer means that we will
902     // immediately display a spinner if ourBrowser isn't
903     // ready yet. Typically it will already be ready
904     // though. If it's not, we're probably in a new window,
905     // in which case we have no other tabs to display anyway.
906     this.maybeClearLoadTimer("onEndSwapDocShells");
908     let { state: otherState } = this.swapMap.get(otherBrowser);
910     this.swapMap.delete(otherBrowser);
912     let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
913     if (ourTab) {
914       this.setTabStateNoAction(ourTab, otherState);
915     }
916   }
918   /**
919    * Check if the browser should be deactivated. If the browser is a print preivew or
920    * PiP browser then we won't deactive it.
921    * @param browser The browser to check if it should be deactivated
922    * @returns false if a print preview or PiP browser else true
923    */
924   shouldDeactivateDocShell(browser) {
925     return !(
926       this.tabbrowser._printPreviewBrowsers.has(browser) ||
927       lazy.PictureInPicture.isOriginatingBrowser(browser)
928     );
929   }
931   shouldActivateDocShell(browser) {
932     let tab = this.tabbrowser.getTabForBrowser(browser);
933     let state = this.getTabState(tab);
934     return state == this.STATE_LOADING || state == this.STATE_LOADED;
935   }
937   activateBrowserForPrintPreview(browser) {
938     let tab = this.tabbrowser.getTabForBrowser(browser);
939     let state = this.getTabState(tab);
940     if (state != this.STATE_LOADING && state != this.STATE_LOADED) {
941       this.setTabState(tab, this.STATE_LOADING);
942       this.logState(
943         "Activated browser " + this.tinfo(tab) + " for print preview"
944       );
945     }
946   }
948   canWarmTab(tab) {
949     if (!lazy.gTabWarmingEnabled) {
950       return false;
951     }
953     if (!tab) {
954       return false;
955     }
957     // If the tab is not yet inserted, closing, not remote,
958     // crashed, already visible, or already requested, warming
959     // up the tab makes no sense.
960     if (
961       this.windowHidden ||
962       !tab.linkedPanel ||
963       tab.closing ||
964       !tab.linkedBrowser.isRemoteBrowser ||
965       !tab.linkedBrowser.frameLoader.remoteTab
966     ) {
967       return false;
968     }
970     return true;
971   }
973   shouldWarmTab(tab) {
974     if (this.canWarmTab(tab)) {
975       // Tabs that are already in STATE_LOADING or STATE_LOADED
976       // have no need to be warmed up.
977       let state = this.getTabState(tab);
978       if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) {
979         return true;
980       }
981     }
983     return false;
984   }
986   unwarmTab(tab) {
987     this.warmingTabs.delete(tab);
988   }
990   warmupTab(tab) {
991     if (!this.shouldWarmTab(tab)) {
992       return;
993     }
995     this.logState("warmupTab " + this.tinfo(tab));
997     this.warmingTabs.add(tab);
998     this.setTabState(tab, this.STATE_LOADING);
999     this.queueUnload(lazy.gTabWarmingUnloadDelayMs);
1000   }
1002   cleanUpTabAfterEviction(tab) {
1003     this.assert(tab !== this.requestedTab);
1004     let browser = tab.linkedBrowser;
1005     if (browser) {
1006       browser.preserveLayers(false);
1007     }
1008     this.setTabState(tab, this.STATE_UNLOADING);
1009   }
1011   evictOldestTabFromCache() {
1012     let tab = this.tabLayerCache.shift();
1013     this.cleanUpTabAfterEviction(tab);
1014   }
1016   maybePromoteTabInLayerCache(tab) {
1017     if (
1018       lazy.gTabCacheSize > 1 &&
1019       tab.linkedBrowser.isRemoteBrowser &&
1020       tab.linkedBrowser.currentURI.spec != "about:blank"
1021     ) {
1022       let tabIndex = this.tabLayerCache.indexOf(tab);
1024       if (tabIndex != -1) {
1025         this.tabLayerCache.splice(tabIndex, 1);
1026       }
1028       this.tabLayerCache.push(tab);
1030       if (this.tabLayerCache.length > lazy.gTabCacheSize) {
1031         this.evictOldestTabFromCache();
1032       }
1033     }
1034   }
1036   // Called when the user asks to switch to a given tab.
1037   requestTab(tab) {
1038     if (tab === this.requestedTab) {
1039       return;
1040     }
1042     let tabState = this.getTabState(tab);
1043     this.noteTabRequested(tab, tabState);
1045     this.logState("requestTab " + this.tinfo(tab));
1046     this.startTabSwitch();
1048     let oldBrowser = this.requestedTab.linkedBrowser;
1049     oldBrowser.deprioritize();
1050     this.requestedTab = tab;
1051     if (tabState == this.STATE_LOADED) {
1052       this.maybeVisibleTabs.clear();
1053     }
1055     tab.linkedBrowser.setAttribute("primary", "true");
1056     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1057       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1058     }
1059     this.lastPrimaryTab = tab;
1061     this.queueUnload(this.UNLOAD_DELAY);
1062   }
1064   queueUnload(unloadTimeout) {
1065     this.handleEvent({ type: "queueUnload", unloadTimeout });
1066   }
1068   onQueueUnload(unloadTimeout) {
1069     if (this.unloadTimer) {
1070       this.clearTimer(this.unloadTimer);
1071     }
1072     this.unloadTimer = this.setTimer(
1073       () => this.handleEvent({ type: "unloadTimeout" }),
1074       unloadTimeout
1075     );
1076   }
1078   handleEvent(event, delayed = false) {
1079     if (this._processing) {
1080       this.setTimer(() => this.handleEvent(event, true), 0);
1081       return;
1082     }
1083     if (delayed && this.tabbrowser._switcher != this) {
1084       // if we delayed processing this event, we might be out of date, in which
1085       // case we drop the delayed events
1086       return;
1087     }
1088     this._processing = true;
1089     try {
1090       this.preActions();
1092       switch (event.type) {
1093         case "queueUnload":
1094           this.onQueueUnload(event.unloadTimeout);
1095           break;
1096         case "unloadTimeout":
1097           this.onUnloadTimeout();
1098           break;
1099         case "loadTimeout":
1100           this.onLoadTimeout();
1101           break;
1102         case "tabRemoved":
1103           this.onTabRemovedImpl(event.tab);
1104           break;
1105         case "MozLayerTreeReady": {
1106           let browser = event.originalTarget;
1107           if (!browser.renderLayers) {
1108             // By the time we handle this event, it's possible that something
1109             // else has already set renderLayers to false, in which case this
1110             // event is stale and we can safely ignore it.
1111             return;
1112           }
1113           this.onLayersReady(browser);
1114           break;
1115         }
1116         case "MozAfterPaint":
1117           this.onPaint(event);
1118           break;
1119         case "MozLayerTreeCleared": {
1120           let browser = event.originalTarget;
1121           if (browser.renderLayers) {
1122             // By the time we handle this event, it's possible that something
1123             // else has already set renderLayers to true, in which case this
1124             // event is stale and we can safely ignore it.
1125             return;
1126           }
1127           this.onLayersCleared(browser);
1128           break;
1129         }
1130         case "TabRemotenessChange":
1131           this.onRemotenessChange(event.target);
1132           break;
1133         case "visibilitychange":
1134           this.onVisibilityChange();
1135           break;
1136         case "SwapDocShells":
1137           this.onSwapDocShells(event.originalTarget, event.detail);
1138           break;
1139         case "EndSwapDocShells":
1140           this.onEndSwapDocShells(event.originalTarget, event.detail);
1141           break;
1142       }
1144       this.postActions(event.type);
1145     } finally {
1146       this._processing = false;
1147     }
1148   }
1150   /*
1151    * Telemetry and Profiler related helpers for recording tab switch
1152    * timing.
1153    */
1155   startTabSwitch() {
1156     this.noteStartTabSwitch();
1157     this.switchInProgress = true;
1158   }
1160   /**
1161    * Something has occurred that might mean that we've completed
1162    * the tab switch (layers are ready, paints are done, spinners
1163    * are hidden). This checks to make sure all conditions are
1164    * satisfied, and then records the tab switch as finished.
1165    */
1166   maybeFinishTabSwitch() {
1167     if (
1168       this.switchInProgress &&
1169       this.requestedTab &&
1170       (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1171         this.requestedTab === this.blankTab)
1172     ) {
1173       if (this.requestedTab !== this.blankTab) {
1174         this.maybePromoteTabInLayerCache(this.requestedTab);
1175       }
1177       this.noteFinishTabSwitch();
1178       this.switchInProgress = false;
1180       let event = new this.window.CustomEvent("TabSwitched", {
1181         bubbles: true,
1182         detail: {
1183           tab: this.requestedTab,
1184         },
1185       });
1186       this.tabbrowser.dispatchEvent(event);
1187     }
1188   }
1190   /*
1191    * Debug related logging for switcher.
1192    */
1193   logging() {
1194     if (this._useDumpForLogging) {
1195       return true;
1196     }
1197     if (this._logInit) {
1198       return this._shouldLog;
1199     }
1200     let result = Services.prefs.getBoolPref(
1201       "browser.tabs.remote.logSwitchTiming",
1202       false
1203     );
1204     this._shouldLog = result;
1205     this._logInit = true;
1206     return this._shouldLog;
1207   }
1209   tinfo(tab) {
1210     if (tab) {
1211       return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1212     }
1213     return "null";
1214   }
1216   log(s) {
1217     if (!this.logging()) {
1218       return;
1219     }
1220     if (this._useDumpForLogging) {
1221       dump(s + "\n");
1222     } else {
1223       Services.console.logStringMessage(s);
1224     }
1225   }
1227   addLogFlag(flag, ...subFlags) {
1228     if (this.logging()) {
1229       if (subFlags.length) {
1230         flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
1231       }
1232       this._logFlags.push(flag);
1233     }
1234   }
1236   logState(suffix) {
1237     if (!this.logging()) {
1238       return;
1239     }
1241     let getTabString = tab => {
1242       let tabString = "";
1244       let state = this.getTabState(tab);
1245       let isWarming = this.warmingTabs.has(tab);
1246       let isCached = this.tabLayerCache.includes(tab);
1247       let isClosing = tab.closing;
1248       let linkedBrowser = tab.linkedBrowser;
1249       let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1250       let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1251       let isPiP =
1252         linkedBrowser &&
1253         lazy.PictureInPicture.isOriginatingBrowser(linkedBrowser);
1255       if (tab === this.lastVisibleTab) {
1256         tabString += "V";
1257       }
1258       if (tab === this.loadingTab) {
1259         tabString += "L";
1260       }
1261       if (tab === this.requestedTab) {
1262         tabString += "R";
1263       }
1264       if (tab === this.blankTab) {
1265         tabString += "B";
1266       }
1267       if (this.maybeVisibleTabs.has(tab)) {
1268         tabString += "M";
1269       }
1271       let extraStates = "";
1272       if (isWarming) {
1273         extraStates += "W";
1274       }
1275       if (isCached) {
1276         extraStates += "C";
1277       }
1278       if (isClosing) {
1279         extraStates += "X";
1280       }
1281       if (isActive) {
1282         extraStates += "A";
1283       }
1284       if (isRendered) {
1285         extraStates += "R";
1286       }
1287       if (isPiP) {
1288         extraStates += "P";
1289       }
1290       if (extraStates != "") {
1291         tabString += `(${extraStates})`;
1292       }
1294       switch (state) {
1295         case this.STATE_LOADED: {
1296           tabString += "(loaded)";
1297           break;
1298         }
1299         case this.STATE_LOADING: {
1300           tabString += "(loading)";
1301           break;
1302         }
1303         case this.STATE_UNLOADING: {
1304           tabString += "(unloading)";
1305           break;
1306         }
1307         case this.STATE_UNLOADED: {
1308           tabString += "(unloaded)";
1309           break;
1310         }
1311       }
1313       return tabString;
1314     };
1316     let accum = "";
1318     // This is a bit tricky to read, but what we're doing here is collapsing
1319     // identical tab states down to make the overal string shorter and easier
1320     // to read, and we move all simply unloaded tabs to the back of the list.
1321     // I.e., we turn
1322     //   "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
1323     // into
1324     //   "3:(loaded) 0...2:(unloaded)"
1325     let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
1326     let lastMatch = -1;
1327     let unloadedTabsStrings = [];
1328     for (let i = 0; i <= tabStrings.length; i++) {
1329       if (i > 0) {
1330         if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
1331           continue;
1332         }
1334         if (tabStrings[lastMatch] == "(unloaded)") {
1335           if (lastMatch == i - 1) {
1336             unloadedTabsStrings.push(lastMatch.toString());
1337           } else {
1338             unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
1339           }
1340         } else if (lastMatch == i - 1) {
1341           accum += `${lastMatch}:${tabStrings[lastMatch]} `;
1342         } else {
1343           accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
1344         }
1345       }
1347       lastMatch = i;
1348     }
1350     if (unloadedTabsStrings.length) {
1351       accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
1352     }
1354     accum += "cached: " + this.tabLayerCache.length + " ";
1356     if (this._logFlags.length) {
1357       accum += `[${this._logFlags.join(",")}] `;
1358       this._logFlags = [];
1359     }
1361     // It can be annoying to read through the entirety of a log string just
1362     // to check if something changed or not. So if we can tell that nothing
1363     // changed, just write "unchanged" to save the reader's time.
1364     let logString;
1365     if (this._lastLogString == accum) {
1366       accum = "unchanged";
1367     } else {
1368       this._lastLogString = accum;
1369     }
1370     logString = `ATS: ${accum}{${suffix}}`;
1372     if (this._useDumpForLogging) {
1373       dump(logString + "\n");
1374     } else {
1375       Services.console.logStringMessage(logString);
1376     }
1377   }
1379   noteMakingTabVisibleWithoutLayers() {
1380     // We're making the tab visible even though we haven't yet got layers for it.
1381     // It's hard to know which composite the layers will first be available in (and
1382     // the parent process might not even get MozAfterPaint delivered for it), so just
1383     // give up measuring this for now. :(
1384     Glean.performanceInteraction.tabSwitchComposite.cancel(
1385       this._tabswitchTimerId
1386     );
1387     this._tabswitchTimerId = null;
1388   }
1390   notePaint(event) {
1391     if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
1392       if (this._tabswitchTimerId) {
1393         Glean.performanceInteraction.tabSwitchComposite.stopAndAccumulate(
1394           this._tabswitchTimerId
1395         );
1396         this._tabswitchTimerId = null;
1397       }
1398       let { innerWindowId } = this.window.windowGlobalChild;
1399       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited", {
1400         innerWindowId,
1401       });
1402       this.switchPaintId = -1;
1403     }
1404   }
1406   noteTabRequested(tab, tabState) {
1407     if (lazy.gTabWarmingEnabled) {
1408       let warmingState = "disqualified";
1410       if (this.canWarmTab(tab)) {
1411         if (tabState == this.STATE_LOADING) {
1412           warmingState = "stillLoading";
1413         } else if (tabState == this.STATE_LOADED) {
1414           warmingState = "loaded";
1415         } else if (
1416           tabState == this.STATE_UNLOADING ||
1417           tabState == this.STATE_UNLOADED
1418         ) {
1419           // At this point, if the tab's browser was being inserted
1420           // lazily, we never had a chance to warm it up, and unfortunately
1421           // there's no great way to detect that case. Those cases will
1422           // end up in the "notWarmed" bucket, along with legitimate cases
1423           // where tabs could have been warmed but weren't.
1424           warmingState = "notWarmed";
1425         }
1426       }
1428       Services.telemetry
1429         .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
1430         .add(warmingState);
1431     }
1432   }
1434   noteStartTabSwitch() {
1435     TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1436     TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1438     if (this._tabswitchTimerId) {
1439       Glean.performanceInteraction.tabSwitchComposite.cancel(
1440         this._tabswitchTimerId
1441       );
1442     }
1443     this._tabswitchTimerId =
1444       Glean.performanceInteraction.tabSwitchComposite.start();
1445     let { innerWindowId } = this.window.windowGlobalChild;
1446     ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start", { innerWindowId });
1447   }
1449   noteFinishTabSwitch() {
1450     // After this point the tab has switched from the content thread's point of view.
1451     // The changes will be visible after the next refresh driver tick + composite.
1452     let time = TelemetryStopwatch.timeElapsed(
1453       "FX_TAB_SWITCH_TOTAL_E10S_MS",
1454       this.window
1455     );
1456     if (time != -1) {
1457       TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1458       this.log("DEBUG: tab switch time = " + time);
1459       let { innerWindowId } = this.window.windowGlobalChild;
1460       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish", { innerWindowId });
1461     }
1462   }
1464   noteSpinnerDisplayed() {
1465     this.assert(!this.spinnerTab);
1466     let browser = this.requestedTab.linkedBrowser;
1467     this.assert(browser.isRemoteBrowser);
1468     TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1469     // We have a second, similar probe for capturing recordings of
1470     // when the spinner is displayed for very long periods.
1471     TelemetryStopwatch.start(
1472       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1473       this.window
1474     );
1475     let { innerWindowId } = this.window.windowGlobalChild;
1476     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown", {
1477       innerWindowId,
1478     });
1479     Services.telemetry
1480       .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1481       .add(this._loadTimerClearedBy);
1482     if (AppConstants.NIGHTLY_BUILD) {
1483       Services.obs.notifyObservers(null, "tabswitch-spinner");
1484     }
1485   }
1487   noteSpinnerHidden() {
1488     this.assert(this.spinnerTab);
1489     this.log(
1490       "DEBUG: spinner time = " +
1491         TelemetryStopwatch.timeElapsed(
1492           "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
1493           this.window
1494         )
1495     );
1496     TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1497     TelemetryStopwatch.finish(
1498       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1499       this.window
1500     );
1501     let { innerWindowId } = this.window.windowGlobalChild;
1502     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden", {
1503       innerWindowId,
1504     });
1505     // we do not get a onPaint after displaying the spinner
1506     this._loadTimerClearedBy = "none";
1507   }