Merge pull request #21115 from calixteman/improve_reftest_perfs2

Improve reftest runner memory usage and load balancing
This commit is contained in:
calixteman 2026-04-18 23:07:57 +02:00 committed by GitHub
commit 92f862bae9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 177 additions and 111 deletions

View File

@ -168,7 +168,9 @@ function startNode(args, options) {
// (such as `issue6360.pdf`), so we need to restore this value. Note that // (such as `issue6360.pdf`), so we need to restore this value. Note that
// this argument needs to be before all other arguments as it needs to be // this argument needs to be before all other arguments as it needs to be
// passed to the Node.js process itself and not to the script that it runs. // passed to the Node.js process itself and not to the script that it runs.
args.unshift("--max-http-header-size=80000"); // Increase the max heap size to avoid OOM caused by a Puppeteer/BiDi memory
// leak: https://github.com/puppeteer/puppeteer/issues/14876
args.unshift("--max-http-header-size=80000", "--max-old-space-size=8192");
return spawn("node", args, options); return spawn("node", args, options);
} }

View File

@ -549,7 +549,6 @@ class Driver {
}; };
this._info("User agent: " + navigator.userAgent); this._info("User agent: " + navigator.userAgent);
this._log(`Harness thinks this browser is ${this.browser}\n`); this._log(`Harness thinks this browser is ${this.browser}\n`);
this._log('Fetching manifest "' + this.manifestFile + '"... ');
if (this.delay > 0) { if (this.delay > 0) {
this._log("\nDelaying for " + this.delay + " ms...\n"); this._log("\nDelaying for " + this.delay + " ms...\n");
@ -562,32 +561,39 @@ class Driver {
this.ws.addEventListener("open", resolve, { once: true }); this.ws.addEventListener("open", resolve, { once: true });
}); });
} }
const response = await fetch(this.manifestFile);
if (!response.ok) {
throw new Error(response.statusText);
}
this._log("done\n");
this.manifest = await response.json();
if (this.testFilter?.length) { // Dynamic task queue: server sends tasks on demand.
this.manifest = this.manifest.filter(item => { this.taskQueue = [];
if (this.testFilter.includes(item.id)) { this.serverDone = false;
return true; this.pendingTaskResolve = null;
this.currentTask = null;
this.tasksDone = 0;
this.ws.addEventListener("message", event => {
if (typeof event.data !== "string") {
return;
}
const msg = JSON.parse(event.data);
if (msg.type === "task") {
if (this.pendingTaskResolve) {
this.pendingTaskResolve(msg.task);
this.pendingTaskResolve = null;
} else {
this.taskQueue.push(msg.task);
// Prefetch PDF for this task if it's now first in queue.
if (this.taskQueue.length === 1) {
this._prefetchNextTask();
}
}
} else if (msg.type === "done") {
this.serverDone = true;
if (this.pendingTaskResolve) {
this.pendingTaskResolve(null);
this.pendingTaskResolve = null;
}
} }
return false;
}); });
}
if (this.sessionCount > 1) {
const { sessionIndex, sessionCount } = this;
const start = Math.floor(
(this.manifest.length * sessionIndex) / sessionCount
);
const end = Math.floor(
(this.manifest.length * (sessionIndex + 1)) / sessionCount
);
this.manifest = this.manifest.slice(start, end);
}
this.currentTask = 0;
this._nextTask(); this._nextTask();
}, this.delay); }, this.delay);
} }
@ -602,23 +608,38 @@ class Driver {
*/ */
log(msg) { log(msg) {
let id = this.browser; let id = this.browser;
const task = this.manifest[this.currentTask]; if (this.currentTask) {
if (task) { id += `-${this.currentTask.id}`;
id += `-${task.id}`; }
this._info(`${id}: ${msg}`);
} }
this._info(`${id}: ${msg}`); _waitForNextTask() {
if (this.taskQueue.length > 0) {
return Promise.resolve(this.taskQueue.shift());
}
if (this.serverDone) {
return Promise.resolve(null);
}
this.ws.send(
JSON.stringify({ type: "requestTask", browser: this.browser })
);
return new Promise(resolve => {
this.pendingTaskResolve = resolve;
});
} }
_nextTask() { _nextTask() {
let failure = ""; let failure = "";
this._cleanup().then(() => { this._cleanup().then(async () => {
if (this.currentTask === this.manifest.length) { const task = await this._waitForNextTask();
if (!task) {
this._done(); this._done();
return; return;
} }
const task = this.manifest[this.currentTask]; this.currentTask = task;
task.round = 0; task.round = 0;
task.pageNum = task.firstPage || 1; task.pageNum = task.firstPage || 1;
task.stats = { times: [] }; task.stats = { times: [] };
@ -658,13 +679,10 @@ class Driver {
md5FileMap.set(task.md5, task.file); md5FileMap.set(task.md5, task.file);
} }
this._log( this._log(`[${++this.tasksDone}] ${task.id}:\n`);
`[${this.currentTask + 1}/${this.manifest.length}] ${task.id}:\n`
);
if (task.type === "skip-because-failing") { if (task.type === "skip-because-failing") {
this._log(` Skipping file "${task.file} because it's failing"\n`); this._log(` Skipping file "${task.file} because it's failing"\n`);
this.currentTask++;
this._nextTask(); this._nextTask();
return; return;
} }
@ -678,7 +696,6 @@ class Driver {
this._nextPage(task, 'Expected "other" test-case to be linked.'); this._nextPage(task, 'Expected "other" test-case to be linked.');
return; return;
} }
this.currentTask++;
this._nextTask(); this._nextTask();
return; return;
} }
@ -885,11 +902,10 @@ class Driver {
} }
_prefetchNextTask() { _prefetchNextTask() {
const nextIdx = this.currentTask + 1; const task = this.taskQueue[0];
if (nextIdx >= this.manifest.length) { if (!task) {
return; return;
} }
const task = this.manifest[nextIdx];
// Skip tasks that do not load a PDF or that need DOM setup (XFA style // Skip tasks that do not load a PDF or that need DOM setup (XFA style
// element injection) to happen synchronously before getDocument. // element injection) to happen synchronously before getDocument.
if ( if (
@ -899,8 +915,10 @@ class Driver {
) { ) {
return; return;
} }
if (!task._prefetchedLoadingTask) {
task._prefetchedLoadingTask = getDocument(this._getDocumentOptions(task)); task._prefetchedLoadingTask = getDocument(this._getDocumentOptions(task));
} }
}
_cleanup() { _cleanup() {
// Clear out all the stylesheets since a new one is created for each font. // Clear out all the stylesheets since a new one is created for each font.
@ -918,10 +936,10 @@ class Driver {
const destroyedPromises = []; const destroyedPromises = [];
// Wipe out the link to the pdfdoc so it can be GC'ed. // Wipe out the link to the pdfdoc so it can be GC'ed.
for (let i = 0; i < this.manifest.length; i++) { for (const task of [this.currentTask, ...this.taskQueue]) {
if (this.manifest[i].pdfDoc) { if (task?.pdfDoc) {
destroyedPromises.push(this.manifest[i].pdfDoc.destroy()); destroyedPromises.push(task.pdfDoc.destroy());
delete this.manifest[i].pdfDoc; delete task.pdfDoc;
} }
} }
return Promise.all(destroyedPromises); return Promise.all(destroyedPromises);
@ -955,7 +973,6 @@ class Driver {
.then(blob => this._sendResult(blob, task, failure)) .then(blob => this._sendResult(blob, task, failure))
.then(() => { .then(() => {
this._log(`done${failure ? ` (failed !: ${failure})` : ""}\n`); this._log(`done${failure ? ` (failed !: ${failure})` : ""}\n`);
this.currentTask++;
this._nextTask(); this._nextTask();
}); });
return; return;
@ -966,7 +983,6 @@ class Driver {
this._log(` Round ${1 + task.round}\n`); this._log(` Round ${1 + task.round}\n`);
task.pageNum = task.firstPage || 1; task.pageNum = task.firstPage || 1;
} else { } else {
this.currentTask++;
this._nextTask(); this._nextTask();
return; return;
} }

View File

@ -290,6 +290,9 @@ async function startRefTest(masterMode, showRefImages) {
startTime = Date.now(); startTime = Date.now();
startServer(); startServer();
server.hooks.POST.push(refTestPostHandler); server.hooks.POST.push(refTestPostHandler);
taskQueue = new Map();
refPngCache = new Map();
server.hooks.WS.push(ws => { server.hooks.WS.push(ws => {
let pendingOps = 0; let pendingOps = 0;
let pendingQuit = null; let pendingQuit = null;
@ -304,7 +307,29 @@ async function startRefTest(masterMode, showRefImages) {
}); });
} else { } else {
const msg = JSON.parse(data.toString()); const msg = JSON.parse(data.toString());
if (msg.type === "quit") { if (msg.type === "requestTask") {
const session = getSession(msg.browser);
session.taskResults ??= {};
session.tasks ??= {};
session.remaining ??= 0;
const browserType = session.browserType ?? session.name;
if (!taskQueue.has(browserType)) {
taskQueue.set(browserType, [...manifest]);
}
const task = taskQueue.get(browserType).shift();
if (task) {
const rounds = task.rounds || 1;
const roundsResults = [];
roundsResults.length = rounds;
session.taskResults[task.id] = roundsResults;
session.tasks[task.id] = task;
session.remaining++;
ws.send(JSON.stringify({ type: "task", task }));
prefetchRefPngs(browserType, task);
} else {
ws.send(JSON.stringify({ type: "done" }));
}
} else if (msg.type === "quit") {
const session = getSession(msg.browser); const session = getSession(msg.browser);
monitorBrowserTimeout(session, null); monitorBrowserTimeout(session, null);
const doQuit = () => closeSession(session.name); const doQuit = () => closeSession(session.name);
@ -324,21 +349,9 @@ async function startRefTest(masterMode, showRefImages) {
numSessions: options.jobs, numSessions: options.jobs,
initializeSession: session => { initializeSession: session => {
session.masterMode = masterMode; session.masterMode = masterMode;
session.taskResults = {}; session.taskResults ??= {};
session.tasks = {}; session.tasks ??= {};
const sessionManifest = getSessionManifest( session.remaining ??= 0;
manifest,
session.sessionIndex,
session.sessionCount
);
session.remaining = sessionManifest.length;
sessionManifest.forEach(function (item) {
var rounds = item.rounds || 1;
var roundsResults = [];
roundsResults.length = rounds;
session.taskResults[item.id] = roundsResults;
session.tasks[item.id] = item;
});
session.numRuns = 0; session.numRuns = 0;
session.numErrors = 0; session.numErrors = 0;
session.numFBFFailures = 0; session.numFBFFailures = 0;
@ -379,6 +392,7 @@ async function startRefTest(masterMode, showRefImages) {
if (!manifest) { if (!manifest) {
return; return;
} }
if (!options.noDownload) { if (!options.noDownload) {
await ensurePDFsDownloaded(); await ensurePDFsDownloaded();
} }
@ -418,13 +432,32 @@ function getTestManifest() {
return manifest; return manifest;
} }
function getSessionManifest(manifest, sessionIndex, sessionCount) { function prefetchRefPngs(browserType, task) {
if (sessionCount <= 1) { if (
return manifest; task.type !== "eq" &&
task.type !== "partial" &&
task.type !== "text" &&
task.type !== "highlight" &&
task.type !== "extract"
) {
return;
} }
const start = Math.floor((manifest.length * sessionIndex) / sessionCount); const refSnapshotDir = path.join(
const end = Math.floor((manifest.length * (sessionIndex + 1)) / sessionCount); refsDir,
return manifest.slice(start, end); os.platform(),
browserType,
task.id
);
const firstPage = task.firstPage || 1;
const lastPage = task.lastPage;
// 0-indexed so pages[p-1] = promise for `${p}.png`, matching checkEq's loop.
const pages = [];
for (let p = firstPage; p <= lastPage; p++) {
pages[p - 1] = fs.promises
.readFile(path.join(refSnapshotDir, `${p}.png`))
.catch(err => (err.code === "ENOENT" ? null : Promise.reject(err)));
}
refPngCache.set(`${browserType}/${task.id}`, pages);
} }
async function checkEq(task, results, session, masterMode) { async function checkEq(task, results, session, masterMode) {
@ -446,20 +479,28 @@ async function checkEq(task, results, session, masterMode) {
let numEqNoSnapshot = 0; let numEqNoSnapshot = 0;
let numEqFailures = 0; let numEqFailures = 0;
// Read all reference PNGs in parallel, skipping pages with no valid snapshot. const cacheKey = `${browserType}/${taskId}`;
const cachedPages = refPngCache.get(cacheKey);
refPngCache.delete(cacheKey);
// Consume pre-started ref PNG reads (started when the task was dispatched),
// falling back to a fresh read if the cache entry is missing.
const refSnapshots = await Promise.all( const refSnapshots = await Promise.all(
pageResults.map((pageResult, page) => { pageResults.map((pageResult, page) => {
if (!pageResult || !(pageResult.snapshot instanceof Buffer)) { if (!pageResult || !(pageResult.snapshot instanceof Buffer)) {
return null; return null;
} }
return fs.promises return (
cachedPages?.[page] ??
fs.promises
.readFile(path.join(refSnapshotDir, `${page + 1}.png`)) .readFile(path.join(refSnapshotDir, `${page + 1}.png`))
.catch(err => { .catch(err => {
if (err.code === "ENOENT") { if (err.code === "ENOENT") {
return null; return null;
} }
throw err; throw err;
}); })
);
}) })
); );
@ -653,9 +694,10 @@ async function checkRefTestResults(browser, id, results) {
} }
}); });
}); });
const browserType = session.browserType ?? session.name;
if (failed) { if (failed) {
return; refPngCache.delete(`${browserType}/${id}`);
} } else {
switch (task.type) { switch (task.type) {
case "eq": case "eq":
case "partial": case "partial":
@ -673,12 +715,18 @@ async function checkRefTestResults(browser, id, results) {
default: default:
throw new Error("Unknown test type"); throw new Error("Unknown test type");
} }
// clear memory }
results.forEach(function (roundResults, round) { // Clear snapshot buffers and drop the task entry from the session.
roundResults.forEach(function (pageResult, page) { results.forEach(function (roundResults) {
roundResults.forEach(function (pageResult) {
if (pageResult) {
pageResult.snapshot = null; pageResult.snapshot = null;
pageResult.baselineSnapshot = null;
}
}); });
}); });
delete session.taskResults[id];
delete session.tasks[id];
} }
async function handleWsBinaryResult(data) { async function handleWsBinaryResult(data) {
@ -689,10 +737,13 @@ async function handleWsBinaryResult(data) {
const meta = JSON.parse(data.subarray(4, 4 + metaLen).toString("utf8")); const meta = JSON.parse(data.subarray(4, 4 + metaLen).toString("utf8"));
const snapshotLen = data.readUInt32BE(4 + metaLen); const snapshotLen = data.readUInt32BE(4 + metaLen);
const snapshotOffset = 8 + metaLen; const snapshotOffset = 8 + metaLen;
const snapshot = data.subarray(snapshotOffset, snapshotOffset + snapshotLen); // Copy slices so the original WS frame buffer can be GC'd immediately.
const snapshot = Buffer.from(
data.subarray(snapshotOffset, snapshotOffset + snapshotLen)
);
const baseline = const baseline =
data.length > snapshotOffset + snapshotLen data.length > snapshotOffset + snapshotLen
? data.subarray(snapshotOffset + snapshotLen) ? Buffer.from(data.subarray(snapshotOffset + snapshotLen))
: null; : null;
const { browser, id, round, page, failure, lastPageNum, numberOfTasks } = const { browser, id, round, page, failure, lastPageNum, numberOfTasks } =
@ -724,8 +775,8 @@ async function handleWsBinaryResult(data) {
const lastTaskResults = taskResults.at(-1); const lastTaskResults = taskResults.at(-1);
const isDone = const isDone =
lastTaskResults?.[lastPageNum - 1] || !!lastTaskResults?.[lastPageNum - 1] ||
lastTaskResults?.filter(result => !!result).length === numberOfTasks; lastTaskResults?.filter(Boolean).length === numberOfTasks;
if (isDone) { if (isDone) {
await checkRefTestResults(browser, id, taskResults); await checkRefTestResults(browser, id, taskResults);
session.remaining--; session.remaining--;
@ -1003,14 +1054,9 @@ async function startBrowsers({ baseUrl, initializeSession, numSessions = 1 }) {
if (baseUrl) { if (baseUrl) {
startUrl = startUrl =
`${baseUrl}?browser=${encodeURIComponent(sessionName)}` + `${baseUrl}?browser=${encodeURIComponent(sessionName)}` +
`&manifestFile=${encodeURIComponent(`/test/${options.manifestFile}`)}` +
`&testFilter=${JSON.stringify(options.testfilter)}` + `&testFilter=${JSON.stringify(options.testfilter)}` +
`&delay=${options.statsDelay}&masterMode=${options.masterMode}` + `&delay=${options.statsDelay}&masterMode=${options.masterMode}`;
(numSessions > 1
? `&sessionIndex=${i}&sessionCount=${numSessions}`
: "");
} }
await startBrowser({ browserName, startUrl }) await startBrowser({ browserName, startUrl })
.then(async function (browser) { .then(async function (browser) {
session.browser = browser; session.browser = browser;
@ -1189,6 +1235,8 @@ var host = "127.0.0.1";
var options = parseOptions(); var options = parseOptions();
var stats; var stats;
var tempDir = null; var tempDir = null;
var taskQueue = new Map();
var refPngCache = new Map();
main(); main();