tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

AsyncTabSwitcher.sys.mjs (46673B)


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