/** *Thisfunctioncanbecalledifthetestneedstotriggerframedirtying *outsideofthenormalmechanism. * *@paramwin(domwindow) *Thewindowinwhichtheframetreeneedstobemarkedasdirty.
*/ function dirtyFrame(win) {
let dwu = win.windowUtils; 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);
}
}
/** *Asyncutilityfunctiontocollectthestacksofuninterruptiblereflows *occuringduringsomeperiodoftimeinawindow. * *@paramtestPromise(Promise) *Apromisethatisresolvedwhenthedatacollectionshouldstop. * *@paramwin(browserwindow,optional) *Thebrowserwindowtomonitor.Defaultstothecurrentwindow. * *@returnAnarrayofreflowstacks
*/
async function recordReflows(testPromise, win = window) { // Collect all reflow stacks, we'll process them later.
let reflows = [];
let observer = {
reflow() { // Gather information about the current code path.
reflows.push(new Error().stack);
// Just in case, dirty the frame now that we've reflowed. This will // allow us to detect additional reflows that occur in this same tick // of the event loop.
ChromeUtils.addProfilerMarker( "dirtyFrame",
{ category: "Test" }, "Intentionally dirtying the frame to help ensure that synchrounous " + "reflows will be detected."
);
dirtyFrame(win);
},
reflowInterruptible() { // Interruptible reflows are the reflows caused by the refresh // driver ticking. These are fine.
},
/** *Utilityfunctiontoreportunexpectedreflows. * *@paramreflows(Array) *AnarrayofreflowstacksreturnedbyrecordReflows. * *@paramexpectedReflows(Array,optional) *AnArrayofObjectsrepresentingreflows. * *Example: * *[ *{ *// This reflow is caused by lorem ipsum. *// Sometimes, due to unpredictable timings, the reflow may be hit *// less times. *stack:[ *"somefunction@chrome://somepackage/content/somefile.mjs", *"otherfunction@chrome://otherpackage/content/otherfile.js", *"morecode@resource://somewhereelse/SomeModule.sys.mjs", *], *// 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:[ *"somefunction@chrome://somepackage/content/somefile.mjs", *"otherfunction@chrome://otherpackage/content/otherfile.js", *"morecode@resource://somewhereelse/SomeModule.sys.mjs", *], *} *] * *Notethatlinenumbersarenotincludedinthestacks. * *Orderofthereflowsdoesn'tmatter.Expectedreflowsthataren'tseen *willcauseanassertionfailure.Whenthisargumentisnotpassed, *itdefaultstotheemptyArray,meaningnoreflowsareexpected.
*/ 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();
if (knownReflows.some(r => r.path.includes("*"))) { Assert.ok( false, "Do not include async frames in the stack, as " + "that feature is not available on all trees."
);
}
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;
}
// Functions from EventUtils.js calculate coordinates and // dimensions, causing us to reflow. That's the test // harness and we don't care about that, so we'll filter that out. if (
/^(synthesize|send|createDragEventObject).*?@chrome:\/\/mochikit.*?EventUtils\.js/.test(
path
)
) { 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 list of reflows."
);
} 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.
NewTabPagePreloading.removePreloadedBrowser(win);
// Onboarding puts a badge on the fxa toolbar button a while after startup // which confuses tests that look at repaints in the toolbar. Use this // function to cancel the badge update. function disableFxaBadge() {
let { ToolbarBadgeHub } = ChromeUtils.importESModule( "resource:///modules/asrouter/ToolbarBadgeHub.sys.mjs"
);
ToolbarBadgeHub.removeAllNotifications();
// Also prevent a new timer from being set return SpecialPowers.pushPrefEnv({
set: [["identity.fxaccounts.toolbar.accessed", true]],
});
}
async function getBookmarksToolbarRect() { // Temporarily open the bookmarks toolbar to measure its rect
let bookmarksToolbar = gNavToolbox.querySelector("#PersonalToolbar");
let wasVisible = !bookmarksToolbar.collapsed; if (!wasVisible) {
setToolbarVisibility(bookmarksToolbar, true, false, false);
await TestUtils.waitForCondition(
() => bookmarksToolbar.getBoundingClientRect().height > 0, "wait for non-zero bookmarks toolbar height"
);
}
let bookmarksToolbarRect = bookmarksToolbar.getBoundingClientRect(); if (!wasVisible) {
setToolbarVisibility(bookmarksToolbar, false, false, false);
await TestUtils.waitForCondition(
() => bookmarksToolbar.getBoundingClientRect().height == 0, "wait for zero bookmarks toolbar height"
);
} return bookmarksToolbarRect;
}
async function ensureAnimationsFinished(win = window) {
let animations = win.document.getAnimations();
info(`Waiting for ${animations.length} animations`);
await Promise.allSettled(animations.map(a => a.finished));
}
async function prepareSettledWindow() {
let win = await BrowserTestUtils.openNewBrowserWindow();
await ensureNoPreloadedBrowser(win);
await ensureAnimationsFinished(win); return win;
}
/** *Calculateandreturnhowmanyadditionaltabscanbefitintothe *tabstripwithoutcausingittooverflow. * *@returnint *Themaximumadditionaltabsthatcanbefitintothe *tabstripwithoutcausingittooverflow.
*/ function computeMaxTabCount() {
let currentTabCount = gBrowser.tabs.length;
let newTabButton = gBrowser.tabContainer.newTabButton;
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;
}
/** *Helperfunctionthatopensupsomenumberofabout:blanktabs,andwait *untilthey'reallfullyopen. * *@paramhowMany(int) *Howmanyabout:blanktabstoopen.
*/
async function createTabs(howMany) {
let uris = []; while (howMany--) {
uris.push("about:blank");
}
for (let i = 0; i < NUM_VISITS; ++i) {
visits.push({ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
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();
});
}
// 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;
} elseif (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;
});
let unexpectedRects = 0; for (let i = 1; i < frames.length; ++i) {
let frame = frames[i],
previousFrame = frames[i - 1];
let rects = compareFrames(frame, previousFrame);
let rectText = r => `${r.toSource()}, window width: ${frame.width}`;
rects = rects.filter(rect => { for (let e of expectations.exceptions || []) { if (e.condition(rect)) {
todo(false, e.name + ", " + rectText(rect)); returnfalse;
}
} returntrue;
});
if (expectations.filter) {
rects = expectations.filter(rects, frame, previousFrame);
}
// 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");
}
URLBar.focus();
URLBar.value = SEARCH_TERM;
let testFn = async function () {
let popup = URLBar.view;
let oldOnQueryResults = popup.onQueryResults.bind(popup);
let oldOnQueryFinished = popup.onQueryFinished.bind(popup);
// We need to invalidate the frame tree outside of the normal // mechanism since invalidations and result additions to the // URL bar occur without firing JS events (which is how we // normally know to dirty the frame tree).
popup.onQueryResults = context => {
dirtyFrame(win);
oldOnQueryResults(context);
};
let waitExtra = async () => { // There are several setTimeout(fn, 0); calls inside autocomplete.xml // that we need to wait for. Since those have higher priority than // idle callbacks, we can be sure they will have run once this // idle callback is called. The timeout seems to be required in // automation - presumably because the machines can be pretty busy // especially if it's GC'ing from previous tests.
await new Promise(resolve =>
win.requestIdleCallback(resolve, { timeout: 1000 })
);
};
if (keyed) { // Only keying in 6 characters because the number of reflows triggered // is so high that we risk timing out the test if we key in any more.
let searchTerm = "ows-10"; for (let i = 0; i < searchTerm.length; ++i) {
let char = searchTerm[i];
EventUtils.synthesizeKey(char, {}, win);
await UrlbarTestUtils.promiseSearchComplete(win);
await waitExtra();
}
} else {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window: win,
waitForFocus: SimpleTest.waitForFocus,
value: URLBar.value,
});
await waitExtra();
}
await UrlbarTestUtils.promisePopupClose(win);
};
let urlbarRect = URLBar.textbox.getBoundingClientRect(); const SHADOW_SIZE = 17;
let expectedRects = {
filter: rects => { // We put text into the urlbar so expect its textbox to change. // We expect many changes in the results view. // So we just allow changes anywhere in the urlbar. We don't check the // bottom of the rect because the result view height varies depending on // the results. // We use floor/ceil because the Urlbar dimensions aren't always // integers. return rects.filter(
r =>
!(
r.x1 >= Math.floor(urlbarRect.left) - SHADOW_SIZE &&
r.x2 <= Math.ceil(urlbarRect.right) + SHADOW_SIZE &&
r.y1 >= Math.floor(urlbarRect.top) - SHADOW_SIZE
)
);
},
};
if (loadedList[scriptType].length) {
console.log("Unexpected scripts:", loadedList[scriptType]);
}
is(
loadedList[scriptType].length, 0,
`should have no unexpected ${scriptType} loaded on content process startup`
);
for (let script of loadedList[scriptType]) {
record( false,
`Unexpected ${scriptType} loaded during content process startup: ${script}`,
undefined,
loadedInfo[scriptType].get(script)
);
}
is(
known[scriptType].size, 0,
`all known ${scriptType} scripts should have been loaded`
);
for (let script of known[scriptType]) {
ok( false,
`${scriptType} is expected to load for content process startup but wasn't: ${script}`
);
}
if (dumpAllStacks) {
info(`Stacks for all loaded ${scriptType}:`); for (let [file, stack] of loadedInfo[scriptType]) { if (stack) {
info(
`${file}\n------------------------------------\n` + stack + "\n"
);
}
}
}
}
for (let scriptType in forbidden) { for (let script of forbidden[scriptType]) {
let loaded = loadedInfo[scriptType].has(script); if (loaded) {
record( false,
`Forbidden ${scriptType} loaded during content process startup: ${script}`,
undefined,
loadedInfo[scriptType].get(script)
);
}
}
// The first screenshot we get in OSX / Windows shows an unfocused browser // window for some reason. See bug 1445161. This function allows to deal with // that in a central place. function isLikelyFocusChange(rects, frame) { if (rects.length > 3 && rects.every(r => r.y2 < 100)) { // There are at least 4 areas that changed near the top of the screen. // Note that we need a bit more leeway than the titlebar height, because on // OSX other toolbarbuttons in the navigation toolbar also get disabled // state. returntrue;
} if (
rects.every(r => r.y1 == 0 && r.x1 == 0 && r.w == frame.width && r.y2 < 100)
) { // Full-width rect in the top of the titlebar. returntrue;
} returnfalse;
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.19 Sekunden
(vorverarbeitet am 2026-06-10)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.