Bug 1362866 - Remove MozTabChildNotReady event usage from tabbrowser.xml, and replace with nsITabParent.hasPresented. r?billm draft
authorMike Conley <mconley@mozilla.com>
Sat, 20 May 2017 17:04:48 -0400
changeset 583125 1f7313a6fc772f6b87f9fe1e96423e3a4a5e3044
parent 582257 9851fcb0bf4d855c36729d7de19f0fa5c9f69776
child 583126 80ee5b1e6392224192eb0837d8e860310e8c3594
push id60287
push usermconley@mozilla.com
push dateTue, 23 May 2017 17:22:48 +0000
reviewersbillm
bugs1362866
milestone55.0a1
Bug 1362866 - Remove MozTabChildNotReady event usage from tabbrowser.xml, and replace with nsITabParent.hasPresented. r?billm nsITabParent.hasPresented already encapsulates the case where the TabChild might not have been constructed, since such a tab could never have been presented to the user. If we're async tab switching to a tab that's never presented before, we blank it out initially while waiting for its layers. MozReview-Commit-ID: CL76wLmjRxc
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.xml
browser/base/content/test/performance/browser_tabopen_reflows.js
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -92,24 +92,20 @@ 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[blank],
 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
@@ -3863,22 +3863,16 @@
 
             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(),
 
@@ -3943,16 +3937,29 @@
                   this.onLayersReady(browser);
                 }
               } else if (state == this.STATE_UNLOADING) {
                 browser.docShellIsActive = false;
                 if (!tabParent) {
                   this.onLayersCleared(browser);
                 }
               }
+
+              if (!tab.linkedBrowser.isRemoteBrowser) {
+                // setTabState is potentially re-entrant in the non-remote case,
+                // so we must re-get the state for this assertion.
+                let nonRemoteState = this.getTabState(tab);
+                // Non-remote tabs can never stay in the STATE_LOADING
+                // or STATE_UNLOADING states. By the time this function
+                // exits, a non-remote tab must be in STATE_LOADED or
+                // STATE_UNLOADED, since the painting and the layer
+                // upload happen synchronously.
+                this.assert(nonRemoteState == this.STATE_UNLOADED ||
+                            nonRemoteState == this.STATE_LOADED);
+              }
             },
 
             get minimized() {
               return window.windowState == window.STATE_MINIMIZED;
             },
 
             init() {
               this.log("START");
@@ -3964,17 +3971,16 @@
 
               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);
 
               let tab = this.requestedTab;
               let browser = tab.linkedBrowser;
               let tabIsLoaded = !browser.isRemoteBrowser ||
                                 browser.frameLoader.tabParent.hasPresented;
 
               if (!this.minimized) {
                 this.log("Initial tab is loaded?: " + tabIsLoaded);
@@ -3995,17 +4001,16 @@
 
               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();
             },
