Repository navigation
memory(gc): heapUsed and major-GC pacing use block high-water, not live bytes #7879
Copy link
Copy link
Closed
Labels
bugConfirmed defect or regressionConfirmed defect or regressionperformanceRuntime, compile-time, build-size, or memory performanceRuntime, compile-time, build-size, or memory performance
Description
Summary
Perry currently uses arena bump offsets as a proxy for live heap bytes in both public memory telemetry and major-GC pacing. That proxy includes tail slack and dead/reusable holes, and the two implementations do not even apply the same correction:
js_arena_stats()sums committed block offsets and subtracts only old-generation free-list bytes.arena_in_use_bytes()sums block offsets without subtracting old-generation free-list bytes, despite its documentation and policy call sites describing the result as live/in-use bytes.
As a result, a tiny survivor can account for an entire 1 MiB block, process.memoryUsage().heapUsed can substantially overstate live memory, and full-GC pacing decisions can be driven by fragmentation/high-water placement rather than what a collection actually failed to reclaim.
Evidence
js_arena_stats()sums block offsets and only subtractsold_free_bytes.arena_in_use_bytes()sums the same offsets without subtracting reusable old-generation holes.- Major-GC policy treats the result as live/in-use bytes, including post-full measurements used to decide when another full collection is warranted.
process.memoryUsage()exposesjs_arena_stats().used_bytesasheapUsed; the same arena statistics also feed V8-compatible heap APIs and process reports.- The ratchet documentation explicitly records the 1 MiB granularity artifact: one retained object can keep a full block represented in
heap_used_bytes, producing a sawtooth even with a near-zero live set. - The remaining amplification was acknowledged when gc: 05_closure_capture retains +16.44% with every collector counter at +0.00% — surfaced when the ratchet started measuring again #7559 was closed; that issue removed a conservative-scan trigger rather than fixing the accounting model.
- gc-ratchet: 7 gating rows red on main since 0.5.1451 — and 04_dead_after_deep_stack / 11_collect_at_depth are NOT among them #7843 currently reports several block-sized
heap_used_bytesratchet regressions. This issue does not assert the accounting model caused those regressions, but it makes the signal and the collector policy unusually sensitive to block-boundary changes.
Impact
- Public Node/V8-compatible telemetry gives users a misleading measure of live JS heap usage.
- Major/full collection pacing can compare block occupancy/fragmentation while naming and reasoning about it as live bytes.
- Ratchet failures can be quantized by block placement, obscuring small real leaks and exaggerating boundary crossings.
- The policy metric and public metric disagree about old-generation reusable holes, making diagnosis harder.
Proposed direction / acceptance criteria
- Define distinct, documented quantities for at least reserved/committed bytes, allocation high-water, reusable bytes, and live allocated bytes.
- Maintain exact or bounded live/reusable byte counters per generation, or perform a post-collection census where exactness is required.
- Make major-GC pacing compare one deliberately chosen semantic quantity before and after collection; rename it if occupancy rather than liveness is intentional.
- Make
process.memoryUsage().heapUsedand compatible V8 APIs report a tested live-allocation quantity rather than whole-block high-water. - Gate the ratchet on the precise live metric while retaining reserved/high-water metrics as separate fragmentation diagnostics.
- Add a regression test showing that retaining one tiny object does not add an otherwise empty 1 MiB block to reported
heapUsed. - Cross-check the new counters against a heap walk/snapshot in stress tests, including general free-list and old-generation free-list reuse.
Audit context
Found during the August 2026 GC/performance audit of the last 200 origin/main commits. This is a source/accounting finding; no heavy benchmark or compilation was run.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugConfirmed defect or regressionConfirmed defect or regressionperformanceRuntime, compile-time, build-size, or memory performanceRuntime, compile-time, build-size, or memory performance
Activity
fix(gc): report live arena bytes after collection (#7879) (#7886)
proggeramlug commentedon Aug 12, 2026
Re-audit note after #7886: the direct post-sweep high-water-as-live defect is fixed. #7901 tracks a separate cross-mode accounting error in the replacement census: a copied minor subtracts from-space bump high-water (including dead holes already excluded by the exact census), which can undercount unrelated old live bytes. Keeping this issue closed is appropriate; the regression sequence belongs in #7901.