perf(gc): let a tiny-parse boundary collect when the young generation reaches its cap - #10254
proggeramlug wants to merge 3 commits into
Conversation
… reaches its cap The tiny-parse guard prices forced collections on total arena in-use (#9831), which a collection cannot lower below the live set. It never asked the generational question of whether the young generation is at its scavenge cap, and a loop of tiny JSON.parse calls allocates only inside the parser's suppression window and at bounded inline-object births, so nothing else arms the nursery safepoint for it. The young generation grew to the guard's 48 MB floor before its first minor: small_record:parse peaked at 80 MiB against Node's 59 with 0-9 permille of each collection surviving, and object_1k:parse at 65 against 61. The boundary now also collects when the young scavenge cap is due. A minor lowers that quantity to the survivors, so unlike the absolute in-use guard it cannot re-fire until the cap has been refilled.
…ary touched gc/policy.rs
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe runtime now checks the young-generation scavenge cap at tiny-parse boundaries. Generational collection uses arena pressure or cap due-ness. Full-GC behavior remains pressure-only. Tests cover scheduling below the in-use guard. ChangesTiny-parse nursery-cap collection
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~12 minutes Change: Bug fix Sequence Diagram(s)sequenceDiagram
participant ParseBoundary
participant CollectionScheduler
participant GenerationalPolicy
participant NurseryCap
participant PendingFlag
ParseBoundary->>CollectionScheduler: Check boundary pressure
CollectionScheduler->>GenerationalPolicy: Evaluate arena pressure and nursery cap
GenerationalPolicy->>NurseryCap: Check cap due-ness
NurseryCap-->>GenerationalPolicy: Return cap status
GenerationalPolicy-->>CollectionScheduler: Return collection decision
CollectionScheduler->>PendingFlag: Set pending when collection is due
Merge Risk: ⚪ Minimal · up to The change adds nursery-cap collection scheduling for generational tiny-parse boundaries and includes targeted coverage; no actionable merge risk is established. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Description checkExplanation The description provides a detailed summary, rationale, implementation details, benchmark results, and validation results. However, it does not follow the repository template: it omits the required Summary, Changes, Related issue, Test plan, Screenshots / output, and Checklist headings and does not state whether the checklist items are complete. Resolution Restructure the description to use the repository template. Add the required headings, list the concrete changes, specify a related issue or use “n/a,” convert validation into the Test plan section, include relevant command results, and complete the Checklist items. Preserve the existing technical details under the appropriate sections.
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
A loop of small
JSON.parsecalls let the young generation grow to the tiny-parse guard's 48 MB floor before its first minor collection. This PR lets the parse boundary also collect when the young generation reaches its scavenge cap.Problem
The tiny-parse guard (#9831, priced in #9838) forces a collection at a parse boundary when total arena in-use reaches 48 MB. Total in-use cannot fall below the live set, so that guard is deliberately absolute and coarse. It never asks the generational question of whether the young generation is at its scavenge cap.
A loop of tiny
JSON.parsecalls allocates only inside the parser's suppression window and at bounded inline-object births, so nothing else arms the nursery safepoint for it. The young generation therefore filled to the 48 MB floor before its first minor collection, while each collection found 0–9 permille of it surviving.small_record:parsepeaked at 80 MiB against Node's 59 MiB.object_1k:parsepeaked at 65 MiB against Node's 61 MiB.Change
gc/policy.rsaddstiny_parse_generational_collection_due(in_use, trigger), which is the existing in-use guard ORyoung_scavenge_cap_due(). It is used at the three generational decision points of the tiny-parse path:gc_bump_malloc_trigger_inner(generational branch)gc_collect_pending_suppressed_parse_slow(generational branch)gc_schedule_parse_boundary_collection_if_pressureA minor lowers the young generation to its survivors, so unlike the absolute in-use guard this arm cannot re-fire until the cap has been refilled. Full-GC mode keeps the in-use guard only.
Test:
gc/tests/tiny_parse_pressure.rs::a_due_nursery_cap_schedules_the_boundary_collection_below_the_in_use_guard.Measured
Quiet bench mini (Apple M1), best of 3 interleaved rounds per engine,
/usr/bin/time -l, Node 26.5.1 and Bun 1.3.14 measured in the same run. Both Perry arms carry #10220, #10241 and #10249; the only difference between them is this commit.small_record:parseCPU ms / peak MiBobject_1k:parseCPU ms / peak MiBNo other row of the 50-row JSON matrix moved beyond noise. The only other differences over 3 % are 1.0 against 1.1 ms on the two 1 MB string parse rows, which is timer resolution, and
long_string_1m:stringifyat −8.5 %.Validation
eb13fa188d(main) against this head, 7 repeats each, same host: all 14 probes pass correctness, and every gated counter is identical on all 14. Walls moved in both directions on a loaded laptop, which is noise.RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib: 3797 passed, 1 failed. The failure isheap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds, a debug-assertion test that fails in every release-profile run, on main as well../scripts/run_lint_gates.sh: the runtime GC-pointer holder custody audit failed because this change touchedgc/policy.rs, a pinned source of thePASS1_MARKEDcensus window. The window was re-audited and re-pinned in725f8a28aa, and the audit and its self-test now pass. The remaining reds are the same as on main: public benchmark evidence freshness, the rustc warnings tier, and the API-docs drift check, which fails on the gate's own truncation ofdocs/src/api/reference.md.young_scavenge_cap_due(), the same predicategc_budgeted_due_triggeralready uses to start aYoungScavengeCapminor at every precise safepoint. In a program with ordinary safepoints, which claude-code has, the arm can only move that minor earlier to the parse boundary. It cannot add a collection the safepoint path would not already run. Only a loop that allocates solely inside the parser, like these two rows, gains minors it had none of.Summary by CodeRabbit
Performance
Bug Fixes