From 01ee6551b4a39088cb93546f9f1cf4fe62956519 Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:15:25 +0800 Subject: [PATCH] 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) --- .../analysis/JavaScriptAnalysisWorker.ts | 19 ++++++----- .../javascriptParentResultAdmission.test.ts | 32 +++++++++++++------ 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/javascript/analysis/JavaScriptAnalysisWorker.ts b/src/javascript/analysis/JavaScriptAnalysisWorker.ts index 7f978e2352..ca7a22f2a8 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorker.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorker.ts @@ -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, ), ); diff --git a/tests/boundary/javascript/javascriptParentResultAdmission.test.ts b/tests/boundary/javascript/javascriptParentResultAdmission.test.ts index d1e1f7a017..76835777e7 100644 --- a/tests/boundary/javascript/javascriptParentResultAdmission.test.ts +++ b/tests/boundary/javascript/javascriptParentResultAdmission.test.ts @@ -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(); 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, +);