fix(engine): avoid polynomial capture failure regex

This commit is contained in:
James
2026-07-17 05:22:53 -04:00
parent acb3d81b99
commit 5da9f7ab3d
3 changed files with 32 additions and 1 deletions
@@ -6,6 +6,7 @@ describe("classifyCaptureFailure", () => {
["Target closed", "transient_browser"],
["Runtime.callFunctionOn timed out after 30000ms", "protocol_timeout"],
["Runtime.evaluate timed out", "protocol_timeout"],
["drawElement worker encode timed out (frame 42)", "protocol_timeout"],
["Waiting failed: 30000ms exceeded", "protocol_timeout"],
["JavaScript heap out of memory", "memory_exhaustion"],
["drawElement self-verify failed", "verification"],
@@ -55,6 +56,14 @@ describe("classifyCaptureFailure", () => {
expect(Object.isFrozen(failure.workerDiagnostics[0]?.lines)).toBe(true);
});
it("classifies repeated operation text in linear time", () => {
const repeatedCopy = "copy".repeat(25_000);
expect(classifyCaptureFailure(new Error(repeatedCopy)).kind).toBe("authoring");
expect(classifyCaptureFailure(new Error(`${repeatedCopy} failed`)).kind).toBe("io");
expect(classifyCaptureFailure(new Error("copy\nfailed")).kind).toBe("authoring");
});
it("marks structural failures fatal but leaves retryable failures non-fatal", () => {
expect(
isFatalCaptureFailure(new CaptureFailure({ kind: "authoring", message: "bad source" })),
+16 -1
View File
@@ -58,6 +58,7 @@ const PROTOCOL_TIMEOUT_PATTERNS = [
/Runtime\.callFunctionOn timed out/i,
/Runtime\.evaluate timed out/i,
/HeadlessExperimental\.beginFrame timed out/i,
/drawElement worker encode timed out \(frame \d+\)/i,
/Protocol error[\s\S]*tim(?:ed|e) out/i,
/Waiting failed:\s*\d+\s*ms exceeded/i,
/Waiting failed[\s\S]*timeout/i,
@@ -103,11 +104,25 @@ function matchesAny(message: string, patterns: readonly RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(message));
}
const IO_OPERATION_TOKENS = ["read", "write", "rename", "copy", "open", "file", "directory"];
function hasIoOperationFailure(message: string): boolean {
const normalized = message.toLowerCase();
return normalized.split(/[\r\n]/).some((line) =>
IO_OPERATION_TOKENS.some((operation) => {
const operationIndex = line.indexOf(operation);
if (operationIndex < 0) return false;
const failureStart = operationIndex + operation.length;
return line.indexOf("failed", failureStart) >= 0 || line.indexOf("error", failureStart) >= 0;
}),
);
}
function ioError(error: unknown, message: string): boolean {
const code = error instanceof Error ? (error as NodeJS.ErrnoException).code : undefined;
return (
Boolean(code && /^(?:EACCES|EEXIST|EIO|EMFILE|ENFILE|ENOENT|ENOSPC|EPERM|EROFS)$/.test(code)) ||
/(?:read|write|rename|copy|open|file|directory).*(?:failed|error)/i.test(message)
hasIoOperationFailure(message)
);
}
@@ -1513,6 +1513,13 @@ describe("adaptive missing-frame retry helpers", () => {
new Error("[Parallel] Capture failed: Worker 1: HeadlessExperimental.beginFrame timed out"),
),
).toBe(true);
expect(
isRecoverableParallelCaptureError(
new Error(
"[Parallel] Capture failed: Worker 0: drawElement worker encode timed out (frame 42)",
),
),
).toBe(true);
expect(isRecoverableParallelCaptureError(new Error("Encoding failed: ffmpeg exited"))).toBe(
false,
);