Skip to content

perf(gc): census-free budgeted cycles + RSS floor (#6179, #6180 Stage 2)#6239

Merged
proggeramlug merged 6 commits into
mainfrom
perf/gc-classifier-valid-ptrs
Jul 10, 2026
Merged

perf(gc): census-free budgeted cycles + RSS floor (#6179, #6180 Stage 2)#6239
proggeramlug merged 6 commits into
mainfrom
perf/gc-classifier-valid-ptrs

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Part of #6179 and the #6180 Stage-2 (default-on) campaign. Three measured steps:

1. Classifier differential-verify mode (groundwork)

PERRY_GC_VERIFY_CLASSIFIER=1 asserts on every positive contains() query against a census-built set that the page-metadata classifier accepts the object — the superset property gating any replacement of the exact set. Verified 1227/0 across the full suite (single-threaded run is authoritative; parallel runs can observe synthetic cross-test heap states). Three principled carve-outs: test-fabricated sets (built_by_census), test-only page-metadata wipes (CLASSIFIER_VERIFY_SUPPRESSED), and FORWARDED headers (censused-then-moved is by-design divergence).

2. Budgeted cycles skip the exact census (#6179)

Budgeted (precise, non-moving) cycles no longer build the O(heap) BTreeSet held across the sliced cycle. Membership resolves via classifier_valid_object_start: the union of exact malloc-registry membership and plausible-arena-header — not the exclusive generation branch of current_heap_header_for_user_ptr, which left live malloc roots unmarked when page metadata attributed their range to an arena generation. Safety anchors: classifier-mode cycles never conservative-scan (traced conservative words can't tolerate heuristic false positives, even if a ManualGcScanGuard appears mid-cycle via drain-before-manual-gc); maybe_contains passes everything through (call sites prefilter separately); synchronous cycles keep the exact set.

3. RSS floor: pacing gain 1/32 + allocator trim on budgeted cycles

Measurement redirect: dropping the census barely moved RSS (929→928 MB on the churn benchmark) — the gap was pacing equilibrium (floating garbage across the debt window) and allocator pages never returned to the OS (audit finding: run_malloc_trim skipped budgeted cycles). With gain doubled and trim enabled:

workload STW RSS incremental RSS before after
B1 large-live-heap + churn 521 MB 935 MB 521 MB — parity
B2 extreme ring churn 524 MB 929 MB 928 MB (gain-insensitive residual, under diagnosis)

B1 also holds the latency win: max frame 131 ms vs STW's 636 ms. Differential battery (± manual gc ± force-evacuate): clean on every run. Full suite 1227/0 with and without verification.

PERRY_GC_INCREMENTAL remains default-off; this stack is the flip prerequisite (no O(heap) census on the default-to-be path) plus the first RSS-parity result.

4. Darwin allocator trim + 5. budgeted sweeps don't age-bump

Two more measured fixes while chasing the churn-benchmark residual: malloc_zone_pressure_relief wired as darwin's malloc_trim counterpart (the budgeted-trim lever was glibc-only), and budgeted sweeps no longer age-bump — whole-cycle allocate-black marks every mid-cycle birth, and the age-bump read MARKED as "survived", tenuring dead churn into old-gen where minors never reclaim it. Promotion under incremental flows through the copied-minor path only.

Final RSS attribution (vmmap, live process)

The churn benchmark's residual 928 MB peak is not a leak: libc malloc ~16 MB, cycle-end arena 203 MB, steady-state footprint 572 MB. The peak is the transient mid-cycle high-water — allocation continuing while a sliced cycle completes — i.e. the structural trade of slicing vs STW's pause wall, bounded by the debt controller. The realistic workload (B1) sits at exact STW parity (521 MB) with max frame 136 ms vs STW's 636 ms.

Summary by CodeRabbit

  • Performance
    • Reduced conservative stack/root scanning during conservative GC phases, improving incremental cycle granularity.
    • Tightened mutator assist pacing and improved arena work budgeting.
    • Skips whole-heap weak-target holder scanning when no weakref holders exist on the current thread.
  • Memory Management
    • Budgeted reclaim cycles now run allocator memory trimming when supported, improving reclamation consistency.
  • Reliability
    • Strengthened GC object-tracking verification for budgeted/census-based modes, with updated telemetry expectations and trace assertions.

Ralph Küpper added 3 commits July 10, 2026 11:57
…et (#6179 groundwork)

PERRY_GC_VERIFY_CLASSIFIER=1 asserts, on every positive contains()
query against a CENSUS-BUILT set, that the page-metadata classifier
(current_heap_header_for_user_ptr) accepts the object - the SUPERSET
property that must hold before the exact BTreeSet can be skipped on
precise cycles. Reverse divergence (classifier accepts, census lacks)
is expected: objects born after the census; safe over-approximation
under allocate-black.

Verification carve-outs, each principled:
- built_by_census: tests fabricating sets bypass verification.
- CLASSIFIER_VERIFY_SUPPRESSED: test-only page-metadata wipes
  (old_arena_page_index_clear_for_tests) make real objects
  unclassifiable; production never clears the index.
- FORWARDED headers: censused-then-moved objects are rejected by the
  plausible-header check BY DESIGN (the copying machinery owns the
  redirect).

Full suite under verification: 1227/0.
Budgeted (precise, non-moving) cycles no longer build the O(heap)
BTreeSet census held across the whole sliced cycle - membership
resolves through classifier_valid_object_start: the UNION of exact
malloc-registry membership and a plausible arena header on an
arena-classified page. Union, not the exclusive generation branch of
current_heap_header_for_user_ptr: malloc allocations can sit inside
ranges the page metadata attributes to an arena generation, and the
exclusive branch left live malloc roots unmarked.

Safety anchors:
- classifier-mode cycles NEVER conservative-scan (traced conservative
  words cannot tolerate heuristic false positives) - even if a
  ManualGcScanGuard appears mid-cycle via drain-before-manual-gc.
- maybe_contains passes everything through in classifier mode (no
  census, no range; call sites prefilter separately).
- Synchronous cycles keep the exact set: they force the conservative
  scan, which requires exactness.
- The census walk still runs for budgeted cycles (evacuation policy
  reads tenured_nursery_bytes); only the set inserts are skipped.

Differentially verified: PERRY_GC_VERIFY_CLASSIFIER=1 across the full
suite asserts the classifier is a superset of the census on every
census-built set. 1227/0 with and without verification.
…ycles (#6180)

Measurement redirect: removing the exact census barely moved RSS (929
vs 921 MB on the churn benchmark) - the ~1.8x gap over the synchronous
collector is pacing equilibrium (floating garbage across the debt
window + allocate-black retention) and allocator pages never returned
to the OS, not the BTreeSet. Two levers:

- GC_ASSIST_DEBT_BYTES_PER_WORK_UNIT 64 -> 32: doubles the pacing gain,
  halving equilibrium debt and with it the per-cycle floating-garbage
  window.
- run_malloc_trim no longer skips budgeted cycles (2026-07-09 audit
  finding): a long-lived incremental process never returned freed
  allocator pages. Trim runs at Reclaim, outside the atomic tail.

Telemetry/reclaim tests updated: budgeted trim outcome is platform-
dependent (executed on glibc, unsupported elsewhere), never the old
ordinary_budgeted skip.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6444059-1f3c-4077-b416-6d5b85ce1c7d

📥 Commits

Reviewing files that changed from the base of the PR and between ab62578 and 6e838b1.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/weakref.rs

📝 Walkthrough

Walkthrough

Budgeted GC cycles now use classifier-based pointer membership, skip conservative stack scanning, perform allocator trimming, avoid minor-cycle age bumps, and adjust assist pacing. Differential classifier verification, weak-target scanning, and related telemetry tests are also updated.

Changes

GC membership classifier

Layer / File(s) Summary
Classifier membership and verification
crates/perry-runtime/src/gc/trace.rs
Valid-pointer sets support classifier mode, heuristic membership checks, census tracking, and optional differential verification.
Budgeted cycle integration
crates/perry-runtime/src/gc/cycle.rs, crates/perry-runtime/src/gc/progress.rs
Budgeted cycles use classifier builders, skip conservative stack scanning, run allocator trimming, disable minor-cycle age bumps, and apply the updated assist pacing constant.
Verification and telemetry updates
crates/perry-runtime/src/arena/page_meta.rs, crates/perry-runtime/src/gc/tests/*
Tests validate budgeted malloc trimming and telemetry, while old-arena test cleanup suppresses classifier verification.
Weak-target scan gating
crates/perry-runtime/src/weakref.rs
Weak-target processing skips the arena walk when no weak-target holders are registered.

Estimated code review effort: 4 (Complex) | ~50 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GC Cycle
  participant ValidPointerSetBuilder
  participant ValidPointerSet
  participant Root Scanner
  participant Allocator Maintenance
  GC Cycle->>ValidPointerSetBuilder: select classifier builder for budgeted cycle
  ValidPointerSetBuilder->>ValidPointerSet: create classifier-mode pointer set
  GC Cycle->>Root Scanner: skip conservative stack scan
  GC Cycle->>Allocator Maintenance: run platform-specific malloc trimming
Loading

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5977: Both changes gate weak-target processing using registered weak-target holder state.
  • PerryTS/perry#6208: Both changes modify weak-target holder tracking and classifier-based weak-target liveness.
  • PerryTS/perry#6226: Both changes modify budgeted GC cycle behavior, assist pacing, and related cycle mechanics.

Suggested labels: run-extended-tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Title matches the main GC performance changes: census-free budgeted cycles and RSS improvements.
Description check ✅ Passed The description covers summary, changes, related issues, and test outcomes, even if it doesn't mirror the template headings exactly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 perf/gc-classifier-valid-ptrs

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.

…aim (#6180)

The budgeted-trim lever only worked on glibc; darwin returned
Unsupported, so macOS incremental processes never returned clean
allocator pages (B2 churn RSS pinned at ~928MB with healthy pacing).
NULL zone = all zones, goal 0 = release everything releasable. Same
placement as glibc malloc_trim: Reclaim, outside the atomic tail.
Tests updated for the platform matrix.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs (1)

559-583: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

test_malloc_trim_call_count() should not stay at 0 on glibc. record_test_malloc_trim_call() wraps the same libc::malloc_trim(0) path that reports status: "executed", so this assertion should expect 1 here or be gated to non-glibc builds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs` around lines
559 - 583, Update the assertions in the budgeted GC test around
complete_budgeted_gc_cycle: since budgeted reclaim now invokes malloc_trim on
glibc, expect test_malloc_trim_call_count() to be 1 there, while gating the
expectation appropriately for non-glibc builds if needed. Keep the trace
assertions consistent with the allocator_maintenance malloc_trim status.
🧹 Nitpick comments (1)
crates/perry-runtime/src/gc/cycle.rs (1)

751-782: 🚀 Performance & Scalability | 🔵 Trivial

Confirm per-cycle malloc_trim(0) frequency is acceptable.

Removing the budgeted skip means every budgeted reclaim now calls libc::malloc_trim(0). Unlike the surrounding reclaim work, malloc_trim is not budget-bounded — glibc walks all arena free-lists and madvise/munmaps, with cost driven by fragmentation. For a long-lived incremental process completing budgeted cycles frequently, running it every cycle can add non-trivial CPU. Consider throttling (e.g., pressure-gated or every N completed cycles) so the RSS floor is preserved without paying full trim cost on every reclaim.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/cycle.rs` around lines 751 - 782, Throttle calls
to malloc_trim(0) from run_malloc_trim instead of executing on every budgeted
reclaim cycle; add pressure-based or periodic gating (such as every N completed
cycles) while retaining immediate trimming for explicit or emergency paths, and
update the related allocator-maintenance status/reason handling and tests to
cover skipped versus executed trims.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs`:
- Around line 559-583: Update the assertions in the budgeted GC test around
complete_budgeted_gc_cycle: since budgeted reclaim now invokes malloc_trim on
glibc, expect test_malloc_trim_call_count() to be 1 there, while gating the
expectation appropriately for non-glibc builds if needed. Keep the trace
assertions consistent with the allocator_maintenance malloc_trim status.

---

Nitpick comments:
In `@crates/perry-runtime/src/gc/cycle.rs`:
- Around line 751-782: Throttle calls to malloc_trim(0) from run_malloc_trim
instead of executing on every budgeted reclaim cycle; add pressure-based or
periodic gating (such as every N completed cycles) while retaining immediate
trimming for explicit or emergency paths, and update the related
allocator-maintenance status/reason handling and tests to cover skipped versus
executed trims.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a1c51ac-3ba0-4fa6-8bef-28e474c3406c

📥 Commits

Reviewing files that changed from the base of the PR and between 12695e6 and 4e711ba.

📒 Files selected for processing (6)
  • crates/perry-runtime/src/arena/page_meta.rs
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/progress.rs
  • crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs
  • crates/perry-runtime/src/gc/tests/telemetry_verifier.rs
  • crates/perry-runtime/src/gc/trace.rs

Ralph Küpper added 2 commits July 10, 2026 12:34
…rvival (#6180)

Whole-cycle allocate-black (#6224 soundness) marks every mid-cycle
birth; the sweep age-bump read MARKED as survived, so dead churn
tenured into old-gen two cycles later where minors never reclaim it.
Measured on the churn benchmark: ~700 MB of tenured garbage (928 MB
RSS vs 524 MB synchronous, identical 203-block arena commitment).
Budgeted sweeps skip aging; promotion under incremental flows through
the copied-minor path, which ages genuinely-surviving objects only.
…ers exist (#6180)

The AtomicFinalize WeakProcessing pass walks the entire arena solely to
FIND weak holders; the #6182 registry already tracks every live one.
Empty registry (the common case) -> the O(heap) pass is a no-op,
removing the single largest atomic-finalize cost for weakref-free
programs. Registry-iteration for the nonzero case is the tracked
refinement.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/gc/cycle.rs (1)

565-573: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Record whether the conservative scan actually ran.

If a ManualGcScanGuard appears during a classifier cycle, this branch skips scanning, but Lines 1214-1219 may still see Scan and bypass block persistence. Register-held recent objects can then be swept while live.

Make the block-persistence decision depend on an actual-scan flag, or never skip persistence when valid_ptrs.classifier_mode is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/cycle.rs` around lines 565 - 573, Ensure
classifier-mode cycles do not bypass block persistence when conservative
scanning is skipped. Update the root-scan state around the classifier-mode
branch in the cycle collector to record whether a scan actually ran, then make
the persistence decision near the Scan handling depend on that flag;
alternatively, always preserve blocks when valid_ptrs.classifier_mode is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/perry-runtime/src/gc/cycle.rs`:
- Around line 565-573: Ensure classifier-mode cycles do not bypass block
persistence when conservative scanning is skipped. Update the root-scan state
around the classifier-mode branch in the cycle collector to record whether a
scan actually ran, then make the persistence decision near the Scan handling
depend on that flag; alternatively, always preserve blocks when
valid_ptrs.classifier_mode is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: df8a0d5b-43b1-42bd-bc35-a2cc2d14ff67

📥 Commits

Reviewing files that changed from the base of the PR and between 4e711ba and ab62578.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs
  • crates/perry-runtime/src/gc/tests/telemetry_verifier.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs
  • crates/perry-runtime/src/gc/tests/telemetry_verifier.rs

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