Read the V8 limit and used heap together for initial admission, checkpoint decode and remaining result capacity. Remove the duplicated second heap/RSS observation while preserving every reserve and ceiling. Make the controlled parent-capacity fixture internally consistent and test both unchanged usage and a later one-MiB collection. Real worker/transfer coverage still retains static/module facts and declines only the large graph. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { afterAll, expect, it, vi } from "vitest";
|
|
|
|
const heapObservation = vi.hoisted(() => ({ reclaimedBytes: 0 }));
|
|
|
|
// Give the parent a stable 64 MiB result envelope after its transfer reserve.
|
|
// The child analyzer remains a real separately owned Node process with its
|
|
// normal heap and real transfer files/descriptors.
|
|
vi.mock("node:v8", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("node:v8")>();
|
|
return {
|
|
...actual,
|
|
getHeapStatistics: () => {
|
|
const snapshot = actual.getHeapStatistics();
|
|
// A separate later memoryUsage() read can observe a collection. Keep the
|
|
// limit and usage in this earlier snapshot consistent with each other.
|
|
const used = snapshot.used_heap_size + heapObservation.reclaimedBytes;
|
|
return {
|
|
...snapshot,
|
|
used_heap_size: used,
|
|
heap_size_limit: used + 320 * 1024 * 1024,
|
|
};
|
|
},
|
|
};
|
|
});
|
|
|
|
import { JavaScriptAnalysisWorker } from "../../../src/javascript/analysis/JavaScriptAnalysisWorker.js";
|
|
import { AnalysisResourceConstraintError } from "../../../src/domain/analysisErrorCore.js";
|
|
import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js";
|
|
|
|
afterAll(() => {
|
|
vi.doUnmock("node:v8");
|
|
vi.resetModules();
|
|
});
|
|
|
|
async function verifyParentAdmission(reclaimedBytes: number) {
|
|
heapObservation.reclaimedBytes = reclaimedBytes;
|
|
const methodsCount = 600;
|
|
const text = Array.from(
|
|
{ length: methodsCount },
|
|
(_, index) =>
|
|
`export function method_${String(index)}(input) { const value = input + ${String(index)}; const pair = { value }; return pair.value; }`,
|
|
).join("\n");
|
|
const file: JavaScriptArtifactFile = {
|
|
path: "a-large.mjs",
|
|
sha256: createHash("sha256").update(text).digest("hex"),
|
|
container_sha256: "1".repeat(64),
|
|
bytes: Buffer.byteLength(text),
|
|
inventory_artifact_id: "parent-admission-fixture",
|
|
kind: "javascript",
|
|
unpacked: false,
|
|
text: { included: true, value: text },
|
|
};
|
|
const worker = new JavaScriptAnalysisWorker({
|
|
heapMb: 1024,
|
|
timeoutMs: 30_000,
|
|
});
|
|
try {
|
|
const result = await worker.analyze(file, {
|
|
nodes: 20_000,
|
|
unknowns: 20_000,
|
|
});
|
|
if (result.ok)
|
|
throw new Error(
|
|
"Expected the parent to decline the large semantic graph",
|
|
);
|
|
const { error, javascript, module, projection } = result.error;
|
|
expect(javascript?.parse_status).toBe("complete");
|
|
expect(module?.callables).toHaveLength(methodsCount);
|
|
expect(projection).toBeNull();
|
|
expect(error).toMatchObject({
|
|
_tag: "AnalysisResourceConstraintError",
|
|
resource: "memory",
|
|
reason:
|
|
"Semantic graph for a-large.mjs exceeds the parent's remaining result budget; static and module facts were retained",
|
|
});
|
|
if (!(error instanceof AnalysisResourceConstraintError))
|
|
throw new Error("Expected a typed resource constraint");
|
|
expect(error.reportedLimits).toMatchObject(
|
|
process.versions.bun === undefined
|
|
? { worker_old_space_mb: 1024 }
|
|
: { worker_heap_limit_bytes: null },
|
|
);
|
|
const limits = error.reportedLimits;
|
|
if (limits === null)
|
|
throw new Error("Parent admission omitted its measured limits");
|
|
expect(limits.parent_result_budget_bytes).toBe(64 * 1024 * 1024);
|
|
const moduleEstimate = limits.module_retention_estimate_bytes;
|
|
const graphEstimate = limits.graph_retention_estimate_bytes;
|
|
const available = limits.parent_available_result_budget_bytes;
|
|
expect(typeof moduleEstimate).toBe("number");
|
|
expect(typeof graphEstimate).toBe("number");
|
|
expect(typeof available).toBe("number");
|
|
if (
|
|
typeof moduleEstimate !== "number" ||
|
|
typeof graphEstimate !== "number" ||
|
|
typeof available !== "number"
|
|
)
|
|
throw new Error(
|
|
"Parent admission omitted its measured transfer estimates",
|
|
);
|
|
expect(moduleEstimate).toBeLessThanOrEqual(available);
|
|
expect(moduleEstimate + graphEstimate).toBeGreaterThan(available);
|
|
} finally {
|
|
await worker.close();
|
|
}
|
|
}
|
|
|
|
it.each([0, 1024 * 1024])(
|
|
"retains module facts and declines only the graph with %i bytes reclaimed between heap observations",
|
|
verifyParentAdmission,
|
|
);
|