Repository navigation
memory(gc): untraced promotion can retain an old-heap-sized phase-change cohort as live #7902
Description
Summary
The untraced whole-block promotion path charges retained garbage from the previous cycle's survival ratio, not from the liveness of the bytes it is currently promoting. After a live-to-dead phase change, a prior 1000‰ measurement makes implied_dead = 0, so the advertised 32 MiB PROMOTED_DEAD_BUDGET_BYTES is not charged while the collector blindly promotes the new dead cohort.
The separate stale-estimate budget allows max(128 MiB, old-gen-at-last-measurement) of such promotion before forcing a traced cycle. On a large live heap that can be hundreds of MiB or GiB. The eventual traced cycle measures only its current young cohort; it cannot recover or retroactively charge garbage already promoted by the preceding untraced cycles. Those bytes remain in old-gen until a full collection.
This is a bounded-policy design, not an immediate memory-safety bug, but the bound is much larger than the comments/changelog's 32 MiB footprint claim and grows with old-gen size.
Phase-change sequence
- A traced copying minor observes 1000‰ young survival.
- The application changes phase and begins allocating/droping short-lived cohorts.
- The next promotion(s) satisfy the stale 1000‰ predictor and skip tracing.
- Every parseable young object is promoted and accounted as live, including objects that are now unreachable.
note_untraced_promotion()computes zero implied dead bytes from the stale 1000‰ value, soPROMOTED_DEAD_BYTESremains unchanged.- Only after
UNTRACED_PROMOTED_BYTESreachesmax(128 MiB, old-gen-at-last-measurement)does a traced promoting cycle run. It updates the future predictor but cannot identify/reclaim the earlier promoted garbage. credit_promoted_bytes_to_old_baseline()and live-census publication treat all untraced promoted bytes as live, which can defer the full collection needed to reclaim them.
Evidence
- The untraced threshold is based solely on the last measured ratio.
- The stale-estimate allowance is
max(128 MiB, old-gen-at-last-measurement). note_untraced_promotion()charges dead bytes by extrapolating the previous ratio. A prior 1000‰ value charges zero regardless of current liveness.- An untraced finish explicitly uses
PromotionLiveness::AssumeAllLive. - Every object is then counted/indexed as live.
- All promoted bytes are credited to the old-reclaim baseline as “live by construction”, but that premise is false for
AssumeAllLivepromotions. - The focused unit test validates stationary extrapolation at 990‰ and that the stale-estimate budget eventually forces measurement; it does not assert a retained-byte bound across a 1000‰→0‰ phase change.
Impact
- A phase-changing server can park up to the old generation's prior live size again as dead-but-accounted-live memory.
- Major pacing and old-reclaim pacing are biased toward retaining it because the promoted delta is credited as clean/live.
- On device-constrained heaps, the fixed 128 MiB floor alone is larger than some intended heap budgets.
- Telemetry reports the assumed-live promoted bytes as
heapUseduntil a full census/reclaim, so the policy cannot distinguish confidence from measured liveness.
Acceptance criteria
- State and enforce a worst-case retained-garbage bound for untraced promotion under abrupt phase changes, not only stationary ratios.
- Scale the untraced allowance to the device heap budget; do not retain a 128 MiB minimum on a smaller configured heap.
- Do not credit
AssumeAllLivebytes to the clean old-reclaim baseline as equivalent to marked-live promotions, or track them in a separate uncertain/debt class. - When the forced measuring cycle contradicts the predictor, schedule/recover the previously assumed-live cohort promptly rather than merely changing the next-cycle decision.
- Add an end-to-end
1000‰ live phase -> dead churn phaseratchet that asserts old-gen growth, full-GC timing, RSS, andheapUsedremain within the documented bound. - Add policy tests at a large old-gen baseline and at a small
PERRY_GC_HEAP_LIMIT.
Audit context
Found while re-auditing the v0.5.1473→v0.5.1481 GC delta, specifically #7888 and #7895. No heavy benchmark was run for this audit; the finding follows from the policy/state transitions and existing source tests.
Activity
perf(codegen): the PIC miss block re-derived the whole receiver ladde…
fix(gc): bound and heap-scale the untraced-promotion budget (#7902) (#…
fix(gc): credit every promotion to the old-reclaim baseline, untraced…