Bug 1342927 - Make async tab switcher show a blank tab when switching to tabs that have no TabChild yet. r=billm
☠☠ backed out by 5e950d79f1b5 ☠ ☠
authorMike Conley <mconley@mozilla.com>
Wed, 01 Mar 2017 14:26:43 -0500
changeset 394857 692743c2995e380dfe5279547dd8bfc3e43edcae
parent 394856 7b38f4123214a2704ec62203f3c902826c92fb90
child 394858 bba8780c5312ebdaf798fc12152ed13075ad2d2c
push id1468
push userasasaki@mozilla.com
push dateMon, 05 Jun 2017 19:31:07 +0000
treeherdermozilla-release@0641fc6ee9d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbillm
bugs1342927
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1342927 - Make async tab switcher show a blank tab when switching to tabs that have no TabChild yet. r=billm MozReview-Commit-ID: J09CZHFGM2B
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.xml
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -92,19 +92,24 @@ tabpanels {
  * memory that to-be-restored tabs would otherwise consume simply by setting
  * their browsers to 'display: none' as that will prevent them from having to
  * create a presentation and the like.
  */
 browser[pending] {
   display: none;
 }
 
+browser[pendingtabchild],
 browser[pendingpaint] {
   opacity: 0;
 }
 
+tabbrowser[pendingtabchild] {
+  background-color: #ffffff !important;
+}
+
 tabbrowser[pendingpaint] {
   background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
   background-repeat: no-repeat;
   background-position: center center;
   background-color: #f9f9f9 !important;
   background-size: 30px;
 }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3719,25 +3719,32 @@
 
             // We show this tab in case the requestedTab hasn't loaded yet.
             lastVisibleTab: this.selectedTab,
 
             // Auxilliary state variables:
 
             visibleTab: this.selectedTab,   // Tab that's on screen.
             spinnerTab: null,               // Tab showing a spinner.
+            blankTab: null,                 // Tab showing blank.
             originalTab: this.selectedTab,  // Tab that we started on.
 
             tabbrowser: this,  // Reference to gBrowser.
             loadTimer: null,   // TAB_SWITCH_TIMEOUT nsITimer instance.
             unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
 
             // Map from tabs to STATE_* (below).
             tabState: new Map(),
 
+            // Holds a collection of <xul:browser>'s for this tabbrowser
+            // that we cannot force paint since their TabChild's haven't
+            // been constructed yet. Instead, we show blank tabs for them
+            // when attempting to switch to them.
+            pendingTabChild: new WeakSet(),
+
             // True if we're in the midst of switching tabs.
             switchInProgress: false,
 
             // Keep an exact list of content processes (tabParent) in which
             // we're actively suppressing the display port. This gives a robust
             // way to make sure we don't forget to un-suppress.
             activeSuppressDisplayport: new Set(),
 
@@ -3823,16 +3830,17 @@
 
               window.addEventListener("MozAfterPaint", this);
               window.addEventListener("MozLayerTreeReady", this);
               window.addEventListener("MozLayerTreeCleared", this);
               window.addEventListener("TabRemotenessChange", this);
               window.addEventListener("sizemodechange", this);
               window.addEventListener("SwapDocShells", this, true);
               window.addEventListener("EndSwapDocShells", this, true);
+              window.addEventListener("MozTabChildNotReady", this, true);
               if (!this.minimized) {
                 this.setTabState(this.requestedTab, this.STATE_LOADED);
               }
             },
 
             destroy() {
               if (this.unloadTimer) {
                 this.clearTimer(this.unloadTimer);
@@ -3845,31 +3853,33 @@
 
               window.removeEventListener("MozAfterPaint", this);
               window.removeEventListener("MozLayerTreeReady", this);
               window.removeEventListener("MozLayerTreeCleared", this);
               window.removeEventListener("TabRemotenessChange", this);
               window.removeEventListener("sizemodechange", this);
               window.removeEventListener("SwapDocShells", this, true);
               window.removeEventListener("EndSwapDocShells", this, true);
+              window.removeEventListener("MozTabChildNotReady", this, true);
 
               this.tabbrowser._switcher = null;
 
               this.activeSuppressDisplayport.forEach(function(tabParent) {
                 tabParent.suppressDisplayport(false);
               });
               this.activeSuppressDisplayport.clear();
             },
 
             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.minimized || this.getTabState(this.requestedTab) == this.STATE_LOADED);
 
               this.destroy();
 
               let toBrowser = this.requestedTab.linkedBrowser;
@@ -3890,30 +3900,52 @@
                 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 shouldBeBlank = this.pendingTabChild.has(
+                this.requestedTab.linkedBrowser);
+
               // Figure out which tab we actually want visible right now.
               let showTab = null;
               if (this.getTabState(this.requestedTab) != this.STATE_LOADED &&
-                  this.lastVisibleTab && this.loadTimer) {
+                  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.
+                // 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.tabbrowser.removeAttribute("pendingtabchild");
+                this.blankTab.linkedBrowser.removeAttribute("pendingtabchild");
+                this.blankTab = null;
+              } else if (shouldBeBlank && this.blankTab !== showTab) {
+                if (this.blankTab) {
+                  this.blankTab.linkedBrowser.removeAttribute("pendingtabchild");
+                }
+                this.blankTab = showTab;
+                this.tabbrowser.setAttribute("pendingtabchild", "true");
+                this.blankTab.linkedBrowser.setAttribute("pendingtabchild", "true");
+              }
+
               // Show or hide the spinner as needed.
