--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1546,16 +1546,21 @@ pref("browser.tabs.remote.desktopbehavio
#if !defined(RELEASE_OR_BETA) || defined(MOZ_DEV_EDITION)
// At the moment, autostart.2 is used, while autostart.1 is unused.
// We leave it here set to false to reset users' defaults and allow
// us to change everybody to true in the future, when desired.
pref("browser.tabs.remote.autostart.1", false);
pref("browser.tabs.remote.autostart.2", true);
#endif
+// For speculatively warming up tabs to improve perceived
+// performance while using the async tab switcher.
+pref("browser.tabs.remote.maxWarmingTabs", 3);
+pref("browser.tabs.remote.warmingUnloadDelayMs", 2000);
+
// For the about:tabcrashed page
pref("browser.tabs.crashReporting.sendReport", true);
pref("browser.tabs.crashReporting.includeURL", false);
pref("browser.tabs.crashReporting.requestEmail", false);
pref("browser.tabs.crashReporting.emailMe", false);
pref("browser.tabs.crashReporting.email", "");
// Enable e10s add-on interposition by default.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4095,16 +4095,22 @@
// Set of tabs that might be visible right now. We maintain
// this set because we can't be sure when a tab is actually
// drawn. A tab is added to this set when we ask to make it
// visible. All tabs but the most recently shown tab are
// removed from the set upon MozAfterPaint.
maybeVisibleTabs: new Set([this.selectedTab]),
+ // This holds onto the set of tabs that we've been asked to warm up.
+ // This is used only for Telemetry and logging, and (in order to not
+ // over-complicate the async tab switcher any further) has nothing to do
+ // with how warmed tabs are loaded and unloaded.
+ warmingTabs: new WeakSet(),
+
STATE_UNLOADED: 0,
STATE_LOADING: 1,
STATE_LOADED: 2,
STATE_UNLOADING: 3,
// re-entrancy guard:
_processing: false,
@@ -4149,16 +4155,17 @@
let {tabParent} = browser.frameLoader;
if (state == this.STATE_LOADING) {
this.assert(!this.minimizedOrFullyOccluded);
browser.docShellIsActive = true;
if (!tabParent) {
this.onLayersReady(browser);
}
} else if (state == this.STATE_UNLOADING) {
+ this.unwarmTab(tab);
browser.docShellIsActive = false;
if (!tabParent) {
this.onLayersCleared(browser);
}
}
if (!tab.linkedBrowser.isRemoteBrowser) {
// setTabState is potentially re-entrant in the non-remote case,
@@ -4177,16 +4184,21 @@
get minimizedOrFullyOccluded() {
return window.windowState == window.STATE_MINIMIZED ||
window.isFullyOccluded;
},
init() {
this.log("START");
+ XPCOMUtils.defineLazyPreferenceGetter(this, "MAX_WARMING_TABS",
+ "browser.tabs.remote.maxWarmingTabs", 3);
+ XPCOMUtils.defineLazyPreferenceGetter(this, "WARMING_UNLOAD_DELAY" /* ms */,
+ "browser.tabs.remote.warmingUnloadDelayMs", 2000);
+
// If we minimized the window before the switcher was activated,
// we might have set the preserveLayers flag for the current
// browser. Let's clear it.
this.tabbrowser.mCurrentBrowser.preserveLayers(false);
window.addEventListener("MozAfterPaint", this);
window.addEventListener("MozLayerTreeReady", this);
window.addEventListener("MozLayerTreeCleared", this);
@@ -4426,16 +4438,17 @@
// to account for closed tabs.
preActions() {
this.assert(this.tabbrowser._switcher);
this.assert(this.tabbrowser._switcher === this);
for (let [tab, ] of this.tabState) {
if (!tab.linkedBrowser) {
this.tabState.delete(tab);
+ this.unwarmTab(tab);
}
}
if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
this.lastVisibleTab = null;
}
if (this.blankTab && !this.blankTab.linkedBrowser) {
this.blankTab = null;
@@ -4483,54 +4496,66 @@
if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
(requestedState == this.STATE_UNLOADED ||
requestedState == this.STATE_UNLOADING)) {
this.loadRequestedTab();
}
// See how many tabs still have work to do.
let numPending = 0;
+ let numWarming = 0;
for (let [tab, state] of this.tabState) {
// Skip print preview browsers since they shouldn't affect tab switching.
if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
continue;
}
if (state == this.STATE_LOADED && tab !== this.requestedTab) {
numPending++;
+
+ if (tab !== this.visibleTab) {
+ numWarming++;
+ }
}
if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
numPending++;
}
}
this.updateDisplay();
// 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();
+ this.maybeFinishTabSwitch();
+
+ if (numWarming > this.MAX_WARMING_TABS) {
+ this.logState("Hit MAX_WARMING_TABS");
+ if (this.unloadTimer) {
+ this.clearTimer(this.unloadTimer);
+ }
+ this.onUnloadTimeout();
}
if (numPending == 0) {
this.finish();
}
this.logState("done");
},
// Fires when we're ready to unload unused tabs.
onUnloadTimeout() {
this.logState("onUnloadTimeout");
this.unloadTimer = null;
+ this.warmingTabs = new WeakSet();
this.preActions();
let numPending = 0;
// Unload any tabs that can be unloaded.
for (let [tab, state] of this.tabState) {
if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
continue;
@@ -4570,31 +4595,28 @@
onLayersReady(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();
-
if (this.loadingTab === tab) {
this.clearTimer(this.loadTimer);
this.loadTimer = null;
this.loadingTab = null;
}
},
// Fires when we paint the screen. Any tab switches we initiated
// previously are done, so there's no need to keep the old layers
// around.
onPaint() {
this.maybeVisibleTabs.clear();
- this.maybeFinishTabSwitch();
},
// Called when we're done clearing the layers for a tab.
onLayersCleared(browser) {
let tab = this.tabbrowser.getTabForBrowser(browser);
if (tab) {
this.logState(`onLayersCleared(${tab._tPos})`);
this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
@@ -4715,45 +4737,91 @@
return state == this.STATE_LOADING || state == this.STATE_LOADED;
},
activateBrowserForPrintPreview(browser) {
let tab = this.tabbrowser.getTabForBrowser(browser);
this.setTabState(tab, this.STATE_LOADING);
},
+ canWarmTab(tab) {
+ // If the tab is not yet inserted, closing, not remote,
+ // crashed, already visible, or already requested, warming
+ // up the tab makes no sense.
+ if (this.minimizedOrFullyOccluded ||
+ !tab.linkedPanel ||
+ tab.closing ||
+ !tab.linkedBrowser.isRemoteBrowser ||
+ !tab.linkedBrowser.frameLoader.tabParent) {
+ return false;
+ }
+
+ // Similarly, if the tab is already in STATE_LOADING or
+ // STATE_LOADED somehow, there's no point in trying to
+ // warm it up.
+ let state = this.getTabState(tab);
+ if (state === this.STATE_LOADING ||
+ state === this.STATE_LOADED) {
+ return false;
+ }
+
+ return true;
+ },
+
+ unwarmTab(tab) {
+ this.warmingTabs.delete(tab);
+ },
+
+ warmupTab(tab) {
+ if (!this.canWarmTab(tab)) {
+ return;
+ }
+
+ this.logState("warmupTab " + this.tinfo(tab));
+
+ this.warmingTabs.add(tab);
+ this.setTabState(tab, this.STATE_LOADING);
+ this.suppressDisplayPortAndQueueUnload(tab, this.WARMING_UNLOAD_DELAY);
+ },
+
// Called when the user asks to switch to a given tab.
requestTab(tab) {
if (tab === this.requestedTab) {
return;
}
+ this.unwarmTab(tab);
+
this._requestingTab = true;
this.logState("requestTab " + this.tinfo(tab));
this.startTabSwitch();
this.requestedTab = tab;
- let browser = this.requestedTab.linkedBrowser;
+ this.suppressDisplayPortAndQueueUnload(this.requestedTab, this.UNLOAD_DELAY);
+ this._requestingTab = false;
+ },
+
+ suppressDisplayPortAndQueueUnload(tab, unloadTimeout) {
+ let browser = tab.linkedBrowser;
let fl = browser.frameLoader;
if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
fl.tabParent.suppressDisplayport(true);
this.activeSuppressDisplayport.add(fl.tabParent);
}
this.preActions();
if (this.unloadTimer) {
this.clearTimer(this.unloadTimer);
}
- this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+ this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
this.postActions();
- this._requestingTab = false;
},
handleEvent(event, delayed = false) {
if (this._processing) {
this.setTimer(() => this.handleEvent(event, true), 0);
return;
}
if (delayed && this.tabbrowser._switcher != this) {
@@ -4833,17 +4901,16 @@
spinnerHidden() {
this.assert(this.spinnerTab);
this.log("DEBUG: spinner time = " +
TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
this.addMarker("AsyncTabSwitch:SpinnerHidden");
// we do not get a onPaint after displaying the spinner
- this.maybeFinishTabSwitch();
},
addMarker(marker) {
if (Services.profiler) {
Services.profiler.AddMarker(marker);
}
},
@@ -4885,22 +4952,24 @@
logState(prefix) {
if (!this.logging())
return;
let accum = prefix + " ";
for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
let tab = this.tabbrowser.tabs[i];
let state = this.getTabState(tab);
+ let isWarming = this.warmingTabs.has(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 (isWarming) accum += "(W)";
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");
@@ -4910,16 +4979,25 @@
},
};
this._switcher = switcher;
switcher.init();
return switcher;
]]></body>
</method>
+ <method name="warmupTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ this._getSwitcher().warmupTab(aTab);
+ ]]>
+ </body>
+ </method>
+
<!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
MAKE SURE TO ADD IT HERE AS WELL. -->
<property name="canGoBack"
onget="return this.mCurrentBrowser.canGoBack;"
readonly="true"/>
<property name="canGoForward"
onget="return this.mCurrentBrowser.canGoForward;"