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/. */
8 var EXPORTED_SYMBOLS = ["AsyncTabSwitcher"];
10 const { XPCOMUtils } = ChromeUtils.import(
11 "resource://gre/modules/XPCOMUtils.jsm"
13 XPCOMUtils.defineLazyModuleGetters(this, {
14 AppConstants: "resource://gre/modules/AppConstants.jsm",
15 Services: "resource://gre/modules/Services.jsm",
18 XPCOMUtils.defineLazyPreferenceGetter(
21 "browser.tabs.remote.warmup.enabled"
23 XPCOMUtils.defineLazyPreferenceGetter(
26 "browser.tabs.remote.warmup.maxTabs"
28 XPCOMUtils.defineLazyPreferenceGetter(
30 "gTabWarmingUnloadDelayMs",
31 "browser.tabs.remote.warmup.unloadDelayMs"
33 XPCOMUtils.defineLazyPreferenceGetter(
36 "browser.tabs.remote.tabCacheSize"
40 * The tab switcher is responsible for asynchronously switching
41 * tabs in e10s. It waits until the new tab is ready (i.e., the
42 * layer tree is available) before switching to it. Then it
43 * unloads the layer tree for the old tab.
45 * The tab switcher is a state machine. For each tab, it
46 * maintains state about whether the layer tree for the tab is
47 * available, being loaded, being unloaded, or unavailable. It
48 * also keeps track of the tab currently being displayed, the tab
49 * it's trying to load, and the tab the user has asked to switch
50 * to. The switcher object is created upon tab switch. It is
51 * released when there are no pending tabs to load or unload.
53 * The following general principles have guided the design:
55 * 1. We only request one layer tree at a time. If the user
56 * switches to a different tab while waiting, we don't request
57 * the new layer tree until the old tab has loaded or timed out.
59 * 2. If loading the layers for a tab times out, we show the
60 * spinner and possibly request the layer tree for another tab if
61 * the user has requested one.
63 * 3. We discard layer trees on a delay. This way, if the user is
64 * switching among the same tabs frequently, we don't continually
67 * It's important that we always show either the spinner or a tab
68 * whose layers are available. Otherwise the compositor will draw
69 * an entirely black frame, which is very jarring. To ensure this
70 * never happens when switching away from a tab, we assume the
71 * old tab might still be drawn until a MozAfterPaint event
72 * occurs. Because layout and compositing happen asynchronously,
73 * we don't have any other way of knowing when the switch
74 * actually takes place. Therefore, we don't unload the old tab
75 * until the next MozAfterPaint event.
77 class AsyncTabSwitcher {
78 constructor(tabbrowser) {
81 // How long to wait for a tab's layers to load. After this
82 // time elapses, we're free to put up the spinner and start
83 // trying to load a different tab.
84 this.TAB_SWITCH_TIMEOUT = 400; // ms
86 // When the user hasn't switched tabs for this long, we unload
87 // layers for all tabs that aren't in use.
88 this.UNLOAD_DELAY = 300; // ms
90 // The next three tabs form the principal state variables.
91 // See the assertions in postActions for their invariants.
93 // Tab the user requested most recently.
94 this.requestedTab = tabbrowser.selectedTab;
96 // Tab we're currently trying to load.
97 this.loadingTab = null;
99 // We show this tab in case the requestedTab hasn't loaded yet.
100 this.lastVisibleTab = tabbrowser.selectedTab;
102 // Auxilliary state variables:
104 this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
105 this.spinnerTab = null; // Tab showing a spinner.
106 this.blankTab = null; // Tab showing blank.
107 this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
109 this.tabbrowser = tabbrowser;
110 this.window = tabbrowser.ownerGlobal;
111 this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
112 this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
114 // Map from tabs to STATE_* (below).
115 this.tabState = new Map();
117 // True if we're in the midst of switching tabs.
118 this.switchInProgress = false;
120 // Transaction id for the composite that will show the requested
121 // tab for the first tab after a tab switch.
122 // Set to -1 when we're not waiting for notification of a
124 this.switchPaintId = -1;
126 // Set of tabs that might be visible right now. We maintain
127 // this set because we can't be sure when a tab is actually
128 // drawn. A tab is added to this set when we ask to make it
129 // visible. All tabs but the most recently shown tab are
130 // removed from the set upon MozAfterPaint.
131 this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
133 // This holds onto the set of tabs that we've been asked to warm up,
134 // and tabs are evicted once they're done loading or are unloaded.
135 this.warmingTabs = new WeakSet();
137 this.STATE_UNLOADED = 0;
138 this.STATE_LOADING = 1;
139 this.STATE_LOADED = 2;
140 this.STATE_UNLOADING = 3;
142 // re-entrancy guard:
143 this._processing = false;
145 // For telemetry, keeps track of what most recently cleared
146 // the loadTimer, which can tell us something about the cause
147 // of tab switch spinners.
148 this._loadTimerClearedBy = "none";
150 this._useDumpForLogging = false;
151 this._logInit = false;
154 this.window.addEventListener("MozAfterPaint", this);
155 this.window.addEventListener("MozLayerTreeReady", this);
156 this.window.addEventListener("MozLayerTreeCleared", this);
157 this.window.addEventListener("TabRemotenessChange", this);
158 this.window.addEventListener("sizemodechange", this);
159 this.window.addEventListener("occlusionstatechange", this);
160 this.window.addEventListener("SwapDocShells", this, true);
161 this.window.addEventListener("EndSwapDocShells", this, true);
163 let initialTab = this.requestedTab;
164 let initialBrowser = initialTab.linkedBrowser;
167 !initialBrowser.isRemoteBrowser ||
168 initialBrowser.frameLoader.remoteTab.hasLayers;
170 // If we minimized the window before the switcher was activated,
171 // we might have set the preserveLayers flag for the current
172 // browser. Let's clear it.
173 initialBrowser.preserveLayers(false);
175 if (!this.minimizedOrFullyOccluded) {
176 this.log("Initial tab is loaded?: " + tabIsLoaded);
179 tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING
183 for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
184 let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
185 let state = ppBrowser.hasLayers ? this.STATE_LOADED : this.STATE_LOADING;
186 this.setTabState(ppTab, state);
191 if (this.unloadTimer) {
192 this.clearTimer(this.unloadTimer);
193 this.unloadTimer = null;
195 if (this.loadTimer) {
196 this.clearTimer(this.loadTimer);
197 this.loadTimer = null;
200 this.window.removeEventListener("MozAfterPaint", this);
201 this.window.removeEventListener("MozLayerTreeReady", this);
202 this.window.removeEventListener("MozLayerTreeCleared", this);
203 this.window.removeEventListener("TabRemotenessChange", this);
204 this.window.removeEventListener("sizemodechange", this);
205 this.window.removeEventListener("occlusionstatechange", this);
206 this.window.removeEventListener("SwapDocShells", this, true);
207 this.window.removeEventListener("EndSwapDocShells", this, true);
209 this.tabbrowser._switcher = null;
212 // Wraps nsITimer. Must not use the vanilla setTimeout and
213 // clearTimeout, because they will be blocked by nsIPromptService
215 setTimer(callback, timeout) {
220 var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
221 timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
230 let state = this.tabState.get(tab);
232 // As an optimization, we lazily evaluate the state of tabs
233 // that we've never seen before. Once we've figured it out,
234 // we stash it in our state map.
235 if (state === undefined) {
236 state = this.STATE_UNLOADED;
238 if (tab && tab.linkedPanel) {
239 let b = tab.linkedBrowser;
240 if (b.renderLayers && b.hasLayers) {
241 state = this.STATE_LOADED;
242 } else if (b.renderLayers && !b.hasLayers) {
243 state = this.STATE_LOADING;
244 } else if (!b.renderLayers && b.hasLayers) {
245 state = this.STATE_UNLOADING;
249 this.setTabStateNoAction(tab, state);
255 setTabStateNoAction(tab, state) {
256 if (state == this.STATE_UNLOADED) {
257 this.tabState.delete(tab);
259 this.tabState.set(tab, state);
263 setTabState(tab, state) {
264 if (state == this.getTabState(tab)) {
268 this.setTabStateNoAction(tab, state);
270 let browser = tab.linkedBrowser;
271 let { remoteTab } = browser.frameLoader;
272 if (state == this.STATE_LOADING) {
273 this.assert(!this.minimizedOrFullyOccluded);
275 // If we're not in the process of warming this tab, we
276 // don't need to delay activating its DocShell.
277 if (!this.warmingTabs.has(tab)) {
278 browser.docShellIsActive = true;
282 browser.renderLayers = true;
284 this.onLayersReady(browser);
286 } else if (state == this.STATE_UNLOADING) {
288 // Setting the docShell to be inactive will also cause it
289 // to stop rendering layers.
290 browser.docShellIsActive = false;
292 this.onLayersCleared(browser);
294 } else if (state == this.STATE_LOADED) {
295 this.maybeActivateDocShell(tab);
298 if (!tab.linkedBrowser.isRemoteBrowser) {
299 // setTabState is potentially re-entrant in the non-remote case,
300 // so we must re-get the state for this assertion.
301 let nonRemoteState = this.getTabState(tab);
302 // Non-remote tabs can never stay in the STATE_LOADING
303 // or STATE_UNLOADING states. By the time this function
304 // exits, a non-remote tab must be in STATE_LOADED or
305 // STATE_UNLOADED, since the painting and the layer
306 // upload happen synchronously.
308 nonRemoteState == this.STATE_UNLOADED ||
309 nonRemoteState == this.STATE_LOADED
314 get minimizedOrFullyOccluded() {
316 this.window.windowState == this.window.STATE_MINIMIZED ||
317 this.window.isFullyOccluded
321 get tabLayerCache() {
322 return this.tabbrowser._tabLayerCache;
328 this.assert(this.tabbrowser._switcher);
329 this.assert(this.tabbrowser._switcher === this);
330 this.assert(!this.spinnerTab);
331 this.assert(!this.blankTab);
332 this.assert(!this.loadTimer);
333 this.assert(!this.loadingTab);
334 this.assert(this.lastVisibleTab === this.requestedTab);
336 this.minimizedOrFullyOccluded ||
337 this.getTabState(this.requestedTab) == this.STATE_LOADED
342 this.window.document.commandDispatcher.unlock();
344 let event = new this.window.CustomEvent("TabSwitchDone", {
348 this.tabbrowser.dispatchEvent(event);
351 // This function is called after all the main state changes to
352 // make sure we display the right tab.
354 let requestedTabState = this.getTabState(this.requestedTab);
355 let requestedBrowser = this.requestedTab.linkedBrowser;
357 // It is often more desirable to show a blank tab when appropriate than
358 // the tab switch spinner - especially since the spinner is usually
359 // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
360 // tab switch. We can hide this lag, and hide the time being spent
361 // constructing BrowserChild's, layer trees, etc, by showing a blank
362 // tab instead and focusing it immediately.
363 let shouldBeBlank = false;
364 if (requestedBrowser.isRemoteBrowser) {
365 // If a tab is remote and the window is not minimized, we can show a
366 // blank tab instead of a spinner in the following cases:
368 // 1. The tab has just crashed, and we haven't started showing the
369 // tab crashed page yet (in this case, the RemoteTab is null)
370 // 2. The tab has never presented, and has not finished loading
371 // a non-local-about: page.
373 // For (2), "finished loading a non-local-about: page" is
374 // determined by the busy state on the tab element and checking
375 // if the loaded URI is local.
376 let isBusy = this.requestedTab.hasAttribute("busy");
377 let isLocalAbout = requestedBrowser.currentURI.schemeIs("about");
378 let hasSufficientlyLoaded = !isBusy && !isLocalAbout;
380 let fl = requestedBrowser.frameLoader;
382 !this.minimizedOrFullyOccluded &&
384 (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
386 if (this.logging()) {
387 let flag = shouldBeBlank ? "blank" : "nonblank";
390 this.minimizedOrFullyOccluded,
394 fl.remoteTab ? fl.remoteTab.hasPresented : 0
399 if (requestedBrowser.isRemoteBrowser) {
400 this.addLogFlag("isRemote");
403 // Figure out which tab we actually want visible right now.
406 requestedTabState != this.STATE_LOADED &&
407 this.lastVisibleTab &&
411 // If we can't show the requestedTab, and lastVisibleTab is
412 // available, show it.
413 showTab = this.lastVisibleTab;
415 // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
416 showTab = this.requestedTab;
419 // First, let's deal with blank tabs, which we show instead
420 // of the spinner when the tab is not currently set up
421 // properly in the content process.
422 if (!shouldBeBlank && this.blankTab) {
423 this.blankTab.linkedBrowser.removeAttribute("blank");
424 this.blankTab = null;
425 } else if (shouldBeBlank && this.blankTab !== showTab) {
427 this.blankTab.linkedBrowser.removeAttribute("blank");
429 this.blankTab = showTab;
430 this.blankTab.linkedBrowser.setAttribute("blank", "true");
433 // Show or hide the spinner as needed.
435 this.getTabState(showTab) != this.STATE_LOADED &&
436 !this.minimizedOrFullyOccluded &&
440 if (!needSpinner && this.spinnerTab) {
441 this.noteSpinnerHidden();
442 this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
443 this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
444 this.spinnerTab = null;
445 } else if (needSpinner && this.spinnerTab !== showTab) {
446 if (this.spinnerTab) {
447 this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
449 this.noteSpinnerDisplayed();
451 this.spinnerTab = showTab;
452 this.tabbrowser.tabpanels.setAttribute("pendingpaint", "true");
453 this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
456 // Switch to the tab we've decided to make visible.
457 if (this.visibleTab !== showTab) {
458 this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
459 this.visibleTab = showTab;
461 this.maybeVisibleTabs.add(showTab);
463 let tabpanels = this.tabbrowser.tabpanels;
464 let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
465 let index = Array.prototype.indexOf.call(tabpanels.children, showPanel);
467 this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
468 tabpanels.setAttribute("selectedIndex", index);
469 if (showTab === this.requestedTab) {
470 if (requestedTabState == this.STATE_LOADED) {
471 // The new tab will be made visible in the next paint, record the expected
472 // transaction id for that, and we'll mark when we get notified of its
474 this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
476 this.noteMakingTabVisibleWithoutLayers();
479 this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
480 this.window.gURLBar.afterTabSwitchFocusChange();
481 this.maybeActivateDocShell(this.requestedTab);
485 // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
486 if (this.lastVisibleTab) {
487 this.lastVisibleTab._visuallySelected = false;
490 this.visibleTab._visuallySelected = true;
491 this.tabbrowser.tabContainer._setPositionalAttributes();
494 this.lastVisibleTab = this.visibleTab;
499 dump("Assertion failure\n" + Error().stack);
501 // Don't break a user's browser if an assertion fails.
502 if (AppConstants.DEBUG) {
503 throw new Error("Assertion failure");
508 maybeClearLoadTimer(caller) {
509 if (this.loadingTab) {
510 this._loadTimerClearedBy = caller;
511 this.loadingTab = null;
512 if (this.loadTimer) {
513 this.clearTimer(this.loadTimer);
514 this.loadTimer = null;
519 // We've decided to try to load requestedTab.
521 this.assert(!this.loadTimer);
522 this.assert(!this.minimizedOrFullyOccluded);
524 // loadingTab can be non-null here if we timed out loading the current tab.
525 // In that case we just overwrite it with a different tab; it's had its chance.
526 this.loadingTab = this.requestedTab;
527 this.log("Loading tab " + this.tinfo(this.loadingTab));
529 this.loadTimer = this.setTimer(
530 () => this.handleEvent({ type: "loadTimeout" }),
531 this.TAB_SWITCH_TIMEOUT
533 this.setTabState(this.requestedTab, this.STATE_LOADING);
536 maybeActivateDocShell(tab) {
537 // If we've reached the point where the requested tab has entered
538 // the loaded state, but the DocShell is still not yet active, we
539 // should activate it.
540 let browser = tab.linkedBrowser;
541 let state = this.getTabState(tab);
542 let canCheckDocShellState =
543 !browser.mDestroyed &&
544 (browser.docShell || browser.frameLoader.remoteTab);
546 tab == this.requestedTab &&
547 canCheckDocShellState &&
548 state == this.STATE_LOADED &&
549 !browser.docShellIsActive &&
550 !this.minimizedOrFullyOccluded
552 browser.docShellIsActive = true;
554 "Set requested tab docshell to active and preserveLayers to false"
556 // If we minimized the window before the switcher was activated,
557 // we might have set the preserveLayers flag for the current
558 // browser. Let's clear it.
559 browser.preserveLayers(false);
563 // This function runs before every event. It fixes up the state
564 // to account for closed tabs.
566 this.assert(this.tabbrowser._switcher);
567 this.assert(this.tabbrowser._switcher === this);
569 for (let i = 0; i < this.tabLayerCache.length; i++) {
570 let tab = this.tabLayerCache[i];
571 if (!tab.linkedBrowser) {
572 this.tabState.delete(tab);
573 this.tabLayerCache.splice(i, 1);
578 for (let [tab] of this.tabState) {
579 if (!tab.linkedBrowser) {
580 this.tabState.delete(tab);
585 if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
586 this.lastVisibleTab = null;
588 if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
589 this.lastPrimaryTab = null;
591 if (this.blankTab && !this.blankTab.linkedBrowser) {
592 this.blankTab = null;
594 if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
595 this.noteSpinnerHidden();
596 this.spinnerTab = null;
598 if (this.loadingTab && !this.loadingTab.linkedBrowser) {
599 this.maybeClearLoadTimer("preActions");
603 // This code runs after we've responded to an event or requested a new
604 // tab. It's expected that we've already updated all the principal
605 // state variables. This function takes care of updating any auxilliary
607 postActions(eventString) {
608 // Once we finish loading loadingTab, we null it out. So the state should
609 // always be LOADING.
612 this.getTabState(this.loadingTab) == this.STATE_LOADING
615 // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
616 // the timer is set only when we're loading something.
617 this.assert(!this.loadTimer || this.loadingTab);
618 this.assert(!this.loadingTab || this.loadTimer);
620 // If we're switching to a non-remote tab, there's no need to wait
621 // for it to send layers to the compositor, as this will happen
622 // synchronously. Clearing this here means that in the next step,
623 // we can load the non-remote browser immediately.
624 if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
625 this.maybeClearLoadTimer("postActions");
628 // If we're not loading anything, try loading the requested tab.
629 let stateOfRequestedTab = this.getTabState(this.requestedTab);
632 !this.minimizedOrFullyOccluded &&
633 (stateOfRequestedTab == this.STATE_UNLOADED ||
634 stateOfRequestedTab == this.STATE_UNLOADING ||
635 this.warmingTabs.has(this.requestedTab))
637 this.assert(stateOfRequestedTab != this.STATE_LOADED);
638 this.loadRequestedTab();
641 let numBackgroundCached = 0;
642 for (let tab of this.tabLayerCache) {
643 if (tab !== this.requestedTab) {
644 numBackgroundCached++;
648 // See how many tabs still have work to do.
651 for (let [tab, state] of this.tabState) {
652 // Skip print preview browsers since they shouldn't affect tab switching.
653 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
658 state == this.STATE_LOADED &&
659 tab !== this.requestedTab &&
660 !this.tabLayerCache.includes(tab)
664 if (tab !== this.visibleTab) {
668 if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
673 this.updateDisplay();
675 // It's possible for updateDisplay to trigger one of our own event
676 // handlers, which might cause finish() to already have been called.
677 // Check for that before calling finish() again.
678 if (!this.tabbrowser._switcher) {
682 this.maybeFinishTabSwitch();
684 if (numBackgroundCached > 0) {
685 this.deactivateCachedBackgroundTabs();
688 if (numWarming > gTabWarmingMax) {
689 this.logState("Hit tabWarmingMax");
690 if (this.unloadTimer) {
691 this.clearTimer(this.unloadTimer);
693 this.unloadNonRequiredTabs();
696 if (numPending == 0) {
700 this.logState("/" + eventString);
703 // Fires when we're ready to unload unused tabs.
705 this.unloadTimer = null;
706 this.unloadNonRequiredTabs();
709 deactivateCachedBackgroundTabs() {
710 for (let tab of this.tabLayerCache) {
711 if (tab !== this.requestedTab) {
712 let browser = tab.linkedBrowser;
713 browser.preserveLayers(true);
714 browser.docShellIsActive = false;
719 // If there are any non-visible and non-requested tabs in
720 // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
721 // up the unloadTimer to run onUnloadTimeout if there are still
722 // tabs in the process of unloading.
723 unloadNonRequiredTabs() {
724 this.warmingTabs = new WeakSet();
727 // Unload any tabs that can be unloaded.
728 for (let [tab, state] of this.tabState) {
729 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
733 let isInLayerCache = this.tabLayerCache.includes(tab);
736 state == this.STATE_LOADED &&
737 !this.maybeVisibleTabs.has(tab) &&
738 tab !== this.lastVisibleTab &&
739 tab !== this.loadingTab &&
740 tab !== this.requestedTab &&
743 this.setTabState(tab, this.STATE_UNLOADING);
747 state != this.STATE_UNLOADED &&
748 tab !== this.requestedTab &&
756 // Keep the timer going since there may be more tabs to unload.
757 this.unloadTimer = this.setTimer(
758 () => this.handleEvent({ type: "unloadTimeout" }),
764 // Fires when an ongoing load has taken too long.
766 this.maybeClearLoadTimer("onLoadTimeout");
769 // Fires when the layers become available for a tab.
770 onLayersReady(browser) {
771 let tab = this.tabbrowser.getTabForBrowser(browser);
773 // We probably got a layer update from a tab that got before
774 // the switcher was created, or for browser that's not being
775 // tracked by the async tab switcher (like the preloaded about:newtab).
779 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);
810 this.logState(`onLayersCleared(${tab._tPos})`);
812 this.getTabState(tab) == this.STATE_UNLOADING ||
813 this.getTabState(tab) == this.STATE_UNLOADED
815 this.setTabState(tab, this.STATE_UNLOADED);
819 // Called when a tab switches from remote to non-remote. In this case
820 // a MozLayerTreeReady notification that we requested may never fire,
821 // so we need to simulate it.
822 onRemotenessChange(tab) {
824 `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`
826 if (!tab.linkedBrowser.isRemoteBrowser) {
827 if (this.getTabState(tab) == this.STATE_LOADING) {
828 this.onLayersReady(tab.linkedBrowser);
829 } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
830 this.onLayersCleared(tab.linkedBrowser);
832 } else if (this.getTabState(tab) == this.STATE_LOADED) {
833 // A tab just changed from non-remote to remote, which means
834 // that it's gone back into the STATE_LOADING state until
835 // it sends up a layer tree.
836 this.setTabState(tab, this.STATE_LOADING);
841 if (this.lastVisibleTab == tab) {
842 this.handleEvent({ type: "tabRemoved", tab });
846 // Called when a tab has been removed, and the browser node is
847 // about to be removed from the DOM.
848 onTabRemovedImpl(tab) {
849 this.lastVisibleTab = null;
852 onSizeModeOrOcclusionStateChange() {
853 if (this.minimizedOrFullyOccluded) {
854 for (let [tab, state] of this.tabState) {
855 // Skip print preview browsers since they shouldn't affect tab switching.
856 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
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);
918 shouldActivateDocShell(browser) {
919 let tab = this.tabbrowser.getTabForBrowser(browser);
920 let state = this.getTabState(tab);
921 return state == this.STATE_LOADING || state == this.STATE_LOADED;
924 activateBrowserForPrintPreview(browser) {
925 let tab = this.tabbrowser.getTabForBrowser(browser);
926 let state = this.getTabState(tab);
927 if (state != this.STATE_LOADING && state != this.STATE_LOADED) {
928 this.setTabState(tab, this.STATE_LOADING);
930 "Activated browser " + this.tinfo(tab) + " for print preview"
936 if (!gTabWarmingEnabled) {
944 // If the tab is not yet inserted, closing, not remote,
945 // crashed, already visible, or already requested, warming
946 // up the tab makes no sense.
948 this.minimizedOrFullyOccluded ||
951 !tab.linkedBrowser.isRemoteBrowser ||
952 !tab.linkedBrowser.frameLoader.remoteTab
961 if (this.canWarmTab(tab)) {
962 // Tabs that are already in STATE_LOADING or STATE_LOADED
963 // have no need to be warmed up.
964 let state = this.getTabState(tab);
965 if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) {
974 this.warmingTabs.delete(tab);
978 if (!this.shouldWarmTab(tab)) {
982 this.logState("warmupTab " + this.tinfo(tab));
984 this.warmingTabs.add(tab);
985 this.setTabState(tab, this.STATE_LOADING);
986 this.queueUnload(gTabWarmingUnloadDelayMs);
989 cleanUpTabAfterEviction(tab) {
990 this.assert(tab !== this.requestedTab);
991 let browser = tab.linkedBrowser;
993 browser.preserveLayers(false);
995 this.setTabState(tab, this.STATE_UNLOADING);
998 evictOldestTabFromCache() {
999 let tab = this.tabLayerCache.shift();
1000 this.cleanUpTabAfterEviction(tab);
1003 maybePromoteTabInLayerCache(tab) {
1005 gTabCacheSize > 1 &&
1006 tab.linkedBrowser.isRemoteBrowser &&
1007 tab.linkedBrowser.currentURI.spec != "about:blank"
1009 let tabIndex = this.tabLayerCache.indexOf(tab);
1011 if (tabIndex != -1) {
1012 this.tabLayerCache.splice(tabIndex, 1);
1015 this.tabLayerCache.push(tab);
1017 if (this.tabLayerCache.length > gTabCacheSize) {
1018 this.evictOldestTabFromCache();
1023 // Called when the user asks to switch to a given tab.
1025 if (tab === this.requestedTab) {
1029 let tabState = this.getTabState(tab);
1030 this.noteTabRequested(tab, tabState);
1032 this.logState("requestTab " + this.tinfo(tab));
1033 this.startTabSwitch();
1035 let oldBrowser = this.requestedTab.linkedBrowser;
1036 oldBrowser.deprioritize();
1037 this.requestedTab = tab;
1038 if (tabState == this.STATE_LOADED) {
1039 this.maybeVisibleTabs.clear();
1042 tab.linkedBrowser.setAttribute("primary", "true");
1043 if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1044 this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1046 this.lastPrimaryTab = tab;
1048 this.queueUnload(this.UNLOAD_DELAY);
1051 queueUnload(unloadTimeout) {
1052 this.handleEvent({ type: "queueUnload", unloadTimeout });
1055 onQueueUnload(unloadTimeout) {
1056 if (this.unloadTimer) {
1057 this.clearTimer(this.unloadTimer);
1059 this.unloadTimer = this.setTimer(
1060 () => this.handleEvent({ type: "unloadTimeout" }),
1065 handleEvent(event, delayed = false) {
1066 if (this._processing) {
1067 this.setTimer(() => this.handleEvent(event, true), 0);
1070 if (delayed && this.tabbrowser._switcher != this) {
1071 // if we delayed processing this event, we might be out of date, in which
1072 // case we drop the delayed events
1075 this._processing = true;
1079 switch (event.type) {
1081 this.onQueueUnload(event.unloadTimeout);
1083 case "unloadTimeout":
1084 this.onUnloadTimeout();
1087 this.onLoadTimeout();
1090 this.onTabRemovedImpl(event.tab);
1092 case "MozLayerTreeReady":
1093 this.onLayersReady(event.originalTarget);
1095 case "MozAfterPaint":
1096 this.onPaint(event);
1098 case "MozLayerTreeCleared":
1099 this.onLayersCleared(event.originalTarget);
1101 case "TabRemotenessChange":
1102 this.onRemotenessChange(event.target);
1104 case "sizemodechange":
1105 case "occlusionstatechange":
1106 this.onSizeModeOrOcclusionStateChange();
1108 case "SwapDocShells":
1109 this.onSwapDocShells(event.originalTarget, event.detail);
1111 case "EndSwapDocShells":
1112 this.onEndSwapDocShells(event.originalTarget, event.detail);
1116 this.postActions(event.type);
1118 this._processing = false;
1123 * Telemetry and Profiler related helpers for recording tab switch
1128 this.noteStartTabSwitch();
1129 this.switchInProgress = true;
1133 * Something has occurred that might mean that we've completed
1134 * the tab switch (layers are ready, paints are done, spinners
1135 * are hidden). This checks to make sure all conditions are
1136 * satisfied, and then records the tab switch as finished.
1138 maybeFinishTabSwitch() {
1140 this.switchInProgress &&
1141 this.requestedTab &&
1142 (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1143 this.requestedTab === this.blankTab)
1145 if (this.requestedTab !== this.blankTab) {
1146 this.maybePromoteTabInLayerCache(this.requestedTab);
1149 this.noteFinishTabSwitch();
1150 this.switchInProgress = false;
1155 * Debug related logging for switcher.
1158 if (this._useDumpForLogging) {
1161 if (this._logInit) {
1162 return this._shouldLog;
1164 let result = Services.prefs.getBoolPref(
1165 "browser.tabs.remote.logSwitchTiming",
1168 this._shouldLog = result;
1169 this._logInit = true;
1170 return this._shouldLog;
1175 return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1181 if (!this.logging()) {
1184 if (this._useDumpForLogging) {
1187 Services.console.logStringMessage(s);
1191 addLogFlag(flag, ...subFlags) {
1192 if (this.logging()) {
1193 if (subFlags.length) {
1194 flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
1196 this._logFlags.push(flag);
1201 if (!this.logging()) {
1205 let getTabString = tab => {
1208 let state = this.getTabState(tab);
1209 let isWarming = this.warmingTabs.has(tab);
1210 let isCached = this.tabLayerCache.includes(tab);
1211 let isClosing = tab.closing;
1212 let linkedBrowser = tab.linkedBrowser;
1213 let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1214 let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1216 if (tab === this.lastVisibleTab) {
1219 if (tab === this.loadingTab) {
1222 if (tab === this.requestedTab) {
1225 if (tab === this.blankTab) {
1228 if (this.maybeVisibleTabs.has(tab)) {
1232 let extraStates = "";
1248 if (extraStates != "") {
1249 tabString += `(${extraStates})`;
1253 case this.STATE_LOADED: {
1254 tabString += "(loaded)";
1257 case this.STATE_LOADING: {
1258 tabString += "(loading)";
1261 case this.STATE_UNLOADING: {
1262 tabString += "(unloading)";
1265 case this.STATE_UNLOADED: {
1266 tabString += "(unloaded)";
1276 // This is a bit tricky to read, but what we're doing here is collapsing
1277 // identical tab states down to make the overal string shorter and easier
1278 // to read, and we move all simply unloaded tabs to the back of the list.
1280 // "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
1282 // "3:(loaded) 0...2:(unloaded)"
1283 let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
1285 let unloadedTabsStrings = [];
1286 for (let i = 0; i <= tabStrings.length; i++) {
1288 if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
1292 if (tabStrings[lastMatch] == "(unloaded)") {
1293 if (lastMatch == i - 1) {
1294 unloadedTabsStrings.push(lastMatch.toString());
1296 unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
1298 } else if (lastMatch == i - 1) {
1299 accum += `${lastMatch}:${tabStrings[lastMatch]} `;
1301 accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
1308 if (unloadedTabsStrings.length) {
1309 accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
1312 accum += "cached: " + this.tabLayerCache.length + " ";
1314 if (this._logFlags.length) {
1315 accum += `[${this._logFlags.join(",")}] `;
1316 this._logFlags = [];
1319 // It can be annoying to read through the entirety of a log string just
1320 // to check if something changed or not. So if we can tell that nothing
1321 // changed, just write "unchanged" to save the reader's time.
1323 if (this._lastLogString == accum) {
1324 accum = "unchanged";
1326 this._lastLogString = accum;
1328 logString = `ATS: ${accum}{${suffix}}`;
1330 if (this._useDumpForLogging) {
1331 dump(logString + "\n");
1333 Services.console.logStringMessage(logString);
1337 noteMakingTabVisibleWithoutLayers() {
1338 // We're making the tab visible even though we haven't yet got layers for it.
1339 // It's hard to know which composite the layers will first be available in (and
1340 // the parent process might not even get MozAfterPaint delivered for it), so just
1341 // give up measuring this for now. :(
1342 TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1346 if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
1348 TelemetryStopwatch.running(
1349 "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1353 let time = TelemetryStopwatch.timeElapsed(
1354 "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1358 TelemetryStopwatch.finish(
1359 "FX_TAB_SWITCH_COMPOSITE_E10S_MS",
1364 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited");
1365 this.switchPaintId = -1;
1369 noteTabRequested(tab, tabState) {
1370 if (gTabWarmingEnabled) {
1371 let warmingState = "disqualified";
1373 if (this.canWarmTab(tab)) {
1374 if (tabState == this.STATE_LOADING) {
1375 warmingState = "stillLoading";
1376 } else if (tabState == this.STATE_LOADED) {
1377 warmingState = "loaded";
1379 tabState == this.STATE_UNLOADING ||
1380 tabState == this.STATE_UNLOADED
1382 // At this point, if the tab's browser was being inserted
1383 // lazily, we never had a chance to warm it up, and unfortunately
1384 // there's no great way to detect that case. Those cases will
1385 // end up in the "notWarmed" bucket, along with legitimate cases
1386 // where tabs could have been warmed but weren't.
1387 warmingState = "notWarmed";
1392 .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
1397 noteStartTabSwitch() {
1398 TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1399 TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1402 TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)
1404 TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1406 TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1407 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start");
1410 noteFinishTabSwitch() {
1411 // After this point the tab has switched from the content thread's point of view.
1412 // The changes will be visible after the next refresh driver tick + composite.
1413 let time = TelemetryStopwatch.timeElapsed(
1414 "FX_TAB_SWITCH_TOTAL_E10S_MS",
1418 TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1419 this.log("DEBUG: tab switch time = " + time);
1420 ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish");
1424 noteSpinnerDisplayed() {
1425 this.assert(!this.spinnerTab);
1426 let browser = this.requestedTab.linkedBrowser;
1427 this.assert(browser.isRemoteBrowser);
1428 TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1429 // We have a second, similar probe for capturing recordings of
1430 // when the spinner is displayed for very long periods.
1431 TelemetryStopwatch.start(
1432 "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1435 ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown");
1437 .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1438 .add(this._loadTimerClearedBy);
1439 if (AppConstants.NIGHTLY_BUILD) {
1440 Services.obs.notifyObservers(null, "tabswitch-spinner");
1444 noteSpinnerHidden() {
1445 this.assert(this.spinnerTab);
1447 "DEBUG: spinner time = " +
1448 TelemetryStopwatch.timeElapsed(
1449 "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
1453 TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1454 TelemetryStopwatch.finish(
1455 "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
1458 ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden");
1459 // we do not get a onPaint after displaying the spinner
1460 this._loadTimerClearedBy = "none";