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