@@ -4045,20 +4050,36 @@
               });
               this.tabbrowser.dispatchEvent(event);
             },
 
             // This function is called after all the main state changes to
             // make sure we display the right tab.
             updateDisplay() {
               let requestedTabState = this.getTabState(this.requestedTab);
-
-              let shouldBeBlank =
-                this.pendingTabChild.has(this.requestedTab.linkedBrowser) &&
-                requestedTabState == this.STATE_LOADING;
+              let requestedBrowser = this.requestedTab.linkedBrowser;
+
+              // It is more desirable to show a blank tab when appropriate than
+              // the tab switch spinner - especially since the spinner is usually
+              // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
+              // tab switch. We can hide this lag, and hide the time being spent
+              // constructing TabChild's, layer trees, etc, by showing a blank
+              // tab instead and focusing it immediately.
+              let shouldBeBlank = false;
+              if (requestedBrowser.isRemoteBrowser) {
+                // If a tab is remote, we can show a blank tab instead of a
+                // spinner if we know it has never presented before, or if it
+                // has just crashed and we haven't started showing the tab crashed
+                // page yet.
+                let fl = requestedBrowser.frameLoader;
+                shouldBeBlank = (!fl.tabParent || !fl.tabParent.hasPresented);
+              }
+
+              this.log("Tab should be blank: " + shouldBeBlank);
+              this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
 
               // Figure out which tab we actually want visible right now.
               let showTab = null;
               if (requestedTabState != this.STATE_LOADED &&
                   this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
                 // If we can't show the requestedTab, and lastVisibleTab is
                 // available, show it.
                 showTab = this.lastVisibleTab;
@@ -4066,26 +4087,24 @@
                 // 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.linkedBrowser.removeAttribute("blank");
                 this.blankTab = null;
               } else if (shouldBeBlank && this.blankTab !== showTab) {
                 if (this.blankTab) {
-                  this.blankTab.linkedBrowser.removeAttribute("pendingtabchild");
+                  this.blankTab.linkedBrowser.removeAttribute("blank");
                 }
                 this.blankTab = showTab;
-                this.tabbrowser.setAttribute("pendingtabchild", "true");
-                this.blankTab.linkedBrowser.setAttribute("pendingtabchild", "true");
+                this.blankTab.linkedBrowser.setAttribute("blank", "true");
               }
 
               // Show or hide the spinner as needed.
               let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
                                 !this.minimized &&
                                 !shouldBeBlank;
 
               if (!needSpinner && this.spinnerTab) {
@@ -4196,16 +4215,28 @@
               this.assert(!this.loadingTab ||
                           this.getTabState(this.loadingTab) == this.STATE_LOADING);
 
               // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
               // the timer is set only when we're loading something.
               this.assert(!this.loadTimer || this.loadingTab);
               this.assert(!this.loadingTab || this.loadTimer);
 
+              // If we're switching to a non-remote tab, there's no need to wait
+              // for it to send layers to the compositor, as this will happen
+              // synchronously. Clearing this here means that in the next step,
+              // we can load the non-remote browser immediately.
+              if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
+                this.loadingTab = null;
+                if (this.loadTimer) {
+                  this.clearTimer(this.loadTimer);
+                  this.loadTimer = null;
+                }
+              }
+
               // If we're not loading anything, try loading the requested tab.
               let requestedState = this.getTabState(this.requestedTab);
               if (!this.loadTimer && !this.minimized &&
                   (requestedState == this.STATE_UNLOADED ||
                    requestedState == this.STATE_UNLOADING)) {
                 this.loadRequestedTab();
               }
 
@@ -4229,16 +4260,20 @@
 
               // It's possible for updateDisplay to trigger one of our own event
               // handlers, which might cause finish() to already have been called.
               // Check for that before calling finish() again.
               if (!this.tabbrowser._switcher) {
                 return;
               }
 
+              if (this.blankTab) {
+                this.maybeFinishTabSwitch();
+              }
+
               if (numPending == 0) {
                 this.finish();
               }
 
               this.logState("done");
             },
 
             // Fires when we're ready to unload unused tabs.
@@ -4282,17 +4317,16 @@
               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}, ${browser.isRemoteBrowser})`);
 
               this.assert(this.getTabState(tab) == this.STATE_LOADING ||
                           this.getTabState(tab) == this.STATE_LOADED);
               this.setTabState(tab, this.STATE_LOADED);
 
               this.maybeFinishTabSwitch();
@@ -4309,17 +4343,16 @@
             // 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);
               }
             },
@@ -4333,21 +4366,17 @@
                 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);
+                // it sends up a layer tree.
                 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) {
@@ -4385,40 +4414,31 @@
 
             onSwapDocShells(ourBrowser, otherBrowser) {
               // This event fires before the swap. ourBrowser is from
               // our window. We save the state of otherBrowser since ourBrowser
               // needs to take on that state at the end of the swap.
 
               let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
               let otherState;
-              let pendingTabChild = false;
               if (otherTabbrowser && otherTabbrowser._switcher) {
                 let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
                 let otherSwitcher = otherTabbrowser._switcher;
                 otherState = otherSwitcher.getTabState(otherTab);
-                pendingTabChild = otherSwitcher.pendingTabChild.has(otherBrowser);
-
-                if (pendingTabChild) {
-                  this.assert(otherState == this.STATE_LOADING);
-                }
-
-                otherSwitcher.pendingTabChild.delete(otherBrowser);
               } else {
                 otherState = (otherBrowser.docShellIsActive
                               ? this.STATE_LOADED
                               : this.STATE_UNLOADED);
               }
 
               if (!this.swapMap) {
                 this.swapMap = new WeakMap();
               }
               this.swapMap.set(otherBrowser, {
                 state: otherState,
-                pendingTabChild,
               });
             },
 
             onEndSwapDocShells(ourBrowser, otherBrowser) {
               // The swap has happened. We reset the loadingTab in
               // case it has been swapped. We also set ourBrowser's state
               // to whatever otherBrowser's state was before the swap.
 
@@ -4428,76 +4448,51 @@
                 // ready yet. Typically it will already be ready
                 // though. If it's not, we're probably in a new window,
                 // in which case we have no other tabs to display anyway.
                 this.clearTimer(this.loadTimer);
                 this.loadTimer = null;
               }
               this.loadingTab = null;
 
-              let { state: otherState, pendingTabChild } =
-                this.swapMap.get(otherBrowser);
+              let { state: otherState } = this.swapMap.get(otherBrowser);
 
               this.swapMap.delete(otherBrowser);
 
               let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
               if (ourTab) {
                 this.setTabStateNoAction(ourTab, otherState);
-                if (pendingTabChild) {
-                  this.pendingTabChild.add(ourTab.linkedBrowser);
-                }
               }
             },
 
             shouldActivateDocShell(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               let state = this.getTabState(tab);
               return state == this.STATE_LOADING || state == this.STATE_LOADED;
             },
 
             activateBrowserForPrintPreview(browser) {
               let tab = this.tabbrowser.getTabForBrowser(browser);
               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) {
-              this.assert(browser.isRemoteBrowser);
-
-              let tab = this.tabbrowser.getTabForBrowser(browser);
-
-              this.assert(this.getTabState(tab) == this.STATE_LOADING);
-
-              this.logState(`onTabChildNotReady(${tab._tPos})`);
-              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();
 
               this.requestedTab = tab;
 
               let browser = this.requestedTab.linkedBrowser;
               let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+
               if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
                 fl.tabParent.suppressDisplayport(true);
                 this.activeSuppressDisplayport.add(fl.tabParent);
               }
 
               this.preActions();
 
               if (this.unloadTimer) {
@@ -4530,18 +4525,16 @@
               } 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
@@ -4576,16 +4569,17 @@
                 this.switchInProgress = false;
               }
             },
 
             spinnerDisplayed() {
               this.assert(!this.spinnerTab);
               let browser = this.requestedTab.linkedBrowser;
               this.assert(browser.isRemoteBrowser);
+              this.assert(browser.frameLoader.tabParent.hasPresented);
               TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
               // We have a second, similar probe for capturing recordings of
               // when the spinner is displayed for very long periods.
               TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
               this.addMarker("AsyncTabSwitch:SpinnerShown");
 
               // What kind of tab is about to display this spinner? We have three basic
               // kinds:
--- a/browser/base/content/test/performance/browser_tabopen_reflows.js
+++ b/browser/base/content/test/performance/browser_tabopen_reflows.js
@@ -41,16 +41,24 @@ const EXPECTED_REFLOWS = [
     "BrowserOpenTab@chrome://browser/content/browser.js",
   ],
 
   [
     "openLinkIn@chrome://browser/content/utilityOverlay.js",
     "openUILinkIn@chrome://browser/content/utilityOverlay.js",
     "BrowserOpenTab@chrome://browser/content/browser.js",
   ],
+
+  [
+    // switching focus in updateCurrentBrowser() causes reflows
+    "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
+    "updateDisplay@chrome://browser/content/tabbrowser.xml",
+    "postActions@chrome://browser/content/tabbrowser.xml",
+    "requestTab@chrome://browser/content/tabbrowser.xml"
+  ],
 ];
 
 if (!gMultiProcessBrowser) {
   EXPECTED_REFLOWS.push(
     [
       "_adjustFocusAfterTabSwitch@chrome://browser/content/tabbrowser.xml",
       "updateCurrentBrowser@chrome://browser/content/tabbrowser.xml",
       "onselect@chrome://browser/content/browser.xul",