refactor(process): own worker frame wait lifecycle

This commit is contained in:
morluto committed 2026-10-22 08:20:30 +08:00
1 parent 2765fc12bb
commit 1158dc2a94
3 files changed
+64 -74

No files matched your search

+8 -36
View File
@@ -1,3 +1,4 @@
import { WorkerFrameSignal } from "../process/WorkerFrameSignal.js";
import { createHash, randomUUID } from "node:crypto";
import { open, rm } from "node:fs/promises";
import { join } from "node:path";
@@ -139,7 +140,7 @@ export class WebBundleAnalysisWorker {
#activeDeadline: ProviderStartupDeadline | undefined;
#activeJob: WebBundleActiveJob | undefined;
#activeTerminalResponseObserved = false;
readonly #frameWaiters = new Set<(outcome: "frame" | "failure") => void>();
readonly #frameSignal = new WorkerFrameSignal();
#retainedResultBytes = 0;
#ready = false;
readonly #stdout = (chunk: Buffer | string): void => {
@@ -172,12 +173,12 @@ export class WebBundleAnalysisWorker {
this.#observeFrame(this.#frames.at(-1)!);
this.#pending = this.#pending.slice(end + 1);
}
this.#wakeFrameWaiters("frame");
this.#frameSignal.wake("frame");
} catch (cause) {
this.#pending = "";
this.#protocolFailure = cause;
this.#latchFailure({ kind: "protocol", cause });
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
};
@@ -200,7 +201,7 @@ export class WebBundleAnalysisWorker {
onInputFailure: (cause) => {
if (this.#activeDeadline?.interruption !== undefined) return;
this.#latchFailure({ kind: "input", cause });
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
},
supervisorOptions: {
captureStdout: false,
@@ -232,7 +233,7 @@ export class WebBundleAnalysisWorker {
!this.#activeTerminalResponseObserved)
)
this.#latchFailure({ kind: "incomplete-output" });
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
} else if (event.type === "output") {
const failure = this.#observedFailure;
if (failure?.kind !== "exit") return;
@@ -780,41 +781,12 @@ export class WebBundleAnalysisWorker {
};
}
#wakeFrameWaiters(outcome: "frame" | "failure"): void {
for (const wake of this.#frameWaiters) wake(outcome);
this.#frameWaiters.clear();
}
async #waitForFrameOrOutputClose(
supervisor: NonNullable<OwnedWorkerRuntime["supervisor"]>,
deadline: ProviderStartupDeadline,
): Promise<"frame" | "failure" | "closed" | "interrupted"> {
if (this.#frames.length > 0) return "frame";
let wakeFrame!: (outcome: "frame" | "failure") => void;
const frameAvailable = new Promise<"frame" | "failure">((resolve) => {
wakeFrame = resolve;
this.#frameWaiters.add(wakeFrame);
});
const outputCloseController = new AbortController();
const interruptOutputClose = () =>
outputCloseController.abort(deadline.signal.reason);
if (deadline.signal.aborted) interruptOutputClose();
else
deadline.signal.addEventListener("abort", interruptOutputClose, {
once: true,
});
try {
const outputClose = supervisor
.waitForOutputClose(undefined, outputCloseController.signal)
.then((closed): "closed" | "interrupted" =>
closed ? "closed" : "interrupted",
);
return await Promise.race([frameAvailable, outputClose]);
} finally {
outputCloseController.abort();
deadline.signal.removeEventListener("abort", interruptOutputClose);
this.#frameWaiters.delete(wakeFrame);
}
return this.#frameSignal.waitForOutputClose(supervisor, deadline.signal);
}
#completedSnapshot(cause: unknown): StableArtifactSnapshot | undefined {
@@ -875,7 +847,7 @@ export class WebBundleAnalysisWorker {
this.#activeDeadline = undefined;
this.#activeJob = undefined;
this.#activeTerminalResponseObserved = false;
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
#cleanupFailure(cause: unknown, primary?: AnalysisError): AnalysisError {
@@ -1,3 +1,4 @@
import { WorkerFrameSignal } from "../../process/WorkerFrameSignal.js";
import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js";
import { createHash, randomUUID } from "node:crypto";
import { readFile, rm, stat, writeFile } from "node:fs/promises";
@@ -100,7 +101,7 @@ export class JavaScriptAnalysisWorker {
#ready = false;
#heapLimitBytes = 0;
#frames: JavaScriptWorkerMessage[] = [];
#frameWaiters = new Set<(outcome: "frame" | "failure") => void>();
readonly #frameSignal = new WorkerFrameSignal();
#pending = "";
#decoder = new StringDecoder("utf8");
#primaryFailure: WorkerPrimaryFailure | undefined;
@@ -141,7 +142,7 @@ export class JavaScriptAnalysisWorker {
const frame = javaScriptWorkerMessageSchema.parse(raw);
this.#frames.push(frame);
this.#observeJobFrame(frame);
this.#wakeFrameWaiters("frame");
this.#frameSignal.wake("frame");
}
if (Buffer.byteLength(this.#pending) > DEFAULTS.protocolBytes)
this.#latchOutputFailure(
@@ -837,7 +838,7 @@ export class JavaScriptAnalysisWorker {
)
) {
this.#primaryFailure = { ...this.#primaryFailure, heapExhaustion: true };
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
if (event.type === "exit" && (event.code !== 0 || event.signal !== null)) {
if (this.#primaryFailure === undefined)
@@ -860,7 +861,7 @@ export class JavaScriptAnalysisWorker {
...this.#primaryFailure,
heapExhaustion: true,
};
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
}
}
@@ -880,7 +881,7 @@ export class JavaScriptAnalysisWorker {
this.#primaryFailure?.kind === "memory" ||
this.#primaryFailure?.kind === "exit"
)
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
}
#observeJobFrame(frame: JavaScriptWorkerMessage): void {
@@ -906,7 +907,7 @@ export class JavaScriptAnalysisWorker {
kind: "output",
reason: "JavaScript worker completed before a static checkpoint",
};
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
return;
}
this.#terminalResponseId = frame.id;
@@ -925,48 +926,20 @@ export class JavaScriptAnalysisWorker {
if (this.#deadline?.interruption !== undefined) return;
if (this.#primaryFailure === undefined)
this.#primaryFailure = { kind: "output", reason };
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
#latchInputFailure(cause: Error): void {
if (this.#deadline?.interruption !== undefined) return;
if (this.#primaryFailure === undefined)
this.#primaryFailure = { kind: "input", cause };
this.#wakeFrameWaiters("failure");
this.#frameSignal.wake("failure");
}
async #waitForFrameOrOutputClose(
supervisor: ProviderProcessSupervisor,
deadline: ProviderStartupDeadline,
): Promise<"frame" | "failure" | "closed" | "interrupted"> {
if (this.#frames.length !== 0) return "frame";
let wakeFrame!: (outcome: "frame" | "failure") => void;
const frameArrival = new Promise<"frame" | "failure">((resolve) => {
wakeFrame = resolve;
this.#frameWaiters.add(wakeFrame);
});
const outputCloseController = new AbortController();
const interruptOutputClose = () =>
outputCloseController.abort(deadline.signal.reason);
if (deadline.signal.aborted) interruptOutputClose();
else
deadline.signal.addEventListener("abort", interruptOutputClose, {
once: true,
});
try {
const outputClose = supervisor
.waitForOutputClose(undefined, outputCloseController.signal)
.then((closed): "closed" | "interrupted" =>
closed ? "closed" : "interrupted",
);
return await Promise.race([frameArrival, outputClose]);
} finally {
outputCloseController.abort();
deadline.signal.removeEventListener("abort", interruptOutputClose);
this.#frameWaiters.delete(wakeFrame);
}
}
#wakeFrameWaiters(outcome: "frame" | "failure"): void {
for (const wake of this.#frameWaiters) wake(outcome);
this.#frameWaiters.clear();
return this.#frameSignal.waitForOutputClose(supervisor, deadline.signal);
}
#selectedWorkerFailure(path: string): AnalysisError {
if (this.#primaryFailure !== undefined)
@@ -1231,7 +1204,7 @@ export class JavaScriptAnalysisWorker {
#resetState(): void {
this.#ready = false;
this.#frames = [];
this.#frameWaiters.clear();
this.#frameSignal.clear();
this.#pending = "";
this.#decoder = new StringDecoder("utf8");
this.#primaryFailure = undefined;
+45
View File
@@ -0,0 +1,45 @@
import type { ProviderProcessSupervisor } from "./ProviderProcess.js";
type FrameOutcome = "frame" | "failure";
/** Own frame waiters and release the losing output-close wait after each race. */
export class WorkerFrameSignal {
readonly #waiters = new Set<(outcome: FrameOutcome) => void>();
wake(outcome: FrameOutcome): void {
for (const wake of this.#waiters) wake(outcome);
this.clear();
}
clear(): void {
this.#waiters.clear();
}
async waitForOutputClose(
supervisor: ProviderProcessSupervisor,
signal: AbortSignal,
): Promise<FrameOutcome | "closed" | "interrupted"> {
let wakeFrame!: (outcome: FrameOutcome) => void;
const frameAvailable = new Promise<FrameOutcome>((resolve) => {
wakeFrame = resolve;
this.#waiters.add(wakeFrame);
});
const outputCloseController = new AbortController();
const interruptOutputClose = () =>
outputCloseController.abort(signal.reason);
if (signal.aborted) interruptOutputClose();
else signal.addEventListener("abort", interruptOutputClose, { once: true });
try {
const outputClose = supervisor
.waitForOutputClose(undefined, outputCloseController.signal)
.then((closed): "closed" | "interrupted" =>
closed ? "closed" : "interrupted",
);
return await Promise.race([frameAvailable, outputClose]);
} finally {
outputCloseController.abort();
signal.removeEventListener("abort", interruptOutputClose);
this.#waiters.delete(wakeFrame);
}
}
}