forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1508 lines
		
	
	
	
		
			46 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1508 lines
		
	
	
	
		
			46 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
 | |
|  * This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | |
| import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
 | |
| 
 | |
| const lazy = {};
 | |
| 
 | |
| ChromeUtils.defineESModuleGetters(lazy, {
 | |
|   PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
 | |
| });
 | |
| 
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gTabWarmingEnabled",
 | |
|   "browser.tabs.remote.warmup.enabled"
 | |
| );
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gTabWarmingMax",
 | |
|   "browser.tabs.remote.warmup.maxTabs"
 | |
| );
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gTabWarmingUnloadDelayMs",
 | |
|   "browser.tabs.remote.warmup.unloadDelayMs"
 | |
| );
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gTabCacheSize",
 | |
|   "browser.tabs.remote.tabCacheSize"
 | |
| );
 | |
| XPCOMUtils.defineLazyPreferenceGetter(
 | |
|   lazy,
 | |
|   "gTabUnloadDelay",
 | |
|   "browser.tabs.remote.unloadDelayMs",
 | |
|   300
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * The tab switcher is responsible for asynchronously switching
 | |
|  * tabs in e10s. It waits until the new tab is ready (i.e., the
 | |
|  * layer tree is available) before switching to it. Then it
 | |
|  * unloads the layer tree for the old tab.
 | |
|  *
 | |
|  * The tab switcher is a state machine. For each tab, it
 | |
|  * maintains state about whether the layer tree for the tab is
 | |
|  * available, being loaded, being unloaded, or unavailable. It
 | |
|  * also keeps track of the tab currently being displayed, the tab
 | |
|  * it's trying to load, and the tab the user has asked to switch
 | |
|  * to. The switcher object is created upon tab switch. It is
 | |
|  * released when there are no pending tabs to load or unload.
 | |
|  *
 | |
|  * The following general principles have guided the design:
 | |
|  *
 | |
|  * 1. We only request one layer tree at a time. If the user
 | |
|  * switches to a different tab while waiting, we don't request
 | |
|  * the new layer tree until the old tab has loaded or timed out.
 | |
|  *
 | |
|  * 2. If loading the layers for a tab times out, we show the
 | |
|  * spinner and possibly request the layer tree for another tab if
 | |
|  * the user has requested one.
 | |
|  *
 | |
|  * 3. We discard layer trees on a delay. This way, if the user is
 | |
|  * switching among the same tabs frequently, we don't continually
 | |
|  * load the same tabs.
 | |
|  *
 | |
|  * It's important that we always show either the spinner or a tab
 | |
|  * whose layers are available. Otherwise the compositor will draw
 | |
|  * an entirely black frame, which is very jarring. To ensure this
 | |
|  * never happens when switching away from a tab, we assume the
 | |
|  * old tab might still be drawn until a MozAfterPaint event
 | |
|  * occurs. Because layout and compositing happen asynchronously,
 | |
|  * we don't have any other way of knowing when the switch
 | |
|  * actually takes place. Therefore, we don't unload the old tab
 | |
|  * until the next MozAfterPaint event.
 | |
|  */
 | |
| export class AsyncTabSwitcher {
 | |
|   constructor(tabbrowser) {
 | |
|     this.log("START");
 | |
| 
 | |
|     // How long to wait for a tab's layers to load. After this
 | |
|     // time elapses, we're free to put up the spinner and start
 | |
|     // trying to load a different tab.
 | |
|     this.TAB_SWITCH_TIMEOUT = 400; // ms
 | |
| 
 | |
|     // When the user hasn't switched tabs for this long, we unload
 | |
|     // layers for all tabs that aren't in use.
 | |
|     this.UNLOAD_DELAY = lazy.gTabUnloadDelay; // ms
 | |
| 
 | |
|     // The next three tabs form the principal state variables.
 | |
|     // See the assertions in postActions for their invariants.
 | |
| 
 | |
|     // Tab the user requested most recently.
 | |
|     this.requestedTab = tabbrowser.selectedTab;
 | |
| 
 | |
|     // Tab we're currently trying to load.
 | |
|     this.loadingTab = null;
 | |
| 
 | |
|     // We show this tab in case the requestedTab hasn't loaded yet.
 | |
|     this.lastVisibleTab = tabbrowser.selectedTab;
 | |
| 
 | |
|     // Auxilliary state variables:
 | |
| 
 | |
|     this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen.
 | |
|     this.spinnerTab = null; // Tab showing a spinner.
 | |
|     this.blankTab = null; // Tab showing blank.
 | |
|     this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true"
 | |
| 
 | |
|     this.tabbrowser = tabbrowser;
 | |
|     this.window = tabbrowser.ownerGlobal;
 | |
|     this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance.
 | |
|     this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance.
 | |
| 
 | |
|     // Map from tabs to STATE_* (below).
 | |
|     this.tabState = new Map();
 | |
| 
 | |
|     // True if we're in the midst of switching tabs.
 | |
|     this.switchInProgress = false;
 | |
| 
 | |
|     // Transaction id for the composite that will show the requested
 | |
|     // tab for the first tab after a tab switch.
 | |
|     // Set to -1 when we're not waiting for notification of a
 | |
|     // completed switch.
 | |
|     this.switchPaintId = -1;
 | |
| 
 | |
|     // Set of tabs that might be visible right now. We maintain
 | |
|     // this set because we can't be sure when a tab is actually
 | |
|     // drawn. A tab is added to this set when we ask to make it
 | |
|     // visible. All tabs but the most recently shown tab are
 | |
|     // removed from the set upon MozAfterPaint.
 | |
|     this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]);
 | |
| 
 | |
|     // This holds onto the set of tabs that we've been asked to warm up,
 | |
|     // and tabs are evicted once they're done loading or are unloaded.
 | |
|     this.warmingTabs = new WeakSet();
 | |
| 
 | |
|     this.STATE_UNLOADED = 0;
 | |
|     this.STATE_LOADING = 1;
 | |
|     this.STATE_LOADED = 2;
 | |
|     this.STATE_UNLOADING = 3;
 | |
| 
 | |
|     // re-entrancy guard:
 | |
|     this._processing = false;
 | |
| 
 | |
|     // For telemetry, keeps track of what most recently cleared
 | |
|     // the loadTimer, which can tell us something about the cause
 | |
|     // of tab switch spinners.
 | |
|     this._loadTimerClearedBy = "none";
 | |
| 
 | |
|     this._useDumpForLogging = false;
 | |
|     this._logInit = false;
 | |
|     this._logFlags = [];
 | |
| 
 | |
|     this.window.addEventListener("MozAfterPaint", this);
 | |
|     this.window.addEventListener("MozLayerTreeReady", this);
 | |
|     this.window.addEventListener("MozLayerTreeCleared", this);
 | |
|     this.window.addEventListener("TabRemotenessChange", this);
 | |
|     this.window.addEventListener("SwapDocShells", this, true);
 | |
|     this.window.addEventListener("EndSwapDocShells", this, true);
 | |
|     this.window.document.addEventListener("visibilitychange", this);
 | |
| 
 | |
|     let initialTab = this.requestedTab;
 | |
|     let initialBrowser = initialTab.linkedBrowser;
 | |
| 
 | |
|     let tabIsLoaded =
 | |
|       !initialBrowser.isRemoteBrowser ||
 | |
|       initialBrowser.frameLoader.remoteTab?.hasLayers;
 | |
| 
 | |
|     // If we minimized the window before the switcher was activated,
 | |
|     // we might have set  the preserveLayers flag for the current
 | |
|     // browser. Let's clear it.
 | |
|     initialBrowser.preserveLayers(false);
 | |
| 
 | |
|     if (!this.windowHidden) {
 | |
|       this.log("Initial tab is loaded?: " + tabIsLoaded);
 | |
|       this.setTabState(
 | |
|         initialTab,
 | |
|         tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
 | |
|       let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
 | |
|       let state = ppBrowser.hasLayers ? this.STATE_LOADED : this.STATE_LOADING;
 | |
|       this.setTabState(ppTab, state);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   destroy() {
 | |
|     if (this.unloadTimer) {
 | |
|       this.clearTimer(this.unloadTimer);
 | |
|       this.unloadTimer = null;
 | |
|     }
 | |
|     if (this.loadTimer) {
 | |
|       this.clearTimer(this.loadTimer);
 | |
|       this.loadTimer = null;
 | |
|     }
 | |
| 
 | |
|     this.window.removeEventListener("MozAfterPaint", this);
 | |
|     this.window.removeEventListener("MozLayerTreeReady", this);
 | |
|     this.window.removeEventListener("MozLayerTreeCleared", this);
 | |
|     this.window.removeEventListener("TabRemotenessChange", this);
 | |
|     this.window.removeEventListener("SwapDocShells", this, true);
 | |
|     this.window.removeEventListener("EndSwapDocShells", this, true);
 | |
|     this.window.document.removeEventListener("visibilitychange", this);
 | |
| 
 | |
|     this.tabbrowser._switcher = null;
 | |
|   }
 | |
| 
 | |
|   // Wraps nsITimer. Must not use the vanilla setTimeout and
 | |
|   // clearTimeout, because they will be blocked by nsIPromptService
 | |
|   // dialogs.
 | |
|   setTimer(callback, timeout) {
 | |
|     let event = {
 | |
|       notify: callback,
 | |
|     };
 | |
| 
 | |
|     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|     timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
 | |
|     return timer;
 | |
|   }
 | |
| 
 | |
|   clearTimer(timer) {
 | |
|     timer.cancel();
 | |
|   }
 | |
| 
 | |
|   getTabState(tab) {
 | |
|     let state = this.tabState.get(tab);
 | |
| 
 | |
|     // As an optimization, we lazily evaluate the state of tabs
 | |
|     // that we've never seen before. Once we've figured it out,
 | |
|     // we stash it in our state map.
 | |
|     if (state === undefined) {
 | |
|       state = this.STATE_UNLOADED;
 | |
| 
 | |
|       if (tab && tab.linkedPanel) {
 | |
|         let b = tab.linkedBrowser;
 | |
|         if (b.renderLayers && b.hasLayers) {
 | |
|           state = this.STATE_LOADED;
 | |
|         } else if (b.renderLayers && !b.hasLayers) {
 | |
|           state = this.STATE_LOADING;
 | |
|         } else if (!b.renderLayers && b.hasLayers) {
 | |
|           state = this.STATE_UNLOADING;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       this.setTabStateNoAction(tab, state);
 | |
|     }
 | |
| 
 | |
|     return state;
 | |
|   }
 | |
| 
 | |
|   setTabStateNoAction(tab, state) {
 | |
|     if (state == this.STATE_UNLOADED) {
 | |
|       this.tabState.delete(tab);
 | |
|     } else {
 | |
|       this.tabState.set(tab, state);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   setTabState(tab, state) {
 | |
|     if (state == this.getTabState(tab)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.setTabStateNoAction(tab, state);
 | |
| 
 | |
|     let browser = tab.linkedBrowser;
 | |
|     let { remoteTab } = browser.frameLoader;
 | |
|     if (state == this.STATE_LOADING) {
 | |
|       this.assert(!this.windowHidden);
 | |
| 
 | |
|       // If we're not in the process of warming this tab, we
 | |
|       // don't need to delay activating its DocShell.
 | |
|       if (!this.warmingTabs.has(tab)) {
 | |
|         browser.docShellIsActive = true;
 | |
|       }
 | |
| 
 | |
|       if (remoteTab) {
 | |
|         browser.renderLayers = true;
 | |
|         remoteTab.priorityHint = true;
 | |
|       }
 | |
|       if (browser.hasLayers) {
 | |
|         this.onLayersReady(browser);
 | |
|       }
 | |
|     } else if (state == this.STATE_UNLOADING) {
 | |
|       this.unwarmTab(tab);
 | |
|       // Setting the docShell to be inactive will also cause it
 | |
|       // to stop rendering layers.
 | |
|       browser.docShellIsActive = false;
 | |
|       if (remoteTab) {
 | |
|         remoteTab.priorityHint = false;
 | |
|       }
 | |
|       if (!browser.hasLayers) {
 | |
|         this.onLayersCleared(browser);
 | |
|       }
 | |
|     } else if (state == this.STATE_LOADED) {
 | |
|       this.maybeActivateDocShell(tab);
 | |
|     }
 | |
| 
 | |
|     if (!tab.linkedBrowser.isRemoteBrowser) {
 | |
|       // setTabState is potentially re-entrant, so we must re-get the state for
 | |
|       // this assertion.
 | |
|       let nonRemoteState = this.getTabState(tab);
 | |
|       // Non-remote tabs can never stay in the STATE_LOADING
 | |
|       // or STATE_UNLOADING states. By the time this function
 | |
|       // exits, a non-remote tab must be in STATE_LOADED or
 | |
|       // STATE_UNLOADED, since the painting and the layer
 | |
|       // upload happen synchronously.
 | |
|       this.assert(
 | |
|         nonRemoteState == this.STATE_UNLOADED ||
 | |
|           nonRemoteState == this.STATE_LOADED
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   get windowHidden() {
 | |
|     return this.window.document.hidden;
 | |
|   }
 | |
| 
 | |
|   get tabLayerCache() {
 | |
|     return this.tabbrowser._tabLayerCache;
 | |
|   }
 | |
| 
 | |
|   finish() {
 | |
|     this.log("FINISH");
 | |
| 
 | |
|     this.assert(this.tabbrowser._switcher);
 | |
|     this.assert(this.tabbrowser._switcher === this);
 | |
|     this.assert(!this.spinnerTab);
 | |
|     this.assert(!this.blankTab);
 | |
|     this.assert(!this.loadTimer);
 | |
|     this.assert(!this.loadingTab);
 | |
|     this.assert(this.lastVisibleTab === this.requestedTab);
 | |
|     this.assert(
 | |
|       this.windowHidden ||
 | |
|         this.getTabState(this.requestedTab) == this.STATE_LOADED
 | |
|     );
 | |
| 
 | |
|     this.destroy();
 | |
| 
 | |
|     this.window.document.commandDispatcher.unlock();
 | |
| 
 | |
|     let event = new this.window.CustomEvent("TabSwitchDone", {
 | |
|       bubbles: true,
 | |
|       cancelable: true,
 | |
|     });
 | |
|     this.tabbrowser.dispatchEvent(event);
 | |
|   }
 | |
| 
 | |
|   // This function is called after all the main state changes to
 | |
|   // make sure we display the right tab.
 | |
|   updateDisplay() {
 | |
|     let requestedTabState = this.getTabState(this.requestedTab);
 | |
|     let requestedBrowser = this.requestedTab.linkedBrowser;
 | |
| 
 | |
|     // It is often more desirable to show a blank tab when appropriate than
 | |
|     // the tab switch spinner - especially since the spinner is usually
 | |
|     // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
 | |
|     // tab switch. We can hide this lag, and hide the time being spent
 | |
|     // constructing BrowserChild's, layer trees, etc, by showing a blank
 | |
|     // tab instead and focusing it immediately.
 | |
|     let shouldBeBlank = false;
 | |
|     if (requestedBrowser.isRemoteBrowser) {
 | |
|       // If a tab is remote and the window is not minimized, we can show a
 | |
|       // blank tab instead of a spinner in the following cases:
 | |
|       //
 | |
|       // 1. The tab has just crashed, and we haven't started showing the
 | |
|       //    tab crashed page yet (in this case, the RemoteTab is null)
 | |
|       // 2. The tab has never presented, and has not finished loading
 | |
|       //    a non-local-about: page.
 | |
|       //
 | |
|       // For (2), "finished loading a non-local-about: page" is
 | |
|       // determined by the busy state on the tab element and checking
 | |
|       // if the loaded URI is local.
 | |
|       let isBusy = this.requestedTab.hasAttribute("busy");
 | |
|       let isLocalAbout = requestedBrowser.currentURI.schemeIs("about");
 | |
|       let hasSufficientlyLoaded = !isBusy && !isLocalAbout;
 | |
| 
 | |
|       let fl = requestedBrowser.frameLoader;
 | |
|       shouldBeBlank =
 | |
|         !this.windowHidden &&
 | |
|         (!fl.remoteTab ||
 | |
|           (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented));
 | |
| 
 | |
|       if (this.logging()) {
 | |
|         let flag = shouldBeBlank ? "blank" : "nonblank";
 | |
|         this.addLogFlag(
 | |
|           flag,
 | |
|           this.windowHidden,
 | |
|           fl.remoteTab,
 | |
|           isBusy,
 | |
|           isLocalAbout,
 | |
|           fl.remoteTab ? fl.remoteTab.hasPresented : 0
 | |
|         );
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (requestedBrowser.isRemoteBrowser) {
 | |
|       this.addLogFlag("isRemote");
 | |
|     }
 | |
| 
 | |
|     // Figure out which tab we actually want visible right now.
 | |
|     let showTab = null;
 | |
|     if (
 | |
|       requestedTabState != this.STATE_LOADED &&
 | |
|       this.lastVisibleTab &&
 | |
|       this.loadTimer &&
 | |
|       !shouldBeBlank
 | |
|     ) {
 | |
|       // If we can't show the requestedTab, and lastVisibleTab is
 | |
|       // available, show it.
 | |
|       showTab = this.lastVisibleTab;
 | |
|     } else {
 | |
|       // Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
 | |
|       showTab = this.requestedTab;
 | |
|     }
 | |
| 
 | |
|     // First, let's deal with blank tabs, which we show instead
 | |
|     // of the spinner when the tab is not currently set up
 | |
|     // properly in the content process.
 | |
|     if (!shouldBeBlank && this.blankTab) {
 | |
|       this.blankTab.linkedBrowser.removeAttribute("blank");
 | |
|       this.blankTab = null;
 | |
|     } else if (shouldBeBlank && this.blankTab !== showTab) {
 | |
|       if (this.blankTab) {
 | |
|         this.blankTab.linkedBrowser.removeAttribute("blank");
 | |
|       }
 | |
|       this.blankTab = showTab;
 | |
|       this.blankTab.linkedBrowser.setAttribute("blank", "true");
 | |
|     }
 | |
| 
 | |
|     // Show or hide the spinner as needed.
 | |
|     let needSpinner =
 | |
|       this.getTabState(showTab) != this.STATE_LOADED &&
 | |
|       !this.windowHidden &&
 | |
|       !shouldBeBlank &&
 | |
|       !this.loadTimer;
 | |
| 
 | |
|     if (!needSpinner && this.spinnerTab) {
 | |
|       this.noteSpinnerHidden();
 | |
|       this.tabbrowser.tabpanels.removeAttribute("pendingpaint");
 | |
|       this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
 | |
|       this.spinnerTab = null;
 | |
|     } else if (needSpinner && this.spinnerTab !== showTab) {
 | |
|       if (this.spinnerTab) {
 | |
|         this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
 | |
|       } else {
 | |
|         this.noteSpinnerDisplayed();
 | |
|       }
 | |
|       this.spinnerTab = showTab;
 | |
|       this.tabbrowser.tabpanels.toggleAttribute("pendingpaint", true);
 | |
|       this.spinnerTab.linkedBrowser.toggleAttribute("pendingpaint", true);
 | |
|     }
 | |
| 
 | |
|     // Switch to the tab we've decided to make visible.
 | |
|     if (this.visibleTab !== showTab) {
 | |
|       this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
 | |
|       this.visibleTab = showTab;
 | |
| 
 | |
|       this.maybeVisibleTabs.add(showTab);
 | |
| 
 | |
|       let tabpanels = this.tabbrowser.tabpanels;
 | |
|       let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab);
 | |
|       let index = Array.prototype.indexOf.call(tabpanels.children, showPanel);
 | |
|       if (index != -1) {
 | |
|         this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
 | |
|         tabpanels.updateSelectedIndex(index);
 | |
|         if (showTab === this.requestedTab) {
 | |
|           if (requestedTabState == this.STATE_LOADED) {
 | |
|             // The new tab will be made visible in the next paint, record the expected
 | |
|             // transaction id for that, and we'll mark when we get notified of its
 | |
|             // completion.
 | |
|             this.switchPaintId = this.window.windowUtils.lastTransactionId + 1;
 | |
|           } else {
 | |
|             this.noteMakingTabVisibleWithoutLayers();
 | |
|           }
 | |
| 
 | |
|           this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
 | |
|           this.window.gURLBar.afterTabSwitchFocusChange();
 | |
|           this.maybeActivateDocShell(this.requestedTab);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
 | |
|       if (this.lastVisibleTab) {
 | |
|         this.lastVisibleTab._visuallySelected = false;
 | |
|       }
 | |
| 
 | |
|       this.visibleTab._visuallySelected = true;
 | |
|     }
 | |
| 
 | |
|     this.lastVisibleTab = this.visibleTab;
 | |
|   }
 | |
| 
 | |
|   assert(cond) {
 | |
|     if (!cond) {
 | |
|       dump("Assertion failure\n" + Error().stack);
 | |
| 
 | |
|       // Don't break a user's browser if an assertion fails.
 | |
|       if (AppConstants.DEBUG) {
 | |
|         throw new Error("Assertion failure");
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   maybeClearLoadTimer(caller) {
 | |
|     if (this.loadingTab) {
 | |
|       this._loadTimerClearedBy = caller;
 | |
|       this.loadingTab = null;
 | |
|       if (this.loadTimer) {
 | |
|         this.clearTimer(this.loadTimer);
 | |
|         this.loadTimer = null;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // We've decided to try to load requestedTab.
 | |
|   loadRequestedTab() {
 | |
|     this.assert(!this.loadTimer);
 | |
|     this.assert(!this.windowHidden);
 | |
| 
 | |
|     // loadingTab can be non-null here if we timed out loading the current tab.
 | |
|     // In that case we just overwrite it with a different tab; it's had its chance.
 | |
|     this.loadingTab = this.requestedTab;
 | |
|     this.log("Loading tab " + this.tinfo(this.loadingTab));
 | |
| 
 | |
|     this.loadTimer = this.setTimer(
 | |
|       () => this.handleEvent({ type: "loadTimeout" }),
 | |
|       this.TAB_SWITCH_TIMEOUT
 | |
|     );
 | |
|     this.setTabState(this.requestedTab, this.STATE_LOADING);
 | |
|   }
 | |
| 
 | |
|   maybeActivateDocShell(tab) {
 | |
|     // If we've reached the point where the requested tab has entered
 | |
|     // the loaded state, but the DocShell is still not yet active, we
 | |
|     // should activate it.
 | |
|     let browser = tab.linkedBrowser;
 | |
|     let state = this.getTabState(tab);
 | |
|     let canCheckDocShellState =
 | |
|       !browser.mDestroyed &&
 | |
|       (browser.docShell || browser.frameLoader.remoteTab);
 | |
|     if (
 | |
|       tab == this.requestedTab &&
 | |
|       canCheckDocShellState &&
 | |
|       state == this.STATE_LOADED &&
 | |
|       !browser.docShellIsActive &&
 | |
|       !this.windowHidden
 | |
|     ) {
 | |
|       browser.docShellIsActive = true;
 | |
|       this.logState(
 | |
|         "Set requested tab docshell to active and preserveLayers to false"
 | |
|       );
 | |
|       // If we minimized the window before the switcher was activated,
 | |
|       // we might have set the preserveLayers flag for the current
 | |
|       // browser. Let's clear it.
 | |
|       browser.preserveLayers(false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // This function runs before every event. It fixes up the state
 | |
|   // to account for closed tabs.
 | |
|   preActions() {
 | |
|     this.assert(this.tabbrowser._switcher);
 | |
|     this.assert(this.tabbrowser._switcher === this);
 | |
| 
 | |
|     for (let i = 0; i < this.tabLayerCache.length; i++) {
 | |
|       let tab = this.tabLayerCache[i];
 | |
|       if (!tab.linkedBrowser) {
 | |
|         this.tabState.delete(tab);
 | |
|         this.tabLayerCache.splice(i, 1);
 | |
|         i--;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     for (let [tab] of this.tabState) {
 | |
|       if (!tab.linkedBrowser) {
 | |
|         this.tabState.delete(tab);
 | |
|         this.unwarmTab(tab);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
 | |
|       this.lastVisibleTab = null;
 | |
|     }
 | |
|     if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
 | |
|       this.lastPrimaryTab = null;
 | |
|     }
 | |
|     if (this.blankTab && !this.blankTab.linkedBrowser) {
 | |
|       this.blankTab = null;
 | |
|     }
 | |
|     if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
 | |
|       this.noteSpinnerHidden();
 | |
|       this.spinnerTab = null;
 | |
|     }
 | |
|     if (this.loadingTab && !this.loadingTab.linkedBrowser) {
 | |
|       this.maybeClearLoadTimer("preActions");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // This code runs after we've responded to an event or requested a new
 | |
|   // tab. It's expected that we've already updated all the principal
 | |
|   // state variables. This function takes care of updating any auxilliary
 | |
|   // state.
 | |
|   postActions(eventString) {
 | |
|     // Once we finish loading loadingTab, we null it out. So the state should
 | |
|     // always be LOADING.
 | |
|     this.assert(
 | |
|       !this.loadingTab ||
 | |
|         this.getTabState(this.loadingTab) == this.STATE_LOADING
 | |
|     );
 | |
| 
 | |
|     // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
 | |
|     // the timer is set only when we're loading something.
 | |
|     this.assert(!this.loadTimer || this.loadingTab);
 | |
|     this.assert(!this.loadingTab || this.loadTimer);
 | |
| 
 | |
|     // If we're switching to a non-remote tab, there's no need to wait
 | |
|     // for it to send layers to the compositor, as this will happen
 | |
|     // synchronously. Clearing this here means that in the next step,
 | |
|     // we can load the non-remote browser immediately.
 | |
|     if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
 | |
|       this.maybeClearLoadTimer("postActions");
 | |
|     }
 | |
| 
 | |
|     // If we're not loading anything, try loading the requested tab.
 | |
|     let stateOfRequestedTab = this.getTabState(this.requestedTab);
 | |
|     if (
 | |
|       !this.loadTimer &&
 | |
|       !this.windowHidden &&
 | |
|       (stateOfRequestedTab == this.STATE_UNLOADED ||
 | |
|         stateOfRequestedTab == this.STATE_UNLOADING ||
 | |
|         this.warmingTabs.has(this.requestedTab))
 | |
|     ) {
 | |
|       this.assert(stateOfRequestedTab != this.STATE_LOADED);
 | |
|       this.loadRequestedTab();
 | |
|     }
 | |
| 
 | |
|     let numBackgroundCached = 0;
 | |
|     for (let tab of this.tabLayerCache) {
 | |
|       if (tab !== this.requestedTab) {
 | |
|         numBackgroundCached++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // See how many tabs still have work to do.
 | |
|     let numPending = 0;
 | |
|     let numWarming = 0;
 | |
|     for (let [tab, state] of this.tabState) {
 | |
|       if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         state == this.STATE_LOADED &&
 | |
|         tab !== this.requestedTab &&
 | |
|         !this.tabLayerCache.includes(tab)
 | |
|       ) {
 | |
|         numPending++;
 | |
| 
 | |
|         if (tab !== this.visibleTab) {
 | |
|           numWarming++;
 | |
|         }
 | |
|       }
 | |
|       if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
 | |
|         numPending++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.updateDisplay();
 | |
| 
 | |
|     // It's possible for updateDisplay to trigger one of our own event
 | |
|     // handlers, which might cause finish() to already have been called.
 | |
|     // Check for that before calling finish() again.
 | |
|     if (!this.tabbrowser._switcher) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.maybeFinishTabSwitch();
 | |
| 
 | |
|     if (numBackgroundCached > 0) {
 | |
|       this.deactivateCachedBackgroundTabs();
 | |
|     }
 | |
| 
 | |
|     if (numWarming > lazy.gTabWarmingMax) {
 | |
|       this.logState("Hit tabWarmingMax");
 | |
|       if (this.unloadTimer) {
 | |
|         this.clearTimer(this.unloadTimer);
 | |
|       }
 | |
|       this.unloadNonRequiredTabs();
 | |
|     }
 | |
| 
 | |
|     if (numPending == 0) {
 | |
|       this.finish();
 | |
|     }
 | |
| 
 | |
|     this.logState("/" + eventString);
 | |
|   }
 | |
| 
 | |
|   // Fires when we're ready to unload unused tabs.
 | |
|   onUnloadTimeout() {
 | |
|     this.unloadTimer = null;
 | |
|     this.unloadNonRequiredTabs();
 | |
|   }
 | |
| 
 | |
|   deactivateCachedBackgroundTabs() {
 | |
|     for (let tab of this.tabLayerCache) {
 | |
|       if (tab !== this.requestedTab) {
 | |
|         let browser = tab.linkedBrowser;
 | |
|         browser.preserveLayers(true);
 | |
|         browser.docShellIsActive = false;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // If there are any non-visible and non-requested tabs in
 | |
|   // STATE_LOADED, sets them to STATE_UNLOADING. Also queues
 | |
|   // up the unloadTimer to run onUnloadTimeout if there are still
 | |
|   // tabs in the process of unloading.
 | |
|   unloadNonRequiredTabs() {
 | |
|     this.warmingTabs = new WeakSet();
 | |
|     let numPending = 0;
 | |
| 
 | |
|     // Unload any tabs that can be unloaded.
 | |
|     for (let [tab, state] of this.tabState) {
 | |
|       if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       let isInLayerCache = this.tabLayerCache.includes(tab);
 | |
| 
 | |
|       if (
 | |
|         state == this.STATE_LOADED &&
 | |
|         !this.maybeVisibleTabs.has(tab) &&
 | |
|         tab !== this.lastVisibleTab &&
 | |
|         tab !== this.loadingTab &&
 | |
|         tab !== this.requestedTab &&
 | |
|         !isInLayerCache
 | |
|       ) {
 | |
|         this.setTabState(tab, this.STATE_UNLOADING);
 | |
|       }
 | |
| 
 | |
|       if (
 | |
|         state != this.STATE_UNLOADED &&
 | |
|         tab !== this.requestedTab &&
 | |
|         !isInLayerCache
 | |
|       ) {
 | |
|         numPending++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (numPending) {
 | |
|       // Keep the timer going since there may be more tabs to unload.
 | |
|       this.unloadTimer = this.setTimer(
 | |
|         () => this.handleEvent({ type: "unloadTimeout" }),
 | |
|         this.UNLOAD_DELAY
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Fires when an ongoing load has taken too long.
 | |
|   onLoadTimeout() {
 | |
|     this.maybeClearLoadTimer("onLoadTimeout");
 | |
|   }
 | |
| 
 | |
|   // Fires when the layers become available for a tab.
 | |
|   onLayersReady(browser) {
 | |
|     let tab = this.tabbrowser.getTabForBrowser(browser);
 | |
|     if (!tab) {
 | |
|       // We probably got a layer update from a tab that got before
 | |
|       // the switcher was created, or for browser that's not being
 | |
|       // tracked by the async tab switcher (like the preloaded about:newtab).
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
 | |
|     this.assert(
 | |
|       this.getTabState(tab) == this.STATE_LOADING ||
 | |
|         this.getTabState(tab) == this.STATE_LOADED
 | |
|     );
 | |
|     this.setTabState(tab, this.STATE_LOADED);
 | |
|     this.unwarmTab(tab);
 | |
| 
 | |
|     if (this.loadingTab === tab) {
 | |
|       this.maybeClearLoadTimer("onLayersReady");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Fires when we paint the screen. Any tab switches we initiated
 | |
|   // previously are done, so there's no need to keep the old layers
 | |
|   // around.
 | |
|   onPaint(event) {
 | |
|     this.addLogFlag(
 | |
|       "onPaint",
 | |
|       this.switchPaintId != -1,
 | |
|       event.transactionId >= this.switchPaintId
 | |
|     );
 | |
|     this.notePaint(event);
 | |
|     this.maybeVisibleTabs.clear();
 | |
|   }
 | |
| 
 | |
|   // Called when we're done clearing the layers for a tab.
 | |
|   onLayersCleared(browser) {
 | |
|     let tab = this.tabbrowser.getTabForBrowser(browser);
 | |
|     if (!tab) {
 | |
|       return;
 | |
|     }
 | |
|     this.logState(`onLayersCleared(${tab._tPos})`);
 | |
|     this.assert(
 | |
|       this.getTabState(tab) == this.STATE_UNLOADING ||
 | |
|         this.getTabState(tab) == this.STATE_UNLOADED
 | |
|     );
 | |
|     this.setTabState(tab, this.STATE_UNLOADED);
 | |
|   }
 | |
| 
 | |
|   // Called when a tab switches from remote to non-remote. In this case
 | |
|   // a MozLayerTreeReady notification that we requested may never fire,
 | |
|   // so we need to simulate it.
 | |
|   onRemotenessChange(tab) {
 | |
|     this.logState(
 | |
|       `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`
 | |
|     );
 | |
|     if (!tab.linkedBrowser.isRemoteBrowser) {
 | |
|       if (this.getTabState(tab) == this.STATE_LOADING) {
 | |
|         this.onLayersReady(tab.linkedBrowser);
 | |
|       } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
 | |
|         this.onLayersCleared(tab.linkedBrowser);
 | |
|       }
 | |
|     } else if (this.getTabState(tab) == this.STATE_LOADED) {
 | |
|       // A tab just changed from non-remote to remote, which means
 | |
|       // that it's gone back into the STATE_LOADING state until
 | |
|       // it sends up a layer tree.
 | |
|       this.setTabState(tab, this.STATE_LOADING);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   onTabRemoved(tab) {
 | |
|     if (this.lastVisibleTab == tab) {
 | |
|       this.handleEvent({ type: "tabRemoved", tab });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Called when a tab has been removed, and the browser node is
 | |
|   // about to be removed from the DOM.
 | |
|   onTabRemovedImpl() {
 | |
|     this.lastVisibleTab = null;
 | |
|   }
 | |
| 
 | |
|   onVisibilityChange() {
 | |
|     if (this.windowHidden) {
 | |
|       for (let [tab, state] of this.tabState) {
 | |
|         if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
 | |
|           this.setTabState(tab, this.STATE_UNLOADING);
 | |
|         }
 | |
|       }
 | |
|       this.maybeClearLoadTimer("onSizeModeOrOcc");
 | |
|     } else {
 | |
|       // We're no longer minimized or occluded. This means we might want
 | |
|       // to activate the current tab's docShell.
 | |
|       this.maybeActivateDocShell(this.tabbrowser.selectedTab);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   onSwapDocShells(ourBrowser, otherBrowser) {
 | |
|     // This event fires before the swap. ourBrowser is from
 | |
|     // our window. We save the state of otherBrowser since ourBrowser
 | |
|     // needs to take on that state at the end of the swap.
 | |
| 
 | |
|     let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
 | |
|     let otherState;
 | |
|     if (otherTabbrowser && otherTabbrowser._switcher) {
 | |
|       let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
 | |
|       let otherSwitcher = otherTabbrowser._switcher;
 | |
|       otherState = otherSwitcher.getTabState(otherTab);
 | |
|     } else {
 | |
|       otherState = otherBrowser.docShellIsActive
 | |
|         ? this.STATE_LOADED
 | |
|         : this.STATE_UNLOADED;
 | |
|     }
 | |
|     if (!this.swapMap) {
 | |
|       this.swapMap = new WeakMap();
 | |
|     }
 | |
|     this.swapMap.set(otherBrowser, {
 | |
|       state: otherState,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   onEndSwapDocShells(ourBrowser, otherBrowser) {
 | |
|     // The swap has happened. We reset the loadingTab in
 | |
|     // case it has been swapped. We also set ourBrowser's state
 | |
|     // to whatever otherBrowser's state was before the swap.
 | |
| 
 | |
|     // Clearing the load timer means that we will
 | |
|     // immediately display a spinner if ourBrowser isn't
 | |
|     // ready yet. Typically it will already be ready
 | |
|     // though. If it's not, we're probably in a new window,
 | |
|     // in which case we have no other tabs to display anyway.
 | |
|     this.maybeClearLoadTimer("onEndSwapDocShells");
 | |
| 
 | |
|     let { state: otherState } = this.swapMap.get(otherBrowser);
 | |
| 
 | |
|     this.swapMap.delete(otherBrowser);
 | |
| 
 | |
|     let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
 | |
|     if (ourTab) {
 | |
|       this.setTabStateNoAction(ourTab, otherState);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Check if the browser should be deactivated. If the browser is a print preivew or
 | |
|    * PiP browser then we won't deactive it.
 | |
|    * @param browser The browser to check if it should be deactivated
 | |
|    * @returns false if a print preview or PiP browser else true
 | |
|    */
 | |
|   shouldDeactivateDocShell(browser) {
 | |
|     return !(
 | |
|       this.tabbrowser._printPreviewBrowsers.has(browser) ||
 | |
|       lazy.PictureInPicture.isOriginatingBrowser(browser)
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   shouldActivateDocShell(browser) {
 | |
|     let tab = this.tabbrowser.getTabForBrowser(browser);
 | |
|     let state = this.getTabState(tab);
 | |
|     return state == this.STATE_LOADING || state == this.STATE_LOADED;
 | |
|   }
 | |
| 
 | |
|   activateBrowserForPrintPreview(browser) {
 | |
|     let tab = this.tabbrowser.getTabForBrowser(browser);
 | |
|     let state = this.getTabState(tab);
 | |
|     if (state != this.STATE_LOADING && state != this.STATE_LOADED) {
 | |
|       this.setTabState(tab, this.STATE_LOADING);
 | |
|       this.logState(
 | |
|         "Activated browser " + this.tinfo(tab) + " for print preview"
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   canWarmTab(tab) {
 | |
|     if (!lazy.gTabWarmingEnabled) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     if (!tab) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // If the tab is not yet inserted, closing, not remote,
 | |
|     // crashed, already visible, or already requested, warming
 | |
|     // up the tab makes no sense.
 | |
|     if (
 | |
|       this.windowHidden ||
 | |
|       !tab.linkedPanel ||
 | |
|       tab.closing ||
 | |
|       !tab.linkedBrowser.isRemoteBrowser ||
 | |
|       !tab.linkedBrowser.frameLoader.remoteTab
 | |
|     ) {
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   shouldWarmTab(tab) {
 | |
|     if (this.canWarmTab(tab)) {
 | |
|       // Tabs that are already in STATE_LOADING or STATE_LOADED
 | |
|       // have no need to be warmed up.
 | |
|       let state = this.getTabState(tab);
 | |
|       if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) {
 | |
|         return true;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   unwarmTab(tab) {
 | |
|     this.warmingTabs.delete(tab);
 | |
|   }
 | |
| 
 | |
|   warmupTab(tab) {
 | |
|     if (!this.shouldWarmTab(tab)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.logState("warmupTab " + this.tinfo(tab));
 | |
| 
 | |
|     this.warmingTabs.add(tab);
 | |
|     this.setTabState(tab, this.STATE_LOADING);
 | |
|     this.queueUnload(lazy.gTabWarmingUnloadDelayMs);
 | |
|   }
 | |
| 
 | |
|   cleanUpTabAfterEviction(tab) {
 | |
|     this.assert(tab !== this.requestedTab);
 | |
|     let browser = tab.linkedBrowser;
 | |
|     if (browser) {
 | |
|       browser.preserveLayers(false);
 | |
|     }
 | |
|     this.setTabState(tab, this.STATE_UNLOADING);
 | |
|   }
 | |
| 
 | |
|   evictOldestTabFromCache() {
 | |
|     let tab = this.tabLayerCache.shift();
 | |
|     this.cleanUpTabAfterEviction(tab);
 | |
|   }
 | |
| 
 | |
|   maybePromoteTabInLayerCache(tab) {
 | |
|     if (
 | |
|       lazy.gTabCacheSize > 1 &&
 | |
|       tab.linkedBrowser.isRemoteBrowser &&
 | |
|       tab.linkedBrowser.currentURI.spec != "about:blank"
 | |
|     ) {
 | |
|       let tabIndex = this.tabLayerCache.indexOf(tab);
 | |
| 
 | |
|       if (tabIndex != -1) {
 | |
|         this.tabLayerCache.splice(tabIndex, 1);
 | |
|       }
 | |
| 
 | |
|       this.tabLayerCache.push(tab);
 | |
| 
 | |
|       if (this.tabLayerCache.length > lazy.gTabCacheSize) {
 | |
|         this.evictOldestTabFromCache();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Called when the user asks to switch to a given tab.
 | |
|   requestTab(tab) {
 | |
|     if (tab === this.requestedTab) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let tabState = this.getTabState(tab);
 | |
|     this.noteTabRequested(tab, tabState);
 | |
| 
 | |
|     this.logState("requestTab " + this.tinfo(tab));
 | |
|     this.startTabSwitch();
 | |
| 
 | |
|     let oldBrowser = this.requestedTab.linkedBrowser;
 | |
|     oldBrowser.deprioritize();
 | |
|     this.requestedTab = tab;
 | |
|     if (tabState == this.STATE_LOADED) {
 | |
|       this.maybeVisibleTabs.clear();
 | |
|     }
 | |
| 
 | |
|     tab.linkedBrowser.setAttribute("primary", "true");
 | |
|     if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
 | |
|       this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
 | |
|     }
 | |
|     this.lastPrimaryTab = tab;
 | |
| 
 | |
|     this.queueUnload(this.UNLOAD_DELAY);
 | |
|   }
 | |
| 
 | |
|   queueUnload(unloadTimeout) {
 | |
|     this.handleEvent({ type: "queueUnload", unloadTimeout });
 | |
|   }
 | |
| 
 | |
|   onQueueUnload(unloadTimeout) {
 | |
|     if (this.unloadTimer) {
 | |
|       this.clearTimer(this.unloadTimer);
 | |
|     }
 | |
|     this.unloadTimer = this.setTimer(
 | |
|       () => this.handleEvent({ type: "unloadTimeout" }),
 | |
|       unloadTimeout
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   handleEvent(event, delayed = false) {
 | |
|     if (this._processing) {
 | |
|       this.setTimer(() => this.handleEvent(event, true), 0);
 | |
|       return;
 | |
|     }
 | |
|     if (delayed && this.tabbrowser._switcher != this) {
 | |
|       // if we delayed processing this event, we might be out of date, in which
 | |
|       // case we drop the delayed events
 | |
|       return;
 | |
|     }
 | |
|     this._processing = true;
 | |
|     try {
 | |
|       this.preActions();
 | |
| 
 | |
|       switch (event.type) {
 | |
|         case "queueUnload":
 | |
|           this.onQueueUnload(event.unloadTimeout);
 | |
|           break;
 | |
|         case "unloadTimeout":
 | |
|           this.onUnloadTimeout();
 | |
|           break;
 | |
|         case "loadTimeout":
 | |
|           this.onLoadTimeout();
 | |
|           break;
 | |
|         case "tabRemoved":
 | |
|           this.onTabRemovedImpl(event.tab);
 | |
|           break;
 | |
|         case "MozLayerTreeReady": {
 | |
|           let browser = event.originalTarget;
 | |
|           if (!browser.renderLayers) {
 | |
|             // By the time we handle this event, it's possible that something
 | |
|             // else has already set renderLayers to false, in which case this
 | |
|             // event is stale and we can safely ignore it.
 | |
|             return;
 | |
|           }
 | |
|           this.onLayersReady(browser);
 | |
|           break;
 | |
|         }
 | |
|         case "MozAfterPaint":
 | |
|           this.onPaint(event);
 | |
|           break;
 | |
|         case "MozLayerTreeCleared": {
 | |
|           let browser = event.originalTarget;
 | |
|           if (browser.renderLayers) {
 | |
|             // By the time we handle this event, it's possible that something
 | |
|             // else has already set renderLayers to true, in which case this
 | |
|             // event is stale and we can safely ignore it.
 | |
|             return;
 | |
|           }
 | |
|           this.onLayersCleared(browser);
 | |
|           break;
 | |
|         }
 | |
|         case "TabRemotenessChange":
 | |
|           this.onRemotenessChange(event.target);
 | |
|           break;
 | |
|         case "visibilitychange":
 | |
|           this.onVisibilityChange();
 | |
|           break;
 | |
|         case "SwapDocShells":
 | |
|           this.onSwapDocShells(event.originalTarget, event.detail);
 | |
|           break;
 | |
|         case "EndSwapDocShells":
 | |
|           this.onEndSwapDocShells(event.originalTarget, event.detail);
 | |
|           break;
 | |
|       }
 | |
| 
 | |
|       this.postActions(event.type);
 | |
|     } finally {
 | |
|       this._processing = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /*
 | |
|    * Telemetry and Profiler related helpers for recording tab switch
 | |
|    * timing.
 | |
|    */
 | |
| 
 | |
|   startTabSwitch() {
 | |
|     this.noteStartTabSwitch();
 | |
|     this.switchInProgress = true;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Something has occurred that might mean that we've completed
 | |
|    * the tab switch (layers are ready, paints are done, spinners
 | |
|    * are hidden). This checks to make sure all conditions are
 | |
|    * satisfied, and then records the tab switch as finished.
 | |
|    */
 | |
|   maybeFinishTabSwitch() {
 | |
|     if (
 | |
|       this.switchInProgress &&
 | |
|       this.requestedTab &&
 | |
|       (this.getTabState(this.requestedTab) == this.STATE_LOADED ||
 | |
|         this.requestedTab === this.blankTab)
 | |
|     ) {
 | |
|       if (this.requestedTab !== this.blankTab) {
 | |
|         this.maybePromoteTabInLayerCache(this.requestedTab);
 | |
|       }
 | |
| 
 | |
|       this.noteFinishTabSwitch();
 | |
|       this.switchInProgress = false;
 | |
| 
 | |
|       let event = new this.window.CustomEvent("TabSwitched", {
 | |
|         bubbles: true,
 | |
|         detail: {
 | |
|           tab: this.requestedTab,
 | |
|         },
 | |
|       });
 | |
|       this.tabbrowser.dispatchEvent(event);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /*
 | |
|    * Debug related logging for switcher.
 | |
|    */
 | |
|   logging() {
 | |
|     if (this._useDumpForLogging) {
 | |
|       return true;
 | |
|     }
 | |
|     if (this._logInit) {
 | |
|       return this._shouldLog;
 | |
|     }
 | |
|     let result = Services.prefs.getBoolPref(
 | |
|       "browser.tabs.remote.logSwitchTiming",
 | |
|       false
 | |
|     );
 | |
|     this._shouldLog = result;
 | |
|     this._logInit = true;
 | |
|     return this._shouldLog;
 | |
|   }
 | |
| 
 | |
|   tinfo(tab) {
 | |
|     if (tab) {
 | |
|       return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
 | |
|     }
 | |
|     return "null";
 | |
|   }
 | |
| 
 | |
|   log(s) {
 | |
|     if (!this.logging()) {
 | |
|       return;
 | |
|     }
 | |
|     if (this._useDumpForLogging) {
 | |
|       dump(s + "\n");
 | |
|     } else {
 | |
|       Services.console.logStringMessage(s);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   addLogFlag(flag, ...subFlags) {
 | |
|     if (this.logging()) {
 | |
|       if (subFlags.length) {
 | |
|         flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`;
 | |
|       }
 | |
|       this._logFlags.push(flag);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   logState(suffix) {
 | |
|     if (!this.logging()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let getTabString = tab => {
 | |
|       let tabString = "";
 | |
| 
 | |
|       let state = this.getTabState(tab);
 | |
|       let isWarming = this.warmingTabs.has(tab);
 | |
|       let isCached = this.tabLayerCache.includes(tab);
 | |
|       let isClosing = tab.closing;
 | |
|       let linkedBrowser = tab.linkedBrowser;
 | |
|       let isActive = linkedBrowser && linkedBrowser.docShellIsActive;
 | |
|       let isRendered = linkedBrowser && linkedBrowser.renderLayers;
 | |
|       let isPiP =
 | |
|         linkedBrowser &&
 | |
|         lazy.PictureInPicture.isOriginatingBrowser(linkedBrowser);
 | |
| 
 | |
|       if (tab === this.lastVisibleTab) {
 | |
|         tabString += "V";
 | |
|       }
 | |
|       if (tab === this.loadingTab) {
 | |
|         tabString += "L";
 | |
|       }
 | |
|       if (tab === this.requestedTab) {
 | |
|         tabString += "R";
 | |
|       }
 | |
|       if (tab === this.blankTab) {
 | |
|         tabString += "B";
 | |
|       }
 | |
|       if (this.maybeVisibleTabs.has(tab)) {
 | |
|         tabString += "M";
 | |
|       }
 | |
| 
 | |
|       let extraStates = "";
 | |
|       if (isWarming) {
 | |
|         extraStates += "W";
 | |
|       }
 | |
|       if (isCached) {
 | |
|         extraStates += "C";
 | |
|       }
 | |
|       if (isClosing) {
 | |
|         extraStates += "X";
 | |
|       }
 | |
|       if (isActive) {
 | |
|         extraStates += "A";
 | |
|       }
 | |
|       if (isRendered) {
 | |
|         extraStates += "R";
 | |
|       }
 | |
|       if (isPiP) {
 | |
|         extraStates += "P";
 | |
|       }
 | |
|       if (extraStates != "") {
 | |
|         tabString += `(${extraStates})`;
 | |
|       }
 | |
| 
 | |
|       switch (state) {
 | |
|         case this.STATE_LOADED: {
 | |
|           tabString += "(loaded)";
 | |
|           break;
 | |
|         }
 | |
|         case this.STATE_LOADING: {
 | |
|           tabString += "(loading)";
 | |
|           break;
 | |
|         }
 | |
|         case this.STATE_UNLOADING: {
 | |
|           tabString += "(unloading)";
 | |
|           break;
 | |
|         }
 | |
|         case this.STATE_UNLOADED: {
 | |
|           tabString += "(unloaded)";
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return tabString;
 | |
|     };
 | |
| 
 | |
|     let accum = "";
 | |
| 
 | |
|     // This is a bit tricky to read, but what we're doing here is collapsing
 | |
|     // identical tab states down to make the overal string shorter and easier
 | |
|     // to read, and we move all simply unloaded tabs to the back of the list.
 | |
|     // I.e., we turn
 | |
|     //   "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)""
 | |
|     // into
 | |
|     //   "3:(loaded) 0...2:(unloaded)"
 | |
|     let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t));
 | |
|     let lastMatch = -1;
 | |
|     let unloadedTabsStrings = [];
 | |
|     for (let i = 0; i <= tabStrings.length; i++) {
 | |
|       if (i > 0) {
 | |
|         if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (tabStrings[lastMatch] == "(unloaded)") {
 | |
|           if (lastMatch == i - 1) {
 | |
|             unloadedTabsStrings.push(lastMatch.toString());
 | |
|           } else {
 | |
|             unloadedTabsStrings.push(`${lastMatch}...${i - 1}`);
 | |
|           }
 | |
|         } else if (lastMatch == i - 1) {
 | |
|           accum += `${lastMatch}:${tabStrings[lastMatch]} `;
 | |
|         } else {
 | |
|           accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       lastMatch = i;
 | |
|     }
 | |
| 
 | |
|     if (unloadedTabsStrings.length) {
 | |
|       accum += `${unloadedTabsStrings.join(",")}:(unloaded) `;
 | |
|     }
 | |
| 
 | |
|     accum += "cached: " + this.tabLayerCache.length + " ";
 | |
| 
 | |
|     if (this._logFlags.length) {
 | |
|       accum += `[${this._logFlags.join(",")}] `;
 | |
|       this._logFlags = [];
 | |
|     }
 | |
| 
 | |
|     // It can be annoying to read through the entirety of a log string just
 | |
|     // to check if something changed or not. So if we can tell that nothing
 | |
|     // changed, just write "unchanged" to save the reader's time.
 | |
|     let logString;
 | |
|     if (this._lastLogString == accum) {
 | |
|       accum = "unchanged";
 | |
|     } else {
 | |
|       this._lastLogString = accum;
 | |
|     }
 | |
|     logString = `ATS: ${accum}{${suffix}}`;
 | |
| 
 | |
|     if (this._useDumpForLogging) {
 | |
|       dump(logString + "\n");
 | |
|     } else {
 | |
|       Services.console.logStringMessage(logString);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   noteMakingTabVisibleWithoutLayers() {
 | |
|     // We're making the tab visible even though we haven't yet got layers for it.
 | |
|     // It's hard to know which composite the layers will first be available in (and
 | |
|     // the parent process might not even get MozAfterPaint delivered for it), so just
 | |
|     // give up measuring this for now. :(
 | |
|     Glean.performanceInteraction.tabSwitchComposite.cancel(
 | |
|       this._tabswitchTimerId
 | |
|     );
 | |
|     this._tabswitchTimerId = null;
 | |
|   }
 | |
| 
 | |
|   notePaint(event) {
 | |
|     if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) {
 | |
|       if (this._tabswitchTimerId) {
 | |
|         Glean.performanceInteraction.tabSwitchComposite.stopAndAccumulate(
 | |
|           this._tabswitchTimerId
 | |
|         );
 | |
|         this._tabswitchTimerId = null;
 | |
|       }
 | |
|       let { innerWindowId } = this.window.windowGlobalChild;
 | |
|       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited", {
 | |
|         innerWindowId,
 | |
|       });
 | |
|       this.switchPaintId = -1;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   noteTabRequested(tab, tabState) {
 | |
|     if (lazy.gTabWarmingEnabled) {
 | |
|       let warmingState = "disqualified";
 | |
| 
 | |
|       if (this.canWarmTab(tab)) {
 | |
|         if (tabState == this.STATE_LOADING) {
 | |
|           warmingState = "stillLoading";
 | |
|         } else if (tabState == this.STATE_LOADED) {
 | |
|           warmingState = "loaded";
 | |
|         } else if (
 | |
|           tabState == this.STATE_UNLOADING ||
 | |
|           tabState == this.STATE_UNLOADED
 | |
|         ) {
 | |
|           // At this point, if the tab's browser was being inserted
 | |
|           // lazily, we never had a chance to warm it up, and unfortunately
 | |
|           // there's no great way to detect that case. Those cases will
 | |
|           // end up in the "notWarmed" bucket, along with legitimate cases
 | |
|           // where tabs could have been warmed but weren't.
 | |
|           warmingState = "notWarmed";
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       Services.telemetry
 | |
|         .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
 | |
|         .add(warmingState);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   noteStartTabSwitch() {
 | |
|     TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
 | |
|     TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
 | |
| 
 | |
|     if (this._tabswitchTimerId) {
 | |
|       Glean.performanceInteraction.tabSwitchComposite.cancel(
 | |
|         this._tabswitchTimerId
 | |
|       );
 | |
|     }
 | |
|     this._tabswitchTimerId =
 | |
|       Glean.performanceInteraction.tabSwitchComposite.start();
 | |
|     let { innerWindowId } = this.window.windowGlobalChild;
 | |
|     ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start", { innerWindowId });
 | |
|   }
 | |
| 
 | |
|   noteFinishTabSwitch() {
 | |
|     // After this point the tab has switched from the content thread's point of view.
 | |
|     // The changes will be visible after the next refresh driver tick + composite.
 | |
|     let time = TelemetryStopwatch.timeElapsed(
 | |
|       "FX_TAB_SWITCH_TOTAL_E10S_MS",
 | |
|       this.window
 | |
|     );
 | |
|     if (time != -1) {
 | |
|       TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window);
 | |
|       this.log("DEBUG: tab switch time = " + time);
 | |
|       let { innerWindowId } = this.window.windowGlobalChild;
 | |
|       ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish", { innerWindowId });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   noteSpinnerDisplayed() {
 | |
|     this.assert(!this.spinnerTab);
 | |
|     let browser = this.requestedTab.linkedBrowser;
 | |
|     this.assert(browser.isRemoteBrowser);
 | |
|     TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
 | |
|     // We have a second, similar probe for capturing recordings of
 | |
|     // when the spinner is displayed for very long periods.
 | |
|     TelemetryStopwatch.start(
 | |
|       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
 | |
|       this.window
 | |
|     );
 | |
|     let { innerWindowId } = this.window.windowGlobalChild;
 | |
|     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown", {
 | |
|       innerWindowId,
 | |
|     });
 | |
|     Services.telemetry
 | |
|       .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER")
 | |
|       .add(this._loadTimerClearedBy);
 | |
|     if (AppConstants.NIGHTLY_BUILD) {
 | |
|       Services.obs.notifyObservers(null, "tabswitch-spinner");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   noteSpinnerHidden() {
 | |
|     this.assert(this.spinnerTab);
 | |
|     this.log(
 | |
|       "DEBUG: spinner time = " +
 | |
|         TelemetryStopwatch.timeElapsed(
 | |
|           "FX_TAB_SWITCH_SPINNER_VISIBLE_MS",
 | |
|           this.window
 | |
|         )
 | |
|     );
 | |
|     TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window);
 | |
|     TelemetryStopwatch.finish(
 | |
|       "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS",
 | |
|       this.window
 | |
|     );
 | |
|     let { innerWindowId } = this.window.windowGlobalChild;
 | |
|     ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden", {
 | |
|       innerWindowId,
 | |
|     });
 | |
|     // we do not get a onPaint after displaying the spinner
 | |
|     this._loadTimerClearedBy = "none";
 | |
|   }
 | |
| }
 | 
