fune/browser/base/content/test/performance/head.js
Kris Maglione a259026c9d Bug 1456035: Part 4 - Convert callers of XPCOMUtils.generateQI to ChromeUtils.generateQI. r=mccr8
This also removes any redundant Ci.nsISupports elements in the interface
lists.

This was done using the following script:

acecb401b7/processors/chromeutils-generateQI.jsm

MozReview-Commit-ID: AIx10P8GpZY

--HG--
extra : rebase_source : a29c07530586dc18ba040f19215475ac20fcfb3b
2018-04-22 20:55:06 -07:00

634 lines
21 KiB
JavaScript

"use strict";
ChromeUtils.defineModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm");
/**
* This function can be called if the test needs to trigger frame dirtying
* outside of the normal mechanism.
*
* @param win (dom window)
* The window in which the frame tree needs to be marked as dirty.
*/
function dirtyFrame(win) {
let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
try {
dwu.ensureDirtyRootFrame();
} catch (e) {
// If this fails, we should probably make note of it, but it's not fatal.
info("Note: ensureDirtyRootFrame threw an exception:" + e);
}
}
/**
* Async utility function to collect the stacks of uninterruptible reflows
* occuring during some period of time in a window.
*
* @param testPromise (Promise)
* A promise that is resolved when the data collection should stop.
*
* @param win (browser window, optional)
* The browser window to monitor. Defaults to the current window.
*
* @return An array of reflow stacks
*/
async function recordReflows(testPromise, win = window) {
// Collect all reflow stacks, we'll process them later.
let reflows = [];
let observer = {
reflow(start, end) {
// Gather information about the current code path.
reflows.push(new Error().stack);
// Just in case, dirty the frame now that we've reflowed.
dirtyFrame(win);
},
reflowInterruptible(start, end) {
// Interruptible reflows are the reflows caused by the refresh
// driver ticking. These are fine.
},
QueryInterface: ChromeUtils.generateQI([Ci.nsIReflowObserver,
Ci.nsISupportsWeakReference])
};
let docShell = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docShell.addWeakReflowObserver(observer);
let dirtyFrameFn = event => {
if (event.type != "MozAfterPaint") {
dirtyFrame(win);
}
};
Services.els.addListenerForAllEvents(win, dirtyFrameFn, true);
try {
dirtyFrame(win);
await testPromise;
} finally {
Services.els.removeListenerForAllEvents(win, dirtyFrameFn, true);
docShell.removeWeakReflowObserver(observer);
}
return reflows;
}
/**
* Utility function to report unexpected reflows.
*
* @param reflows (Array)
* An array of reflow stacks returned by recordReflows.
*
* @param expectedReflows (Array, optional)
* An Array of Objects representing reflows.
*
* Example:
*
* [
* {
* // This reflow is caused by lorem ipsum.
* // Sometimes, due to unpredictable timings, the reflow may be hit
* // less times.
* stack: [
* "select@chrome://global/content/bindings/textbox.xml",
* "focusAndSelectUrlBar@chrome://browser/content/browser.js",
* "openLinkIn@chrome://browser/content/utilityOverlay.js",
* "openUILinkIn@chrome://browser/content/utilityOverlay.js",
* "BrowserOpenTab@chrome://browser/content/browser.js",
* ],
* // We expect this particular reflow to happen up to 2 times.
* maxCount: 2,
* },
*
* {
* // This reflow is caused by lorem ipsum. We expect this reflow
* // to only happen once, so we can omit the "maxCount" property.
* stack: [
* "get_scrollPosition@chrome://global/content/bindings/scrollbox.xml",
* "_fillTrailingGap@chrome://browser/content/tabbrowser.xml",
* "_handleNewTab@chrome://browser/content/tabbrowser.xml",
* "onxbltransitionend@chrome://browser/content/tabbrowser.xml",
* ],
* }
* ]
*
* Note that line numbers are not included in the stacks.
*
* Order of the reflows doesn't matter. Expected reflows that aren't seen
* will cause an assertion failure. When this argument is not passed,
* it defaults to the empty Array, meaning no reflows are expected.
*/
function reportUnexpectedReflows(reflows, expectedReflows = []) {
let knownReflows = expectedReflows.map(r => {
return {stack: r.stack, path: r.stack.join("|"),
count: 0, maxCount: r.maxCount || 1,
actualStacks: new Map()};
});
let unexpectedReflows = new Map();
for (let stack of reflows) {
let path =
stack.split("\n").slice(1) // the first frame which is our test code.
.map(line => line.replace(/:\d+:\d+$/, "")) // strip line numbers.
.join("|");
// Stack trace is empty. Reflow was triggered by native code, which
// we ignore.
if (path === "") {
continue;
}
// synthesizeKey from EventUtils.js causes us to reflow. That's the test
// harness and we don't care about that, so we'll filter that out.
if (path.startsWith("synthesizeKey@chrome://mochikit/content/tests/SimpleTest/EventUtils.js")) {
continue;
}
let index = knownReflows.findIndex(reflow => path.startsWith(reflow.path));
if (index != -1) {
let reflow = knownReflows[index];
++reflow.count;
reflow.actualStacks.set(stack, (reflow.actualStacks.get(stack) || 0) + 1);
} else {
unexpectedReflows.set(stack, (unexpectedReflows.get(stack) || 0) + 1);
}
}
let formatStack = stack =>
stack.split("\n").slice(1).map(frame => " " + frame).join("\n");
for (let reflow of knownReflows) {
let firstFrame = reflow.stack[0];
if (!reflow.count) {
Assert.ok(false,
`Unused expected reflow at ${firstFrame}:\nStack:\n` +
reflow.stack.map(frame => " " + frame).join("\n") + "\n" +
"This is probably a good thing - just remove it from the whitelist.");
} else {
if (reflow.count > reflow.maxCount) {
Assert.ok(false,
`reflow at ${firstFrame} was encountered ${reflow.count} times,\n` +
`it was expected to happen up to ${reflow.maxCount} times.`);
} else {
todo(false, `known reflow at ${firstFrame} was encountered ${reflow.count} times`);
}
for (let [stack, count] of reflow.actualStacks) {
info("Full stack" + (count > 1 ? ` (hit ${count} times)` : "") + ":\n" +
formatStack(stack));
}
}
}
for (let [stack, count] of unexpectedReflows) {
let location = stack.split("\n")[1].replace(/:\d+:\d+$/, "");
Assert.ok(false,
`unexpected reflow at ${location} hit ${count} times\n` +
"Stack:\n" +
formatStack(stack));
}
Assert.ok(!unexpectedReflows.size,
unexpectedReflows.size + " unexpected reflows");
}
async function ensureNoPreloadedBrowser(win = window) {
// If we've got a preloaded browser, get rid of it so that it
// doesn't interfere with the test if it's loading. We have to
// do this before we disable preloading or changing the new tab
// URL, otherwise _getPreloadedBrowser will return null, despite
// the preloaded browser existing.
let preloaded = win.gBrowser._getPreloadedBrowser();
if (preloaded) {
preloaded.remove();
}
await SpecialPowers.pushPrefEnv({
set: [["browser.newtab.preload", false]],
});
let aboutNewTabService = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
.getService(Ci.nsIAboutNewTabService);
aboutNewTabService.newTabURL = "about:blank";
registerCleanupFunction(() => {
aboutNewTabService.resetNewTabURL();
});
}
/**
* The navigation toolbar is overflowable, meaning that some items
* will be moved and held within a sub-panel if the window gets too
* small to show their icons. The calculation for hiding those items
* occurs after resize events, and is debounced using a DeferredTask.
* This utility function allows us to fast-forward to just running
* that function for that DeferredTask instead of waiting for the
* debounce timeout to occur.
*/
function forceImmediateToolbarOverflowHandling(win) {
let overflowableToolbar = win.document.getElementById("nav-bar").overflowable;
if (overflowableToolbar._lazyResizeHandler && overflowableToolbar._lazyResizeHandler.isArmed) {
overflowableToolbar._lazyResizeHandler.disarm();
// Ensure the root frame is dirty before resize so that, if we're
// in the middle of a reflow test, we record the reflows deterministically.
let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
dwu.ensureDirtyRootFrame();
overflowableToolbar._onLazyResize();
}
}
async function prepareSettledWindow() {
let win = await BrowserTestUtils.openNewBrowserWindow();
await ensureNoPreloadedBrowser(win);
forceImmediateToolbarOverflowHandling(win);
return win;
}
// Use this function to avoid catching a reflow related to calling focus on the
// urlbar and changed rects for its dropmarker when opening new tabs.
async function ensureFocusedUrlbar() {
// The switchingtabs attribute prevents the historydropmarker opacity
// transition, so if we expect a transitionend event when this attribute
// is set, we wait forever. (it's removed off a MozAfterPaint event listener)
await BrowserTestUtils.waitForCondition(() =>
!gURLBar.hasAttribute("switchingtabs"));
let dropmarker = document.getAnonymousElementByAttribute(gURLBar, "anonid",
"historydropmarker");
let opacityPromise = BrowserTestUtils.waitForEvent(dropmarker, "transitionend",
false, e => e.propertyName === "opacity");
gURLBar.focus();
await opacityPromise;
}
/**
* Calculate and return how many additional tabs can be fit into the
* tabstrip without causing it to overflow.
*
* @return int
* The maximum additional tabs that can be fit into the
* tabstrip without causing it to overflow.
*/
function computeMaxTabCount() {
let currentTabCount = gBrowser.tabs.length;
let newTabButton =
document.getAnonymousElementByAttribute(gBrowser.tabContainer,
"anonid", "tabs-newtab-button");
let newTabRect = newTabButton.getBoundingClientRect();
let tabStripRect = gBrowser.tabContainer.arrowScrollbox.getBoundingClientRect();
let availableTabStripWidth = tabStripRect.width - newTabRect.width;
let tabMinWidth =
parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth, 10);
let maxTabCount = Math.floor(availableTabStripWidth / tabMinWidth) - currentTabCount;
Assert.ok(maxTabCount > 0,
"Tabstrip needs to be wide enough to accomodate at least 1 more tab " +
"without overflowing.");
return maxTabCount;
}
/**
* Helper function that opens up some number of about:blank tabs, and wait
* until they're all fully open.
*
* @param howMany (int)
* How many about:blank tabs to open.
*/
async function createTabs(howMany) {
let uris = [];
while (howMany--) {
uris.push("about:blank");
}
gBrowser.loadTabs(uris, true, false);
await BrowserTestUtils.waitForCondition(() => {
return Array.from(gBrowser.tabs).every(tab => tab._fullyOpen);
});
}
/**
* Removes all of the tabs except the originally selected
* tab, and waits until all of the DOM nodes have been
* completely removed from the tab strip.
*/
async function removeAllButFirstTab() {
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.warnOnCloseOtherTabs", false]],
});
gBrowser.removeAllTabsBut(gBrowser.tabs[0]);
await BrowserTestUtils.waitForCondition(() => gBrowser.tabs.length == 1);
await SpecialPowers.popPrefEnv();
}
/**
* Adds some entries to the Places database so that we can
* do semi-realistic look-ups in the URL bar.
*
* @param searchStr (string)
* Optional text to add to the search history items.
*/
async function addDummyHistoryEntries(searchStr = "") {
await PlacesUtils.history.clear();
const NUM_VISITS = 10;
let visits = [];
for (let i = 0; i < NUM_VISITS; ++i) {
visits.push({
uri: `http://example.com/urlbar-reflows-${i}`,
title: `Reflow test for URL bar entry #${i} - ${searchStr}`,
});
}
await PlacesTestUtils.addVisits(visits);
registerCleanupFunction(async function() {
await PlacesUtils.history.clear();
});
}
/**
* Async utility function to capture a screenshot of each painted frame.
*
* @param testPromise (Promise)
* A promise that is resolved when the data collection should stop.
*
* @param win (browser window, optional)
* The browser window to monitor. Defaults to the current window.
*
* @return An array of screenshots
*/
async function recordFrames(testPromise, win = window) {
let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml",
"canvas");
canvas.mozOpaque = true;
let ctx = canvas.getContext("2d", {alpha: false, willReadFrequently: true});
let frames = [];
let afterPaintListener = event => {
let width, height;
canvas.width = width = win.innerWidth;
canvas.height = height = win.innerHeight;
ctx.drawWindow(win, 0, 0, width, height, "white",
ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_VIEW |
ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES |
ctx.DRAWWINDOW_USE_WIDGET_LAYERS);
let data = Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {});
if (frames.length) {
// Compare this frame with the previous one to avoid storing duplicate
// frames and running out of memory.
let previous = frames[frames.length - 1];
if (previous.width == width && previous.height == height) {
let equals = true;
for (let i = 0; i < data.length; ++i) {
if (data[i] != previous.data[i]) {
equals = false;
break;
}
}
if (equals) {
return;
}
}
}
frames.push({data, width, height});
};
win.addEventListener("MozAfterPaint", afterPaintListener);
// If the test is using an existing window, capture a frame immediately.
if (win.document.readyState == "complete") {
afterPaintListener();
}
try {
await testPromise;
} finally {
win.removeEventListener("MozAfterPaint", afterPaintListener);
}
return frames;
}
// How many identical pixels to accept between 2 rects when deciding to merge
// them.
const kMaxEmptyPixels = 3;
function compareFrames(frame, previousFrame) {
// Accessing the Math global is expensive as the test executes in a
// non-syntactic scope. Accessing it as a lexical variable is enough
// to make the code JIT well.
const M = Math;
function expandRect(x, y, rect) {
if (rect.x2 < x)
rect.x2 = x;
else if (rect.x1 > x)
rect.x1 = x;
if (rect.y2 < y)
rect.y2 = y;
}
function isInRect(x, y, rect) {
return (rect.y2 == y || rect.y2 == y - 1) && rect.x1 - 1 <= x && x <= rect.x2 + 1;
}
if (frame.height != previousFrame.height ||
frame.width != previousFrame.width) {
// If the frames have different sizes, assume the whole window has
// been repainted when the window was resized.
return [{x1: 0, x2: frame.width, y1: 0, y2: frame.height}];
}
let l = frame.data.length;
let different = [];
let rects = [];
for (let i = 0; i < l; i += 4) {
let x = (i / 4) % frame.width;
let y = M.floor((i / 4) / frame.width);
for (let j = 0; j < 4; ++j) {
let index = i + j;
if (frame.data[index] != previousFrame.data[index]) {
let found = false;
for (let rect of rects) {
if (isInRect(x, y, rect)) {
expandRect(x, y, rect);
found = true;
break;
}
}
if (!found)
rects.unshift({x1: x, x2: x, y1: y, y2: y});
different.push(i);
break;
}
}
}
rects.reverse();
// The following code block merges rects that are close to each other
// (less than kMaxEmptyPixels away).
// This is needed to avoid having a rect for each letter when a label moves.
let areRectsContiguous = function(r1, r2) {
return r1.y2 >= r2.y1 - 1 - kMaxEmptyPixels &&
r2.x1 - 1 - kMaxEmptyPixels <= r1.x2 &&
r2.x2 >= r1.x1 - 1 - kMaxEmptyPixels;
};
let hasMergedRects;
do {
hasMergedRects = false;
for (let r = rects.length - 1; r > 0; --r) {
let rr = rects[r];
for (let s = r - 1; s >= 0; --s) {
let rs = rects[s];
if (areRectsContiguous(rs, rr)) {
rs.x1 = Math.min(rs.x1, rr.x1);
rs.y1 = Math.min(rs.y1, rr.y1);
rs.x2 = Math.max(rs.x2, rr.x2);
rs.y2 = Math.max(rs.y2, rr.y2);
rects.splice(r, 1);
hasMergedRects = true;
break;
}
}
}
} while (hasMergedRects);
// For convenience, pre-compute the width and height of each rect.
rects.forEach(r => {
r.w = r.x2 - r.x1 + 1;
r.h = r.y2 - r.y1 + 1;
});
return rects;
}
function dumpFrame({data, width, height}) {
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
canvas.mozOpaque = true;
canvas.width = width;
canvas.height = height;
canvas.getContext("2d", {alpha: false, willReadFrequently: true})
.putImageData(new ImageData(data, width, height), 0, 0);
info(canvas.toDataURL());
}
/**
* Utility function to report unexpected changed areas on screen.
*
* @param frames (Array)
* An array of frames captured by recordFrames.
*
* @param expectations (Object)
* An Object indicating which changes on screen are expected.
* If can contain the following optional fields:
* - filter: a function used to exclude changed rects that are expected.
* It takes the following parameters:
* - rects: an array of changed rects
* - frame: the current frame
* - previousFrame: the previous frame
* It returns an array of rects. This array is typically a copy of
* the rects parameter, from which identified expected changes have
* been excluded.
* - exceptions: an array of objects describing known flicker bugs.
* Example:
* exceptions: [
* {name: "bug 1nnnnnn - the foo icon shouldn't flicker",
* condition: r => r.w == 14 && r.y1 == 0 && ... }
* },
* {name: "bug ...
* ]
*/
function reportUnexpectedFlicker(frames, expectations) {
info("comparing " + frames.length + " frames");
let unexpectedRects = 0;
for (let i = 1; i < frames.length; ++i) {
let frame = frames[i], previousFrame = frames[i - 1];
let rects = compareFrames(frame, previousFrame);
if (expectations.filter) {
rects = expectations.filter(rects, frame, previousFrame);
}
rects = rects.filter(rect => {
let rectText = `${rect.toSource()}, window width: ${frame.width}`;
for (let e of (expectations.exceptions || [])) {
if (e.condition(rect)) {
todo(false, e.name + ", " + rectText);
return false;
}
}
ok(false, "unexpected changed rect: " + rectText);
return true;
});
if (!rects.length)
continue;
// Before dumping a frame with unexpected differences for the first time,
// ensure at least one previous frame has been logged so that it's possible
// to see the differences when examining the log.
if (!unexpectedRects) {
dumpFrame(previousFrame);
}
unexpectedRects += rects.length;
dumpFrame(frame);
}
is(unexpectedRects, 0, "should have 0 unknown flickering areas");
}
/**
* This is the main function that performance tests in this folder will call.
*
* The general idea is that individual tests provide a test function (testFn)
* that will perform some user interactions we care about (eg. open a tab), and
* this withPerfObserver function takes care of setting up and removing the
* observers and listener we need to detect common performance issues.
*
* Once testFn is done, withPerfObserver will analyse the collected data and
* report anything unexpected.
*
* @param testFn (async function)
* An async function that exercises some part of the browser UI.
*
* @param exceptions (object, optional)
* An Array of Objects representing expectations and known issues.
* It can contain the following fields:
* - expectedReflows: an array of expected reflow stacks.
* (see the comment above reportUnexpectedReflows for an example)
* - frames: an object setting expectations for what will change
* on screen during the test, and the known flicker bugs.
* (see the comment above reportUnexpectedFlicker for an example)
*/
async function withPerfObserver(testFn, exceptions = {}, win = window) {
let resolveFn, rejectFn;
let promiseTestDone = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
let promiseReflows = recordReflows(promiseTestDone, win);
let promiseFrames = recordFrames(promiseTestDone, win);
testFn().then(resolveFn, rejectFn);
await promiseTestDone;
let reflows = await promiseReflows;
reportUnexpectedReflows(reflows, exceptions.expectedReflows);
let frames = await promiseFrames;
reportUnexpectedFlicker(frames, exceptions.frames);
}