Bug 1675375 Part 7: Update expectations in helper_hittest_clippath.html. r=botond
[gecko.git] / browser / modules / AsyncTabSwitcher.jsm
blobb5a6e32c9f1411c07aad60eb685540adfedf7a04
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(
11   "resource://gre/modules/XPCOMUtils.jsm"
13 XPCOMUtils.defineLazyModuleGetters(this, {
14   AppConstants: "resource://gre/modules/AppConstants.jsm",
15   Services: "resource://gre/modules/Services.jsm",
16 });
18 XPCOMUtils.defineLazyPreferenceGetter(
19   this,
20   "gTabWarmingEnabled",
21   "browser.tabs.remote.warmup.enabled"
23 XPCOMUtils.defineLazyPreferenceGetter(
24   this,
25   "gTabWarmingMax",
26   "browser.tabs.remote.warmup.maxTabs"
28 XPCOMUtils.defineLazyPreferenceGetter(
29   this,
30   "gTabWarmingUnloadDelayMs",
31   "browser.tabs.remote.warmup.unloadDelayMs"
33 XPCOMUtils.defineLazyPreferenceGetter(
34   this,
35   "gTabCacheSize",
36   "browser.tabs.remote.tabCacheSize"
39 /**
40  * The tab switcher is responsible for asynchronously switching
41  * tabs in e10s. It waits until the new tab is ready (i.e., the
42  * layer tree is available) before switching to it. Then it
43  * unloads the layer tree for the old tab.
44  *
45  * The tab switcher is a state machine. For each tab, it
46  * maintains state about whether the layer tree for the tab is
47  * available, being loaded, being unloaded, or unavailable. It
48  * also keeps track of the tab currently being displayed, the tab
49  * it's trying to load, and the tab the user has asked to switch
50  * to. The switcher object is created upon tab switch. It is
51  * released when there are no pending tabs to load or unload.
52  *
53  * The following general principles have guided the design:
54  *
55  * 1. We only request one layer tree at a time. If the user
56  * switches to a different tab while waiting, we don't request
57  * the new layer tree until the old tab has loaded or timed out.
58  *
59  * 2. If loading the layers for a tab times out, we show the
60  * spinner and possibly request the layer tree for another tab if
61  * the user has requested one.
62  *
63  * 3. We discard layer trees on a delay. This way, if the user is
64  * switching among the same tabs frequently, we don't continually
65  * load the same tabs.
66  *
67  * It's important that we always show either the spinner or a tab
68  * whose layers are available. Otherwise the compositor will draw
69  * an entirely black frame, which is very jarring. To ensure this
70  * never happens when switching away from a tab, we assume the
71  * old tab might still be drawn until a MozAfterPaint event
72  * occurs. Because layout and compositing happen asynchronously,
73  * we don't have any other way of knowing when the switch
74  * actually takes place. Therefore, we don't unload the old tab
75  * until the next MozAfterPaint event.
76  */
77 class AsyncTabSwitcher {
78   constructor(tabbrowser) {
79     this.log("START");
81     // How long to wait for a tab's layers to load. After this
82     // time elapses, we're free to put up the spinner and start
83     // trying to load a different tab.
84     this.TAB_SWITCH_TIMEOUT = 400; // ms
86     // When the user hasn't switched tabs for this long, we unload
87     // layers for all tabs that aren't in use.
88     this.UNLOAD_DELAY = 300; // ms
90     // The next three tabs form the principal state variables.
91     // See the assertions in postActions for their invariants.
93     // Tab the user requested most recently.
94     this.requestedTab = tabbrowser.selectedTab;
96     // Tab we're currently trying to load.
97     this.loadingTab = null;
99     // We show this tab in case the requestedTab hasn't loaded yet.
100     this.lastVisibleTab = tabbrowser.selectedTab;
102     // Auxilliary state variables:
104     this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
105     this.spinnerTab = null; // Tab showing a spinner.
106     this.blankTab = null; // Tab showing blank.
107     this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
109     this.tabbrowser = tabbrowser;
110     this.window = tabbrowser.ownerGlobal;
111     this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
112     this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
114     // Map from tabs to STATE_* (below).
115     this.tabState = new Map();
117     // True if we're in the midst of switching tabs.
118     this.switchInProgress = false;
120     // Transaction id for the composite that will show the requested
121     // tab for the first tab after a tab switch.
122     // Set to -1 when we're not waiting for notification of a
123     // completed switch.
124     this.switchPaintId = -1;
126     // Set of tabs that might be visible right now. We maintain
127     // this set because we can't be sure when a tab is actually
128     // drawn. A tab is added to this set when we ask to make it
129     // visible. All tabs but the most recently shown tab are
130     // removed from the set upon MozAfterPaint.
131     this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
133     // This holds onto the set of tabs that we've been asked to warm up,
134     // and tabs are evicted once they're done loading or are unloaded.
135     this.warmingTabs = new WeakSet();
137     this.STATE_UNLOADED = 0;
138     this.STATE_LOADING = 1;
139     this.STATE_LOADED = 2;
140     this.STATE_UNLOADING = 3;
142     // re-entrancy guard:
143     this._processing = false;
145     // For telemetry, keeps track of what most recently cleared
146     // the loadTimer, which can tell us something about the cause
147     // of tab switch spinners.
148     this._loadTimerClearedBy = "none";
150     this._useDumpForLogging = false;
151     this._logInit = false;
152     this._logFlags = [];
154     this.window.addEventListener("MozAfterPaint", this);
155     this.window.addEventListener("MozLayerTreeReady", this);
156     this.window.addEventListener("MozLayerTreeCleared", this);
157     this.window.addEventListener("TabRemotenessChange", this);
158     this.window.addEventListener("sizemodechange", this);
159     this.window.addEventListener("occlusionstatechange", this);
160     this.window.addEventListener("SwapDocShells", this, true);
161     this.window.addEventListener("EndSwapDocShells", this, true);
163     let initialTab = this.requestedTab;
164     let initialBrowser = initialTab.linkedBrowser;
166     let tabIsLoaded =
167       !initialBrowser.isRemoteBrowser ||
168       initialBrowser.frameLoader.remoteTab.hasLayers;
170     // If we minimized the window before the switcher was activated,
171     // we might have set  the preserveLayers flag for the current
172     // browser. Let's clear it.
173     initialBrowser.preserveLayers(false);
175     if (!this.minimizedOrFullyOccluded) {
176       this.log("Initial tab is loaded?: " + tabIsLoaded);
177       this.setTabState(
178         initialTab,
179         tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING
180       );
181     }
183     for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
184       let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
185       let state = ppBrowser.hasLayers ? this.STATE_LOADED : this.STATE_LOADING;
186       this.setTabState(ppTab, state);
187     }
188   }
190   destroy() {
191     if (this.unloadTimer) {
192       this.clearTimer(this.unloadTimer);
193       this.unloadTimer = null;
194     }
195     if (this.loadTimer) {
196       this.clearTimer(this.loadTimer);
197       this.loadTimer = null;
198     }
200     this.window.removeEventListener("MozAfterPaint", this);
201     this.window.removeEventListener("MozLayerTreeReady", this);
202     this.window.removeEventListener("MozLayerTreeCleared", this);
203     this.window.removeEventListener("TabRemotenessChange", this);
204     this.window.removeEventListener("sizemodechange", this);
205     this.window.removeEventListener("occlusionstatechange", this);
206     this.window.removeEventListener("SwapDocShells", this, true);
207     this.window.removeEventListener("EndSwapDocShells", this, true);
209     this.tabbrowser._switcher = null;
210   }
212   // Wraps nsITimer. Must not use the vanilla setTimeout and
213   // clearTimeout, because they will be blocked by nsIPromptService
214   // dialogs.
215   setTimer(callback, timeout) {
216     let event = {
217       notify: callback,
218     };
220     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
221     timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
222     return timer;
223   }
225   clearTimer(timer) {
226     timer.cancel();
227   }
229   getTabState(tab) {
230     let state = this.tabState.get(tab);
232     // As an optimization, we lazily evaluate the state of tabs
233     // that we've never seen before. Once we've figured it out,
234     // we stash it in our state map.
235     if (state === undefined) {
236       state = this.STATE_UNLOADED;
238       if (tab && tab.linkedPanel) {
239         let b = tab.linkedBrowser;
240         if (b.renderLayers && b.hasLayers) {
241           state = this.STATE_LOADED;
242         } else if (b.renderLayers && !b.hasLayers) {
243           state = this.STATE_LOADING;
244         } else if (!b.renderLayers && b.hasLayers) {
245           state = this.STATE_UNLOADING;
246         }
247       }
249       this.setTabStateNoAction(tab, state);
250     }
252     return state;
253   }
255   setTabStateNoAction(tab, state) {
256     if (state == this.STATE_UNLOADED) {
257       this.tabState.delete(tab);
258     } else {
259       this.tabState.set(tab, state);
260     }
261   }
263   setTabState(tab, state) {
264     if (state == this.getTabState(tab)) {
265       return;
266     }
268     this.setTabStateNoAction(tab, state);
270     let browser = tab.linkedBrowser;
271     let { remoteTab } = browser.frameLoader;
272     if (state == this.STATE_LOADING) {
273       this.assert(!this.minimizedOrFullyOccluded);
275       // If we're not in the process of warming this tab, we
276       // don't need to delay activating its DocShell.
277       if (!this.warmingTabs.has(tab)) {
278         browser.docShellIsActive = true;
279       }
281       if (remoteTab) {
282         browser.renderLayers = true;
283       } else {
284         this.onLayersReady(browser);
285       }
286     } else if (state == this.STATE_UNLOADING) {
287       this.unwarmTab(tab);
288       // Setting the docShell to be inactive will also cause it
289       // to stop rendering layers.
290       browser.docShellIsActive = false;
291       if (!remoteTab) {
292         this.onLayersCleared(browser);
293       }
294     } else if (state == this.STATE_LOADED) {
295       this.maybeActivateDocShell(tab);
296     }
298     if (!tab.linkedBrowser.isRemoteBrowser) {
299       // setTabState is potentially re-entrant in the non-remote case,
300       // so we must re-get the state for this assertion.
301       let nonRemoteState = this.getTabState(tab);
302       // Non-remote tabs can never stay in the STATE_LOADING
303       // or STATE_UNLOADING states. By the time this function
304       // exits, a non-remote tab must be in STATE_LOADED or
305       // STATE_UNLOADED, since the painting and the layer
306       // upload happen synchronously.
307       this.assert(
308         nonRemoteState == this.STATE_UNLOADED ||
309           nonRemoteState == this.STATE_LOADED
310       );
311     }
312   }
314   get minimizedOrFullyOccluded() {
315     return (
316       this.window.windowState == this.window.STATE_MINIMIZED ||
317       this.window.isFullyOccluded
318     );
319   }
321   get tabLayerCache() {
322     return this.tabbrowser._tabLayerCache;
323   }
325   finish() {
326     this.log("FINISH");
328     this.assert(this.tabbrowser._switcher);
329     this.assert(this.tabbrowser._switcher === this);
330     this.assert(!this.spinnerTab);
331     this.assert(!this.blankTab);
332     this.assert(!this.loadTimer);
333     this.assert(!this.loadingTab);
334     this.assert(this.lastVisibleTab === this.requestedTab);
335     this.assert(
336       this.minimizedOrFullyOccluded ||
337         this.getTabState(this.requestedTab) == this.STATE_LOADED
338     );
340     this.destroy();
342     this.window.document.commandDispatcher.unlock();
344     let event = new this.window.CustomEvent("TabSwitchDone", {
345       bubbles: true,
346       cancelable: true,
347     });
348     this.tabbrowser.dispatchEvent(event);
349   }
351   // This function is called after all the main state changes to
352   // make sure we display the right tab.
353   updateDisplay() {
354     let requestedTabState = this.getTabState(this.requestedTab);
355     let requestedBrowser = this.requestedTab.linkedBrowser;
357     // It is often more desirable to show a blank tab when appropriate than
358     // the tab switch spinner - especially since the spinner is usually
359     // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
360     // tab switch. We can hide this lag, and hide the time being spent
361     // constructing BrowserChild's, layer trees, etc, by showing a blank
362     // tab instead and focusing it immediately.
363     let shouldBeBlank = false;
364     if (requestedBrowser.isRemoteBrowser) {
365       // If a tab is remote and the window is not minimized, we can show a
366       // blank tab instead of a spinner in the following cases:
367       //
368       // 1. The tab has just crashed, and we haven't started showing the
369       //    tab crashed page yet (in this case, the RemoteTab is null)
370       // 2. The tab has never presented, and has not finished loading
371       //    a non-local-about: page.
372       //
373       // For (2), "finished loading a non-local-about: page" is
374       // determined by the busy state on the tab element and checking
375       // if the loaded URI is local.
376       let isBusy = this.requestedTab.hasAttribute("busy");
377       let isLocalAbout = requestedBrowser.currentURI.schemeIs("about");
378       let hasSufficientlyLoaded = !isBusy && !isLocalAbout;
380       let fl = requestedBrowser.frameLoader;
381       shouldBeBlank =
382         !this.minimizedOrFullyOccluded &&
383         (!fl.remoteTab ||
384           (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
386       if (this.logging()) {
387         let flag = shouldBeBlank ? "blank" : "nonblank";
388         this.addLogFlag(
389           flag,
390           this.minimizedOrFullyOccluded,
391           fl.remoteTab,
392           isBusy,
393           isLocalAbout,
394           fl.remoteTab ? fl.remoteTab.hasPresented : 0
395         );
396       }
397     }
399     if (requestedBrowser.isRemoteBrowser) {
400       this.addLogFlag("isRemote");
401     }
403     // Figure out which tab we actually want visible right now.
404     let showTab = null;
405     if (
406       requestedTabState != this.STATE_LOADED &&
407       this.lastVisibleTab &&
408       this.loadTimer &&
409       !shouldBeBlank
410     ) {
411       // If we can't show the requestedTab, and lastVisibleTab is
412       // available, show it.
413       showTab = this.lastVisibleTab;
414     } else {
415       // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
416       showTab = this.requestedTab;
417     }
419     // First, let's deal with blank tabs, which we show instead
420     // of the spinner when the tab is not currently set up
421     // properly in the content process.
422     if (!shouldBeBlank && this.blankTab) {
423       this.blankTab.linkedBrowser.removeAttribute("blank");
424       this.blankTab = null;
425     } else if (shouldBeBlank && this.blankTab !== showTab) {
426       if (this.blankTab) {
427         this.blankTab.linkedBrowser.removeAttribute("blank");
428       }
429       this.blankTab = showTab;
430       this.blankTab.linkedBrowser.setAttribute("blank", "true");
431     }
433     // Show or hide the spinner as needed.
434     let needSpinner =
435       this.getTabState(showTab) != this.STATE_LOADED &&
436       !this.minimizedOrFullyOccluded &&
437       !shouldBeBlank &&
438       !this.loadTimer;
440     if (!needSpinner && this.spinnerTab) {
441       this.noteSpinnerHidden();
442       this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
443       this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
444       this.spinnerTab = null;
445     } else if (needSpinner && this.spinnerTab !== showTab) {
446       if (this.spinnerTab) {
447         this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
448       } else {
449         this.noteSpinnerDisplayed();
450       }
451       this.spinnerTab = showTab;
452       this.tabbrowser.tabpanels.setAttribute("pendingpaint", "true");
453       this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
454     }
456     // Switch to the tab we've decided to make visible.
457     if (this.visibleTab !== showTab) {
458       this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
459       this.visibleTab = showTab;
461       this.maybeVisibleTabs.add(showTab);
463       let tabpanels = this.tabbrowser.tabpanels;
464       let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
465       let index = Array.prototype.indexOf.call(tabpanels.children, showPanel);
466       if (index != -1) {
467         this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
468         tabpanels.setAttribute("selectedIndex", index);
469         if (showTab === this.requestedTab) {
470           if (requestedTabState == this.STATE_LOADED) {
471             // The new tab will be made visible in the next paint, record the expected
472             // transaction id for that, and we'll mark when we get notified of its
473             // completion.
474             this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
475           } else {
476             this.noteMakingTabVisibleWithoutLayers();
477           }
479           this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
480           this.window.gURLBar.afterTabSwitchFocusChange();
481           this.maybeActivateDocShell(this.requestedTab);
482         }
483       }
485       // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
486       if (this.lastVisibleTab) {
487         this.lastVisibleTab._visuallySelected = false;
488       }
490       this.visibleTab._visuallySelected = true;
491       this.tabbrowser.tabContainer._setPositionalAttributes();
492     }
494     this.lastVisibleTab = this.visibleTab;
495   }
497   assert(cond) {
498     if (!cond) {
499       dump("Assertion failure\n" + Error().stack);
501       // Don't break a user's browser if an assertion fails.
502       if (AppConstants.DEBUG) {
503         throw new Error("Assertion failure");
504       }
505     }
506   }
508   maybeClearLoadTimer(caller) {
509     if (this.loadingTab) {
510       this._loadTimerClearedBy = caller;
511       this.loadingTab = null;
512       if (this.loadTimer) {
513         this.clearTimer(this.loadTimer);
514         this.loadTimer = null;
515       }
516     }
517   }
519   // We've decided to try to load requestedTab.
520   loadRequestedTab() {
521     this.assert(!this.loadTimer);
522     this.assert(!this.minimizedOrFullyOccluded);
524     // loadingTab can be non-null here if we timed out loading the current tab.
525     // In that case we just overwrite it with a different tab; it's had its chance.
526     this.loadingTab = this.requestedTab;
527     this.log("Loading tab " + this.tinfo(this.loadingTab));
529     this.loadTimer = this.setTimer(
530       () => this.handleEvent({ type: "loadTimeout" }),
531       this.TAB_SWITCH_TIMEOUT
532     );
533     this.setTabState(this.requestedTab, this.STATE_LOADING);
534   }
536   maybeActivateDocShell(tab) {
537     // If we've reached the point where the requested tab has entered
538     // the loaded state, but the DocShell is still not yet active, we
539     // should activate it.
540     let browser = tab.linkedBrowser;
541     let state = this.getTabState(tab);
542     let canCheckDocShellState =
543       !browser.mDestroyed &&
544       (browser.docShell || browser.frameLoader.remoteTab);
545     if (
546       tab == this.requestedTab &&
547       canCheckDocShellState &&
548       state == this.STATE_LOADED &&
549       !browser.docShellIsActive &&
550       !this.minimizedOrFullyOccluded
551     ) {
552       browser.docShellIsActive = true;
553       this.logState(
554         "Set requested tab docshell to active and preserveLayers to false"
555       );
556       // If we minimized the window before the switcher was activated,
557       // we might have set the preserveLayers flag for the current
558       // browser. Let's clear it.
559       browser.preserveLayers(false);
560     }
561   }
563   // This function runs before every event. It fixes up the state
564   // to account for closed tabs.
565   preActions() {
566     this.assert(this.tabbrowser._switcher);
567     this.assert(this.tabbrowser._switcher === this);
569     for (let i = 0; i < this.tabLayerCache.length; i++) {
570       let tab = this.tabLayerCache[i];
571       if (!tab.linkedBrowser) {
572         this.tabState.delete(tab);
573         this.tabLayerCache.splice(i, 1);
574         i--;
575       }
576     }
578     for (let [tab] of this.tabState) {
579       if (!tab.linkedBrowser) {
580         this.tabState.delete(tab);
581         this.unwarmTab(tab);
582       }
583     }
585     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
586       this.lastVisibleTab = null;
587     }
588     if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
589       this.lastPrimaryTab = null;
590     }
591     if (this.blankTab && !this.blankTab.linkedBrowser) {
592       this.blankTab = null;
593     }
594     if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
595       this.noteSpinnerHidden();
596       this.spinnerTab = null;
597     }
598     if (this.loadingTab && !this.loadingTab.linkedBrowser) {
599       this.maybeClearLoadTimer("preActions");
600     }
601   }
603   // This code runs after we've responded to an event or requested a new
604   // tab. It's expected that we've already updated all the principal
605   // state variables. This function takes care of updating any auxilliary
606   // state.
607   postActions(eventString) {
608     // Once we finish loading loadingTab, we null it out. So the state should
609     // always be LOADING.
610     this.assert(
611       !this.loadingTab ||
612         this.getTabState(this.loadingTab) == this.STATE_LOADING
613     );
615     // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
616     // the timer is set only when we're loading something.
617     this.assert(!this.loadTimer || this.loadingTab);
618     this.assert(!this.loadingTab || this.loadTimer);
620     // If we're switching to a non-remote tab, there's no need to wait
621     // for it to send layers to the compositor, as this will happen
622     // synchronously. Clearing this here means that in the next step,
623     // we can load the non-remote browser immediately.
624     if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
625       this.maybeClearLoadTimer("postActions");
626     }
628     // If we're not loading anything, try loading the requested tab.
629     let stateOfRequestedTab = this.getTabState(this.requestedTab);
630     if (
631       !this.loadTimer &&
632       !this.minimizedOrFullyOccluded &&
633       (stateOfRequestedTab == this.STATE_UNLOADED ||
634         stateOfRequestedTab == this.STATE_UNLOADING ||
635         this.warmingTabs.has(this.requestedTab))
636     ) {
637       this.assert(stateOfRequestedTab != this.STATE_LOADED);
638       this.loadRequestedTab();
639     }
641     let numBackgroundCached = 0;
642     for (let tab of this.tabLayerCache) {
643       if (tab !== this.requestedTab) {
644         numBackgroundCached++;
645       }
646     }
648     // See how many tabs still have work to do.
649     let numPending = 0;
650     let numWarming = 0;
651     for (let [tab, state] of this.tabState) {
652       // Skip print preview browsers since they shouldn't affect tab switching.
653       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
654         continue;
655       }
657       if (
658         state == this.STATE_LOADED &&
659         tab !== this.requestedTab &&
660         !this.tabLayerCache.includes(tab)
661       ) {
662         numPending++;
664         if (tab !== this.visibleTab) {
665           numWarming++;
666         }
667       }
668       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
669         numPending++;
670       }
671     }
673     this.updateDisplay();
675     // It's possible for updateDisplay to trigger one of our own event
676     // handlers, which might cause finish() to already have been called.
677     // Check for that before calling finish() again.
678     if (!this.tabbrowser._switcher) {
679       return;
680     }
682     this.maybeFinishTabSwitch();
684     if (numBackgroundCached > 0) {
685       this.deactivateCachedBackgroundTabs();
686     }
688     if (numWarming > gTabWarmingMax) {
689       this.logState("Hit tabWarmingMax");
690       if (this.unloadTimer) {
691         this.clearTimer(this.unloadTimer);
692       }
693       this.unloadNonRequiredTabs();
694     }
696     if (numPending == 0) {
697       this.finish();
698     }
700     this.logState("/" + eventString);
701   }
703   // Fires when we're ready to unload unused tabs.
704   onUnloadTimeout() {
705     this.unloadTimer = null;
706     this.unloadNonRequiredTabs();
707   }
709   deactivateCachedBackgroundTabs() {
710     for (let tab of this.tabLayerCache) {
711       if (tab !== this.requestedTab) {
712         let browser = tab.linkedBrowser;
713         browser.preserveLayers(true);
714         browser.docShellIsActive = false;
715       }
716     }
717   }
719   // If there are any non-visible and non-requested tabs in
720   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
721   // up the unloadTimer to run onUnloadTimeout if there are still
722   // tabs in the process of unloading.
723   unloadNonRequiredTabs() {
724     this.warmingTabs = new WeakSet();
725     let numPending = 0;
727     // Unload any tabs that can be unloaded.
728     for (let [tab, state] of this.tabState) {
729       if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
730         continue;
731       }
733       let isInLayerCache = this.tabLayerCache.includes(tab);
735       if (
736         state == this.STATE_LOADED &&
737         !this.maybeVisibleTabs.has(tab) &&
738         tab !== this.lastVisibleTab &&
739         tab !== this.loadingTab &&
740         tab !== this.requestedTab &&
741         !isInLayerCache
742       ) {
743         this.setTabState(tab, this.STATE_UNLOADING);
744       }
746       if (
747         state != this.STATE_UNLOADED &&
748         tab !== this.requestedTab &&
749         !isInLayerCache
750       ) {
751         numPending++;
752       }
753     }
755     if (numPending) {
756       // Keep the timer going since there may be more tabs to unload.
757       this.unloadTimer = this.setTimer(
758         () => this.handleEvent({ type: "unloadTimeout" }),
759         this.UNLOAD_DELAY
760       );
761     }
762   }
764   // Fires when an ongoing load has taken too long.
765   onLoadTimeout() {
766     this.maybeClearLoadTimer("onLoadTimeout");
767   }
769   // Fires when the layers become available for a tab.
770   onLayersReady(browser) {
771     let tab = this.tabbrowser.getTabForBrowser(browser);
772     if (!tab) {
773       // We probably got a layer update from a tab that got before
774       // the switcher was created, or for browser that's not being
775       // tracked by the async tab switcher (like the preloaded about:newtab).
776       return;
777     }
779     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       this.logState(`onLayersCleared(${tab._tPos})`);
811       this.assert(
812         this.getTabState(tab) == this.STATE_UNLOADING ||
813           this.getTabState(tab) == this.STATE_UNLOADED
814       );
815       this.setTabState(tab, this.STATE_UNLOADED);
816     }
817   }
819   // Called when a tab switches from remote to non-remote. In this case
820   // a MozLayerTreeReady notification that we requested may never fire,
821   // so we need to simulate it.
822   onRemotenessChange(tab) {
823     this.logState(
824       `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`
825     );
826     if (!tab.linkedBrowser.isRemoteBrowser) {
827       if (this.getTabState(tab) == this.STATE_LOADING) {
828         this.onLayersReady(tab.linkedBrowser);
829       } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
830         this.onLayersCleared(tab.linkedBrowser);
831       }
832     } else if (this.getTabState(tab) == this.STATE_LOADED) {
833       // A tab just changed from non-remote to remote, which means
834       // that it's gone back into the STATE_LOADING state until
835       // it sends up a layer tree.
836       this.setTabState(tab, this.STATE_LOADING);
837     }
838   }
840   onTabRemoved(tab) {
841     if (this.lastVisibleTab == tab) {
842       this.handleEvent({ type: "tabRemoved", tab });
843     }
844   }
846   // Called when a tab has been removed, and the browser node is
847   // about to be removed from the DOM.
848   onTabRemovedImpl(tab) {
849     this.lastVisibleTab = null;
850   }
852   onSizeModeOrOcclusionStateChange() {
853     if (this.minimizedOrFullyOccluded) {
854       for (let [tab, state] of this.tabState) {
855         // Skip print preview browsers since they shouldn't affect tab switching.
856         if (this.tabbrowser._printPreviewBrowsers.has(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   shouldActivateDocShell(browser) {
919     let tab = this.tabbrowser.getTabForBrowser(browser);
920     let state = this.getTabState(tab);
921     return state == this.STATE_LOADING || state == this.STATE_LOADED;
922   }
924   activateBrowserForPrintPreview(browser) {
925     let tab = this.tabbrowser.getTabForBrowser(browser);
926     let state = this.getTabState(tab);
927     if (state != this.STATE_LOADING && state != this.STATE_LOADED) {
928       this.setTabState(tab, this.STATE_LOADING);
929       this.logState(
930         "Activated browser " + this.tinfo(tab) + " for print preview"
931       );
932     }
933   }
935   canWarmTab(tab) {
936     if (!gTabWarmingEnabled) {
937       return false;
938     }
940     if (!tab) {
941       return false;
942     }
944     // If the tab is not yet inserted, closing, not remote,
945     // crashed, already visible, or already requested, warming
946     // up the tab makes no sense.
947     if (
948       this.minimizedOrFullyOccluded ||
949       !tab.linkedPanel ||
950       tab.closing ||
951       !tab.linkedBrowser.isRemoteBrowser ||
952       !tab.linkedBrowser.frameLoader.remoteTab
953     ) {
954       return false;
955     }
957     return true;
958   }
960   shouldWarmTab(tab) {
961     if (this.canWarmTab(tab)) {
962       // Tabs that are already in STATE_LOADING or STATE_LOADED
963       // have no need to be warmed up.
964       let state = this.getTabState(tab);
965       if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) {
966         return true;
967       }
968     }
970     return false;
971   }
973   unwarmTab(tab) {
974     this.warmingTabs.delete(tab);
975   }
977   warmupTab(tab) {
978     if (!this.shouldWarmTab(tab)) {
979       return;
980     }
982     this.logState("warmupTab " + this.tinfo(tab));
984     this.warmingTabs.add(tab);
985     this.setTabState(tab, this.STATE_LOADING);
986     this.queueUnload(gTabWarmingUnloadDelayMs);
987   }
989   cleanUpTabAfterEviction(tab) {
990     this.assert(tab !== this.requestedTab);
991     let browser = tab.linkedBrowser;
992     if (browser) {
993       browser.preserveLayers(false);
994     }
995     this.setTabState(tab, this.STATE_UNLOADING);
996   }
998   evictOldestTabFromCache() {
999     let tab = this.tabLayerCache.shift();
1000     this.cleanUpTabAfterEviction(tab);
1001   }
1003   maybePromoteTabInLayerCache(tab) {
1004     if (
1005       gTabCacheSize > 1 &&
1006       tab.linkedBrowser.isRemoteBrowser &&
1007       tab.linkedBrowser.currentURI.spec != "about:blank"
1008     ) {
1009       let tabIndex = this.tabLayerCache.indexOf(tab);
1011       if (tabIndex != -1) {
1012         this.tabLayerCache.splice(tabIndex, 1);
1013       }
1015       this.tabLayerCache.push(tab);
1017       if (this.tabLayerCache.length > gTabCacheSize) {
1018         this.evictOldestTabFromCache();
1019       }
1020     }
1021   }
1023   // Called when the user asks to switch to a given tab.
1024   requestTab(tab) {
1025     if (tab === this.requestedTab) {
1026       return;
1027     }
1029     let tabState = this.getTabState(tab);
1030     this.noteTabRequested(tab, tabState);
1032     this.logState("requestTab " + this.tinfo(tab));
1033     this.startTabSwitch();
1035     let oldBrowser = this.requestedTab.linkedBrowser;
1036     oldBrowser.deprioritize();
1037     this.requestedTab = tab;
1038     if (tabState == this.STATE_LOADED) {
1039       this.maybeVisibleTabs.clear();
1040     }
1042     tab.linkedBrowser.setAttribute("primary", "true");
1043     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1044       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1045     }
1046     this.lastPrimaryTab = tab;
1048     this.queueUnload(this.UNLOAD_DELAY);
1049   }
1051   queueUnload(unloadTimeout) {
1052     this.handleEvent({ type: "queueUnload", unloadTimeout });
1053   }
1055   onQueueUnload(unloadTimeout) {
1056     if (this.unloadTimer) {
1057       this.clearTimer(this.unloadTimer);
1058     }
1059     this.unloadTimer = this.setTimer(
1060       () => this.handleEvent({ type: "unloadTimeout" }),
1061       unloadTimeout
1062     );
1063   }
1065   handleEvent(event, delayed = false) {
1066     if (this._processing) {
1067       this.setTimer(() => this.handleEvent(event, true), 0);
1068       return;
1069     }
1070     if (delayed && this.tabbrowser._switcher != this) {
1071       // if we delayed processing this event, we might be out of date, in which
1072       // case we drop the delayed events
1073       return;
1074     }
1075     this._processing = true;
1076     try {
1077       this.preActions();
1079       switch (event.type) {
1080         case "queueUnload":
1081           this.onQueueUnload(event.unloadTimeout);
1082           break;
1083         case "unloadTimeout":
1084           this.onUnloadTimeout();
1085           break;
1086         case "loadTimeout":
1087           this.onLoadTimeout();
1088           break;
1089         case "tabRemoved":
1090           this.onTabRemovedImpl(event.tab);
1091           break;
1092         case "MozLayerTreeReady":
1093           this.onLayersReady(event.originalTarget);
1094           break;
1095         case "MozAfterPaint":
1096           this.onPaint(event);
1097           break;
1098         case "MozLayerTreeCleared":
1099           this.onLayersCleared(event.originalTarget);
1100           break;
1101         case "TabRemotenessChange":
1102           this.onRemotenessChange(event.target);
1103           break;
1104         case "sizemodechange":
1105         case "occlusionstatechange":
1106           this.onSizeModeOrOcclusionStateChange();
1107           break;
1108         case "SwapDocShells":
1109           this.onSwapDocShells(event.originalTarget, event.detail);
1110           break;
1111         case "EndSwapDocShells":
1112           this.onEndSwapDocShells(event.originalTarget, event.detail);
1113           break;
1114       }
1116       this.postActions(event.type);
1117     } finally {
1118       this._processing = false;
1119     }
1120   }
1122   /*
1123    * Telemetry and Profiler related helpers for recording tab switch
1124    * timing.
1125    */
1127   startTabSwitch() {
1128     this.noteStartTabSwitch();
1129     this.switchInProgress = true;
1130   }
1132   /**
1133    * Something has occurred that might mean that we've completed
1134    * the tab switch (layers are ready, paints are done, spinners
1135    * are hidden). This checks to make sure all conditions are
1136    * satisfied, and then records the tab switch as finished.
1137    */
1138   maybeFinishTabSwitch() {
1139     if (
1140       this.switchInProgress &&
1141       this.requestedTab &&
1142       (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1143         this.requestedTab === this.blankTab)
1144     ) {
1145       if (this.requestedTab !== this.blankTab) {
1146         this.maybePromoteTabInLayerCache(this.requestedTab);
1147       }
1149       this.noteFinishTabSwitch();
1150       this.switchInProgress = false;
1151     }
1152   }
1154   /*
1155    * Debug related logging for switcher.
1156    */
1157   logging() {
1158     if (this._useDumpForLogging) {
1159       return true;
1160     }
1161     if (this._logInit) {
1162       return this._shouldLog;
1163     }
1164     let result = Services.prefs.getBoolPref(
1165       "browser.tabs.remote.logSwitchTiming",
1166       false
1167     );
1168     this._shouldLog = result;
1169     this._logInit = true;
1170     return this._shouldLog;
1171   }
1173   tinfo(tab) {
1174     if (tab) {
1175       return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1176     }
1177     return "null";
1178   }
1180   log(s) {
1181     if (!this.logging()) {
1182       return;
1183     }
1184     if (this._useDumpForLogging) {
1185       dump(s + "\n");
1186     } else {
1187       Services.console.logStringMessage(s);
1188     }
1189   }
1191   addLogFlag(flag, ...subFlags) {
1192     if (this.logging()) {
1193       if (subFlags.length) {
1194         flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
1195       }
1196       this._logFlags.push(flag);
1197     }
1198   }
1200   logState(suffix) {
1201     if (!this.logging()) {
1202       return;
1203     }
1205     let getTabString = tab => {
1206       let tabString = "";
1208       let state = this.getTabState(tab);
1209       let isWarming = this.warmingTabs.has(tab);
1210       let isCached = this.tabLayerCache.includes(tab);
1211       let isClosing = tab.closing;
1212       let linkedBrowser = tab.linkedBrowser;
1213       let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1214       let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1216       if (tab === this.lastVisibleTab) {
1217         tabString += "V";
1218       }
1219       if (tab === this.loadingTab) {
1220         tabString += "L";
1221       }
1222       if (tab === this.requestedTab) {
1223         tabString += "R";
1224       }
1225       if (tab === this.blankTab) {
1226         tabString += "B";
1227       }
1228       if (this.maybeVisibleTabs.has(tab)) {
1229         tabString += "M";
1230       }
1232       let extraStates = "";
1233       if (isWarming) {
1234         extraStates += "W";
1235       }
1236       if (isCached) {
1237         extraStates += "C";
1238       }
1239       if (isClosing) {
1240         extraStates += "X";
1241       }
1242       if (isActive) {
1243         extraStates += "A";
1244       }
1245       if (isRendered) {
1246         extraStates += "R";
1247       }
1248       if (extraStates != "") {
1249         tabString += `(${extraStates})`;
1250       }
1252       switch (state) {
1253         case this.STATE_LOADED: {
1254           tabString += "(loaded)";
1255           break;
1256         }
1257         case this.STATE_LOADING: {
1258           tabString += "(loading)";
1259           break;
1260         }
1261         case this.STATE_UNLOADING: {
1262           tabString += "(unloading)";
1263           break;
1264         }
1265         case this.STATE_UNLOADED: {
1266           tabString += "(unloaded)";
1267           break;
1268         }
1269       }
1271       return tabString;
1272     };
1274     let accum = "";
1276     // This is a bit tricky to read, but what we're doing here is collapsing
1277     // identical tab states down to make the overal string shorter and easier
1278     // to read, and we move all simply unloaded tabs to the back of the list.
1279     // I.e., we turn
1280     //   "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
1281     // into
1282     //   "3:(loaded) 0...2:(unloaded)"
1283     let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
1284     let lastMatch = -1;
1285     let unloadedTabsStrings = [];
1286     for (let i = 0; i <= tabStrings.length; i++) {
1287       if (i > 0) {
1288         if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
1289           continue;
1290         }
1292         if (tabStrings[lastMatch] == "(unloaded)") {
1293           if (lastMatch == i - 1) {
1294             unloadedTabsStrings.push(lastMatch.toString());
1295           } else {
1296             unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
1297           }
1298         } else if (lastMatch == i - 1) {
1299           accum += `${lastMatch}:${tabStrings[lastMatch]} `;
1300         } else {
1301           accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
1302         }
1303       }
1305       lastMatch = i;
1306     }
1308     if (unloadedTabsStrings.length) {
1309       accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
1310     }
1312     accum += "cached: " + this.tabLayerCache.length + " ";
1314     if (this._logFlags.length) {
1315       accum += `[${this._logFlags.join(",")}] `;
1316       this._logFlags = [];
1317     }
1319     // It can be annoying to read through the entirety of a log string just
1320     // to check if something changed or not. So if we can tell that nothing
1321     // changed, just write "unchanged" to save the reader's time.
1322     let logString;
1323     if (this._lastLogString == accum) {
1324       accum = "unchanged";
1325     } else {
1326       this._lastLogString = accum;
1327     }
1328     logString = `ATS: ${accum}{${suffix}}`;
1330     if (this._useDumpForLogging) {
1331       dump(logString + "\n");
1332     } else {
1333       Services.console.logStringMessage(logString);
1334     }
1335   }
1337   noteMakingTabVisibleWithoutLayers() {
1338     // We're making the tab visible even though we haven't yet got layers for it.
1339     // It's hard to know which composite the layers will first be available in (and
1340     // the parent process might not even get MozAfterPaint delivered for it), so just
1341     // give up measuring this for now. :(
1342     TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1343   }
1345   notePaint(event) {
1346     if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
1347       if (
1348         TelemetryStopwatch.running(
1349           "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1350           this.window
1351         )
1352       ) {
1353         let time = TelemetryStopwatch.timeElapsed(
1354           "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1355           this.window
1356         );
1357         if (time != -1) {
1358           TelemetryStopwatch.finish(
1359             "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1360             this.window
1361           );
1362         }
1363       }
1364       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited");
1365       this.switchPaintId = -1;
1366     }
1367   }
1369   noteTabRequested(tab, tabState) {
1370     if (gTabWarmingEnabled) {
1371       let warmingState = "disqualified";
1373       if (this.canWarmTab(tab)) {
1374         if (tabState == this.STATE_LOADING) {
1375           warmingState = "stillLoading";
1376         } else if (tabState == this.STATE_LOADED) {
1377           warmingState = "loaded";
1378         } else if (
1379           tabState == this.STATE_UNLOADING ||
1380           tabState == this.STATE_UNLOADED
1381         ) {
1382           // At this point, if the tab's browser was being inserted
1383           // lazily, we never had a chance to warm it up, and unfortunately
1384           // there's no great way to detect that case. Those cases will
1385           // end up in the "notWarmed" bucket, along with legitimate cases
1386           // where tabs could have been warmed but weren't.
1387           warmingState = "notWarmed";
1388         }
1389       }
1391       Services.telemetry
1392         .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
1393         .add(warmingState);
1394     }
1395   }
1397   noteStartTabSwitch() {
1398     TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1399     TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1401     if (
1402       TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)
1403     ) {
1404       TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1405     }
1406     TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1407     ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start");
1408   }
1410   noteFinishTabSwitch() {
1411     // After this point the tab has switched from the content thread's point of view.
1412     // The changes will be visible after the next refresh driver tick + composite.
1413     let time = TelemetryStopwatch.timeElapsed(
1414       "FX_TAB_SWITCH_TOTAL_E10S_MS",
1415       this.window
1416     );
1417     if (time != -1) {
1418       TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1419       this.log("DEBUG: tab switch time = " + time);
1420       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish");
1421     }
1422   }
1424   noteSpinnerDisplayed() {
1425     this.assert(!this.spinnerTab);
1426     let browser = this.requestedTab.linkedBrowser;
1427     this.assert(browser.isRemoteBrowser);
1428     TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1429     // We have a second, similar probe for capturing recordings of
1430     // when the spinner is displayed for very long periods.
1431     TelemetryStopwatch.start(
1432       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1433       this.window
1434     );
1435     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown");
1436     Services.telemetry
1437       .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1438       .add(this._loadTimerClearedBy);
1439     if (AppConstants.NIGHTLY_BUILD) {
1440       Services.obs.notifyObservers(null, "tabswitch-spinner");
1441     }
1442   }
1444   noteSpinnerHidden() {
1445     this.assert(this.spinnerTab);
1446     this.log(
1447       "DEBUG: spinner time = " +
1448         TelemetryStopwatch.timeElapsed(
1449           "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
1450           this.window
1451         )
1452     );
1453     TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1454     TelemetryStopwatch.finish(
1455       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1456       this.window
1457     );
1458     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden");
1459     // we do not get a onPaint after displaying the spinner
1460     this._loadTimerClearedBy = "none";
1461   }