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