зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1421456 - Add a test to prevent flickering regressions on window opening, r=johannh.
This commit is contained in:
Родитель
2725932278
Коммит
3674c4f0d9
|
@ -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;
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче