Skip to content

perf(gc): let a tiny-parse boundary collect when the young generation reaches its cap - #10254

Closed
proggeramlug wants to merge 3 commits into
mainfrom
gc/tiny-parse-nursery-cap
Closed

proggeramlug wants to merge 3 commits into
mainfrom
gc/tiny-parse-nursery-cap

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

A loop of small JSON.parse calls 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.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 therefore filled to the 48 MB floor before its first minor collection, while each collection found 0–9 permille of it surviving.

  • small_record:parse peaked at 80 MiB against Node's 59 MiB.
  • object_1k:parse peaked at 65 MiB against Node's 61 MiB.

Change

gc/policy.rs adds tiny_parse_generational_collection_due(in_use, trigger), which is the existing in-use guard OR young_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_pressure

A 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.

row without with best of Node/Bun
small_record:parse CPU ms / peak MiB 155.3 / 80 152.7 / 32 390.2 / 59
object_1k:parse CPU ms / peak MiB 160.2 / 65 157.5 / 32 446.4 / 61

No 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:stringify at −8.5 %.

Validation

  • gc-ratchet, 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 is heap_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 touched gc/policy.rs, a pinned source of the PASS1_MARKED census window. The window was re-audited and re-pinned in 725f8a28aa, 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 of docs/src/api/reference.md.
  • Not re-run: the claude-code streaming rig that fix(gc): price the tiny-parse pressure guard by the productivity backoff (#9831) #9838 tuned the tiny-parse guard on. The risk that rig exists for is a collection storm like gc: the ArenaBytes productivity backoff is computed and then discarded above the trigger ceiling #9831's, where an absolute in-use test stayed true after every collection. This arm cannot cause one. It consults young_scavenge_cap_due(), the same predicate gc_budgeted_due_trigger already uses to start a YoungScavengeCap minor 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

    • Improved memory behavior for repeated small JSON parsing operations by triggering collection earlier when the young-generation capacity is reached.
    • Reduced peak memory usage in affected workloads without changing CPU performance.
  • Bug Fixes

    • Ensured parse-boundary garbage collection is scheduled when nursery capacity is reached, even below the usual allocation threshold.

Ralph Küpper added 2 commits September 14, 2026 06:16
… 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.
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 3d685c1b-ce01-43e5-b657-bfa6e29bab20

📥 Commits

Reviewing files that changed from the base of the PR and between eb13fa1 and 0ae752b.

📒 Files selected for processing (4)
  • changelog.d/10254-tiny-parse-nursery-cap.md
  • crates/perry-runtime/src/gc/policy.rs
  • crates/perry-runtime/src/gc/tests/tiny_parse_pressure.rs
  • scripts/gc_runtime_root_holders.json

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Tiny-parse nursery-cap collection

Layer / File(s) Summary
Generational collection policy
crates/perry-runtime/src/gc/policy.rs, changelog.d/10254-tiny-parse-nursery-cap.md, scripts/gc_runtime_root_holders.json
Generational tiny-parse checks now include the nursery scavenge cap for post-parse, deferred, and scheduled boundaries. Full-GC checks remain arena-pressure-only. The changelog and holder audit metadata record the change.
Nursery-cap scheduling test
crates/perry-runtime/src/gc/tests/tiny_parse_pressure.rs
The test verifies that a due nursery cap schedules boundary collection even when the in-use pressure guard is not due. It also verifies the pending flag remains clear before the cap is due.

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
Loading

Merge Risk: ⚪ Minimal · up to 0ae75

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)

Check name Status Explanation Resolution
Description check ⚠️ Warning 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 Su… 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 com…
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (2 skipped: 2 u…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: allowing tiny-parse boundaries to collect when the young generation reaches its cap.
Full details: Description check

Explanation

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.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gc/tiny-parse-nursery-cap

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed in merge train #10261: #10261. The merged main tree matches the validated train, and the fresh-head patch audit confirms the changes arrived.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant