Bug 1261152 - Make TPS more accurate using MozAfterPaint with layer transaction ids. r=jmaher

MozReview-Commit-ID: 1IHrTKMq2E8

--HG--
extra : rebase_source : 86ef0b5907e11d6ad947b7ded76873e1371a0eda
This commit is contained in:
Mike Conley 2016-04-14 16:05:54 -04:00
Родитель be84cb9eff
Коммит b6f80962af
1 изменённых файлов: 354 добавлений и 168 удалений

Просмотреть файл

@ -5,12 +5,15 @@ var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
.getService(Ci.nsIAboutNewTabService);
var aboutBlankTab = null;
var Profiler = null;
let context = {};
let TalosParentProfiler;
var windowListener = {
onOpenWindow: function(aWindow) {
@ -47,153 +50,299 @@ function executeSoon(callback) {
Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
}
function whenDelayedStartupFinished(win, callback) {
const topic = "browser-delayed-startup-finished";
Services.obs.addObserver(function onStartup(subject) {
if (win == subject) {
Services.obs.removeObserver(onStartup, topic);
executeSoon(callback);
}
}, topic, false);
}
function waitForTabLoads(browser, urls, callback) {
// Make sure we get load events for all the urls we need.
// Before we kept a count and sometimes received load events for other tabs
// and got a bad count.
var waitingToLoad = {};
for (var i = 0; i < urls.length; i++) {
waitingToLoad[urls[i]] = true;
}
let listener = {
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
"nsISupportsWeakReference"]),
onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_NETWORK;
if ((aStateFlags & loadedState) == loadedState &&
!aWebProgress.isLoadingDocument &&
aWebProgress.isTopLevel &&
Components.isSuccessCode(aStatus)) {
delete waitingToLoad[aBrowser.currentURI.spec];
dump(`Loaded: ${aBrowser.currentURI.spec}\n`);
aBrowser.messageManager.loadFrameScript("chrome://pageloader/content/talos-content.js", false);
if (Object.keys(waitingToLoad).length == 0) {
browser.removeTabsProgressListener(listener);
callback();
}
/**
* Returns a Promise that resolves when browser-delayed-startup-finished
* fires for a given window
*
* @param win
* The window that we're waiting for the notification for.
* @returns Promise
*/
function waitForDelayedStartup(win) {
return new Promise((resolve) => {
const topic = "browser-delayed-startup-finished";
Services.obs.addObserver(function onStartup(subject) {
if (win == subject) {
Services.obs.removeObserver(onStartup, topic);
resolve();
}
},
onLocationChange: function(aProgress, aRequest, aURI) {
},
onProgressChange: function(aWebProgress, aRequest, curSelf, maxSelf, curTot, maxTot) {},
onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {},
onSecurityChange: function(aWebProgress, aRequest, aState) {}
}
browser.addTabsProgressListener(listener);
}
function loadTabs(urls, win, callback) {
let context = {};
Services.scriptloader.loadSubScript("chrome://pageloader/content/Profiler.js", context);
Profiler = context.Profiler;
// We don't want to catch scrolling the tabstrip in our tests
win.gBrowser.tabContainer.style.visibility = "hidden";
let initialTab = win.gBrowser.selectedTab;
// Set about:blank to be the first tab. This will allow us to use about:blank
// to let paint event stabilize and make all tab switch more even.
initialTab.linkedBrowser.loadURI("about:blank", null, null);
waitForTabLoads(win.gBrowser, urls, function() {
let tabs = win.gBrowser.getTabsToTheEndFrom(initialTab);
callback(tabs);
}, topic, false);
});
win.gBrowser.loadTabs(urls, true);
aboutBlankTab = initialTab;
}
function waitForTabSwitchDone(win, callback) {
if (win.gBrowser.selectedBrowser.isRemoteBrowser) {
var list = function onSwitch() {
win.gBrowser.removeEventListener("TabSwitched", list);
callback();
/**
* For some <xul:tabbrowser>, loads a collection of URLs as new tabs
* in that browser.
*
* @param gBrowser (<xul:tabbrowser>)
* The <xul:tabbrowser> in which to load the new tabs.
* @param urls (Array)
* An array of URL strings to be loaded as new tabs.
* @returns Promise
* Resolves once all tabs have finished loading.
*/
function loadTabs(gBrowser, urls) {
return new Promise((resolve) => {
gBrowser.loadTabs(urls, true);
let waitingToLoad = new Set(urls);
let listener = {
QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
"nsISupportsWeakReference"]),
onStateChange: function(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
Ci.nsIWebProgressListener.STATE_IS_NETWORK;
if ((aStateFlags & loadedState) == loadedState &&
!aWebProgress.isLoadingDocument &&
aWebProgress.isTopLevel &&
Components.isSuccessCode(aStatus)) {
dump(`Loaded: ${aBrowser.currentURI.spec}\n`);
waitingToLoad.delete(aBrowser.currentURI.spec);
if (!waitingToLoad.size) {
gBrowser.removeTabsProgressListener(listener);
dump("Loads complete - starting tab switches\n");
resolve();
}
}
},
};
win.gBrowser.addEventListener("TabSwitched", list);
} else {
// Tab switch is sync so it has already happened.
callback();
}
}
function runTest(tabs, win, callback) {
let startTab = win.gBrowser.selectedTab;
let times = [];
runTestHelper(startTab, tabs, 0, win, times, function() {
callback(times);
gBrowser.addTabsProgressListener(listener);
});
}
function runTestHelper(startTab, tabs, index, win, times, callback) {
/**
* Loads the utility content script for the tps for out-of-process
* browsers into a browser. This should not be used for in-process
* browsers.
*
* The utility script will send a "TPS:ContentSawPaint" message
* through the browser's message manager when it sees that its
* content has been presented to the user.
*
* @param browser (<xul:browser>)
* The remote browser to load the script in.
* @returns Promise
* Resolves once the script has been loaded and executed in
* the remote browser.
*/
function loadTPSContentScript(browser) {
if (!browser.isRemoteBrowser) {
throw new Error("loadTPSContentScript expects a remote browser.");
}
return new Promise((resolve) => {
// Here's our utility script. We'll serialize this and send it down
// to run in the content process for this browser.
let script = function() {
let Cu = Components.utils;
let Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
let tab = tabs[index];
/**
* In order to account for the fact that a MozAfterPaint might fire
* for a composite that's unrelated to this tab's content being
* painted, we'll get the last used layer transaction ID for
* this content's refresh driver, and make sure that the MozAfterPaint
* that we react to has a greater transaction id.
*
* Note also that this comment needs to stay inside this comment
* block. No // comments allowed when serializing JS to content
* scripts this way.
*/
let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let lastTransactionId = cwu.lastTransactionId;
Services.profiler.AddMarker("Content waiting for id > " + lastTransactionId);
// Clean up garbage so GC/CC doesn't randomly occur during our test
// making the results noisy
win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.garbageCollect();
forceContentGC(tab.linkedBrowser).then(function() {
if (typeof(Profiler) !== "undefined") {
Profiler.resume(tab.linkedBrowser.currentURI.spec);
}
let start = win.performance.now();
win.gBrowser.selectedTab = tab;
waitForTabSwitchDone(win, function() {
// This will fire when we're about to paint the tab switch
win.requestAnimationFrame(function() {
// This will fire on the next vsync tick after the tab has switched.
// If we have a sync transaction on the compositor, that time will
// be included here. It will not accuratly capture the composite time
// or the time of async transaction.
// XXX: This will need to be adjusted for e10s since we need to block
// on the child/content having painted.
win.requestAnimationFrame(function() {
times.push(win.performance.now() - start);
if (typeof(Profiler) !== "undefined") {
Profiler.pause(tab.linkedBrowser.currentURI.spec);
}
// Select about:blank which will let the browser reach a steady no
// painting state
win.gBrowser.selectedTab = aboutBlankTab;
win.requestAnimationFrame(function() {
win.requestAnimationFrame(function() {
if (index == tabs.length - 1) {
callback();
} else {
runTestHelper(startTab, tabs, index + 1, win, times, function() {
callback();
});
}
});
addEventListener("MozAfterPaint", function onPaint(event) {
Services.profiler.AddMarker("Content saw transaction id: " + event.transactionId);
if (event.transactionId > lastTransactionId) {
Services.profiler.AddMarker("Content saw correct MozAfterPaint");
sendAsyncMessage("TPS:ContentSawPaint", {
time: Date.now().valueOf(),
});
});
removeEventListener("MozAfterPaint", onPaint);
}
});
sendAsyncMessage("TPS:ContentReady");
};
let mm = browser.messageManager;
mm.loadFrameScript("data:,(" + script.toString() + ")();", true);
mm.addMessageListener("TPS:ContentReady", function onReady() {
mm.removeMessageListener("TPS:ContentReady", onReady);
resolve();
});
});
}
function forceContentGC(browser) {
/**
* For some <xul:tab> in a browser window, have that window switch
* to that tab. Returns a Promise that resolves ones the tab content
* has been presented to the user.
*/
function switchToTab(tab) {
let browser = tab.linkedBrowser;
let gBrowser = tab.ownerGlobal.gBrowser;
// Single-process tab switching works quite differently from
// multi-process tab switching. In the single-process case, tab
// switching is synchronous, whereas in the multi-process case,
// it is not. The following two tab switching mechanisms encapsulate
// those two differences.
if (browser.isRemoteBrowser) {
return Task.spawn(function*() {
// The multi-process case requires that we load our utility script
// inside the content, since it's the content that will hear a MozAfterPaint
// once the content is presented to the user.
yield loadTPSContentScript(browser);
let start = Date.now().valueOf();
TalosParentProfiler.resume("start (" + start + "): " + browser.currentURI.spec);
// We need to wait for the TabSwitchDone event to make sure
// that the async tab switcher has shut itself down.
let switchDone = waitForTabSwitchDone(browser);
// Set up our promise that will wait for the content to be
// presented.
let finishPromise = waitForContentPresented(browser);
// Finally, do the tab switch.
gBrowser.selectedTab = tab;
yield switchDone;
let finish = yield finishPromise;
TalosParentProfiler.mark("end (" + finish + ")");
return finish - start;
});
}
return Task.spawn(function*() {
let win = browser.ownerGlobal;
let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let start = Date.now().valueOf();
TalosParentProfiler.resume("start (" + start + "): " + browser.currentURI.spec);
// There is no async tab switcher for the single-process case,
// but tabbrowser.xml will still fire this once the updateCurrentBrowser
// method runs.
let switchDone = waitForTabSwitchDone(browser);
// Do our tab switch
gBrowser.selectedTab = tab;
// Because the above tab switch is synchronous, we know that the
// we want a MozAfterPaint with a greater layer transaction id than
// what is currently the "last transaction id" for the window.
let lastTransactionId = winUtils.lastTransactionId;
yield switchDone;
// Now we'll wait for content to be presented. Because
// this is the single-process case, we pass the last transaction
// id that we got so that we don't get any intermediate MozAfterPaint's
// that might fire before web content is shown.
let finish = yield waitForContentPresented(browser, lastTransactionId);
TalosParentProfiler.mark("end (" + finish + ")");
return finish - start;
});
}
/**
* For some <xul:browser>, find the <xul:tabbrowser> associated with it,
* and wait until that tabbrowser has finished a tab switch. This function
* assumes a tab switch has started, or is about to start.
*
* @param browser (<xul:browser>)
* The browser whose tabbrowser we expect to be involved in a tab
* switch.
* @returns Promise
* Resolves once the TabSwitchDone event is fired.
*/
function waitForTabSwitchDone(browser) {
return new Promise((resolve) => {
let gBrowser = browser.ownerGlobal.gBrowser;
gBrowser.addEventListener("TabSwitchDone", function onTabSwitchDone() {
gBrowser.removeEventListener("TabSwitchDone", onTabSwitchDone);
resolve();
});
});
}
/**
* For some <xul:browser>, returns a Promise that resolves once its
* content has been presented to the user.
*
* @param browser (<xul:browser>)
* The browser we expect to be presented.
* @param lastTransactionId (int, optional)
* In the single-process case, we need to know the last layer
* transaction id that was used before the switch started. That
* way, when the MozAfterPaint fires, we can be sure that its
* transaction id is greater than the one that was last used,
* so we know that the content has definitely been presented.
*
* This argument is ignored in the multi-process browser case.
*
* @returns Promise
* Resolves once the content has been presented. Resolves to
* the system time that the presentation occurred at, in
* milliseconds since midnight 01 January, 1970 UTC.
*/
function waitForContentPresented(browser, lastTransactionId) {
// We treat multi-process browsers differently here - we expect the
// utility script we loaded to inform us once content has been presented.
if (browser.isRemoteBrowser) {
return new Promise((resolve) => {
let mm = browser.messageManager;
mm.addMessageListener("TPS:ContentSawPaint", function onContentPaint(msg) {
mm.removeMessageListener("TPS:ContentSawPaint", onContentPaint);
resolve(msg.data.time);
});
});
}
// Wait for the next MozAfterPaint for this browser's window that has
// a greater transaction id than lastTransactionId.
return new Promise((resolve) => {
let win = browser.ownerGlobal;
win.addEventListener("MozAfterPaint", function onPaint(event) {
if (event instanceof Ci.nsIDOMNotifyPaintEvent) {
TalosParentProfiler.mark("Content saw transaction id: " + event.transactionId);
if (event.transactionId > lastTransactionId) {
win.removeEventListener("MozAfterPaint", onPaint);
TalosParentProfiler.mark("Content saw MozAfterPaint");
resolve(Date.now().valueOf());
}
}
});
});
}
/**
* Given some browser, do a garbage collect in the parent, and then
* a garbage collection in the content process that the browser is
* running in.
*
* @param browser (<xul:browser>)
* The browser in which to do the garbage collection.
* @returns Promise
* Resolves once garbage collection has been completed in the
* parent, and the content process for the browser (if applicable).
*/
function forceGC(win, browser) {
// TODO: Find a better way of letting Talos force GC in the child. We're
// stealing a chunk of pageloader to do this, and we should probably put
// something into TalosPowers instead.
browser.messageManager.loadFrameScript("chrome://pageloader/content/talos-content.js", false);
win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.garbageCollect();
return new Promise((resolve) => {
let mm = browser.messageManager;
mm.addMessageListener("Talos:ForceGC:OK", function onTalosContentForceGC(msg) {
@ -204,47 +353,84 @@ function forceContentGC(browser) {
});
}
/**
* Given some host window, open a new window, browser its initial tab to
* about:blank, then load up our set of testing URLs. Once they've all finished
* loading, switch through each tab, recording their tab switch times. Finally,
* report the results.
*
* @param window
* A host window. Primarily, we just use this for the OpenBrowserWindow
* function defined in that window.
* @returns Promise
*/
function test(window) {
let win = window.OpenBrowserWindow();
let testURLs;
Services.scriptloader.loadSubScript("chrome://talos-powers-content/content/TalosParentProfiler.js", context);
TalosParentProfiler = context.TalosParentProfiler;
try {
let prefFile = Services.prefs.getCharPref("addon.test.tabswitch.urlfile");
if (prefFile) {
testURLs = handleFile(win, prefFile);
return Task.spawn(function*() {
let testURLs = [];
let win = window.OpenBrowserWindow();
try {
let prefFile = Services.prefs.getCharPref("addon.test.tabswitch.urlfile");
if (prefFile) {
testURLs = handleFile(win, prefFile);
}
} catch (ex) { /* error condition handled below */ }
if (!testURLs || testURLs.length == 0) {
dump("no tabs to test, 'addon.test.tabswitch.urlfile' pref isn't set to page set path\n");
return;
}
} catch (ex) { /* error condition handled below */ }
if (!testURLs || testURLs.length == 0) {
dump("no tabs to test, 'addon.test.tabswitch.urlfile' pref isn't set to page set path\n");
return;
}
whenDelayedStartupFinished(win, function() {
loadTabs(testURLs, win, function(tabs) {
runTest(tabs, win, function(times) {
let output = '<!DOCTYPE html>'+
'<html lang="en">'+
'<head><title>Tab Switch Results</title></head>'+
'<body><h1>Tab switch times</h1>' +
'<table>';
let time = 0;
for(let i in times) {
time += times[i];
output += '<tr><td>' + testURLs[i] + '</td><td>' + times[i] + 'ms</td></tr>';
}
output += '</table></body></html>';
dump("total tab switch time:" + time + "\n");
let resultsTab = win.gBrowser.loadOneTab('data:text/html;charset=utf-8,' +
encodeURIComponent(output));
let pref = Services.prefs.getBoolPref("browser.tabs.warnOnCloseOtherTabs");
if (pref)
Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", false);
win.gBrowser.removeAllTabsBut(resultsTab);
if (pref)
Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", pref);
Services.obs.notifyObservers(win, 'tabswitch-test-results', JSON.stringify({'times': times, 'urls': testURLs}));
});
});
yield waitForDelayedStartup(win);
let gBrowser = win.gBrowser;
// We don't want to catch scrolling the tabstrip in our tests
gBrowser.tabContainer.style.visibility = "hidden";
let initialTab = gBrowser.selectedTab;
yield loadTabs(gBrowser, testURLs);
// We'll switch back to about:blank after each tab switch
// in an attempt to put the graphics layer into a "steady"
// state before switching to the next tab.
initialTab.linkedBrowser.loadURI("about:blank", null, null);
let tabs = gBrowser.getTabsToTheEndFrom(initialTab);
let times = [];
for (let tab of tabs) {
yield forceGC(win, tab.linkedBrowser);
let time = yield switchToTab(tab);
dump(`${tab.linkedBrowser.currentURI.spec}: ${time}ms\n`);
times.push(time);
yield switchToTab(initialTab);
}
let output = '<!DOCTYPE html>'+
'<html lang="en">'+
'<head><title>Tab Switch Results</title></head>'+
'<body><h1>Tab switch times</h1>' +
'<table>';
let time = 0;
for(let i in times) {
time += times[i];
output += '<tr><td>' + testURLs[i] + '</td><td>' + times[i] + 'ms</td></tr>';
}
output += '</table></body></html>';
dump("total tab switch time:" + time + "\n");
let resultsTab = win.gBrowser.loadOneTab('data:text/html;charset=utf-8,' +
encodeURIComponent(output));
let pref = Services.prefs.getBoolPref("browser.tabs.warnOnCloseOtherTabs");
if (pref)
Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", false);
win.gBrowser.removeAllTabsBut(resultsTab);
if (pref)
Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", pref);
Services.obs.notifyObservers(win, 'tabswitch-test-results', JSON.stringify({'times': times, 'urls': testURLs}));
});
}