-              let needSpinner = this.getTabState(showTab) != this.STATE_LOADED && !this.minimized;
+              let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
+                                !this.minimized &&
+                                !shouldBeBlank;
+
               if (!needSpinner && this.spinnerTab) {
                 this.spinnerHidden();
                 this.tabbrowser.removeAttribute("pendingpaint");
                 this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
                 this.spinnerTab = null;
               } else if (needSpinner && this.spinnerTab !== showTab) {
                 if (this.spinnerTab) {
                   this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
@@ -3988,16 +4020,19 @@
                 if (!tab.linkedBrowser) {
                   this.tabState.delete(tab);
                 }
               }
 
               if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
                 this.lastVisibleTab = null;
               }
+              if (this.blankTab && !this.blankTab.linkedBrowser) {
+                this.blankTab = null;
+              }
               if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
                 this.spinnerHidden();
                 this.spinnerTab = null;
               }
               if (this.loadingTab && !this.loadingTab.linkedBrowser) {
                 this.loadingTab = null;
                 this.clearTimer(this.loadTimer);
                 this.loadTimer = null;
@@ -4100,18 +4135,19 @@
               this.preActions();
               this.loadTimer = null;
               this.loadingTab = null;
               this.postActions();
             },
 
             // Fires when the layers become available for a tab.
             onLayersReady(browser) {
+              this.pendingTabChild.delete(browser);
               let tab = this.tabbrowser.getTabForBrowser(browser);
-              this.logState(`onLayersReady(${tab._tPos})`);
+              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.maybeFinishTabSwitch();
 
               if (this.loadingTab === tab) {
@@ -4126,16 +4162,17 @@
             // around.
             onPaint() {
               this.maybeVisibleTabs.clear();
               this.maybeFinishTabSwitch();
             },
 
             // Called when we're done clearing the layers for a tab.
             onLayersCleared(browser) {
+              this.pendingTabChild.delete(browser);
               let tab = this.tabbrowser.getTabForBrowser(browser);
               if (tab) {
                 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);
               }
             },
@@ -4146,16 +4183,25 @@
             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. We also add the browser to
+                // the pendingTabChild set since this browser is unlikely
+                // to have its TabChild set up right away, and we want to
+                // make it appear "blank" instead of showing a spinner for it.
+                this.pendingTabChild.add(tab.linkedBrowser);
+                this.setTabState(tab, this.STATE_LOADING);
               }
             },
 
             // Called when a tab has been removed, and the browser node is
             // about to be removed from the DOM.
             onTabRemoved(tab) {
               if (this.lastVisibleTab == tab) {
                 // The browser that was being presented to the user is
@@ -4243,16 +4289,35 @@
               return state == this.STATE_LOADING || state == this.STATE_LOADED;
             },
 
             activateBrowserForPrintPreview(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               this.setTabState(tab, this.STATE_LOADING);
             },
 
+            // The tab for this browser isn't currently set
+            // up in the content process, so we have no chance
+            // of painting it right away. We'll paint a blank
+            // tab instead.
+            onTabChildNotReady(browser) {
+              let tab = this.tabbrowser.getTabForBrowser(browser);
+              this.logState(`onTabChildNotReady(${tab._tPos})`);
+
+              this.assert(this.getTabState(tab) == this.STATE_LOADING);
+              this.pendingTabChild.add(browser);
+              this.maybeFinishTabSwitch();
+
+              if (this.loadingTab === tab) {
+                this.clearTimer(this.loadTimer);
+                this.loadTimer = null;
+                this.loadingTab = null;
+              }
+            },
+
             // Called when the user asks to switch to a given tab.
             requestTab(tab) {
               if (tab === this.requestedTab) {
                 return;
               }
 
               this.logState("requestTab " + this.tinfo(tab));
               this.startTabSwitch();
@@ -4298,16 +4363,18 @@
               } else if (event.type == "TabRemotenessChange") {
                 this.onRemotenessChange(event.target);
               } else if (event.type == "sizemodechange") {
                 this.onSizeModeChange();
               } else if (event.type == "SwapDocShells") {
                 this.onSwapDocShells(event.originalTarget, event.detail);
               } else if (event.type == "EndSwapDocShells") {
                 this.onEndSwapDocShells(event.originalTarget, event.detail);
+              } else if (event.type == "MozTabChildNotReady") {
+                this.onTabChildNotReady(event.originalTarget);
               }
 
               this.postActions();
               this._processing = false;
             },
 
             /*
              * Telemetry and Profiler related helpers for recording tab switch
@@ -4324,17 +4391,18 @@
             /**
              * 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.getTabState(this.requestedTab) == this.STATE_LOADED ||
+                   this.requestedTab === this.blankTab)) {
                 // 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", window);
                 if (time != -1) {
                   TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
                   this.log("DEBUG: tab switch time = " + time);
                   this.addMarker("AsyncTabSwitch:Finish");
                 }
@@ -4415,16 +4483,17 @@
               for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
                 let tab = this.tabbrowser.tabs[i];
                 let state = this.getTabState(tab);
 
                 accum += i + ":";
                 if (tab === this.lastVisibleTab) accum += "V";
                 if (tab === this.loadingTab) accum += "L";
                 if (tab === this.requestedTab) accum += "R";
+                if (tab === this.blankTab) accum += "B";
                 if (state == this.STATE_LOADED) accum += "(+)";
                 if (state == this.STATE_LOADING) accum += "(+?)";
                 if (state == this.STATE_UNLOADED) accum += "(-)";
                 if (state == this.STATE_UNLOADING) accum += "(-?)";
                 accum += " ";
               }
               if (this._useDumpForLogging) {
                 dump(accum + "\n");