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/. */
5
6"use strict";
7
8var EXPORTED_SYMBOLS = ["AsyncTabSwitcher"];
9
10const { XPCOMUtils } = ChromeUtils.import(
11  "resource://gre/modules/XPCOMUtils.jsm"
12);
13XPCOMUtils.defineLazyModuleGetters(this, {
14  AppConstants: "resource://gre/modules/AppConstants.jsm",
15  Services: "resource://gre/modules/Services.jsm",
16});
17
18XPCOMUtils.defineLazyPreferenceGetter(
19  this,
20  "gTabWarmingEnabled",
21  "browser.tabs.remote.warmup.enabled"
22);
23XPCOMUtils.defineLazyPreferenceGetter(
24  this,
25  "gTabWarmingMax",
26  "browser.tabs.remote.warmup.maxTabs"
27);
28XPCOMUtils.defineLazyPreferenceGetter(
29  this,
30  "gTabWarmingUnloadDelayMs",
31  "browser.tabs.remote.warmup.unloadDelayMs"
32);
33XPCOMUtils.defineLazyPreferenceGetter(
34  this,
35  "gTabCacheSize",
36  "browser.tabs.remote.tabCacheSize"
37);
38
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 */
77class AsyncTabSwitcher {
78  constructor(tabbrowser) {
79    this.log("START");
80
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
85
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
89
90    // The next three tabs form the principal state variables.
91    // See the assertions in postActions for their invariants.
92
93    // Tab the user requested most recently.
94    this.requestedTab = tabbrowser.selectedTab;
95
96    // Tab we're currently trying to load.
97    this.loadingTab = null;
98
99    // We show this tab in case the requestedTab hasn't loaded yet.
100    this.lastVisibleTab = tabbrowser.selectedTab;
101
102    // Auxilliary state variables:
103
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"
108
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.
113
114    // Map from tabs to STATE_* (below).
115    this.tabState = new Map();
116
117    // True if we're in the midst of switching tabs.
118    this.switchInProgress = false;
119
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;
125
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]);
132
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();
136
137    this.STATE_UNLOADED = 0;
138    this.STATE_LOADING = 1;
139    this.STATE_LOADED = 2;
140    this.STATE_UNLOADING = 3;
141
142    // re-entrancy guard:
143    this._processing = false;
144
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";
149
150    this._useDumpForLogging = false;
151    this._logInit = false;
152    this._logFlags = [];
153
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);
162
163    let initialTab = this.requestedTab;
164    let initialBrowser = initialTab.linkedBrowser;
165
166    let tabIsLoaded =
167      !initialBrowser.isRemoteBrowser ||
168      initialBrowser.frameLoader.remoteTab.hasLayers;
169
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);
174
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    }
182
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  }
189
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    }
199
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);
208
209    this.tabbrowser._switcher = null;
210  }
211
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    };
219
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  }
224
225  clearTimer(timer) {
226    timer.cancel();
227  }
228
229  getTabState(tab) {
230    let state = this.tabState.get(tab);
231
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;
237
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      }
248
249      this.setTabStateNoAction(tab, state);
250    }
251
252    return state;
253  }
254
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  }
262
263  setTabState(tab, state) {
264    if (state == this.getTabState(tab)) {
265      return;
266    }
267
268    this.setTabStateNoAction(tab, state);
269
270    let browser = tab.linkedBrowser;
271    let { remoteTab } = browser.frameLoader;
272    if (state == this.STATE_LOADING) {
273      this.assert(!this.minimizedOrFullyOccluded);
274
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      }
280
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    }
297
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  }
313
314  get minimizedOrFullyOccluded() {
315    return (
316      this.window.windowState == this.window.STATE_MINIMIZED ||
317      this.window.isFullyOccluded
318    );
319  }
320
321  get tabLayerCache() {
322    return this.tabbrowser._tabLayerCache;
323  }
324
325  finish() {
326    this.log("FINISH");
327
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    );
339
340    this.destroy();
341
342    this.window.document.commandDispatcher.unlock();
343
344    let event = new this.window.CustomEvent("TabSwitchDone", {
345      bubbles: true,
346      cancelable: true,
347    });
348    this.tabbrowser.dispatchEvent(event);
349  }
350
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;
356
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;
379
380      let fl = requestedBrowser.frameLoader;
381      shouldBeBlank =
382        !this.minimizedOrFullyOccluded &&
383        (!fl.remoteTab ||
384          (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
385
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    }
398
399    if (requestedBrowser.isRemoteBrowser) {
400      this.addLogFlag("isRemote");
401    }
402
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    }
418
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    }
432
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;
439
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    }
455
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;
460
461      this.maybeVisibleTabs.add(showTab);
462
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          }
478
479          this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
480          this.window.gURLBar.afterTabSwitchFocusChange();
481          this.maybeActivateDocShell(this.requestedTab);
482        }
483      }
484
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      }
489
490      this.visibleTab._visuallySelected = true;
491      this.tabbrowser.tabContainer._setPositionalAttributes();
492    }
493
494    this.lastVisibleTab = this.visibleTab;
495  }
496
497  assert(cond) {
498    if (!cond) {
499      dump("Assertion failure\n" + Error().stack);
500
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  }
507
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  }
518
519  // We've decided to try to load requestedTab.
520  loadRequestedTab() {
521    this.assert(!this.loadTimer);
522    this.assert(!this.minimizedOrFullyOccluded);
523
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));
528
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  }
535
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  }
562
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);
568
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    }
577
578    for (let [tab] of this.tabState) {
579      if (!tab.linkedBrowser) {
580        this.tabState.delete(tab);
581        this.unwarmTab(tab);
582      }
583    }
584
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  }
602
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    );
614
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);
619
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    }
627
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    }
640
641    let numBackgroundCached = 0;
642    for (let tab of this.tabLayerCache) {
643      if (tab !== this.requestedTab) {
644        numBackgroundCached++;
645      }
646    }
647
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      }
656
657      if (
658        state == this.STATE_LOADED &&
659        tab !== this.requestedTab &&
660        !this.tabLayerCache.includes(tab)
661      ) {
662        numPending++;
663
664        if (tab !== this.visibleTab) {
665          numWarming++;
666        }
667      }
668      if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
669        numPending++;
670      }
671    }
672
673    this.updateDisplay();
674
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    }
681
682    this.maybeFinishTabSwitch();
683
684    if (numBackgroundCached > 0) {
685      this.deactivateCachedBackgroundTabs();
686    }
687
688    if (numWarming > gTabWarmingMax) {
689      this.logState("Hit tabWarmingMax");
690      if (this.unloadTimer) {
691        this.clearTimer(this.unloadTimer);
692      }
693      this.unloadNonRequiredTabs();
694    }
695
696    if (numPending == 0) {
697      this.finish();
698    }
699
700    this.logState("/" + eventString);
701  }
702
703  // Fires when we're ready to unload unused tabs.
704  onUnloadTimeout() {
705    this.unloadTimer = null;
706    this.unloadNonRequiredTabs();
707  }
708
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  }
718
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;
726
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      }
732
733      let isInLayerCache = this.tabLayerCache.includes(tab);
734
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      }
745
746      if (
747        state != this.STATE_UNLOADED &&
748        tab !== this.requestedTab &&
749        !isInLayerCache
750      ) {
751        numPending++;
752      }
753    }
754
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  }
763
764  // Fires when an ongoing load has taken too long.
765  onLoadTimeout() {
766    this.maybeClearLoadTimer("onLoadTimeout");
767  }
768
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    }
778
779    this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
780
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);
787
788    if (this.loadingTab === tab) {
789      this.maybeClearLoadTimer("onLayersReady");
790    }
791  }
792
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  }
805
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  }
818
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  }
839
840  onTabRemoved(tab) {
841    if (this.lastVisibleTab == tab) {
842      this.handleEvent({ type: "tabRemoved", tab });
843    }
844  }
845
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  }
851
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        }
859
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  }
871
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.
876
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  }
895
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.
900
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");
907
908    let { state: otherState } = this.swapMap.get(otherBrowser);
909
910    this.swapMap.delete(otherBrowser);
911
912    let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
913    if (ourTab) {
914      this.setTabStateNoAction(ourTab, otherState);
915    }
916  }
917
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  }
923
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  }
934
935  canWarmTab(tab) {
936    if (!gTabWarmingEnabled) {
937      return false;
938    }
939
940    if (!tab) {
941      return false;
942    }
943
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    }
956
957    return true;
958  }
959
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    }
969
970    return false;
971  }
972
973  unwarmTab(tab) {
974    this.warmingTabs.delete(tab);
975  }
976
977  warmupTab(tab) {
978    if (!this.shouldWarmTab(tab)) {
979      return;
980    }
981
982    this.logState("warmupTab " + this.tinfo(tab));
983
984    this.warmingTabs.add(tab);
985    this.setTabState(tab, this.STATE_LOADING);
986    this.queueUnload(gTabWarmingUnloadDelayMs);
987  }
988
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  }
997
998  evictOldestTabFromCache() {
999    let tab = this.tabLayerCache.shift();
1000    this.cleanUpTabAfterEviction(tab);
1001  }
1002
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);
1010
1011      if (tabIndex != -1) {
1012        this.tabLayerCache.splice(tabIndex, 1);
1013      }
1014
1015      this.tabLayerCache.push(tab);
1016
1017      if (this.tabLayerCache.length > gTabCacheSize) {
1018        this.evictOldestTabFromCache();
1019      }
1020    }
1021  }
1022
1023  // Called when the user asks to switch to a given tab.
1024  requestTab(tab) {
1025    if (tab === this.requestedTab) {
1026      return;
1027    }
1028
1029    let tabState = this.getTabState(tab);
1030    this.noteTabRequested(tab, tabState);
1031
1032    this.logState("requestTab " + this.tinfo(tab));
1033    this.startTabSwitch();
1034
1035    let oldBrowser = this.requestedTab.linkedBrowser;
1036    oldBrowser.deprioritize();
1037    this.requestedTab = tab;
1038    if (tabState == this.STATE_LOADED) {
1039      this.maybeVisibleTabs.clear();
1040    }
1041
1042    tab.linkedBrowser.setAttribute("primary", "true");
1043    if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1044      this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1045    }
1046    this.lastPrimaryTab = tab;
1047
1048    this.queueUnload(this.UNLOAD_DELAY);
1049  }
1050
1051  queueUnload(unloadTimeout) {
1052    this.handleEvent({ type: "queueUnload", unloadTimeout });
1053  }
1054
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  }
1064
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();
1078
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      }
1115
1116      this.postActions(event.type);
1117    } finally {
1118      this._processing = false;
1119    }
1120  }
1121
1122  /*
1123   * Telemetry and Profiler related helpers for recording tab switch
1124   * timing.
1125   */
1126
1127  startTabSwitch() {
1128    this.noteStartTabSwitch();
1129    this.switchInProgress = true;
1130  }
1131
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      }
1148
1149      this.noteFinishTabSwitch();
1150      this.switchInProgress = false;
1151    }
1152  }
1153
1154  addMarker(marker) {
1155    if (Services.profiler) {
1156      Services.profiler.AddMarker(marker);
1157    }
1158  }
1159
1160  /*
1161   * Debug related logging for switcher.
1162   */
1163  logging() {
1164    if (this._useDumpForLogging) {
1165      return true;
1166    }
1167    if (this._logInit) {
1168      return this._shouldLog;
1169    }
1170    let result = Services.prefs.getBoolPref(
1171      "browser.tabs.remote.logSwitchTiming",
1172      false
1173    );
1174    this._shouldLog = result;
1175    this._logInit = true;
1176    return this._shouldLog;
1177  }
1178
1179  tinfo(tab) {
1180    if (tab) {
1181      return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1182    }
1183    return "null";
1184  }
1185
1186  log(s) {
1187    if (!this.logging()) {
1188      return;
1189    }
1190    if (this._useDumpForLogging) {
1191      dump(s + "\n");
1192    } else {
1193      Services.console.logStringMessage(s);
1194    }
1195  }
1196
1197  addLogFlag(flag, ...subFlags) {
1198    if (this.logging()) {
1199      if (subFlags.length) {
1200        flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
1201      }
1202      this._logFlags.push(flag);
1203    }
1204  }
1205
1206  logState(suffix) {
1207    if (!this.logging()) {
1208      return;
1209    }
1210
1211    let getTabString = tab => {
1212      let tabString = "";
1213
1214      let state = this.getTabState(tab);
1215      let isWarming = this.warmingTabs.has(tab);
1216      let isCached = this.tabLayerCache.includes(tab);
1217      let isClosing = tab.closing;
1218      let linkedBrowser = tab.linkedBrowser;
1219      let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1220      let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1221
1222      if (tab === this.lastVisibleTab) {
1223        tabString += "V";
1224      }
1225      if (tab === this.loadingTab) {
1226        tabString += "L";
1227      }
1228      if (tab === this.requestedTab) {
1229        tabString += "R";
1230      }
1231      if (tab === this.blankTab) {
1232        tabString += "B";
1233      }
1234      if (this.maybeVisibleTabs.has(tab)) {
1235        tabString += "M";
1236      }
1237
1238      let extraStates = "";
1239      if (isWarming) {
1240        extraStates += "W";
1241      }
1242      if (isCached) {
1243        extraStates += "C";
1244      }
1245      if (isClosing) {
1246        extraStates += "X";
1247      }
1248      if (isActive) {
1249        extraStates += "A";
1250      }
1251      if (isRendered) {
1252        extraStates += "R";
1253      }
1254      if (extraStates != "") {
1255        tabString += `(${extraStates})`;
1256      }
1257
1258      switch (state) {
1259        case this.STATE_LOADED: {
1260          tabString += "(loaded)";
1261          break;
1262        }
1263        case this.STATE_LOADING: {
1264          tabString += "(loading)";
1265          break;
1266        }
1267        case this.STATE_UNLOADING: {
1268          tabString += "(unloading)";
1269          break;
1270        }
1271        case this.STATE_UNLOADED: {
1272          tabString += "(unloaded)";
1273          break;
1274        }
1275      }
1276
1277      return tabString;
1278    };
1279
1280    let accum = "";
1281
1282    // This is a bit tricky to read, but what we're doing here is collapsing
1283    // identical tab states down to make the overal string shorter and easier
1284    // to read, and we move all simply unloaded tabs to the back of the list.
1285    // I.e., we turn
1286    //   "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
1287    // into
1288    //   "3:(loaded) 0...2:(unloaded)"
1289    let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
1290    let lastMatch = -1;
1291    let unloadedTabsStrings = [];
1292    for (let i = 0; i <= tabStrings.length; i++) {
1293      if (i > 0) {
1294        if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
1295          continue;
1296        }
1297
1298        if (tabStrings[lastMatch] == "(unloaded)") {
1299          if (lastMatch == i - 1) {
1300            unloadedTabsStrings.push(lastMatch.toString());
1301          } else {
1302            unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
1303          }
1304        } else if (lastMatch == i - 1) {
1305          accum += `${lastMatch}:${tabStrings[lastMatch]} `;
1306        } else {
1307          accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
1308        }
1309      }
1310
1311      lastMatch = i;
1312    }
1313
1314    if (unloadedTabsStrings.length) {
1315      accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
1316    }
1317
1318    accum += "cached: " + this.tabLayerCache.length + " ";
1319
1320    if (this._logFlags.length) {
1321      accum += `[${this._logFlags.join(",")}] `;
1322      this._logFlags = [];
1323    }
1324
1325    // It can be annoying to read through the entirety of a log string just
1326    // to check if something changed or not. So if we can tell that nothing
1327    // changed, just write "unchanged" to save the reader's time.
1328    let logString;
1329    if (this._lastLogString == accum) {
1330      accum = "unchanged";
1331    } else {
1332      this._lastLogString = accum;
1333    }
1334    logString = `ATS: ${accum}{${suffix}}`;
1335
1336    if (this._useDumpForLogging) {
1337      dump(logString + "\n");
1338    } else {
1339      Services.console.logStringMessage(logString);
1340    }
1341  }
1342
1343  noteMakingTabVisibleWithoutLayers() {
1344    // We're making the tab visible even though we haven't yet got layers for it.
1345    // It's hard to know which composite the layers will first be available in (and
1346    // the parent process might not even get MozAfterPaint delivered for it), so just
1347    // give up measuring this for now. :(
1348    TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1349  }
1350
1351  notePaint(event) {
1352    if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
1353      if (
1354        TelemetryStopwatch.running(
1355          "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1356          this.window
1357        )
1358      ) {
1359        let time = TelemetryStopwatch.timeElapsed(
1360          "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1361          this.window
1362        );
1363        if (time != -1) {
1364          TelemetryStopwatch.finish(
1365            "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1366            this.window
1367          );
1368        }
1369      }
1370      this.addMarker("AsyncTabSwitch:Composited");
1371      this.switchPaintId = -1;
1372    }
1373  }
1374
1375  noteTabRequested(tab, tabState) {
1376    if (gTabWarmingEnabled) {
1377      let warmingState = "disqualified";
1378
1379      if (this.canWarmTab(tab)) {
1380        if (tabState == this.STATE_LOADING) {
1381          warmingState = "stillLoading";
1382        } else if (tabState == this.STATE_LOADED) {
1383          warmingState = "loaded";
1384        } else if (
1385          tabState == this.STATE_UNLOADING ||
1386          tabState == this.STATE_UNLOADED
1387        ) {
1388          // At this point, if the tab's browser was being inserted
1389          // lazily, we never had a chance to warm it up, and unfortunately
1390          // there's no great way to detect that case. Those cases will
1391          // end up in the "notWarmed" bucket, along with legitimate cases
1392          // where tabs could have been warmed but weren't.
1393          warmingState = "notWarmed";
1394        }
1395      }
1396
1397      Services.telemetry
1398        .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
1399        .add(warmingState);
1400    }
1401  }
1402
1403  noteStartTabSwitch() {
1404    TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1405    TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1406
1407    if (
1408      TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)
1409    ) {
1410      TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1411    }
1412    TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1413    this.addMarker("AsyncTabSwitch:Start");
1414  }
1415
1416  noteFinishTabSwitch() {
1417    // After this point the tab has switched from the content thread's point of view.
1418    // The changes will be visible after the next refresh driver tick + composite.
1419    let time = TelemetryStopwatch.timeElapsed(
1420      "FX_TAB_SWITCH_TOTAL_E10S_MS",
1421      this.window
1422    );
1423    if (time != -1) {
1424      TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1425      this.log("DEBUG: tab switch time = " + time);
1426      this.addMarker("AsyncTabSwitch:Finish");
1427    }
1428  }
1429
1430  noteSpinnerDisplayed() {
1431    this.assert(!this.spinnerTab);
1432    let browser = this.requestedTab.linkedBrowser;
1433    this.assert(browser.isRemoteBrowser);
1434    TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1435    // We have a second, similar probe for capturing recordings of
1436    // when the spinner is displayed for very long periods.
1437    TelemetryStopwatch.start(
1438      "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1439      this.window
1440    );
1441    this.addMarker("AsyncTabSwitch:SpinnerShown");
1442    Services.telemetry
1443      .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1444      .add(this._loadTimerClearedBy);
1445    if (AppConstants.NIGHTLY_BUILD) {
1446      Services.obs.notifyObservers(null, "tabswitch-spinner");
1447    }
1448  }
1449
1450  noteSpinnerHidden() {
1451    this.assert(this.spinnerTab);
1452    this.log(
1453      "DEBUG: spinner time = " +
1454        TelemetryStopwatch.timeElapsed(
1455          "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
1456          this.window
1457        )
1458    );
1459    TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1460    TelemetryStopwatch.finish(
1461      "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1462      this.window
1463    );
1464    this.addMarker("AsyncTabSwitch:SpinnerHidden");
1465    // we do not get a onPaint after displaying the spinner
1466    this._loadTimerClearedBy = "none";
1467  }
1468}
1469