--- 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,45 @@
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);
+
+ let state = this.getTabState(tab);
+ this.assert(state == this.STATE_LOADING ||
+ state == this.STATE_LOADED);
+
+ // Because the TabChildNotReady event is queued from
+ // off of the main thread, it's possible that while
+ // it was being queued, the layer tree became ready
+ // and the state changed to STATE_LOADED. In that
+ // case, there's nothing to do here.
+ if (state == 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();
@@ -4298,16 +4373,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 +4401,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 +4493,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");