--- 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: