fix(ci): use one heap snapshot for JavaScript admission (#6081)

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)
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-22 00:15:25 +08:00
1 parent d8a9673fc4
commit 01ee6551b4
2 files changed
+31 -20

No files matched your search

@@ -218,6 +218,12 @@ export class JavaScriptAnalysisWorker {
return true;
}
/** Keep the limit and usage in one V8 observation for every admission path. */
#remainingParentHeapBytes(): number {
const { heap_size_limit, used_heap_size } = getHeapStatistics();
return heap_size_limit - used_heap_size;
}
/**
* Reserve host-side control, schema/Evidence work, and a maximum record's
* transient copies. Admission also accounts for retained earlier files.
@@ -226,8 +232,7 @@ export class JavaScriptAnalysisWorker {
this.#resultBudgetBytes = Math.max(
0,
Math.floor(
(getHeapStatistics().heap_size_limit -
process.memoryUsage().heapUsed -
(this.#remainingParentHeapBytes() -
24 * JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES) /
2,
),
@@ -674,12 +679,7 @@ export class JavaScriptAnalysisWorker {
OPERATION,
"JavaScript static checkpoint byte count exceeds or differs from its declared budget",
);
if (
bytes * 16 + 4 * MIB >
getHeapStatistics().heap_size_limit -
process.memoryUsage().heapUsed -
64 * MIB
)
if (bytes * 16 + 4 * MIB > this.#remainingParentHeapBytes() - 64 * MIB)
throw new AnalysisResourceConstraintError(
OPERATION,
"memory",
@@ -778,8 +778,7 @@ export class JavaScriptAnalysisWorker {
0,
Math.min(
this.#resultBudgetBytes - this.#estimatedRetainedBytes,
getHeapStatistics().heap_size_limit -
process.memoryUsage().heapUsed -
this.#remainingParentHeapBytes() -
24 * JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES,
),
);
@@ -1,6 +1,8 @@
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.
@@ -8,10 +10,17 @@ vi.mock("node:v8", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:v8")>();
return {
...actual,
getHeapStatistics: () => ({
...actual.getHeapStatistics(),
heap_size_limit: process.memoryUsage().heapUsed + 320 * 1024 * 1024,
}),
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,
};
},
};
});
@@ -24,7 +33,8 @@ afterAll(() => {
vi.resetModules();
});
it("retains module facts and declines only the graph when its real transfer exceeds parent admission", async () => {
async function verifyParentAdmission(reclaimedBytes: number) {
heapObservation.reclaimedBytes = reclaimedBytes;
const methodsCount = 600;
const text = Array.from(
{ length: methodsCount },
@@ -74,10 +84,7 @@ it("retains module facts and declines only the graph when its real transfer exce
const limits = error.reportedLimits;
if (limits === null)
throw new Error("Parent admission omitted its measured limits");
expect(limits.parent_result_budget_bytes).toBeLessThanOrEqual(
64 * 1024 * 1024,
);
expect(limits.parent_result_budget_bytes).toBeGreaterThan(0);
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;
@@ -97,4 +104,9 @@ it("retains module facts and declines only the graph when its real transfer exce
} 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,
);