mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-26 17:07:21 +02:00
Improve reftest runner performance
- Replace base64/JSON POST image submission with binary WebSocket frames, avoiding base64 overhead and per-request HTTP costs; quit is also sent over the same WS channel to guarantee ordering - Prefetch the next task's PDF in the worker while the current task is still rendering - Use `getImageData` instead of `toBlob` for partial-test baseline comparison (synchronous, no encoding); only encode to PNG in master mode - Disable bounce tracking protection in Firefox to prevent EBUSY errors from Puppeteer's profile cleanup on Windows
This commit is contained in:
parent
7c5f7876e9
commit
b6fac76429
205
test/driver.js
205
test/driver.js
@ -480,6 +480,20 @@ class Rasterize {
|
|||||||
* @property {HTMLDivElement} end - Container for a completion message.
|
* @property {HTMLDivElement} end - Container for a completion message.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function buffersEqual(a, b) {
|
||||||
|
if (a.byteLength !== b.byteLength) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const v1 = new Uint8Array(a);
|
||||||
|
const v2 = new Uint8Array(b);
|
||||||
|
for (let i = 0; i < v1.length; i++) {
|
||||||
|
if (v1[i] !== v2[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
class Driver {
|
class Driver {
|
||||||
/**
|
/**
|
||||||
* @param {DriverOptions} options
|
* @param {DriverOptions} options
|
||||||
@ -509,6 +523,11 @@ class Driver {
|
|||||||
this.sessionIndex = parseInt(params.get("sessionindex") || "0", 10);
|
this.sessionIndex = parseInt(params.get("sessionindex") || "0", 10);
|
||||||
this.sessionCount = parseInt(params.get("sessioncount") || "1", 10);
|
this.sessionCount = parseInt(params.get("sessioncount") || "1", 10);
|
||||||
|
|
||||||
|
// Open a persistent WebSocket connection to the server for binary result
|
||||||
|
// submission.
|
||||||
|
this.ws = new WebSocket(`ws://${location.host}`);
|
||||||
|
this.ws.binaryType = "arraybuffer";
|
||||||
|
|
||||||
// Create a working canvas
|
// Create a working canvas
|
||||||
this.canvas = document.createElement("canvas");
|
this.canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
@ -538,6 +557,11 @@ class Driver {
|
|||||||
// When gathering the stats the numbers seem to be more reliable
|
// When gathering the stats the numbers seem to be more reliable
|
||||||
// if the browser is given more time to start.
|
// if the browser is given more time to start.
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
if (this.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
this.ws.addEventListener("open", resolve, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
const response = await fetch(this.manifestFile);
|
const response = await fetch(this.manifestFile);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
@ -622,6 +646,8 @@ class Driver {
|
|||||||
const prevFile = md5FileMap.get(task.md5);
|
const prevFile = md5FileMap.get(task.md5);
|
||||||
if (prevFile) {
|
if (prevFile) {
|
||||||
if (task.file !== prevFile) {
|
if (task.file !== prevFile) {
|
||||||
|
task._prefetchedLoadingTask?.destroy();
|
||||||
|
task._prefetchedLoadingTask = null;
|
||||||
this._nextPage(
|
this._nextPage(
|
||||||
task,
|
task,
|
||||||
`The "${task.file}" file is identical to the previously used "${prevFile}" file.`
|
`The "${task.file}" file is identical to the previously used "${prevFile}" file.`
|
||||||
@ -659,6 +685,10 @@ class Driver {
|
|||||||
|
|
||||||
this._log(` Loading file "${task.file}"\n`);
|
this._log(` Loading file "${task.file}"\n`);
|
||||||
|
|
||||||
|
// Start fetching and parsing the next task's PDF in the worker
|
||||||
|
// now, so it overlaps with the current task's load and render time.
|
||||||
|
this._prefetchNextTask();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let xfaStyleElement = null;
|
let xfaStyleElement = null;
|
||||||
if (task.enableXfa) {
|
if (task.enableXfa) {
|
||||||
@ -670,28 +700,13 @@ class Driver {
|
|||||||
.getElementsByTagName("head")[0]
|
.getElementsByTagName("head")[0]
|
||||||
.append(xfaStyleElement);
|
.append(xfaStyleElement);
|
||||||
}
|
}
|
||||||
const isOffscreenCanvasSupported =
|
|
||||||
task.isOffscreenCanvasSupported === false ? false : undefined;
|
|
||||||
const disableFontFace = task.disableFontFace === true;
|
|
||||||
|
|
||||||
const documentOptions = {
|
const documentOptions = {
|
||||||
url: new URL(task.file, window.location),
|
...this._getDocumentOptions(task),
|
||||||
password: task.password,
|
|
||||||
cMapUrl: CMAP_URL,
|
|
||||||
iccUrl: ICC_URL,
|
|
||||||
standardFontDataUrl: STANDARD_FONT_DATA_URL,
|
|
||||||
wasmUrl: WASM_URL,
|
|
||||||
disableAutoFetch: !task.enableAutoFetch,
|
|
||||||
pdfBug: true,
|
|
||||||
useSystemFonts: task.useSystemFonts,
|
|
||||||
useWasm: task.useWasm,
|
|
||||||
useWorkerFetch: task.useWorkerFetch,
|
|
||||||
enableXfa: task.enableXfa,
|
|
||||||
isOffscreenCanvasSupported,
|
|
||||||
styleElement: xfaStyleElement,
|
styleElement: xfaStyleElement,
|
||||||
disableFontFace,
|
|
||||||
};
|
};
|
||||||
const loadingTask = getDocument(documentOptions);
|
const loadingTask =
|
||||||
|
task._prefetchedLoadingTask ?? getDocument(documentOptions);
|
||||||
|
task._prefetchedLoadingTask = null;
|
||||||
let promise = loadingTask.promise;
|
let promise = loadingTask.promise;
|
||||||
|
|
||||||
if (!this.masterMode && task.type === "extract") {
|
if (!this.masterMode && task.type === "extract") {
|
||||||
@ -849,6 +864,44 @@ class Driver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDocumentOptions(task) {
|
||||||
|
return {
|
||||||
|
url: new URL(task.file, window.location),
|
||||||
|
password: task.password,
|
||||||
|
cMapUrl: CMAP_URL,
|
||||||
|
iccUrl: ICC_URL,
|
||||||
|
standardFontDataUrl: STANDARD_FONT_DATA_URL,
|
||||||
|
wasmUrl: WASM_URL,
|
||||||
|
disableAutoFetch: !task.enableAutoFetch,
|
||||||
|
pdfBug: true,
|
||||||
|
useSystemFonts: task.useSystemFonts,
|
||||||
|
useWasm: task.useWasm,
|
||||||
|
useWorkerFetch: task.useWorkerFetch,
|
||||||
|
enableXfa: task.enableXfa,
|
||||||
|
isOffscreenCanvasSupported:
|
||||||
|
task.isOffscreenCanvasSupported === false ? false : undefined,
|
||||||
|
disableFontFace: task.disableFontFace === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_prefetchNextTask() {
|
||||||
|
const nextIdx = this.currentTask + 1;
|
||||||
|
if (nextIdx >= this.manifest.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task = this.manifest[nextIdx];
|
||||||
|
// Skip tasks that do not load a PDF or that need DOM setup (XFA style
|
||||||
|
// element injection) to happen synchronously before getDocument.
|
||||||
|
if (
|
||||||
|
task.type === "skip-because-failing" ||
|
||||||
|
task.type === "other" ||
|
||||||
|
task.enableXfa
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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.
|
||||||
while (document.styleSheets.length > 0) {
|
while (document.styleSheets.length > 0) {
|
||||||
@ -896,14 +949,15 @@ class Driver {
|
|||||||
let ctx;
|
let ctx;
|
||||||
|
|
||||||
if (!task.pdfDoc) {
|
if (!task.pdfDoc) {
|
||||||
const dataUrl = this.canvas.toDataURL("image/png");
|
new Promise(r => {
|
||||||
this._sendResult(dataUrl, task, failure).then(() => {
|
this.canvas.toBlob(r, "image/png");
|
||||||
this._log(
|
})
|
||||||
"done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
|
.then(blob => this._sendResult(blob, task, failure))
|
||||||
);
|
.then(() => {
|
||||||
this.currentTask++;
|
this._log(`done${failure ? ` (failed !: ${failure})` : ""}\n`);
|
||||||
this._nextTask();
|
this.currentTask++;
|
||||||
});
|
this._nextTask();
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1177,7 +1231,21 @@ class Driver {
|
|||||||
};
|
};
|
||||||
|
|
||||||
clearOutsidePartial();
|
clearOutsidePartial();
|
||||||
const baseline = ctx.canvas.toDataURL("image/png");
|
// Capture pixel data synchronously for the comparison;
|
||||||
|
// only encode to PNG in master mode where the server needs
|
||||||
|
// to save it as the reference image.
|
||||||
|
const baselinePixels = ctx.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
ctx.canvas.width,
|
||||||
|
ctx.canvas.height
|
||||||
|
);
|
||||||
|
const baselineBlob =
|
||||||
|
this.masterMode && !task.knownPartialMismatch
|
||||||
|
? await new Promise(r => {
|
||||||
|
ctx.canvas.toBlob(r, "image/png");
|
||||||
|
})
|
||||||
|
: null;
|
||||||
this._clearCanvas();
|
this._clearCanvas();
|
||||||
|
|
||||||
const recordedBBoxes = page.recordedBBoxes;
|
const recordedBBoxes = page.recordedBBoxes;
|
||||||
@ -1223,7 +1291,8 @@ class Driver {
|
|||||||
// one pixel of a very slightly different shade), so we
|
// one pixel of a very slightly different shade), so we
|
||||||
// avoid compating them to the non-optimized version and
|
// avoid compating them to the non-optimized version and
|
||||||
// instead use the optimized version also for makeref.
|
// instead use the optimized version also for makeref.
|
||||||
task.knownPartialMismatch ? null : baseline
|
task.knownPartialMismatch ? null : baselinePixels,
|
||||||
|
baselineBlob
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1268,32 +1337,31 @@ class Driver {
|
|||||||
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
_snapshot(task, failure, baselineDataUrl = null) {
|
async _snapshot(task, failure, baselinePixels = null, baselineBlob = null) {
|
||||||
this._log("Snapshotting... ");
|
this._log("Snapshotting... ");
|
||||||
|
const snapshotBlob = await new Promise(r => {
|
||||||
const dataUrl = this.canvas.toDataURL("image/png");
|
this.canvas.toBlob(r, "image/png");
|
||||||
|
|
||||||
if (baselineDataUrl && baselineDataUrl !== dataUrl) {
|
|
||||||
failure ||= "Optimized rendering differs from full rendering.";
|
|
||||||
}
|
|
||||||
|
|
||||||
this._sendResult(dataUrl, task, failure, baselineDataUrl).then(() => {
|
|
||||||
this._log(
|
|
||||||
"done" + (failure ? " (failed !: " + failure + ")" : "") + "\n"
|
|
||||||
);
|
|
||||||
task.pageNum++;
|
|
||||||
this._nextPage(task);
|
|
||||||
});
|
});
|
||||||
|
if (baselinePixels) {
|
||||||
|
const snapPixels = this.canvas
|
||||||
|
.getContext("2d")
|
||||||
|
.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
if (!buffersEqual(baselinePixels.data.buffer, snapPixels.data.buffer)) {
|
||||||
|
failure ||= "Optimized rendering differs from full rendering.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this._sendResult(snapshotBlob, task, failure, baselineBlob);
|
||||||
|
this._log(`done${failure ? ` (failed !: ${failure})` : ""}\n`);
|
||||||
|
task.pageNum++;
|
||||||
|
this._nextPage(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
_quit() {
|
_quit() {
|
||||||
this._log("Done !");
|
this._log("Done !");
|
||||||
this.end.textContent = "Tests finished. Close this window!";
|
this.end.textContent = "Tests finished. Close this window!";
|
||||||
|
// Send quit over the same WebSocket channel so the server processes it
|
||||||
// Send the quit request
|
// only after all preceding result frames have been handled.
|
||||||
fetch(`/tellMeToQuit?browser=${escape(this.browser)}`, {
|
this.ws.send(JSON.stringify({ type: "quit", browser: this.browser }));
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_info(message) {
|
_info(message) {
|
||||||
@ -1331,8 +1399,11 @@ class Driver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendResult(snapshot, task, failure, baselineSnapshot = null) {
|
async _sendResult(snapshotBlob, task, failure, baselineBlob = null) {
|
||||||
const result = JSON.stringify({
|
// Build a binary WebSocket frame:
|
||||||
|
// [4 bytes BE: meta_len][meta JSON][4 bytes BE: snapshot_len]
|
||||||
|
// [snapshot PNG][baseline PNG]
|
||||||
|
const meta = JSON.stringify({
|
||||||
browser: this.browser,
|
browser: this.browser,
|
||||||
id: task.id,
|
id: task.id,
|
||||||
numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0,
|
numPages: task.pdfDoc ? task.lastPage || task.pdfDoc.numPages : 0,
|
||||||
@ -1342,14 +1413,34 @@ class Driver {
|
|||||||
file: task.file,
|
file: task.file,
|
||||||
round: task.round,
|
round: task.round,
|
||||||
page: task.pageMapping?.[task.pageNum] ?? task.pageNum,
|
page: task.pageMapping?.[task.pageNum] ?? task.pageNum,
|
||||||
snapshot,
|
|
||||||
baselineSnapshot,
|
|
||||||
stats: task.stats.times,
|
stats: task.stats.times,
|
||||||
viewportWidth: task.viewportWidth,
|
viewportWidth: task.viewportWidth,
|
||||||
viewportHeight: task.viewportHeight,
|
viewportHeight: task.viewportHeight,
|
||||||
outputScale: task.outputScale,
|
outputScale: task.outputScale,
|
||||||
});
|
});
|
||||||
return this._send("/submit_task_results", result);
|
const metaBytes = new TextEncoder().encode(meta);
|
||||||
|
const snapshotBytes = new Uint8Array(await snapshotBlob.arrayBuffer());
|
||||||
|
const baselineBytes = baselineBlob
|
||||||
|
? new Uint8Array(await baselineBlob.arrayBuffer())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const totalLen =
|
||||||
|
4 +
|
||||||
|
metaBytes.length +
|
||||||
|
4 +
|
||||||
|
snapshotBytes.length +
|
||||||
|
(baselineBytes?.length ?? 0);
|
||||||
|
const buf = new ArrayBuffer(totalLen);
|
||||||
|
const view = new DataView(buf);
|
||||||
|
const bytes = new Uint8Array(buf);
|
||||||
|
view.setUint32(0, metaBytes.length);
|
||||||
|
bytes.set(metaBytes, 4);
|
||||||
|
view.setUint32(4 + metaBytes.length, snapshotBytes.length);
|
||||||
|
bytes.set(snapshotBytes, 8 + metaBytes.length);
|
||||||
|
if (baselineBytes) {
|
||||||
|
bytes.set(baselineBytes, 8 + metaBytes.length + snapshotBytes.length);
|
||||||
|
}
|
||||||
|
this.ws.send(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
_send(url, message) {
|
_send(url, message) {
|
||||||
@ -1358,26 +1449,20 @@ class Driver {
|
|||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: message,
|
body: message,
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// Retry until successful.
|
|
||||||
if (!response.ok || response.status !== 200) {
|
if (!response.ok || response.status !== 200) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.inFlightRequests--;
|
this.inFlightRequests--;
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(reason => {
|
.catch(reason => {
|
||||||
console.warn(`Driver._send failed (${url}):`, reason);
|
console.warn(`Driver._send failed (${url}):`, reason);
|
||||||
|
|
||||||
this.inFlightRequests--;
|
this.inFlightRequests--;
|
||||||
resolve();
|
resolve();
|
||||||
|
|
||||||
this._send(url, message);
|
this._send(url, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
293
test/test.mjs
293
test/test.mjs
@ -290,6 +290,33 @@ async function startRefTest(masterMode, showRefImages) {
|
|||||||
startTime = Date.now();
|
startTime = Date.now();
|
||||||
startServer();
|
startServer();
|
||||||
server.hooks.POST.push(refTestPostHandler);
|
server.hooks.POST.push(refTestPostHandler);
|
||||||
|
server.hooks.WS.push(ws => {
|
||||||
|
let pendingOps = 0;
|
||||||
|
let pendingQuit = null;
|
||||||
|
ws.on("message", (data, isBinary) => {
|
||||||
|
if (isBinary) {
|
||||||
|
pendingOps++;
|
||||||
|
handleWsBinaryResult(data).finally(() => {
|
||||||
|
if (--pendingOps === 0 && pendingQuit) {
|
||||||
|
pendingQuit();
|
||||||
|
pendingQuit = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
if (msg.type === "quit") {
|
||||||
|
const session = getSession(msg.browser);
|
||||||
|
monitorBrowserTimeout(session, null);
|
||||||
|
const doQuit = () => closeSession(session.name);
|
||||||
|
if (pendingOps === 0) {
|
||||||
|
doQuit();
|
||||||
|
} else {
|
||||||
|
pendingQuit = doQuit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
onAllSessionsClosed = finalize;
|
onAllSessionsClosed = finalize;
|
||||||
|
|
||||||
await startBrowsers({
|
await startBrowsers({
|
||||||
@ -400,68 +427,94 @@ function getSessionManifest(manifest, sessionIndex, sessionCount) {
|
|||||||
return manifest.slice(start, end);
|
return manifest.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkEq(task, results, session, masterMode) {
|
async function checkEq(task, results, session, masterMode) {
|
||||||
var taskId = task.id;
|
const taskId = task.id;
|
||||||
const browserType = session.browserType ?? session.name;
|
const browserType = session.browserType ?? session.name;
|
||||||
var refSnapshotDir = path.join(refsDir, os.platform(), browserType, taskId);
|
const refSnapshotDir = path.join(refsDir, os.platform(), browserType, taskId);
|
||||||
var testSnapshotDir = path.join(
|
const testSnapshotDir = path.join(
|
||||||
testResultDir,
|
testResultDir,
|
||||||
os.platform(),
|
os.platform(),
|
||||||
browserType,
|
browserType,
|
||||||
taskId
|
taskId
|
||||||
);
|
);
|
||||||
|
const tmpSnapshotDir = masterMode
|
||||||
|
? path.join(refsTmpDir, os.platform(), browserType, taskId)
|
||||||
|
: null;
|
||||||
|
|
||||||
var pageResults = results[0];
|
const pageResults = results[0];
|
||||||
var taskType = task.type;
|
const taskType = task.type;
|
||||||
var numEqNoSnapshot = 0;
|
let numEqNoSnapshot = 0;
|
||||||
var numEqFailures = 0;
|
let numEqFailures = 0;
|
||||||
for (var page = 0; page < pageResults.length; page++) {
|
|
||||||
if (!pageResults[page]) {
|
// Read all reference PNGs in parallel, skipping pages with no valid snapshot.
|
||||||
|
const refSnapshots = await Promise.all(
|
||||||
|
pageResults.map((pageResult, page) => {
|
||||||
|
if (!pageResult || !(pageResult.snapshot instanceof Buffer)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return fs.promises
|
||||||
|
.readFile(path.join(refSnapshotDir, `${page + 1}.png`))
|
||||||
|
.catch(err => {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Compare all pages (in-memory) and collect all I/O writes to fire together.
|
||||||
|
const writePromises = [];
|
||||||
|
const logEntries = [];
|
||||||
|
let testDirCreated = false;
|
||||||
|
let tmpDirCreated = false;
|
||||||
|
|
||||||
|
for (let page = 0; page < pageResults.length; page++) {
|
||||||
|
const pageResult = pageResults[page];
|
||||||
|
if (!pageResult) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const pageResult = pageResults[page];
|
const testSnapshot = pageResult.snapshot;
|
||||||
let testSnapshot = pageResult.snapshot;
|
if (!(testSnapshot instanceof Buffer)) {
|
||||||
if (testSnapshot?.startsWith("data:image/png;base64,")) {
|
|
||||||
testSnapshot = Buffer.from(testSnapshot.substring(22), "base64");
|
|
||||||
} else {
|
|
||||||
console.error("Valid snapshot was not found.");
|
console.error("Valid snapshot was not found.");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
let unoptimizedSnapshot = pageResult.baselineSnapshot;
|
const unoptimizedSnapshot = pageResult.baselineSnapshot ?? null;
|
||||||
if (unoptimizedSnapshot?.startsWith("data:image/png;base64,")) {
|
const refSnapshot = refSnapshots[page];
|
||||||
unoptimizedSnapshot = Buffer.from(
|
|
||||||
unoptimizedSnapshot.substring(22),
|
|
||||||
"base64"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var refSnapshot = null;
|
let eq = false;
|
||||||
var eq = false;
|
if (!refSnapshot) {
|
||||||
var refPath = path.join(refSnapshotDir, `${page + 1}.png`);
|
|
||||||
if (!fs.existsSync(refPath)) {
|
|
||||||
numEqNoSnapshot++;
|
numEqNoSnapshot++;
|
||||||
if (!masterMode) {
|
if (!masterMode) {
|
||||||
console.log(`WARNING: no reference snapshot ${refPath}`);
|
console.log(
|
||||||
|
`WARNING: no reference snapshot ${path.join(refSnapshotDir, `${page + 1}.png`)}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
refSnapshot = fs.readFileSync(refPath);
|
|
||||||
eq =
|
eq =
|
||||||
stripPrivatePngChunks(refSnapshot).toString("hex") ===
|
Buffer.compare(
|
||||||
stripPrivatePngChunks(testSnapshot).toString("hex");
|
stripPrivatePngChunks(refSnapshot),
|
||||||
|
stripPrivatePngChunks(testSnapshot)
|
||||||
|
) === 0;
|
||||||
if (!eq) {
|
if (!eq) {
|
||||||
console.log(
|
console.log(
|
||||||
`TEST-UNEXPECTED-FAIL | ${taskType} ${taskId} | in ${session.name} | rendering of page ${page + 1} != reference rendering`
|
`TEST-UNEXPECTED-FAIL | ${taskType} ${taskId} | in ${session.name} | rendering of page ${page + 1} != reference rendering`
|
||||||
);
|
);
|
||||||
|
|
||||||
ensureDirSync(testSnapshotDir);
|
if (!testDirCreated) {
|
||||||
|
ensureDirSync(testSnapshotDir);
|
||||||
|
testDirCreated = true;
|
||||||
|
}
|
||||||
const testPng = path.join(testSnapshotDir, `${page + 1}.png`);
|
const testPng = path.join(testSnapshotDir, `${page + 1}.png`);
|
||||||
const refPng = path.join(testSnapshotDir, `${page + 1}_ref.png`);
|
const refPng = path.join(testSnapshotDir, `${page + 1}_ref.png`);
|
||||||
fs.writeFileSync(testPng, testSnapshot);
|
writePromises.push(
|
||||||
fs.writeFileSync(refPng, refSnapshot);
|
fs.promises.writeFile(testPng, testSnapshot),
|
||||||
|
fs.promises.writeFile(refPng, refSnapshot)
|
||||||
|
);
|
||||||
|
|
||||||
// This no longer follows the format of Mozilla reftest output.
|
// This no longer follows the format of Mozilla reftest output.
|
||||||
const viewportString = `(${pageResult.viewportWidth}x${pageResult.viewportHeight}x${pageResult.outputScale})`;
|
const viewportString = `(${pageResult.viewportWidth}x${pageResult.viewportHeight}x${pageResult.outputScale})`;
|
||||||
fs.appendFileSync(
|
logEntries.push(
|
||||||
eqLog,
|
|
||||||
`REFTEST TEST-UNEXPECTED-FAIL | ${session.name}-${taskId}-page${page + 1} | image comparison (==)\n` +
|
`REFTEST TEST-UNEXPECTED-FAIL | ${session.name}-${taskId}-page${page + 1} | image comparison (==)\n` +
|
||||||
`REFTEST IMAGE 1 (TEST)${viewportString}: ${testPng}\n` +
|
`REFTEST IMAGE 1 (TEST)${viewportString}: ${testPng}\n` +
|
||||||
`REFTEST IMAGE 2 (REFERENCE)${viewportString}: ${refPng}\n`
|
`REFTEST IMAGE 2 (REFERENCE)${viewportString}: ${refPng}\n`
|
||||||
@ -470,20 +523,24 @@ function checkEq(task, results, session, masterMode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (masterMode && (!refSnapshot || !eq)) {
|
if (masterMode && (!refSnapshot || !eq)) {
|
||||||
var tmpSnapshotDir = path.join(
|
if (!tmpDirCreated) {
|
||||||
refsTmpDir,
|
ensureDirSync(tmpSnapshotDir);
|
||||||
os.platform(),
|
tmpDirCreated = true;
|
||||||
browserType,
|
}
|
||||||
taskId
|
writePromises.push(
|
||||||
);
|
fs.promises.writeFile(
|
||||||
ensureDirSync(tmpSnapshotDir);
|
path.join(tmpSnapshotDir, `${page + 1}.png`),
|
||||||
fs.writeFileSync(
|
unoptimizedSnapshot ?? testSnapshot
|
||||||
path.join(tmpSnapshotDir, `${page + 1}.png`),
|
)
|
||||||
unoptimizedSnapshot ?? testSnapshot
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (logEntries.length) {
|
||||||
|
writePromises.push(fs.promises.appendFile(eqLog, logEntries.join("")));
|
||||||
|
}
|
||||||
|
await Promise.all(writePromises);
|
||||||
|
|
||||||
session.numEqNoSnapshot += numEqNoSnapshot;
|
session.numEqNoSnapshot += numEqNoSnapshot;
|
||||||
if (numEqFailures > 0) {
|
if (numEqFailures > 0) {
|
||||||
session.numEqFailures += numEqFailures;
|
session.numEqFailures += numEqFailures;
|
||||||
@ -506,7 +563,7 @@ function checkFBF(task, results, session, masterMode) {
|
|||||||
if (!r0Page) {
|
if (!r0Page) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (r0Page.snapshot !== r1Page.snapshot) {
|
if (Buffer.compare(r0Page.snapshot, r1Page.snapshot) !== 0) {
|
||||||
// The FBF tests fail intermittently in Firefox and Google Chrome when run
|
// The FBF tests fail intermittently in Firefox and Google Chrome when run
|
||||||
// on the bots, ignoring `makeref` failures for now; see
|
// on the bots, ignoring `makeref` failures for now; see
|
||||||
// - https://github.com/mozilla/pdf.js/pull/12368
|
// - https://github.com/mozilla/pdf.js/pull/12368
|
||||||
@ -543,7 +600,7 @@ function checkLoad(task, results, browser) {
|
|||||||
console.log(`TEST-PASS | load test ${task.id} | in ${browser}`);
|
console.log(`TEST-PASS | load test ${task.id} | in ${browser}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRefTestResults(browser, id, results) {
|
async function checkRefTestResults(browser, id, results) {
|
||||||
var failed = false;
|
var failed = false;
|
||||||
var session = getSession(browser);
|
var session = getSession(browser);
|
||||||
var task = session.tasks[id];
|
var task = session.tasks[id];
|
||||||
@ -605,7 +662,7 @@ function checkRefTestResults(browser, id, results) {
|
|||||||
case "text":
|
case "text":
|
||||||
case "highlight":
|
case "highlight":
|
||||||
case "extract":
|
case "extract":
|
||||||
checkEq(task, results, session, session.masterMode);
|
await checkEq(task, results, session, session.masterMode);
|
||||||
break;
|
break;
|
||||||
case "fbf":
|
case "fbf":
|
||||||
checkFBF(task, results, session, session.masterMode);
|
checkFBF(task, results, session, session.masterMode);
|
||||||
@ -624,16 +681,61 @@ function checkRefTestResults(browser, id, results) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refTestPostHandler(parsedUrl, req, res) {
|
async function handleWsBinaryResult(data) {
|
||||||
var pathname = parsedUrl.pathname;
|
// Binary frame layout:
|
||||||
if (
|
// [4 bytes BE: meta_len][meta JSON][4 bytes BE: snapshot_len]
|
||||||
pathname !== "/tellMeToQuit" &&
|
// [snapshot PNG][baseline PNG (rest)]
|
||||||
pathname !== "/info" &&
|
const metaLen = data.readUInt32BE(0);
|
||||||
pathname !== "/submit_task_results"
|
const meta = JSON.parse(data.subarray(4, 4 + metaLen).toString("utf8"));
|
||||||
) {
|
const snapshotLen = data.readUInt32BE(4 + metaLen);
|
||||||
return false;
|
const snapshotOffset = 8 + metaLen;
|
||||||
|
const snapshot = data.subarray(snapshotOffset, snapshotOffset + snapshotLen);
|
||||||
|
const baseline =
|
||||||
|
data.length > snapshotOffset + snapshotLen
|
||||||
|
? data.subarray(snapshotOffset + snapshotLen)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const { browser, id, round, page, failure, lastPageNum, numberOfTasks } =
|
||||||
|
meta;
|
||||||
|
const session = getSession(browser);
|
||||||
|
monitorBrowserTimeout(session, handleSessionTimeout);
|
||||||
|
|
||||||
|
const taskResults = session.taskResults[id];
|
||||||
|
if (!taskResults[round]) {
|
||||||
|
taskResults[round] = [];
|
||||||
|
}
|
||||||
|
if (taskResults[round][page - 1]) {
|
||||||
|
console.error(
|
||||||
|
`Results for ${browser}:${id}:${round}:${page - 1} were already submitted`
|
||||||
|
);
|
||||||
|
// TODO abort testing here?
|
||||||
|
}
|
||||||
|
taskResults[round][page - 1] = {
|
||||||
|
failure,
|
||||||
|
snapshot,
|
||||||
|
baselineSnapshot: baseline,
|
||||||
|
viewportWidth: meta.viewportWidth,
|
||||||
|
viewportHeight: meta.viewportHeight,
|
||||||
|
outputScale: meta.outputScale,
|
||||||
|
};
|
||||||
|
if (stats) {
|
||||||
|
stats.push({ browser, pdf: id, page: page - 1, round, stats: meta.stats });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastTaskResults = taskResults.at(-1);
|
||||||
|
const isDone =
|
||||||
|
lastTaskResults?.[lastPageNum - 1] ||
|
||||||
|
lastTaskResults?.filter(result => !!result).length === numberOfTasks;
|
||||||
|
if (isDone) {
|
||||||
|
await checkRefTestResults(browser, id, taskResults);
|
||||||
|
session.remaining--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refTestPostHandler(parsedUrl, req, res) {
|
||||||
|
if (parsedUrl.pathname !== "/info") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
var body = "";
|
var body = "";
|
||||||
req.on("data", function (data) {
|
req.on("data", function (data) {
|
||||||
body += data;
|
body += data;
|
||||||
@ -641,80 +743,7 @@ function refTestPostHandler(parsedUrl, req, res) {
|
|||||||
req.on("end", function () {
|
req.on("end", function () {
|
||||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||||
res.end();
|
res.end();
|
||||||
|
console.log(JSON.parse(body).message);
|
||||||
var session;
|
|
||||||
if (pathname === "/tellMeToQuit") {
|
|
||||||
session = getSession(parsedUrl.searchParams.get("browser"));
|
|
||||||
monitorBrowserTimeout(session, null);
|
|
||||||
closeSession(session.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = JSON.parse(body);
|
|
||||||
if (pathname === "/info") {
|
|
||||||
console.log(data.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var browser = data.browser;
|
|
||||||
var round = data.round;
|
|
||||||
var id = data.id;
|
|
||||||
var page = data.page - 1;
|
|
||||||
var failure = data.failure;
|
|
||||||
var snapshot = data.snapshot;
|
|
||||||
var baselineSnapshot = data.baselineSnapshot;
|
|
||||||
var lastPageNum = data.lastPageNum;
|
|
||||||
var numberOfTasks = data.numberOfTasks;
|
|
||||||
|
|
||||||
session = getSession(browser);
|
|
||||||
monitorBrowserTimeout(session, handleSessionTimeout);
|
|
||||||
|
|
||||||
var taskResults = session.taskResults[id];
|
|
||||||
if (!taskResults[round]) {
|
|
||||||
taskResults[round] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskResults[round][page]) {
|
|
||||||
console.error(
|
|
||||||
"Results for " +
|
|
||||||
browser +
|
|
||||||
":" +
|
|
||||||
id +
|
|
||||||
":" +
|
|
||||||
round +
|
|
||||||
":" +
|
|
||||||
page +
|
|
||||||
" were already submitted"
|
|
||||||
);
|
|
||||||
// TODO abort testing here?
|
|
||||||
}
|
|
||||||
|
|
||||||
taskResults[round][page] = {
|
|
||||||
failure,
|
|
||||||
snapshot,
|
|
||||||
baselineSnapshot,
|
|
||||||
viewportWidth: data.viewportWidth,
|
|
||||||
viewportHeight: data.viewportHeight,
|
|
||||||
outputScale: data.outputScale,
|
|
||||||
};
|
|
||||||
if (stats) {
|
|
||||||
stats.push({
|
|
||||||
browser,
|
|
||||||
pdf: id,
|
|
||||||
page,
|
|
||||||
round,
|
|
||||||
stats: data.stats,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastTaskResults = taskResults.at(-1);
|
|
||||||
const isDone =
|
|
||||||
lastTaskResults?.[lastPageNum - 1] ||
|
|
||||||
lastTaskResults?.filter(result => !!result).length === numberOfTasks;
|
|
||||||
if (isDone) {
|
|
||||||
checkRefTestResults(browser, id, taskResults);
|
|
||||||
session.remaining--;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -911,6 +940,10 @@ async function startBrowser({
|
|||||||
// Disable AI/ML functionality.
|
// Disable AI/ML functionality.
|
||||||
"browser.ai.control.default": "blocked",
|
"browser.ai.control.default": "blocked",
|
||||||
"privacy.baselineFingerprintingProtection": false,
|
"privacy.baselineFingerprintingProtection": false,
|
||||||
|
// Disable bounce tracking protection to avoid creating a SQLite database
|
||||||
|
// file that Firefox keeps locked briefly after shutdown, causing EBUSY
|
||||||
|
// errors in Puppeteer's profile cleanup on Windows.
|
||||||
|
"privacy.bounceTrackingProtection.mode": 0,
|
||||||
...extraPrefsFirefox,
|
...extraPrefsFirefox,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import fsPromises from "fs/promises";
|
|||||||
import http from "http";
|
import http from "http";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { pathToFileURL } from "url";
|
import { pathToFileURL } from "url";
|
||||||
|
import { WebSocketServer } from "ws";
|
||||||
|
|
||||||
const MIME_TYPES = {
|
const MIME_TYPES = {
|
||||||
".css": "text/css",
|
".css": "text/css",
|
||||||
@ -49,6 +50,7 @@ class WebServer {
|
|||||||
this.host = host || "localhost";
|
this.host = host || "localhost";
|
||||||
this.port = port || 0;
|
this.port = port || 0;
|
||||||
this.server = null;
|
this.server = null;
|
||||||
|
this.wss = null;
|
||||||
this.verbose = false;
|
this.verbose = false;
|
||||||
this.cacheExpirationTime = cacheExpirationTime || 0;
|
this.cacheExpirationTime = cacheExpirationTime || 0;
|
||||||
this.disableRangeRequests = false;
|
this.disableRangeRequests = false;
|
||||||
@ -56,6 +58,7 @@ class WebServer {
|
|||||||
this.hooks = {
|
this.hooks = {
|
||||||
GET: [crossOriginHandler, redirectHandler],
|
GET: [crossOriginHandler, redirectHandler],
|
||||||
POST: [],
|
POST: [],
|
||||||
|
WS: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,10 +66,18 @@ class WebServer {
|
|||||||
this.#ensureNonZeroPort();
|
this.#ensureNonZeroPort();
|
||||||
this.server = http.createServer(this.#handler.bind(this));
|
this.server = http.createServer(this.#handler.bind(this));
|
||||||
this.server.listen(this.port, this.host, callback);
|
this.server.listen(this.port, this.host, callback);
|
||||||
|
this.wss = new WebSocketServer({ server: this.server });
|
||||||
|
this.wss.on("connection", ws => {
|
||||||
|
for (const handler of this.hooks.WS) {
|
||||||
|
handler(ws);
|
||||||
|
}
|
||||||
|
});
|
||||||
console.log(`Server running at http://${this.host}:${this.port}/`);
|
console.log(`Server running at http://${this.host}:${this.port}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(callback) {
|
stop(callback) {
|
||||||
|
this.wss.close();
|
||||||
|
this.wss = null;
|
||||||
this.server.close(callback);
|
this.server.close(callback);
|
||||||
this.server = null;
|
this.server = null;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user