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("resource://gre/modules/XPCOMUtils.jsm");
11 XPCOMUtils.defineLazyModuleGetters(this, {
12 AppConstants: "resource://gre/modules/AppConstants.jsm",
13 Services: "resource://gre/modules/Services.jsm",
16 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingEnabled",
17 "browser.tabs.remote.warmup.enabled");
18 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingMax",
19 "browser.tabs.remote.warmup.maxTabs");
20 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabWarmingUnloadDelayMs",
21 "browser.tabs.remote.warmup.unloadDelayMs");
22 XPCOMUtils.defineLazyPreferenceGetter(this, "gTabCacheSize",
23 "browser.tabs.remote.tabCacheSize");
26 * The tab switcher is responsible for asynchronously switching
27 * tabs in e10s. It waits until the new tab is ready (i.e., the
28 * layer tree is available) before switching to it. Then it
29 * unloads the layer tree for the old tab.
31 * The tab switcher is a state machine. For each tab, it
32 * maintains state about whether the layer tree for the tab is
33 * available, being loaded, being unloaded, or unavailable. It
34 * also keeps track of the tab currently being displayed, the tab
35 * it's trying to load, and the tab the user has asked to switch
36 * to. The switcher object is created upon tab switch. It is
37 * released when there are no pending tabs to load or unload.
39 * The following general principles have guided the design:
41 * 1. We only request one layer tree at a time. If the user
42 * switches to a different tab while waiting, we don't request
43 * the new layer tree until the old tab has loaded or timed out.
45 * 2. If loading the layers for a tab times out, we show the
46 * spinner and possibly request the layer tree for another tab if
47 * the user has requested one.
49 * 3. We discard layer trees on a delay. This way, if the user is
50 * switching among the same tabs frequently, we don't continually
53 * It's important that we always show either the spinner or a tab
54 * whose layers are available. Otherwise the compositor will draw
55 * an entirely black frame, which is very jarring. To ensure this
56 * never happens when switching away from a tab, we assume the
57 * old tab might still be drawn until a MozAfterPaint event
58 * occurs. Because layout and compositing happen asynchronously,
59 * we don't have any other way of knowing when the switch
60 * actually takes place. Therefore, we don't unload the old tab
61 * until the next MozAfterPaint event.
63 class AsyncTabSwitcher {
64 constructor(tabbrowser) {
67 // How long to wait for a tab's layers to load. After this
68 // time elapses, we're free to put up the spinner and start
69 // trying to load a different tab.
70 this.TAB_SWITCH_TIMEOUT = 400; // ms
72 // When the user hasn't switched tabs for this long, we unload
73 // layers for all tabs that aren't in use.
74 this.UNLOAD_DELAY = 300; // ms
76 // The next three tabs form the principal state variables.
77 // See the assertions in postActions for their invariants.
79 // Tab the user requested most recently.
80 this.requestedTab = tabbrowser.selectedTab;
82 // Tab we're currently trying to load.
83 this.loadingTab = null;
85 // We show this tab in case the requestedTab hasn't loaded yet.
86 this.lastVisibleTab = tabbrowser.selectedTab;
88 // Auxilliary state variables:
90 this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
91 this.spinnerTab = null; // Tab showing a spinner.
92 this.blankTab = null; // Tab showing blank.
93 this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
95 this.tabbrowser = tabbrowser;
96 this.window = tabbrowser.ownerGlobal;
97 this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
98 this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
100 // Map from tabs to STATE_* (below).
101 this.tabState = new Map();
103 // True if we're in the midst of switching tabs.
104 this.switchInProgress = false;
106 // Transaction id for the composite that will show the requested
107 // tab for the first tab after a tab switch.
108 // Set to -1 when we're not waiting for notification of a
110 this.switchPaintId = -1;
112 // Set of tabs that might be visible right now. We maintain
113 // this set because we can't be sure when a tab is actually
114 // drawn. A tab is added to this set when we ask to make it
115 // visible. All tabs but the most recently shown tab are
116 // removed from the set upon MozAfterPaint.
117 this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
119 // This holds onto the set of tabs that we've been asked to warm up,
120 // and tabs are evicted once they're done loading or are unloaded.
121 this.warmingTabs = new WeakSet();
123 this.STATE_UNLOADED = 0;
124 this.STATE_LOADING = 1;
125 this.STATE_LOADED = 2;
126 this.STATE_UNLOADING = 3;
128 // re-entrancy guard:
129 this._processing = false;
131 // For telemetry, keeps track of what most recently cleared
132 // the loadTimer, which can tell us something about the cause
133 // of tab switch spinners.
134 this._loadTimerClearedBy = "none";
136 this._useDumpForLogging = false;
137 this._logInit = false;
139 this.window.addEventListener("MozAfterPaint", this);
140 this.window.addEventListener("MozLayerTreeReady", this);
141 this.window.addEventListener("MozLayerTreeCleared", this);
142 this.window.addEventListener("TabRemotenessChange", this);
143 this.window.addEventListener("sizemodechange", this);
144 this.window.addEventListener("occlusionstatechange", this);
145 this.window.addEventListener("SwapDocShells", this, true);
146 this.window.addEventListener("EndSwapDocShells", this, true);
148 let initialTab = this.requestedTab;
149 let initialBrowser = initialTab.linkedBrowser;
151 let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
152 initialBrowser.frameLoader.tabParent.hasLayers;
154 // If we minimized the window before the switcher was activated,
155 // we might have set the preserveLayers flag for the current
156 // browser. Let's clear it.
157 initialBrowser.preserveLayers(false);
159 if (!this.minimizedOrFullyOccluded) {
160 this.log("Initial tab is loaded?: " + tabIsLoaded);
161 this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
162 : this.STATE_LOADING);
165 for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
166 let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
167 let state = ppBrowser.hasLayers ? this.STATE_LOADED
168 : this.STATE_LOADING;
169 this.setTabState(ppTab, state);
174 if (this.unloadTimer) {
175 this.clearTimer(this.unloadTimer);
176 this.unloadTimer = null;
178 if (this.loadTimer) {
179 this.clearTimer(this.loadTimer);
180 this.loadTimer = null;
183 this.window.removeEventListener("MozAfterPaint", this);
184 this.window.removeEventListener("MozLayerTreeReady", this);
185 this.window.removeEventListener("MozLayerTreeCleared", this);
186 this.window.removeEventListener("TabRemotenessChange", this);
187 this.window.removeEventListener("sizemodechange", this);
188 this.window.removeEventListener("occlusionstatechange", this);
189 this.window.removeEventListener("SwapDocShells", this, true);
190 this.window.removeEventListener("EndSwapDocShells", this, true);
192 this.tabbrowser._switcher = null;
195 // Wraps nsITimer. Must not use the vanilla setTimeout and
196 // clearTimeout, because they will be blocked by nsIPromptService
198 setTimer(callback, timeout) {
203 var timer = Cc["@mozilla.org/timer;1"]
204 .createInstance(Ci.nsITimer);
205 timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
214 let state = this.tabState.get(tab);
216 // As an optimization, we lazily evaluate the state of tabs
217 // that we've never seen before. Once we've figured it out,
218 // we stash it in our state map.
219 if (state === undefined) {
220 state = this.STATE_UNLOADED;
222 if (tab && tab.linkedPanel) {
223 let b = tab.linkedBrowser;
224 if (b.renderLayers && b.hasLayers) {
225 state = this.STATE_LOADED;
226 } else if (b.renderLayers && !b.hasLayers) {
227 state = this.STATE_LOADING;
228 } else if (!b.renderLayers && b.hasLayers) {
229 state = this.STATE_UNLOADING;
233 this.setTabStateNoAction(tab, state);
239 setTabStateNoAction(tab, state) {
240 if (state == this.STATE_UNLOADED) {
241 this.tabState.delete(tab);
243 this.tabState.set(tab, state);
247 setTabState(tab, state) {
248 if (state == this.getTabState(tab)) {
252 this.setTabStateNoAction(tab, state);
254 let browser = tab.linkedBrowser;
255 let { tabParent } = browser.frameLoader;
256 if (state == this.STATE_LOADING) {
257 this.assert(!this.minimizedOrFullyOccluded);
259 // If we're not in the process of warming this tab, we
260 // don't need to delay activating its DocShell.
261 if (!this.warmingTabs.has(tab)) {
262 browser.docShellIsActive = true;
266 browser.renderLayers = true;
268 this.onLayersReady(browser);
270 } else if (state == this.STATE_UNLOADING) {
272 // Setting the docShell to be inactive will also cause it
273 // to stop rendering layers.
274 browser.docShellIsActive = false;
276 this.onLayersCleared(browser);
278 } else if (state == this.STATE_LOADED) {
279 this.maybeActivateDocShell(tab);
282 if (!tab.linkedBrowser.isRemoteBrowser) {
283 // setTabState is potentially re-entrant in the non-remote case,
284 // so we must re-get the state for this assertion.
285 let nonRemoteState = this.getTabState(tab);
286 // Non-remote tabs can never stay in the STATE_LOADING
287 // or STATE_UNLOADING states. By the time this function
288 // exits, a non-remote tab must be in STATE_LOADED or
289 // STATE_UNLOADED, since the painting and the layer
290 // upload happen synchronously.
291 this.assert(nonRemoteState == this.STATE_UNLOADED ||
292 nonRemoteState == this.STATE_LOADED);
296 get minimizedOrFullyOccluded() {
297 return this.window.windowState == this.window.STATE_MINIMIZED ||
298 this.window.isFullyOccluded;
301 get tabLayerCache() {
302 return this.tabbrowser._tabLayerCache;
308 this.assert(this.tabbrowser._switcher);
309 this.assert(this.tabbrowser._switcher === this);
310 this.assert(!this.spinnerTab);
311 this.assert(!this.blankTab);
312 this.assert(!this.loadTimer);
313 this.assert(!this.loadingTab);
314 this.assert(this.lastVisibleTab === this.requestedTab);
315 this.assert(this.minimizedOrFullyOccluded ||
316 this.getTabState(this.requestedTab) == this.STATE_LOADED);
320 this.window.document.commandDispatcher.unlock();
322 let event = new this.window.CustomEvent("TabSwitchDone", {
326 this.tabbrowser.dispatchEvent(event);
329 // This function is called after all the main state changes to
330 // make sure we display the right tab.
332 let requestedTabState = this.getTabState(this.requestedTab);
333 let requestedBrowser = this.requestedTab.linkedBrowser;
335 // It is often more desirable to show a blank tab when appropriate than
336 // the tab switch spinner - especially since the spinner is usually
337 // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
338 // tab switch. We can hide this lag, and hide the time being spent
339 // constructing TabChild's, layer trees, etc, by showing a blank
340 // tab instead and focusing it immediately.
341 let shouldBeBlank = false;
342 if (requestedBrowser.isRemoteBrowser) {
343 // If a tab is remote and the window is not minimized, we can show a
344 // blank tab instead of a spinner in the following cases:
346 // 1. The tab has just crashed, and we haven't started showing the
347 // tab crashed page yet (in this case, the TabParent is null)
348 // 2. The tab has never presented, and has not finished loading
349 // a non-local-about: page.
351 // For (2), "finished loading a non-local-about: page" is
352 // determined by the busy state on the tab element and checking
353 // if the loaded URI is local.
354 let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
355 !this.tabbrowser.isLocalAboutURI(requestedBrowser.currentURI);
357 let fl = requestedBrowser.frameLoader;
358 shouldBeBlank = !this.minimizedOrFullyOccluded &&
360 (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
363 this.log("Tab should be blank: " + shouldBeBlank);
364 this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
366 // Figure out which tab we actually want visible right now.
368 if (requestedTabState != this.STATE_LOADED &&
369 this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
370 // If we can't show the requestedTab, and lastVisibleTab is
371 // available, show it.
372 showTab = this.lastVisibleTab;
374 // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
375 showTab = this.requestedTab;
378 // First, let's deal with blank tabs, which we show instead
379 // of the spinner when the tab is not currently set up
380 // properly in the content process.
381 if (!shouldBeBlank && this.blankTab) {
382 this.blankTab.linkedBrowser.removeAttribute("blank");
383 this.blankTab = null;
384 } else if (shouldBeBlank && this.blankTab !== showTab) {
386 this.blankTab.linkedBrowser.removeAttribute("blank");
388 this.blankTab = showTab;
389 this.blankTab.linkedBrowser.setAttribute("blank", "true");
392 // Show or hide the spinner as needed.
393 let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
394 !this.minimizedOrFullyOccluded &&
397 if (!needSpinner && this.spinnerTab) {
398 this.spinnerHidden();
399 this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
400 this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
401 this.spinnerTab = null;
402 } else if (needSpinner && this.spinnerTab !== showTab) {
403 if (this.spinnerTab) {
404 this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
406 this.spinnerDisplayed();
408 this.spinnerTab = showTab;
409 this.tabbrowser.tabpanels.setAttribute("pendingpaint", "true");
410 this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
413 // Switch to the tab we've decided to make visible.
414 if (this.visibleTab !== showTab) {
415 this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
416 this.visibleTab = showTab;
418 this.maybeVisibleTabs.add(showTab);
420 let tabpanels = this.tabbrowser.tabpanels;
421 let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
422 let index = Array.indexOf(tabpanels.children, showPanel);
424 this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
425 tabpanels.setAttribute("selectedIndex", index);
426 if (showTab === this.requestedTab) {
427 if (requestedTabState == this.STATE_LOADED) {
428 // The new tab will be made visible in the next paint, record the expected
429 // transaction id for that, and we'll mark when we get notified of its
431 this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
433 // We're making the tab visible even though we haven't yet got layers for it.
434 // It's hard to know which composite the layers will first be available in (and
435 // the parent process might not even get MozAfterPaint delivered for it), so just
436 // give up measuring this for now. :(
437 TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
440 this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
441 this.maybeActivateDocShell(this.requestedTab);
445 // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
446 if (this.lastVisibleTab)
447 this.lastVisibleTab._visuallySelected = false;
449 this.visibleTab._visuallySelected = true;
450 this.tabbrowser.tabContainer._setPositionalAttributes();
453 this.lastVisibleTab = this.visibleTab;
458 dump("Assertion failure\n" + Error().stack);
460 // Don't break a user's browser if an assertion fails.
461 if (AppConstants.DEBUG) {
462 throw new Error("Assertion failure");
467 maybeClearLoadTimer(caller) {
468 if (this.loadingTab) {
469 this._loadTimerClearedBy = caller;
470 this.loadingTab = null;
471 if (this.loadTimer) {
472 this.clearTimer(this.loadTimer);
473 this.loadTimer = null;
479 // We've decided to try to load requestedTab.
481 this.assert(!this.loadTimer);
482 this.assert(!this.minimizedOrFullyOccluded);
484 // loadingTab can be non-null here if we timed out loading the current tab.
485 // In that case we just overwrite it with a different tab; it's had its chance.
486 this.loadingTab = this.requestedTab;
487 this.log("Loading tab " + this.tinfo(this.loadingTab));
489 this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
490 this.setTabState(this.requestedTab, this.STATE_LOADING);
493 maybeActivateDocShell(tab) {
494 // If we've reached the point where the requested tab has entered
495 // the loaded state, but the DocShell is still not yet active, we
496 // should activate it.
497 let browser = tab.linkedBrowser;
498 let state = this.getTabState(tab);
499 let canCheckDocShellState = !browser.mDestroyed &&
500 (browser.docShell || browser.frameLoader.tabParent);
501 if (tab == this.requestedTab &&
502 canCheckDocShellState &&
503 state == this.STATE_LOADED &&
504 !browser.docShellIsActive &&
505 !this.minimizedOrFullyOccluded) {
506 browser.docShellIsActive = true;
507 this.logState("Set requested tab docshell to active and preserveLayers to false");
508 // If we minimized the window before the switcher was activated,
509 // we might have set the preserveLayers flag for the current
510 // browser. Let's clear it.
511 browser.preserveLayers(false);
515 // This function runs before every event. It fixes up the state
516 // to account for closed tabs.
518 this.assert(this.tabbrowser._switcher);
519 this.assert(this.tabbrowser._switcher === this);
521 for (let i = 0; i < this.tabLayerCache.length; i++) {
522 let tab = this.tabLayerCache[i];
523 if (!tab.linkedBrowser) {
524 this.tabState.delete(tab);
525 this.tabLayerCache.splice(i, 1);
530 for (let [tab ] of this.tabState) {
531 if (!tab.linkedBrowser) {
532 this.tabState.delete(tab);
537 if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
538 this.lastVisibleTab = null;
540 if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
541 this.lastPrimaryTab = null;
543 if (this.blankTab && !this.blankTab.linkedBrowser) {
544 this.blankTab = null;
546 if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
547 this.spinnerHidden();
548 this.spinnerTab = null;
550 if (this.loadingTab && !this.loadingTab.linkedBrowser) {
551 this.maybeClearLoadTimer("preActions");
555 // This code runs after we've responded to an event or requested a new
556 // tab. It's expected that we've already updated all the principal
557 // state variables. This function takes care of updating any auxilliary
560 // Once we finish loading loadingTab, we null it out. So the state should
561 // always be LOADING.
562 this.assert(!this.loadingTab ||
563 this.getTabState(this.loadingTab) == this.STATE_LOADING);
565 // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
566 // the timer is set only when we're loading something.
567 this.assert(!this.loadTimer || this.loadingTab);
568 this.assert(!this.loadingTab || this.loadTimer);
570 // If we're switching to a non-remote tab, there's no need to wait
571 // for it to send layers to the compositor, as this will happen
572 // synchronously. Clearing this here means that in the next step,
573 // we can load the non-remote browser immediately.
574 if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
575 this.maybeClearLoadTimer("postActions");
578 // If we're not loading anything, try loading the requested tab.
579 let stateOfRequestedTab = this.getTabState(this.requestedTab);
580 if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
581 (stateOfRequestedTab == this.STATE_UNLOADED ||
582 stateOfRequestedTab == this.STATE_UNLOADING ||
583 this.warmingTabs.has(this.requestedTab))) {
584 this.assert(stateOfRequestedTab != this.STATE_LOADED);
585 this.loadRequestedTab();
588 let numBackgroundCached = 0;
589 for (let tab of this.tabLayerCache) {
590 if (tab !== this.requestedTab) {
591 numBackgroundCached++;
595 // See how many tabs still have work to do.
598 for (let [tab, state] of this.tabState) {
599 // Skip print preview browsers since they shouldn't affect tab switching.
600 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
604 if (state == this.STATE_LOADED &&
605 tab !== this.requestedTab &&
606 !this.tabLayerCache.includes(tab)) {
609 if (tab !== this.visibleTab) {
613 if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
618 this.updateDisplay();
620 // It's possible for updateDisplay to trigger one of our own event
621 // handlers, which might cause finish() to already have been called.
622 // Check for that before calling finish() again.
623 if (!this.tabbrowser._switcher) {
627 this.maybeFinishTabSwitch();
629 if (numBackgroundCached > 0) {
630 this.deactivateCachedBackgroundTabs();
633 if (numWarming > gTabWarmingMax) {
634 this.logState("Hit tabWarmingMax");
635 if (this.unloadTimer) {
636 this.clearTimer(this.unloadTimer);
638 this.unloadNonRequiredTabs();
641 if (numPending == 0) {
645 this.logState("done");
648 // Fires when we're ready to unload unused tabs.
650 this.logState("onUnloadTimeout");
652 this.unloadTimer = null;
654 this.unloadNonRequiredTabs();
659 deactivateCachedBackgroundTabs() {
660 for (let tab of this.tabLayerCache) {
661 if (tab !== this.requestedTab) {
662 let browser = tab.linkedBrowser;
663 browser.preserveLayers(true);
664 browser.docShellIsActive = false;
669 // If there are any non-visible and non-requested tabs in
670 // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
671 // up the unloadTimer to run onUnloadTimeout if there are still
672 // tabs in the process of unloading.
673 unloadNonRequiredTabs() {
674 this.warmingTabs = new WeakSet();
677 // Unload any tabs that can be unloaded.
678 for (let [tab, state] of this.tabState) {
679 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
683 let isInLayerCache = this.tabLayerCache.includes(tab);
685 if (state == this.STATE_LOADED &&
686 !this.maybeVisibleTabs.has(tab) &&
687 tab !== this.lastVisibleTab &&
688 tab !== this.loadingTab &&
689 tab !== this.requestedTab &&
691 this.setTabState(tab, this.STATE_UNLOADING);
694 if (state != this.STATE_UNLOADED &&
695 tab !== this.requestedTab &&
702 // Keep the timer going since there may be more tabs to unload.
703 this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
707 // Fires when an ongoing load has taken too long.
709 this.logState("onLoadTimeout");
711 this.maybeClearLoadTimer("onLoadTimeout");
715 // Fires when the layers become available for a tab.
716 onLayersReady(browser) {
717 let tab = this.tabbrowser.getTabForBrowser(browser);
719 // We probably got a layer update from a tab that got before
720 // the switcher was created, or for browser that's not being
721 // tracked by the async tab switcher (like the preloaded about:newtab).
725 this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
727 this.assert(this.getTabState(tab) == this.STATE_LOADING ||
728 this.getTabState(tab) == this.STATE_LOADED);
729 this.setTabState(tab, this.STATE_LOADED);
732 if (this.loadingTab === tab) {
733 this.maybeClearLoadTimer("onLayersReady");
737 // Fires when we paint the screen. Any tab switches we initiated
738 // previously are done, so there's no need to keep the old layers
741 if (this.switchPaintId != -1 &&
742 event.transactionId >= this.switchPaintId) {
743 if (TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)) {
744 let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
746 TelemetryStopwatch.finish("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
747 this.log("DEBUG: tab switch time including compositing = " + time);
750 this.addMarker("AsyncTabSwitch:Composited");
751 this.switchPaintId = -1;
754 this.maybeVisibleTabs.clear();
757 // Called when we're done clearing the layers for a tab.
758 onLayersCleared(browser) {
759 let tab = this.tabbrowser.getTabForBrowser(browser);
761 this.logState(`onLayersCleared(${tab._tPos})`);
762 this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
763 this.getTabState(tab) == this.STATE_UNLOADED);
764 this.setTabState(tab, this.STATE_UNLOADED);
768 // Called when a tab switches from remote to non-remote. In this case
769 // a MozLayerTreeReady notification that we requested may never fire,
770 // so we need to simulate it.
771 onRemotenessChange(tab) {
772 this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
773 if (!tab.linkedBrowser.isRemoteBrowser) {
774 if (this.getTabState(tab) == this.STATE_LOADING) {
775 this.onLayersReady(tab.linkedBrowser);
776 } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
777 this.onLayersCleared(tab.linkedBrowser);
779 } else if (this.getTabState(tab) == this.STATE_LOADED) {
780 // A tab just changed from non-remote to remote, which means
781 // that it's gone back into the STATE_LOADING state until
782 // it sends up a layer tree.
783 this.setTabState(tab, this.STATE_LOADING);
787 // Called when a tab has been removed, and the browser node is
788 // about to be removed from the DOM.
790 if (this.lastVisibleTab == tab) {
791 // The browser that was being presented to the user is
792 // going to be removed during this tick of the event loop.
793 // This will cause us to show a tab spinner instead.
795 this.lastVisibleTab = null;
800 onSizeModeOrOcclusionStateChange() {
801 if (this.minimizedOrFullyOccluded) {
802 for (let [tab, state] of this.tabState) {
803 // Skip print preview browsers since they shouldn't affect tab switching.
804 if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
808 if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
809 this.setTabState(tab, this.STATE_UNLOADING);
812 this.maybeClearLoadTimer("onSizeModeOrOcc");
814 // We're no longer minimized or occluded. This means we might want
815 // to activate the current tab's docShell.
816 this.maybeActivateDocShell(this.tabbrowser.selectedTab);
820 onSwapDocShells(ourBrowser, otherBrowser) {
821 // This event fires before the swap. ourBrowser is from
822 // our window. We save the state of otherBrowser since ourBrowser
823 // needs to take on that state at the end of the swap.
825 let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
827 if (otherTabbrowser && otherTabbrowser._switcher) {
828 let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
829 let otherSwitcher = otherTabbrowser._switcher;
830 otherState = otherSwitcher.getTabState(otherTab);
832 otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
835 this.swapMap = new WeakMap();
837 this.swapMap.set(otherBrowser, {
842 onEndSwapDocShells(ourBrowser, otherBrowser) {
843 // The swap has happened. We reset the loadingTab in
844 // case it has been swapped. We also set ourBrowser's state
845 // to whatever otherBrowser's state was before the swap.
847 // Clearing the load timer means that we will
848 // immediately display a spinner if ourBrowser isn't
849 // ready yet. Typically it will already be ready
850 // though. If it's not, we're probably in a new window,
851 // in which case we have no other tabs to display anyway.
852 this.maybeClearLoadTimer("onEndSwapDocShells");
854 let { state: otherState } = this.swapMap.get(otherBrowser);
856 this.swapMap.delete(otherBrowser);
858 let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
860 this.setTabStateNoAction(ourTab, otherState);
864 shouldActivateDocShell(browser) {
865 let tab = this.tabbrowser.getTabForBrowser(browser);
866 let state = this.getTabState(tab);
867 return state == this.STATE_LOADING || state == this.STATE_LOADED;
870 activateBrowserForPrintPreview(browser) {
871 let tab = this.tabbrowser.getTabForBrowser(browser);
872 let state = this.getTabState(tab);
873 if (state != this.STATE_LOADING &&
874 state != this.STATE_LOADED) {
875 this.setTabState(tab, this.STATE_LOADING);
876 this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
881 if (!gTabWarmingEnabled) {
889 // If the tab is not yet inserted, closing, not remote,
890 // crashed, already visible, or already requested, warming
891 // up the tab makes no sense.
892 if (this.minimizedOrFullyOccluded ||
895 !tab.linkedBrowser.isRemoteBrowser ||
896 !tab.linkedBrowser.frameLoader.tabParent) {
904 if (this.canWarmTab(tab)) {
905 // Tabs that are already in STATE_LOADING or STATE_LOADED
906 // have no need to be warmed up.
907 let state = this.getTabState(tab);
908 if (state === this.STATE_UNLOADING ||
909 state === this.STATE_UNLOADED) {
918 this.warmingTabs.delete(tab);
922 if (!this.shouldWarmTab(tab)) {
926 this.logState("warmupTab " + this.tinfo(tab));
928 this.warmingTabs.add(tab);
929 this.setTabState(tab, this.STATE_LOADING);
930 this.queueUnload(gTabWarmingUnloadDelayMs);
933 cleanUpTabAfterEviction(tab) {
934 this.assert(tab !== this.requestedTab);
935 let browser = tab.linkedBrowser;
937 browser.preserveLayers(false);
939 this.setTabState(tab, this.STATE_UNLOADING);
942 evictOldestTabFromCache() {
943 let tab = this.tabLayerCache.shift();
944 this.cleanUpTabAfterEviction(tab);
947 maybePromoteTabInLayerCache(tab) {
948 if (gTabCacheSize > 1 &&
949 tab.linkedBrowser.isRemoteBrowser &&
950 tab.linkedBrowser.currentURI.spec != "about:blank") {
951 let tabIndex = this.tabLayerCache.indexOf(tab);
953 if (tabIndex != -1) {
954 this.tabLayerCache.splice(tabIndex, 1);
957 this.tabLayerCache.push(tab);
959 if (this.tabLayerCache.length > gTabCacheSize) {
960 this.evictOldestTabFromCache();
965 // Called when the user asks to switch to a given tab.
967 if (tab === this.requestedTab) {
971 let tabState = this.getTabState(tab);
972 if (gTabWarmingEnabled) {
973 let warmingState = "disqualified";
975 if (this.canWarmTab(tab)) {
976 if (tabState == this.STATE_LOADING) {
977 warmingState = "stillLoading";
978 } else if (tabState == this.STATE_LOADED) {
979 warmingState = "loaded";
980 } else if (tabState == this.STATE_UNLOADING ||
981 tabState == this.STATE_UNLOADED) {
982 // At this point, if the tab's browser was being inserted
983 // lazily, we never had a chance to warm it up, and unfortunately
984 // there's no great way to detect that case. Those cases will
985 // end up in the "notWarmed" bucket, along with legitimate cases
986 // where tabs could have been warmed but weren't.
987 warmingState = "notWarmed";
992 .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
996 this.logState("requestTab " + this.tinfo(tab));
997 this.startTabSwitch();
999 let oldBrowser = this.requestedTab.linkedBrowser;
1000 oldBrowser.deprioritize();
1001 this.requestedTab = tab;
1002 if (tabState == this.STATE_LOADED) {
1003 this.maybeVisibleTabs.clear();
1004 if (tab.linkedBrowser.isRemoteBrowser) {
1005 tab.linkedBrowser.forceRepaint();
1009 tab.linkedBrowser.setAttribute("primary", "true");
1010 if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
1011 this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
1013 this.lastPrimaryTab = tab;
1015 this.queueUnload(this.UNLOAD_DELAY);
1018 queueUnload(unloadTimeout) {
1021 if (this.unloadTimer) {
1022 this.clearTimer(this.unloadTimer);
1024 this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
1029 handleEvent(event, delayed = false) {
1030 if (this._processing) {
1031 this.setTimer(() => this.handleEvent(event, true), 0);
1034 if (delayed && this.tabbrowser._switcher != this) {
1035 // if we delayed processing this event, we might be out of date, in which
1036 // case we drop the delayed events
1039 this._processing = true;
1042 switch (event.type) {
1043 case "MozLayerTreeReady":
1044 this.onLayersReady(event.originalTarget);
1046 case "MozAfterPaint":
1047 this.onPaint(event);
1049 case "MozLayerTreeCleared":
1050 this.onLayersCleared(event.originalTarget);
1052 case "TabRemotenessChange":
1053 this.onRemotenessChange(event.target);
1055 case "sizemodechange":
1056 case "occlusionstatechange":
1057 this.onSizeModeOrOcclusionStateChange();
1059 case "SwapDocShells":
1060 this.onSwapDocShells(event.originalTarget, event.detail);
1062 case "EndSwapDocShells":
1063 this.onEndSwapDocShells(event.originalTarget, event.detail);
1068 this._processing = false;
1072 * Telemetry and Profiler related helpers for recording tab switch
1077 TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1078 TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1080 if (TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window)) {
1081 TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1083 TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window);
1084 this.addMarker("AsyncTabSwitch:Start");
1085 this.switchInProgress = true;
1089 * Something has occurred that might mean that we've completed
1090 * the tab switch (layers are ready, paints are done, spinners
1091 * are hidden). This checks to make sure all conditions are
1092 * satisfied, and then records the tab switch as finished.
1094 maybeFinishTabSwitch() {
1095 if (this.switchInProgress && this.requestedTab &&
1096 (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
1097 this.requestedTab === this.blankTab)) {
1098 if (this.requestedTab !== this.blankTab) {
1099 this.maybePromoteTabInLayerCache(this.requestedTab);
1102 // After this point the tab has switched from the content thread's point of view.
1103 // The changes will be visible after the next refresh driver tick + composite.
1104 let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1106 TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
1107 this.log("DEBUG: tab switch time = " + time);
1108 this.addMarker("AsyncTabSwitch:Finish");
1110 this.switchInProgress = false;
1114 spinnerDisplayed() {
1115 this.assert(!this.spinnerTab);
1116 let browser = this.requestedTab.linkedBrowser;
1117 this.assert(browser.isRemoteBrowser);
1118 TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1119 // We have a second, similar probe for capturing recordings of
1120 // when the spinner is displayed for very long periods.
1121 TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
1122 this.addMarker("AsyncTabSwitch:SpinnerShown");
1124 .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
1125 .add(this._loadTimerClearedBy);
1126 if (AppConstants.NIGHTLY_BUILD) {
1127 Services.obs.notifyObservers(null, "tabswitch-spinner");
1132 this.assert(this.spinnerTab);
1133 this.log("DEBUG: spinner time = " +
1134 TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window));
1135 TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
1136 TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", this.window);
1137 this.addMarker("AsyncTabSwitch:SpinnerHidden");
1138 // we do not get a onPaint after displaying the spinner
1139 this._loadTimerClearedBy = "none";
1143 if (Services.profiler) {
1144 Services.profiler.AddMarker(marker);
1149 * Debug related logging for switcher.
1152 if (this._useDumpForLogging)
1155 return this._shouldLog;
1156 let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
1157 this._shouldLog = result;
1158 this._logInit = true;
1159 return this._shouldLog;
1164 return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
1170 if (!this.logging())
1172 if (this._useDumpForLogging) {
1175 Services.console.logStringMessage(s);
1180 if (!this.logging())
1183 let accum = prefix + " ";
1184 for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
1185 let tab = this.tabbrowser.tabs[i];
1186 let state = this.getTabState(tab);
1187 let isWarming = this.warmingTabs.has(tab);
1188 let isCached = this.tabLayerCache.includes(tab);
1189 let isClosing = tab.closing;
1190 let linkedBrowser = tab.linkedBrowser;
1191 let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
1192 let isRendered = linkedBrowser && linkedBrowser.renderLayers;
1195 if (tab === this.lastVisibleTab) accum += "V";
1196 if (tab === this.loadingTab) accum += "L";
1197 if (tab === this.requestedTab) accum += "R";
1198 if (tab === this.blankTab) accum += "B";
1200 let extraStates = "";
1201 if (isWarming) extraStates += "W";
1202 if (isCached) extraStates += "C";
1203 if (isClosing) extraStates += "X";
1204 if (isActive) extraStates += "A";
1205 if (isRendered) extraStates += "R";
1206 if (extraStates != "") {
1207 accum += `(${extraStates})`;
1210 if (state == this.STATE_LOADED) accum += "(+)";
1211 if (state == this.STATE_LOADING) accum += "(+?)";
1212 if (state == this.STATE_UNLOADED) accum += "(-)";
1213 if (state == this.STATE_UNLOADING) accum += "(-?)";
1217 accum += "cached: " + this.tabLayerCache.length;
1219 if (this._useDumpForLogging) {
1222 Services.console.logStringMessage(accum);