Bug 1421456 - Add a test to prevent flickering regressions on window opening, r=johannh.

This commit is contained in:
Florian Quèze 2017-12-01 10:44:24 +01:00
Родитель 2725932278
Коммит 3674c4f0d9
5 изменённых файлов: 395 добавлений и 0 удалений

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

@ -1,6 +1,12 @@
[DEFAULT]
# to avoid overhead when running the browser normally, startupRecorder.js will
# do almost nothing unless browser.startup.record is true.
# gfx.canvas.willReadFrequently.enable is just an optimization, but needs to be
# set during early startup to have an impact as a canvas will be used by
# startupRecorder.js
prefs =
browser.startup.record=true
gfx.canvas.willReadFrequently.enable=true
support-files =
head.js
[browser_appmenu_reflows.js]
@ -9,6 +15,7 @@ skip-if = asan || debug # Bug 1382809, bug 1369959
[browser_startup.js]
[browser_startup_content.js]
skip-if = !e10s
[browser_startup_flicker.js]
[browser_tabclose_grow_reflows.js]
[browser_tabclose_reflows.js]
[browser_tabopen_reflows.js]
@ -20,5 +27,7 @@ skip-if = !e10s
skip-if = (os == 'linux') || (os == 'win' && debug) # Disabled on Linux and Windows debug due to perma failures. Bug 1392320.
[browser_urlbar_search_reflows.js]
[browser_windowclose_reflows.js]
[browser_windowopen_flicker.js]
skip-if = (debug && os == 'win') # Disabled on windows debug for intermittent leaks
[browser_windowopen_reflows.js]
skip-if = os == 'linux' # Disabled due to frequent failures. Bug 1380465.

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

