1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 ChromeUtils.defineESModuleGetters(lazy, {
12 PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
15 XPCOMUtils.defineLazyPreferenceGetter(
18 "browser.tabs.remote.warmup.enabled"
20 XPCOMUtils.defineLazyPreferenceGetter(
23 "browser.tabs.remote.warmup.maxTabs"
25 XPCOMUtils.defineLazyPreferenceGetter(
27 "gTabWarmingUnloadDelayMs",
28 "browser.tabs.remote.warmup.unloadDelayMs"
30 XPCOMUtils.defineLazyPreferenceGetter(
33 "browser.tabs.remote.tabCacheSize"
35 XPCOMUtils.defineLazyPreferenceGetter(
38 "browser.tabs.remote.unloadDelayMs",
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.
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.
56 * The following general principles have guided the design:
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.
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.
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
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.
80 export class AsyncTabSwitcher {
81 constructor(tabbrowser) {
84 // How long to wait for a tab's layers to load. After this
85 // time elapses, we're free to put up the spinner and start
86 // trying to load a different tab.
87 this.TAB_SWITCH_TIMEOUT = 400; // ms
89 // When the user hasn't switched tabs for this long, we unload
90 // layers for all tabs that aren't in use.
91 this.UNLOAD_DELAY = lazy.gTabUnloadDelay; // ms
93 // The next three tabs form the principal state variables.
94 // See the assertions in postActions for their invariants.
96 // Tab the user requested most recently.
97 this.requestedTab = tabbrowser.selectedTab;
99 // Tab we're currently trying to load.
100 this.loadingTab = null;
102 // We show this tab in case the requestedTab hasn't loaded yet.
103 this.lastVisibleTab = tabbrowser.selectedTab;
105 // Auxilliary state variables:
107 this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
108 this.spinnerTab = null; // Tab showing a spinner.
109 this.blankTab = null; // Tab showing blank.
110 this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
112 this.tabbrowser = tabbrowser;
113 this.window = tabbrowser.ownerGlobal;
114 this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
115 this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
117 // Map from tabs to STATE_* (below).
118 this.tabState = new Map();
120 // True if we're in the midst of switching tabs.
121 this.switchInProgress = false;
123 // Transaction id for the composite that will show the requested
124 // tab for the first tab after a tab switch.
125 // Set to -1 when we're not waiting for notification of a
127 this.switchPaintId = -1;
129 // Set of tabs that might be visible right now. We maintain
130 // this set because we can't be sure when a tab is actually
131 // drawn. A tab is added to this set when we ask to make it
132 // visible. All tabs but the most recently shown tab are
133 // removed from the set upon MozAfterPaint.
134 this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
136 // This holds onto the set of tabs that we've been asked to warm up,
137 // and tabs are evicted once they're done loading or are unloaded.
138 this.warmingTabs = new WeakSet();
140 this.STATE_UNLOADED = 0;
141 this.STATE_LOADING = 1;
142 this.STATE_LOADED = 2;
143 this.STATE_UNLOADING = 3;
145 // re-entrancy guard:
146 this._processing = false;
148 // For telemetry, keeps track of what most recently cleared
149 // the loadTimer, which can tell us something about the cause
150 // of tab switch spinners.
151 this._loadTimerClearedBy = "none";
153 this._useDumpForLogging = false;
154 this._logInit = false;
157 this.window.addEventListener("MozAfterPaint", this);
158 this.window.addEventListener("MozLayerTreeReady", this);
159 this.window.addEventListener("MozLayerTreeCleared", this);
160 this.window.addEventListener("TabRemotenessChange", this);
161 this.window.addEventListener("SwapDocShells", this, true);
162 this.window.addEventListener("EndSwapDocShells", this, true);
163 this.window.document.addEventListener("visibilitychange", this);
165 let initialTab = this.requestedTab;
166 let initialBrowser = initialTab.linkedBrowser;
169 !initialBrowser.isRemoteBrowser ||
170 initialBrowser.frameLoader.remoteTab?.hasLayers;
172 // If we minimized the window before the switcher was activated,
173 // we might have set the preserveLayers flag for the current
174 // browser. Let's clear it.
175 initialBrowser.preserveLayers(false);
177 if (!this.windowHidden) {
178 this.log("Initial tab is loaded?: " + tabIsLoaded);
181 tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING
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);
193 if (this.unloadTimer) {
194 this.clearTimer(this.unloadTimer);
195 this.unloadTimer = null;
197 if (this.loadTimer) {
198 this.clearTimer(this.loadTimer);
199 this.loadTimer = null;
202 this.window.removeEventListener("MozAfterPaint", this);
203 this.window.removeEventListener("MozLayerTreeReady", this);
204 this.window.removeEventListener("MozLayerTreeCleared", this);
205 this.window.removeEventListener("TabRemotenessChange", this);
206 this.window.removeEventListener("SwapDocShells", this, true);
207 this.window.removeEventListener("EndSwapDocShells", this, true);
208 this.window.document.removeEventListener("visibilitychange", this);
210 this.tabbrowser._switcher = null;
213 // Wraps nsITimer. Must not use the vanilla setTimeout and
214 // clearTimeout, because they will be blocked by nsIPromptService
216 setTimer(callback, timeout) {
221 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
222 timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
231 let state = this.tabState.get(tab);
233 // As an optimization, we lazily evaluate the state of tabs
234 // that we've never seen before. Once we've figured it out,
235 // we stash it in our state map.
236 if (state === undefined) {
237 state = this.STATE_UNLOADED;
239 if (tab && tab.linkedPanel) {
240 let b = tab.linkedBrowser;
241 if (b.renderLayers && b.hasLayers) {
242 state = this.STATE_LOADED;
243 } else if (b.renderLayers && !b.hasLayers) {
244 state = this.STATE_LOADING;
245 } else if (!b.renderLayers && b.hasLayers) {
246 state = this.STATE_UNLOADING;
250 this.setTabStateNoAction(tab, state);
256 setTabStateNoAction(tab, state) {
257 if (state == this.STATE_UNLOADED) {
258 this.tabState.delete(tab);
260 this.tabState.set(tab, state);
264 setTabState(tab, state) {
265 if (state == this.getTabState(tab)) {
269 this.setTabStateNoAction(tab, state);
271 let browser = tab.linkedBrowser;
272 let { remoteTab } = browser.frameLoader;
273 if (state == this.STATE_LOADING) {
274 this.assert(!this.windowHidden);
276 // If we're not in the process of warming this tab, we
277 // don't need to delay activating its DocShell.
278 if (!this.warmingTabs.has(tab)) {
279 browser.docShellIsActive = true;
283 browser.renderLayers = true;
284 remoteTab.priorityHint = true;
286 if (browser.hasLayers) {
287 this.onLayersReady(browser);
289 } else if (state == this.STATE_UNLOADING) {
291 // Setting the docShell to be inactive will also cause it
292 // to stop rendering layers.
293 browser.docShellIsActive = false;
295 remoteTab.priorityHint = false;
297 if (!browser.hasLayers) {
298 this.onLayersCleared(browser);
300 } else if (state == this.STATE_LOADED) {
301 this.maybeActivateDocShell(tab);
304 if (!tab.linkedBrowser.isRemoteBrowser) {
305 // setTabState is potentially re-entrant, so we must re-get the state for
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.
314 nonRemoteState == this.STATE_UNLOADED ||
315 nonRemoteState == this.STATE_LOADED
321 return this.window.document.hidden;
324 get tabLayerCache() {
325 return this.tabbrowser._tabLayerCache;
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);
340 this.getTabState(this.requestedTab) == this.STATE_LOADED
345 this.window.document.commandDispatcher.unlock();
347 let event = new this.window.CustomEvent("TabSwitchDone", {
351 this.tabbrowser.dispatchEvent(event);
354 // This function is called after all the main state changes to
355 // make sure we display the right tab.
357 let requestedTabState = this.getTabState(this.requestedTab);
358 let requestedBrowser = this.requestedTab.linkedBrowser;
360 // It is often more desirable to show a blank tab when appropriate than
361 // the tab switch spinner - especially since the spinner is usually
362 // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
363 // tab switch. We can hide this lag, and hide the time being spent
364 // constructing BrowserChild's, layer trees, etc, by showing a blank
365 // tab instead and focusing it immediately.
366 let shouldBeBlank = false;
367 if (requestedBrowser.isRemoteBrowser) {
368 // If a tab is remote and the window is not minimized, we can show a
369 // blank tab instead of a spinner in the following cases:
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.
376 // For (2), "finished loading a non-local-about: page" is
377 // determined by the busy state on the tab element and checking
378 // if the loaded URI is local.
379 let isBusy = this.requestedTab.hasAttribute("busy");
380 let isLocalAbout = requestedBrowser.currentURI.schemeIs("about");
381 let hasSufficientlyLoaded = !isBusy && !isLocalAbout;
383 let fl = requestedBrowser.frameLoader;
385 !this.windowHidden &&
387 (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
389 if (this.logging()) {
390 let flag = shouldBeBlank ? "blank" : "nonblank";
397 fl.remoteTab ? fl.remoteTab.hasPresented : 0
402 if (requestedBrowser.isRemoteBrowser) {
403 this.addLogFlag("isRemote");
406 // Figure out which tab we actually want visible right now.
409 requestedTabState != this.STATE_LOADED &&
410 this.lastVisibleTab &&
414 // If we can't show the requestedTab, and lastVisibleTab is
415 // available, show it.
416 showTab = this.lastVisibleTab;
418 // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
419 showTab = this.requestedTab;
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) {
430 this.blankTab.linkedBrowser.removeAttribute("blank");
432 this.blankTab = showTab;
433 this.blankTab.linkedBrowser.setAttribute("blank", "true");
436 // Show or hide the spinner as needed.
438 this.getTabState(showTab) != this.STATE_LOADED &&
439 !this.windowHidden &&
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");
452 this.noteSpinnerDisplayed();
454 this.spinnerTab = showTab;
455 this.tabbrowser.tabpanels.toggleAttribute("pendingpaint", true);
456 this.spinnerTab.linkedBrowser.toggleAttribute("pendingpaint", true);
459 // Switch to the tab we've decided to make visible.
460 if (this.visibleTab !== showTab) {
461 this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
462 this.visibleTab = showTab;
464 this.maybeVisibleTabs.add(showTab);
466 let tabpanels = this.tabbrowser.tabpanels;
467 let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
468 let index = Array.prototype.indexOf.call(tabpanels.children, showPanel);
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
477 this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
479 this.noteMakingTabVisibleWithoutLayers();
482 this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
483 this.window.gURLBar.afterTabSwitchFocusChange();
484 this.maybeActivateDocShell(this.requestedTab);
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;
493 this.visibleTab._visuallySelected = true;
496 this.lastVisibleTab = this.visibleTab;
501 dump("Assertion failure\n" + Error().stack);
503 // Don't break a user's browser if an assertion fails.
504 if (AppConstants.DEBUG) {
505 throw new Error("Assertion failure");
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;
521 // We've decided to try to load requestedTab.
523 this.assert(!this.loadTimer);
524 this.assert(!this.windowHidden);
526 // loadingTab can be non-null here if we timed out loading the current tab.
527 // In that case we just overwrite it with a different tab; it's had its chance.
528 this.loadingTab = this.requestedTab;
529 this.log("Loading tab " + this.tinfo(this.loadingTab));
531 this.loadTimer = this.setTimer(
532 () => this.handleEvent({ type: "loadTimeout" }),
533 this.TAB_SWITCH_TIMEOUT
535 this.setTabState(this.requestedTab, this.STATE_LOADING);
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);
548 tab == this.requestedTab &&
549 canCheckDocShellState &&
550 state == this.STATE_LOADED &&
551 !browser.docShellIsActive &&
554 browser.docShellIsActive = true;
556 "Set requested tab docshell to active and preserveLayers to false"
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);
565 // This function runs before every event. It fixes up the state
566 // to account for closed tabs.
568 this.assert(this.tabbrowser._switcher);
569 this.assert(this.tabbrowser._switcher === this);
571 for (let i = 0; i < this.tabLayerCache.length; i++) {
572 let tab = this.tabLayerCache[i];
573 if (!tab.linkedBrowser) {
574 this.tabState.delete(tab);
575 this.tabLayerCache.splice(i, 1);
580 for (let [tab] of this.tabState) {
581 if (!tab.linkedBrowser) {
582 this.tabState.delete(tab);
587 if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
588 this.lastVisibleTab = null;
590 if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
591 this.lastPrimaryTab = null;
593 if (this.blankTab && !this.blankTab.linkedBrowser) {
594 this.blankTab = null;
596 if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
597 this.noteSpinnerHidden();
598 this.spinnerTab = null;
600 if (this.loadingTab && !this.loadingTab.linkedBrowser) {
601 this.maybeClearLoadTimer("preActions");
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
609 postActions(eventString) {
610 // Once we finish loading loadingTab, we null it out. So the state should
611 // always be LOADING.
614 this.getTabState(this.loadingTab) == this.STATE_LOADING
617 // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
618 // the timer is set only when we're loading something.
619 this.assert(!this.loadTimer || this.loadingTab);
620 this.assert(!this.loadingTab || this.loadTimer);
622 // If we're switching to a non-remote tab, there's no need to wait
623 // for it to send layers to the compositor, as this will happen
624 // synchronously. Clearing this here means that in the next step,
625 // we can load the non-remote browser immediately.
626 if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
627 this.maybeClearLoadTimer("postActions");
630 // If we're not loading anything, try loading the requested tab.
631 let stateOfRequestedTab = this.getTabState(this.requestedTab);
634 !this.windowHidden &&
635 (stateOfRequestedTab == this.STATE_UNLOADED ||
636 stateOfRequestedTab == this.STATE_UNLOADING ||
637 this.warmingTabs.has(this.requestedTab))
639 this.assert(stateOfRequestedTab != this.STATE_LOADED);
640 this.loadRequestedTab();
643 let numBackgroundCached = 0;
644 for (let tab of this.tabLayerCache) {
645 if (tab !== this.requestedTab) {
646 numBackgroundCached++;
650 // See how many tabs still have work to do.
653 for (let [tab, state] of this.tabState) {
654 if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
659 state == this.STATE_LOADED &&
660 tab !== this.requestedTab &&
661 !this.tabLayerCache.includes(tab)
665 if (tab !== this.visibleTab) {
669 if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
674 this.updateDisplay();
676 // It's possible for updateDisplay to trigger one of our own event
677 // handlers, which might cause finish() to already have been called.
678 // Check for that before calling finish() again.
679 if (!this.tabbrowser._switcher) {
683 this.maybeFinishTabSwitch();
685 if (numBackgroundCached > 0) {
686 this.deactivateCachedBackgroundTabs();
689 if (numWarming > lazy.gTabWarmingMax) {
690 this.logState("Hit tabWarmingMax");
691 if (this.unloadTimer) {
692 this.clearTimer(this.unloadTimer);
694 this.unloadNonRequiredTabs();
697 if (numPending == 0) {
701 this.logState("/" + eventString);
704 // Fires when we're ready to unload unused tabs.
706 this.unloadTimer = null;
707 this.unloadNonRequiredTabs();
710 deactivateCachedBackgroundTabs() {
711 for (let tab of this.tabLayerCache) {
712 if (tab !== this.requestedTab) {
713 let browser = tab.linkedBrowser;
714 browser.preserveLayers(true);
715 browser.docShellIsActive = false;
720 // If there are any non-visible and non-requested tabs in
721 // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
722 // up the unloadTimer to run onUnloadTimeout if there are still
723 // tabs in the process of unloading.
724 unloadNonRequiredTabs() {
725 this.warmingTabs = new WeakSet();
728 // Unload any tabs that can be unloaded.
729 for (let [tab, state] of this.tabState) {
730 if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
734 let isInLayerCache = this.tabLayerCache.includes(tab);
737 state == this.STATE_LOADED &&
738 !this.maybeVisibleTabs.has(tab) &&
739 tab !== this.lastVisibleTab &&
740 tab !== this.loadingTab &&
741 tab !== this.requestedTab &&
744 this.setTabState(tab, this.STATE_UNLOADING);
748 state != this.STATE_UNLOADED &&
749 tab !== this.requestedTab &&
757 // Keep the timer going since there may be more tabs to unload.
758 this.unloadTimer = this.setTimer(
759 () => this.handleEvent({ type: "unloadTimeout" }),
765 // Fires when an ongoing load has taken too long.
767 this.maybeClearLoadTimer("onLoadTimeout");
770 // Fires when the layers become available for a tab.
771 onLayersReady(browser) {
772 let tab = this.tabbrowser.getTabForBrowser(browser);
774 // We probably got a layer update from a tab that got before
775 // the switcher was created, or for browser that's not being
776 // tracked by the async tab switcher (like the preloaded about:newtab).
780 this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
782 this.getTabState(tab) == this.STATE_LOADING ||
783 this.getTabState(tab) == this.STATE_LOADED
785 this.setTabState(tab, this.STATE_LOADED);
788 if (this.loadingTab === tab) {
789 this.maybeClearLoadTimer("onLayersReady");
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
799 this.switchPaintId != -1,
800 event.transactionId >= this.switchPaintId
802 this.notePaint(event);
803 this.maybeVisibleTabs.clear();
806 // Called when we're done clearing the layers for a tab.
807 onLayersCleared(browser) {
808 let tab = this.tabbrowser.getTabForBrowser(browser);
812 this.logState(`onLayersCleared(${tab._tPos})`);
814 this.getTabState(tab) == this.STATE_UNLOADING ||
815 this.getTabState(tab) == this.STATE_UNLOADED
817 this.setTabState(tab, this.STATE_UNLOADED);
820 // Called when a tab switches from remote to non-remote. In this case
821 // a MozLayerTreeReady notification that we requested may never fire,
822 // so we need to simulate it.
823 onRemotenessChange(tab) {
825 `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`
827 if (!tab.linkedBrowser.isRemoteBrowser) {
828 if (this.getTabState(tab) == this.STATE_LOADING) {
829 this.onLayersReady(tab.linkedBrowser);
830 } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
831 this.onLayersCleared(tab.linkedBrowser);
833 } else if (this.getTabState(tab) == this.STATE_LOADED) {
834 // A tab just changed from non-remote to remote, which means
835 // that it's gone back into the STATE_LOADING state until
836 // it sends up a layer tree.
837 this.setTabState(tab, this.STATE_LOADING);
842 if (this.lastVisibleTab == tab) {
843 this.handleEvent({ type: "tabRemoved", tab });
847 // Called when a tab has been removed, and the browser node is
848 // about to be removed from the DOM.
850 this.lastVisibleTab = null;
853 onVisibilityChange() {
854 if (this.windowHidden) {
855 for (let [tab, state] of this.tabState) {
856 if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
860 if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
861 this.setTabState(tab, this.STATE_UNLOADING);
864 this.maybeClearLoadTimer("onSizeModeOrOcc");
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);
872 onSwapDocShells(ourBrowser, otherBrowser) {
873 // This event fires before the swap. ourBrowser is from
874 // our window. We save the state of otherBrowser since ourBrowser
875 // needs to take on that state at the end of the swap.
877 let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
879 if (otherTabbrowser && otherTabbrowser._switcher) {
880 let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
881 let otherSwitcher = otherTabbrowser._switcher;
882 otherState = otherSwitcher.getTabState(otherTab);
884 otherState = otherBrowser.docShellIsActive
886 : this.STATE_UNLOADED;
889 this.swapMap = new WeakMap();
891 this.swapMap.set(otherBrowser, {
896 onEndSwapDocShells(ourBrowser, otherBrowser) {
897 // The swap has happened. We reset the loadingTab in
898 // case it has been swapped. We also set ourBrowser's state
899 // to whatever otherBrowser's state was before the swap.
901 // Clearing the load timer means that we will
902 // immediately display a spinner if ourBrowser isn't
903 // ready yet. Typically it will already be ready
904 // though. If it's not, we're probably in a new window,
905 // in which case we have no other tabs to display anyway.
906 this.maybeClearLoadTimer("onEndSwapDocShells");
908 let { state: otherState } = this.swapMap.get(otherBrowser);
910 this.swapMap.delete(otherBrowser);
912 let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
914 this.setTabStateNoAction(ourTab, otherState);
919 * Check if the browser should be deactivated. If the browser is a print preivew or
920 * PiP browser then we won't deactive it.
921 * @param browser The browser to check if it should be deactivated
922 * @returns false if a print preview or PiP browser else true
924 shouldDeactivateDocShell(browser) {
926 this.tabbrowser._printPreviewBrowsers.has(browser) ||
927 lazy.PictureInPicture.isOriginatingBrowser(browser)
931 shouldActivateDocShell(browser) {
932 let tab = this.tabbrowser.getTabForBrowser(browser);
933 let state = this.getTabState(tab);
934 return state == this.STATE_LOADING || state == this.STATE_LOADED;
937 activateBrowserForPrintPreview(browser) {
938 let tab = this.tabbrowser.getTabForBrowser(browser);
939 let state = this.getTabState(tab);
940 if (state != this.STATE_LOADING && state != this.STATE_LOADED) {
941 this.setTabState(tab, this.STATE_LOADING);
943 "Activated browser " + this.tinfo(tab) + " for print preview"
949 if (!lazy.gTabWarmingEnabled) {
957 // If the tab is not yet inserted, closing, not remote,
958 // crashed, already visible, or already requested, warming
959 // up the tab makes no sense.
964 !tab.linkedBrowser.isRemoteBrowser ||
965 !tab.linkedBrowser.frameLoader.remoteTab
974 if (this.canWarmTab(tab)) {
975 // Tabs that are already in STATE_LOADING or STATE_LOADED
976 // have no need to be warmed up.
977 let state = this.getTabState(tab);
978 if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) {
987 this.warmingTabs.delete(tab);
991 if (!this.shouldWarmTab(tab)) {
995 this.logState("warmupTab " + this.tinfo(tab));
997 this.warmingTabs.add(tab);
998 this.setTabState(tab, this.STATE_LOADING);
999 this.queueUnload(lazy.gTabWarmingUnloadDelayMs);
1002 cleanUpTabAfterEviction(tab) {
1003 this.assert(tab !== this.requestedTab);
1004 let browser = tab.linkedBrowser;
1006 browser.preserveLayers(false);
1008 this.setTabState(tab, this.STATE_UNLOADING);
1011 evictOldestTabFromCache() {
1012 let tab = this.tabLayerCache.shift();
1013 this.cleanUpTabAfterEviction(tab);
1016 maybePromoteTabInLayerCache(tab) {
1018 lazy.gTabCacheSize > 1 &&
1019 tab.linkedBrowser.isRemoteBrowser &&
1020 tab.linkedBrowser.currentURI.spec != "about:blank"
1022 let tabIndex = this.tabLayerCache.indexOf(tab);
1024 if (tabIndex != -1) {
1025 this.tabLayerCache.splice(tabIndex, 1);
1028 this.tabLayerCache.push(tab);
1030 if (this.tabLayerCache.length > lazy.gTabCacheSize) {
1031 this.evictOldestTabFromCache();
1036 // Called when the user asks to switch to a given tab.
1038 if (tab === this.requestedTab) {
1042 let tabState = this.getTabState(tab);
1043 this.noteTabRequested(tab, tabState);
1045 this.logState("requestTab " + this.tinfo(tab));
1046 this.startTabSwitch();
1048 let oldBrowser = this.requestedTab.linkedBrowser;
1049 oldBrowser.deprioritize();
1050 this.requestedTab = tab;
1051 if (tabState == this.STATE_LOADED) {
1052 this.maybeVisibleTabs.clear();
1055 tab.linkedBrowser.setAttribute("primary", "true");
1056 if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1057 this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1059 this.lastPrimaryTab = tab;
1061 this.queueUnload(this.UNLOAD_DELAY);
1064 queueUnload(unloadTimeout) {
1065 this.handleEvent({ type: "queueUnload", unloadTimeout });
1068 onQueueUnload(unloadTimeout) {
1069 if (this.unloadTimer) {
1070 this.clearTimer(this.unloadTimer);
1072 this.unloadTimer = this.setTimer(
1073 () => this.handleEvent({ type: "unloadTimeout" }),
1078 handleEvent(event, delayed = false) {
1079 if (this._processing) {
1080 this.setTimer(() => this.handleEvent(event, true), 0);
1083 if (delayed && this.tabbrowser._switcher != this) {
1084 // if we delayed processing this event, we might be out of date, in which
1085 // case we drop the delayed events
1088 this._processing = true;
1092 switch (event.type) {
1094 this.onQueueUnload(event.unloadTimeout);
1096 case "unloadTimeout":
1097 this.onUnloadTimeout();
1100 this.onLoadTimeout();
1103 this.onTabRemovedImpl(event.tab);
1105 case "MozLayerTreeReady": {
1106 let browser = event.originalTarget;
1107 if (!browser.renderLayers) {
1108 // By the time we handle this event, it's possible that something
1109 // else has already set renderLayers to false, in which case this
1110 // event is stale and we can safely ignore it.
1113 this.onLayersReady(browser);
1116 case "MozAfterPaint":
1117 this.onPaint(event);
1119 case "MozLayerTreeCleared": {
1120 let browser = event.originalTarget;
1121 if (browser.renderLayers) {
1122 // By the time we handle this event, it's possible that something
1123 // else has already set renderLayers to true, in which case this
1124 // event is stale and we can safely ignore it.
1127 this.onLayersCleared(browser);
1130 case "TabRemotenessChange":
1131 this.onRemotenessChange(event.target);
1133 case "visibilitychange":
1134 this.onVisibilityChange();
1136 case "SwapDocShells":
1137 this.onSwapDocShells(event.originalTarget, event.detail);
1139 case "EndSwapDocShells":
1140 this.onEndSwapDocShells(event.originalTarget, event.detail);
1144 this.postActions(event.type);
1146 this._processing = false;
1151 * Telemetry and Profiler related helpers for recording tab switch
1156 this.noteStartTabSwitch();
1157 this.switchInProgress = true;
1161 * Something has occurred that might mean that we've completed
1162 * the tab switch (layers are ready, paints are done, spinners
1163 * are hidden). This checks to make sure all conditions are
1164 * satisfied, and then records the tab switch as finished.
1166 maybeFinishTabSwitch() {
1168 this.switchInProgress &&
1169 this.requestedTab &&
1170 (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1171 this.requestedTab === this.blankTab)
1173 if (this.requestedTab !== this.blankTab) {
1174 this.maybePromoteTabInLayerCache(this.requestedTab);
1177 this.noteFinishTabSwitch();
1178 this.switchInProgress = false;
1180 let event = new this.window.CustomEvent("TabSwitched", {
1183 tab: this.requestedTab,
1186 this.tabbrowser.dispatchEvent(event);
1191 * Debug related logging for switcher.
1194 if (this._useDumpForLogging) {
1197 if (this._logInit) {
1198 return this._shouldLog;
1200 let result = Services.prefs.getBoolPref(
1201 "browser.tabs.remote.logSwitchTiming",
1204 this._shouldLog = result;
1205 this._logInit = true;
1206 return this._shouldLog;
1211 return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1217 if (!this.logging()) {
1220 if (this._useDumpForLogging) {
1223 Services.console.logStringMessage(s);
1227 addLogFlag(flag, ...subFlags) {
1228 if (this.logging()) {
1229 if (subFlags.length) {
1230 flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
1232 this._logFlags.push(flag);
1237 if (!this.logging()) {
1241 let getTabString = tab => {
1244 let state = this.getTabState(tab);
1245 let isWarming = this.warmingTabs.has(tab);
1246 let isCached = this.tabLayerCache.includes(tab);
1247 let isClosing = tab.closing;
1248 let linkedBrowser = tab.linkedBrowser;
1249 let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1250 let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1253 lazy.PictureInPicture.isOriginatingBrowser(linkedBrowser);
1255 if (tab === this.lastVisibleTab) {
1258 if (tab === this.loadingTab) {
1261 if (tab === this.requestedTab) {
1264 if (tab === this.blankTab) {
1267 if (this.maybeVisibleTabs.has(tab)) {
1271 let extraStates = "";
1290 if (extraStates != "") {
1291 tabString += `(${extraStates})`;
1295 case this.STATE_LOADED: {
1296 tabString += "(loaded)";
1299 case this.STATE_LOADING: {
1300 tabString += "(loading)";
1303 case this.STATE_UNLOADING: {
1304 tabString += "(unloading)";
1307 case this.STATE_UNLOADED: {
1308 tabString += "(unloaded)";
1318 // This is a bit tricky to read, but what we're doing here is collapsing
1319 // identical tab states down to make the overal string shorter and easier
1320 // to read, and we move all simply unloaded tabs to the back of the list.
1322 // "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
1324 // "3:(loaded) 0...2:(unloaded)"
1325 let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
1327 let unloadedTabsStrings = [];
1328 for (let i = 0; i <= tabStrings.length; i++) {
1330 if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
1334 if (tabStrings[lastMatch] == "(unloaded)") {
1335 if (lastMatch == i - 1) {
1336 unloadedTabsStrings.push(lastMatch.toString());
1338 unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
1340 } else if (lastMatch == i - 1) {
1341 accum += `${lastMatch}:${tabStrings[lastMatch]} `;
1343 accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
1350 if (unloadedTabsStrings.length) {
1351 accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
1354 accum += "cached: " + this.tabLayerCache.length + " ";
1356 if (this._logFlags.length) {
1357 accum += `[${this._logFlags.join(",")}] `;
1358 this._logFlags = [];
1361 // It can be annoying to read through the entirety of a log string just
1362 // to check if something changed or not. So if we can tell that nothing
1363 // changed, just write "unchanged" to save the reader's time.
1365 if (this._lastLogString == accum) {
1366 accum = "unchanged";
1368 this._lastLogString = accum;
1370 logString = `ATS: ${accum}{${suffix}}`;
1372 if (this._useDumpForLogging) {
1373 dump(logString + "\n");
1375 Services.console.logStringMessage(logString);
1379 noteMakingTabVisibleWithoutLayers() {
1380 // We're making the tab visible even though we haven't yet got layers for it.
1381 // It's hard to know which composite the layers will first be available in (and
1382 // the parent process might not even get MozAfterPaint delivered for it), so just
1383 // give up measuring this for now. :(
1384 Glean.performanceInteraction.tabSwitchComposite.cancel(
1385 this._tabswitchTimerId
1387 this._tabswitchTimerId = null;
1391 if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
1392 if (this._tabswitchTimerId) {
1393 Glean.performanceInteraction.tabSwitchComposite.stopAndAccumulate(
1394 this._tabswitchTimerId
1396 this._tabswitchTimerId = null;
1398 let { innerWindowId } = this.window.windowGlobalChild;
1399 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited", {
1402 this.switchPaintId = -1;
1406 noteTabRequested(tab, tabState) {
1407 if (lazy.gTabWarmingEnabled) {
1408 let warmingState = "disqualified";
1410 if (this.canWarmTab(tab)) {
1411 if (tabState == this.STATE_LOADING) {
1412 warmingState = "stillLoading";
1413 } else if (tabState == this.STATE_LOADED) {
1414 warmingState = "loaded";
1416 tabState == this.STATE_UNLOADING ||
1417 tabState == this.STATE_UNLOADED
1419 // At this point, if the tab's browser was being inserted
1420 // lazily, we never had a chance to warm it up, and unfortunately
1421 // there's no great way to detect that case. Those cases will
1422 // end up in the "notWarmed" bucket, along with legitimate cases
1423 // where tabs could have been warmed but weren't.
1424 warmingState = "notWarmed";
1429 .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
1434 noteStartTabSwitch() {
1435 TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1436 TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1438 if (this._tabswitchTimerId) {
1439 Glean.performanceInteraction.tabSwitchComposite.cancel(
1440 this._tabswitchTimerId
1443 this._tabswitchTimerId =
1444 Glean.performanceInteraction.tabSwitchComposite.start();
1445 let { innerWindowId } = this.window.windowGlobalChild;
1446 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start", { innerWindowId });
1449 noteFinishTabSwitch() {
1450 // After this point the tab has switched from the content thread's point of view.
1451 // The changes will be visible after the next refresh driver tick + composite.
1452 let time = TelemetryStopwatch.timeElapsed(
1453 "FX_TAB_SWITCH_TOTAL_E10S_MS",
1457 TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1458 this.log("DEBUG: tab switch time = " + time);
1459 let { innerWindowId } = this.window.windowGlobalChild;
1460 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish", { innerWindowId });
1464 noteSpinnerDisplayed() {
1465 this.assert(!this.spinnerTab);
1466 let browser = this.requestedTab.linkedBrowser;
1467 this.assert(browser.isRemoteBrowser);
1468 TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1469 // We have a second, similar probe for capturing recordings of
1470 // when the spinner is displayed for very long periods.
1471 TelemetryStopwatch.start(
1472 "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1475 let { innerWindowId } = this.window.windowGlobalChild;
1476 ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown", {
1480 .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1481 .add(this._loadTimerClearedBy);
1482 if (AppConstants.NIGHTLY_BUILD) {
1483 Services.obs.notifyObservers(null, "tabswitch-spinner");
1487 noteSpinnerHidden() {
1488 this.assert(this.spinnerTab);
1490 "DEBUG: spinner time = " +
1491 TelemetryStopwatch.timeElapsed(
1492 "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
1496 TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1497 TelemetryStopwatch.finish(
1498 "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1501 let { innerWindowId } = this.window.windowGlobalChild;
1502 ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden", {
1505 // we do not get a onPaint after displaying the spinner
1506 this._loadTimerClearedBy = "none";