@ -0,0 +1,99 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/*
* This test ensures that there is no unexpected flicker
* on the first window opened during startup.
*/
add_task(async function() {
let startupRecorder = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject;
await startupRecorder.done;
// Ensure all the frame data is in the test compartment to avoid traversing
// a cross compartment wrapper for each pixel.
let frames = Cu.cloneInto(startupRecorder.data.frames, {});
let unexpectedRects = 0;
let alreadyFocused = false;
for (let i = 1; i < frames.length; ++i) {
let frame = frames[i], previousFrame = frames[i - 1];
let rects = compareFrames(frame, previousFrame);
// The first screenshot we get shows an unfocused browser window for some
// reason. This is likely due to the test harness, so we want to ignore it.
// We'll assume the changes we are seeing are due to this focus change if
// there are at least 5 areas that changed near the top of the screen, but
// will only ignore this once (hence the alreadyFocused variable).
if (!alreadyFocused && rects.length > 5 && rects.every(r => r.y2 < 100)) {
alreadyFocused = true;
// This is likely an issue caused by the test harness, but log it anyway.
todo(false,
"the window should be focused at first paint, " + rects.toSource());
continue;
}
rects = rects.filter(rect => {
let inRange = (val, min, max) => min <= val && val <= max;
let width = frame.width;
let exceptions = [
{name: "bug 1403648 - urlbar down arrow shouldn't flicker",
condition: r => r.h == 5 && inRange(r.w, 8, 9) && // 5x9px area
inRange(r.y1, 40, 80) && // in the toolbar
// at ~80% of the window width
inRange(r.x1, width * .75, width * .9)
},
{name: "bug 1394914 - sidebar toolbar icon should be visible at first paint",
condition: r => r.h == 13 && inRange(r.w, 14, 16) && // icon size
inRange(r.y1, 40, 80) && // in the toolbar
// near the right end of screen
inRange(r.x1, width - 100, width - 50)
},
{name: "bug 1403648 - urlbar should be focused at first paint",
condition: r => inRange(r.y2, 60, 80) && // in the toolbar
// taking 50% to 75% of the window width
inRange(r.w, width * .5, width * .75) &&
// starting at 15 to 25% of the window width
inRange(r.x1, width * .15, width * .25)
},
{name: "bug 1421460 - restore icon should be visible at first paint",
condition: r => r.w == 9 && r.h == 9 && // 9x9 icon
AppConstants.platform == "win" &&
// near the right end of the screen
inRange(r.x1, width - 80, width - 70)
},
];
let rectText = `${rect.toSource()}, window width: ${width}`;
for (let e of exceptions) {
if (e.condition(rect)) {
todo(false, e.name + ", " + rectText);
return false;
}
}
ok(false, "unexpected changed rect: " + rectText);
return true;
});
if (!rects.length) {
info("ignoring identical frame");
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");
});

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

@ -0,0 +1,151 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/*
* This test ensures that there is no unexpected flicker
* when opening new windows.
*/
add_task(async function() {
// Flushing all caches helps to ensure that we get consistent
// behaviour when opening a new window, even if windows have been
// opened in previous tests.
Services.obs.notifyObservers(null, "startupcache-invalidate");
Services.obs.notifyObservers(null, "chrome-flush-skin-caches");
Services.obs.notifyObservers(null, "chrome-flush-caches");
let win = window.openDialog("chrome://browser/content/", "_blank",
"chrome,all,dialog=no,remote,suppressanimation",
"about:home");
// Avoid showing the remotecontrol UI.
await new Promise(resolve => {
win.addEventListener("DOMContentLoaded", () => {
delete win.Marionette;
win.Marionette = {running: false};
resolve();
}, {once: true});
});
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);
frames.push({data: Cu.cloneInto(ctx.getImageData(0, 0, width, height).data, {}),
width, height});
};
win.addEventListener("MozAfterPaint", afterPaintListener);
await TestUtils.topicObserved("browser-delayed-startup-finished",
subject => subject == win);
await BrowserTestUtils.firstBrowserLoaded(win, false);
await BrowserTestUtils.browserStopped(win.gBrowser.selectedBrowser, "about:home");
await new Promise(resolve => {
// 10 is an arbitrary value here, it needs to be at least 2 to avoid
// races with code initializing itself using idle callbacks.
(function waitForIdle(count = 10) {
if (!count) {
resolve();
return;
}
Services.tm.idleDispatchToMainThread(() => {
waitForIdle(count - 1);
});
})();
});
win.removeEventListener("MozAfterPaint", afterPaintListener);
let unexpectedRects = 0;
let foundTinyPaint = false;
for (let i = 1; i < frames.length; ++i) {
let frame = frames[i], previousFrame = frames[i - 1];
if (!foundTinyPaint &&
previousFrame.width == 1 && previousFrame.height == 1) {
foundTinyPaint = true;
todo(false, "shouldn't first paint a 1x1px window");
continue;
}
let rects = compareFrames(frame, previousFrame).filter(rect => {
let inRange = (val, min, max) => min <= val && val <= max;
let width = frame.width;
const spaceBeforeFirstTab = AppConstants.platform == "macosx" ? 100 : 0;
let inFirstTab = r =>
inRange(r.x1, spaceBeforeFirstTab, spaceBeforeFirstTab + 50) && r.y1 < 30;
let exceptions = [
{name: "bug 1403648 - urlbar down arrow shouldn't flicker",
condition: r => r.h == 5 && inRange(r.w, 8, 9) && // 5x9px area
inRange(r.y1, 40, 80) && // in the toolbar
// at ~80% of the window width
inRange(r.x1, width * .75, width * .9)
},
{name: "bug 1394914 - sidebar toolbar icon should be visible at first paint",
condition: r => r.h == 13 && inRange(r.w, 14, 16) && // icon size
inRange(r.y1, 40, 80) && // in the toolbar
// near the right end of screen
inRange(r.x1, width - 100, width - 50)
},
{name: "bug 1421463 - reload toolbar icon shouldn't flicker",
condition: r => r.h == 13 && inRange(r.w, 14, 16) && // icon size
inRange(r.y1, 40, 80) && // in the toolbar
// near the left side of the screen
inRange(r.x1, 65, 100)
},
{name: "bug 1401955 - about:home favicon should be visible at first paint",
condition: r => inFirstTab(r) && r.h == 14 && r.w == 14
},
{name: "bug 1401955 - space for about:home favicon should be there at first paint",
condition: r => inFirstTab(r) && inRange(r.w, 60, 80) && inRange(r.h, 8, 14)
},
];
let rectText = `${rect.toSource()}, window width: ${width}`;
for (let e of exceptions) {
if (e.condition(rect)) {
todo(false, e.name + ", " + rectText);
return false;
}
}
ok(false, "unexpected changed rect: " + rectText);
return true;
});
if (!rects.length) {
info("ignoring identical frame");
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");
await BrowserTestUtils.closeWindow(win);
});

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

@ -269,3 +269,107 @@ async function addDummyHistoryEntries(searchStr = "") {
await PlacesUtils.history.clear();
});
}
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 maxEmptyPixels away).
// This is needed to avoid having a rect for each letter when a label moves.
const maxEmptyPixels = 3;
let areRectsContiguous = function(r1, r2) {
return r1.y2 >= r2.y1 - 1 - maxEmptyPixels &&
r2.x1 - 1 - maxEmptyPixels <= r1.x2 &&
r2.x2 >= r1.x1 - 1 - maxEmptyPixels;
};
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;
r.h = r.y2 - r.y1;
});
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());
}

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

@ -14,6 +14,24 @@ let firstPaintNotification = "widget-first-paint";
if (AppConstants.platform == "linux")
firstPaintNotification = "xul-window-visible";
let win, canvas;
let paints = [];
let afterPaintListener = () => {
let width, height;
canvas.width = width = win.innerWidth;
canvas.height = height = win.innerHeight;
if (width < 1 || height < 1)
return;
let ctx = canvas.getContext("2d", {alpha: false, willReadFrequently: true});
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);
paints.push({data: ctx.getImageData(0, 0, width, height).data,
width, height});
};
/**
* The startupRecorder component observes notifications at various stages of
* startup and records the set of JS components and modules that were already
@ -83,6 +101,16 @@ startupRecorder.prototype = {
Services.obs.removeObserver(this, topic);
if (topic == firstPaintNotification &&
Services.prefs.getBoolPref("browser.startup.record", false)) {
win = Services.wm.getMostRecentWindow("navigator:browser");
canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml",
"canvas");
canvas.mozOpaque = true;
afterPaintListener();
win.addEventListener("MozAfterPaint", afterPaintListener);
}
if (topic == "sessionstore-windows-restored") {
if (!Services.prefs.getBoolPref("browser.startup.record", false)) {
this._resolve();
@ -107,6 +135,10 @@ startupRecorder.prototype = {
this.record("before becoming idle");
Services.obs.removeObserver(this, "image-drawing");
Services.obs.removeObserver(this, "image-loading");
win.removeEventListener("MozAfterPaint", afterPaintListener);
win = null;
this.data.frames = paints;
paints = null;
this._resolve();
this._resolve = null;
});