Skip to content

gc: make a synchronous full cheaper per live object (#10182) [pacing retry does not meet acceptance] - #10220

Closed
proggeramlug wants to merge 11 commits into
mainfrom
gc/full-throughput
Closed

proggeramlug wants to merge 11 commits into
mainfrom
gc/full-throughput

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Draft for #10182, stacked on #10217 (base gc/block-granular-sweep). It makes a synchronous full collection cheaper per live object with three exact changes. The pacing retry that was the point of the cheaper full still does not meet acceptance, so no pacing change is included.

What changed

A. Census membership from per-block object-start bitmaps (gc/trace.rs)

ValidPointerSet::contains used to do two binary searches: one over the first key of every census run, then one inside the run. On the probe below that was 1,141 of 2,196 mark samples.

The census now opens one entry per arena block it walks (address order) and records each start as a bit:

  • one bit per 8-byte alignment unit of the walked extent;
  • storage in 8 KiB chunks, one or two per block;
  • oversized blocks (extent > 1 MiB) keep a sorted start list.

contains(ptr) is now: range check, a binary search over the block fences, then one bit test. The hit still names its block for #10217's reachability record.

enclosing_object (conservative words) takes the floor by scanning the bitmap backwards.

Kept as they were: malloc-tracked objects (B-tree), classifier_mode, and the PERRY_GC_VERIFY_CLASSIFIER differential check. The census asserts the alignment contract for every start it records.

Chunking is load-bearing. A single contiguous bitmap vector grew into mimalloc's large-page class, and records_array_1m:sparse read +4.8 MiB peak RSS (68 → 73, 7/7 runs) for a ~100 KB index. With chunks it reads 68 again.

B. No remembered-set rebuild when nothing young is live (gc/verify.rs, gc/cycle.rs, gc/trace/block_skip.rs)

The rebuild runs in AtomicFinalize, before the sweep. It remembers a slot of a marked old parent when the child is nursery-classified or a registered malloc object.

So if both of these hold, it can only produce an empty set:

  • no young object is marked or pinned after the mark: every in-use young block is censused, unreached, unchanged since the census, and had no pre-marked or pinned header;
  • the malloc registry is empty (the copying minor's skip_remembering premise).

In that case the full installs an empty sticky set without walking. The pre-cycle dirty-coverage repair in reclaim is unchanged. Diag: [gc-remembered-rebuild] full skipped=young_generation_unmarked. Counter: FULL_REMEMBERED_REBUILDS_SKIPPED, classified in the holder inventory.

C. Sweep page accounting once per page (gc/oldgen/sweep_objects.rs, arena/page_meta/sweep_tally.rs)

old_page_account_swept_object allocated an overlap Vec and did a page-meta hash lookup per swept old object.

The sweep now sums consecutive single-page objects and applies the sum once per page. Multi-page objects keep the old call. The tally is always applied before a page-index flush (step end, or the 4,096-entry auto-flush), because that flush zeroes the accounting of a page it empties. That keeps the order of sums and resets identical on every page.

Cost of one full, before and after each part

Probe scratchpad/ft/fullprobe.ts: one parsed records_array_20m.json tree (585k objects), promoted to old gen by churn-driven minors, then gc() six times. Last three fulls, ms; two interleaved runs per arm agree within 1 ms.

phase base (#10217 head) +A +A+B +A+B+C
census (build_valid_pointer_set) 4.8 4.8 4.9 5.0
mark (trace_worklist) 42.8 24.4 24.7 24.9
remembered-set rebuild (atomic_finalize) 20.7 20.6 0 0
sweep 13.0 12.8 12.8 7.3
pause 82.5 63.7 43.5 38.0
peak footprint (MiB) 129.4 118.7 118.7 118.7

Same probe with the tree still young at each full (parse then gc()): pause 63–68 → 46–51 ms, mark 42–44 → 25–26 ms. The rebuild (6.4 ms) and census (8 ms) are unchanged there, because the live tree is young.

Tests

RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib:

failures:
    gc::tests::heap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds

test result: FAILED. 3763 passed; 1 failed; 4 ignored; 0 measured; 0 filtered out; finished in 12.06s

That test asserts a #[cfg(debug_assertions)] panic, so it cannot pass under --release. It fails on base as well.

New tests. Each protective case has a sabotaged twin behind a #[cfg(test)] switch that shows the harm:

test gc::tests::start_bitmap::start_bitmap_membership_and_floors_match_an_independent_arena_walk ... ok
test gc::tests::start_bitmap::sabotaged_bitmap_probe_is_caught_by_the_oracle ... ok
test gc::tests::start_bitmap::a_full_collection_marks_through_the_bitmap_and_the_sorted_list ... ok
test gc::tests::full_rebuild_skip::a_full_with_no_live_young_object_skips_the_rebuild ... ok
test gc::tests::full_rebuild_skip::a_live_young_child_keeps_the_rebuild_and_its_edge ... ok
test gc::tests::full_rebuild_skip::sabotaged_skip_loses_an_unbarriered_young_edge ... ok
test gc::tests::full_rebuild_skip::a_live_malloc_child_keeps_the_rebuild_and_its_edge ... ok
test gc::tests::full_rebuild_skip::sabotaged_skip_loses_an_unbarriered_malloc_edge ... ok
test gc::tests::sweep_page_tally::full_sweep_page_accounting_matches_the_planted_liveness ... ok
test gc::tests::sweep_page_tally::sabotaged_tally_order_is_caught_by_the_oracle ... ok
  • start_bitmap: every address (step 4) of every census block is compared with an independent arena walk. The population covers word boundaries, a 600 KB object (the floor search crosses zero words) and an oversized sorted block. Sabotage shifts every bit probe by one unit.
  • full_rebuild_skip: each edge is planted with a raw store and no barrier, so only the rebuild can recover it. Sabotage forces the skip, and the known-live parent's edge is then unremembered. The guard for each child kind (young, malloc) is what prevents the skip in its own case.
  • sweep_page_tally: every planted page's accounting is compared with an oracle computed from each object's known fate. The population has live, pinned and dead objects, multi-page objects, whole dead pages inside live blocks, and more than 4,096 dead objects. Sabotage applies the tally after the page-index flush.
  • Adapted:
    • valid_pointer_membership_spans_every_census_run_including_the_partial_one_7646 now checks block fences instead of run fences, and keeps both membership directions.
    • test_minor_skips_whole_heap_old_to_young_rebuild pins one young object across its full, so that full still walks the old generation.

Gates

scripts/run_lint_gates.sh: exactly main's three reds.

  • Script tier: run_lint_gates: 1 of 77 FAILED (compile tier SKIPPED); 2 CI-only skipped. The failure is [Public benchmark evidence freshness] python3 benchmarks/ci_public_baseline_check.py.
  • Compile tier, run command by command:
    • -D warnings product: ok.
    • -D warnings host-compatible, all targets: FAIL, only global_this_webassembly.rs:201/662 (pre-existing). Because that failure stops the build, cargo check -p perry-runtime --all-targets was run separately; its only warnings are the same file's lines 192/201/662.
    • Clippy product: ok.
    • Clippy host-compatible: ok.
    • API docs regen: ok.
    • API docs drift: FAIL (pre-existing, docs/api/perry.d.ts +10, reference.md +13). Both files restored.

Other checks:

  • scripts/gc_runtime_root_holders.py: OK — 1425 holder declarations scanned. census.rs:PASS1_MARKED is re-pinned for gc/cycle.rs with a dated re-audit: the provably-empty branch sits inside the window and adds no GC allocation, relocation, collection or callback.
  • scripts/check_file_size.sh: OK.
  • scripts/check_gc_doc_claims.py: OK. garbage-collector.md gains a paragraph with gc-symbol markers to the new tests.

Seeded stress (PERRY_GC_SCHEDULE_SEED=1..4 RATE=0.2 PROTECT_FROMSPACE=1 DEPTH=32):

  • Worker cells (1m scan and roundtrip; 8m scan, parse and sparse; object_8m parse; 20m parse and roundtrip): identical checksum and VERIFY hash for seeds 0–4. 1–6 retired_set=# per seeded run, except 8m:sparse, which runs no copying minor and shows 0.
  • Full probe (20m, old and young modes): identical output for seeds 0–4. Old mode takes 6 rebuild skips per run; young mode takes 143 block skips per run. The schedule adds no collection to this probe: retired_set 0 and identical minor/full counts. So this probe exercises the new full paths but not the fuzzing.
  • test_gap_gc*: 44 tests ran with the instrument live; 43 are identical to Node and across seeds 1–4.

PERRY_GC_VERIFY_EVACUATION=1: exit 0, no stderr, VERIFY hash equal to the unverified run, on:

  • worker 8m scan, parse and sparse; 20m parse and roundtrip; 1m scan and roundtrip; object_8m parse;
  • the same eight cells with the k=1 pacing switch;
  • the full probe in both modes.

gc-ratchet

7 repeats, base and head back to back.

  • Gated counters: identical medians on all 14 probes (minor_cycles, step_cycles, copied_*, promoted_*, freed_bytes, heap_used_bytes).
  • Other metrics: only RSS and wall move. Peak RSS is −0.5 % to −5.6 % on 10 probes, equal on 04, and +0.3 to +0.7 % on 05, 07 and 14. Wall is noise: an earlier +26 % on probe 13 was host load and re-measured 757 → 755 ms.
  • check --profile shared_ci: red on base and head with the same 30 gated rows and identical values.
  • Rebuild skip is live on probes 04, 05, 06, 08, 10, 13 and 14 (one skip each, plain PERRY_GC_DIAG=1 runs).

JSON matrix: 22 rows, interleaved, best of 3, same tree, cpu ms / peak MiB

cell base (#10217 head) head node/bun best
records_array_16k:scan 156.0 / 33 156.8 / 33 168.6 / 62
records_array_1m:parse 176.3 / 68 171.0 / 67 416.1 / 92
records_array_1m:roundtrip 181.7 / 62 180.8 / 61 395.7 / 97
records_array_1m:scan 189.4 / 67 188.3 / 66 169.6 / 84
records_array_1m:sparse 165.9 / 68 165.5 / 68 399.0 / 98
records_array_1m:stringify 161.9 / 63 162.7 / 62 210.1 / 105
records_array_20m:parse 141.9 / 240 142.7 / 240 208.0 / 220
records_array_20m:roundtrip 103.0 / 260 101.7 / 260 162.1 / 261
records_array_20m:scan 152.2 / 240 150.0 / 240 212.0 / 225
records_array_20m:sparse 144.3 / 240 143.3 / 240 208.4 / 220
records_array_20m:stringify 154.5 / 198 154.0 / 198 219.4 / 373
records_array_8m:parse 138.0 / 109 134.0 / 109 375.0 / 170
records_array_8m:roundtrip 150.2 / 129 148.1 / 129 343.1 / 182
records_array_8m:scan 177.0 / 189 175.8 / 189 188.7 / 110
records_array_8m:sparse 138.7 / 109 138.9 / 109 345.0 / 198
records_array_8m:stringify 151.4 / 123 150.7 / 123 222.1 / 197
records_object_20m:parse 143.6 / 240 141.2 / 240 207.4 / 220
records_object_20m:stringify 154.9 / 198 154.5 / 198 219.6 / 373
records_object_8m:parse 186.2 / 118 185.6 / 118 234.9 / 112
records_object_8m:stringify 150.9 / 123 150.3 / 123 223.9 / 197
small_record:parse 164.0 / 80 164.4 / 80 409.0 / 60
wide_1m:parse 176.0 / 87 175.4 / 86 306.0 / 96

Every row is within ±2 % CPU and none is worse on RSS. The per-part runs (A alone, A+B) also stayed inside ±2 % after 7-rep rechecks of their outliers.

Target rows have the same regime on base and head (PERRY_GC_DIAG=1). None runs a full, so the target rows cannot move on their own.

row minors fulls
8m:scan 3 (2 untraced) 0
20m parse / scan / sparse, object_20m:parse 2 (1 untraced) 0
object_8m:parse 5 evacuating 0
20m:roundtrip 2 (1 untraced) 0

Pacing retry: measured, not shipped

Same cohort bound as #10217, applied through a temporary local switch that is not in any pushed commit: old-reclaim is due when bytes promoted since the last full reach max(floor, k × old live at last full). Interleaved best of 3, same tree; cpu ms / peak MiB:

row base head k=2, floor 64 MB k=1, floor 16 MB k=0.5, floor 0 best
8m:scan 179.4 / 189 175.0 / 189 235.0 / 211 264.2 / 125 263.0 / 132 188.7 / 110
20m:parse 142.9 / 240 142.3 / 240 260.7 / 256 267.4 / 179 362.7 / 258 208.0 / 220
20m:scan 150.3 / 240 149.6 / 240 265.3 / 256 274.5 / 179 369.7 / 258 212.0 / 225
20m:sparse 143.2 / 240 142.0 / 240 259.7 / 256 265.7 / 179 361.8 / 258 208.4 / 220
object_20m:parse 143.2 / 240 141.3 / 240 255.0 / 256 267.8 / 179 359.0 / 257 207.4 / 220
object_8m:parse 185.3 / 118 184.9 / 118 184.7 / 118 185.4 / 118 227.0 / 119 234.9 / 112
20m:roundtrip 101.7 / 260 101.2 / 260 194.0 / 253 101.9 / 261 191.4 / 256 162.1 / 261

For comparison, #10217 measured k=1 on the same rows at 337.8–354.2 ms / 194–196 MiB. The cheaper full buys 68–86 ms and 15–17 MiB per row, but not the bar.

Regime. Every full is alloc_point_old_reclaim (OldGenBytes) behind the forced conservative scan. Per-full pause ms [census, mark, rebuild, sweep]:

row k=2 / 64 MB k=1 / 16 MB k=0.5 / 0
8m:scan 3 minors, 1 full: 54 [18, 11, skipped, 10] 4 minors (3 untraced), 3 fulls: 31 [4, 10, 9, 7], 29 and 32 with the rebuild skipped 5 fulls: two of 2 ms, then 31, 29, 31
20m parse / scan / sparse, object_20m:parse 1 full: 119–123 [24, 56, skipped, 24–27] (it marks two trees) 3 minors (2 untraced), 2 fulls: 68 [10, 25, 21, 10] then 62 [15, 26, skipped, 13] 4 fulls: 2, 2, 67, 63
object_8m:parse no full no full 3 fulls: 2, 2, 29
20m:roundtrip 1 full: 63 1 full: 42 5 fulls: 2, 2, 8, 12, 14

Why no row makes it on both axes:

  • 8m:scan: the CPU lead is 13.7 ms over 8 iterations. A full over its 12 MB tree costs ~30 ms, and reaching 110 MiB needs about three of them. The CPU axis alone rules out any pacing here.
  • 20m parse / scan / sparse, object_20m:parse: k=1 reaches RSS parity (179 MiB). The two fulls cost ~130 ms of pause against a ~64 ms lead. Fitting would need ≤ 32 ms per full over a 29 MB tree.
  • object_8m:parse: only k=0.5 runs fulls. CPU stays under best (227 vs 234.9), but RSS reads 116–119 against 112.
  • 20m:roundtrip: already meets both on base and head. k=2 and k=0.5 each lose it on CPU.

Where a k=1 full's time goes now. sample of 20m:parse over 60 iterations, symbolized; 1,007 samples in fulls:

part share
mark 34 %
census (ValidPointerSetBuilder::step, O(all objects, including the dead promoted tree)) 27 %
sweep 20 %
GcCycleState::new_fullmaterialize_all_promoted_page_runsexpand_promoted_run 15 %

The last line re-materializes the described page runs of every in-place-promoted block, including blocks the block-skip sweep then releases whole. Deferring that materialization to pages a sweep actually reshapes, and a tighter census walk, are the next levers. They are not attempted here.

Corrections to the brief

  1. "Each run is exactly one block's live starts; find_floor over up to ~20k addresses, ~15 probes."
    • Runs were also sealed every 1,024 starts (VALID_POINTER_ARENA_RUN_CAPACITY), and they held every censused start, live and dead.
    • So the inner search covered ≤ 1,024 entries (~10 probes), and the outer search ran over objects/1,024 fences rather than one per block.
    • The measured membership cost was real: contains 14.8 ns (runs) vs 2.9 ns (bitmap) per query on the planted population.
  2. The phase table (mark 45, rebuild 21, sweep 16–19, census 10–15) holds only when the live tree is in the old generation.
    • With the tree young at the full: rebuild 6 ms, sweep 4–6 ms, census 8 ms.
    • Census 10–15 ms is the worker regime, where dead promoted trees are still in the arena and the census walks them.
  3. Part B, "if after the full's sweep the young generation holds no objects": the rebuild runs before the sweep, when young blocks still hold dead objects, so "Eden and survivors empty" never holds at that point. The exact condition is liveness: no marked or pinned young object. Malloc children are remembered too, so the malloc registry must also be empty (the minor's other premise).
  4. Part B, "restrict the rebuild to old blocks that were dirty or received promotions since the last full":
    • A full already runs restore_surviving_dirty_coverage over its pre-cycle dirty snapshot in reclaim.
    • Promotions reach the remembered set through sticky sets (evacuation), or leave no young generation (in-place, skip_remembering).
    • So a restricted rebuild equals dropping the rebuild and trusting the barrier invariant the minors already rely on.
    • Measured with a temporary instrument (not pushed): across 22 cells × 3 pacing settings, plus 206 fulls in test_gap_gc* and the ratchet probes (plain and seeded), the rebuild never added a page the dirty-coverage repair missed.
    • Not implemented. It removes the only whole-heap repair of unbarriered stores, which the new planted tests are made of, and that is a policy decision.
  5. "enclosing_object … measure both."
    • Worst case, on a population dominated by a 600 KB object: bitmap backward scan 58 ns/query vs 15 ns for the old runs.
    • On the real conservative path (PERRY_CONSERVATIVE_STACK_SCAN=full, 20m probe): root marking 0.42–0.49 ms head vs 0.47–0.56 ms base.
    • Kept the bitmap scan. Keeping sorted runs for floors would bring back the 8 B/object index and its build cost.
  6. "k=0.5, floor 0" makes old-reclaim due at every allocation point even with nothing promoted (bound 0 ≤ 0). It ran 3,763 fulls on records_array_16k:scan and over 330,000 on test_gap_gc_symbol_local_rooting, so it is degenerate outside the target rows.
  7. scratchpad/ratchet_diff.py crashes on the current ratchet JSON (correctness is an object). A local fixed copy was used.
  8. 8m:scan's CPU lead is 13.7 ms on this tree, not 11. Still below one full.

Summary by CodeRabbit

  • Performance

    • Synchronous full garbage collections are significantly faster, with measured times reduced from approximately 82–83 ms to 37–39 ms.
    • Peak memory usage during full collection decreased from about 129 MiB to 119 MiB.
    • Collection bookkeeping now scales more efficiently with live objects and avoids unnecessary work when no young objects require tracking.
  • Documentation

    • Updated garbage-collector documentation to describe full-collection performance characteristics and diagnostics.

@coderabbitai

coderabbitai Bot commented Sep 13, 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: c131ca19-b943-4783-8ef3-4bcbbd02de08

📥 Commits

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

📒 Files selected for processing (19)
  • changelog.d/10220-gc-full-throughput.md
  • crates/perry-runtime/src/arena/mod.rs
  • crates/perry-runtime/src/arena/page_meta/mod.rs
  • crates/perry-runtime/src/arena/page_meta/sweep_tally.rs
  • crates/perry-runtime/src/arena/walk.rs
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/oldgen/sweep_batch.rs
  • crates/perry-runtime/src/gc/oldgen/sweep_objects.rs
  • crates/perry-runtime/src/gc/tests/cycle_state.rs
  • crates/perry-runtime/src/gc/tests/full_rebuild_skip.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/gc/tests/oldgen.rs
  • crates/perry-runtime/src/gc/tests/start_bitmap.rs
  • crates/perry-runtime/src/gc/tests/sweep_page_tally.rs
  • crates/perry-runtime/src/gc/trace.rs
  • crates/perry-runtime/src/gc/trace/block_skip.rs
  • crates/perry-runtime/src/gc/verify.rs
  • docs/src/internals/garbage-collector.md
  • scripts/gc_runtime_root_holders.json

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


📝 Walkthrough

Walkthrough

Synchronous full collections now use per-block object-start bitmaps, skip empty remembered-set rebuilds when census data proves they are unnecessary, and batch old-generation page accounting before unregister flushes. Tests, documentation, diagnostics, and root-holder audit data cover these changes.

Changes

Full GC throughput optimizations

Layer / File(s) Summary
Block start bitmap census
crates/perry-runtime/src/gc/trace.rs, crates/perry-runtime/src/gc/tests/cycle_state.rs, crates/perry-runtime/src/gc/tests/start_bitmap.rs, crates/perry-runtime/src/gc/tests/mod.rs
ValidPointerSet now indexes object starts with per-block bitmaps and sorted lists for oversized blocks. Membership and enclosing-object lookups use the block index.
Conditional remembered-set rebuild
crates/perry-runtime/src/gc/trace/block_skip.rs, crates/perry-runtime/src/gc/verify.rs, crates/perry-runtime/src/gc/cycle.rs, crates/perry-runtime/src/arena/walk.rs, crates/perry-runtime/src/gc/tests/full_rebuild_skip.rs, crates/perry-runtime/src/gc/tests/oldgen.rs
Synchronous full cycles install an empty remembered-set state when the census finds no marked or pinned young object and the malloc registry is empty. Diagnostics and skip counters record the path.
Batched old-page sweep accounting
crates/perry-runtime/src/arena/page_meta/sweep_tally.rs, crates/perry-runtime/src/arena/page_meta/mod.rs, crates/perry-runtime/src/arena/mod.rs, crates/perry-runtime/src/gc/oldgen/sweep_batch.rs, crates/perry-runtime/src/gc/oldgen/sweep_objects.rs, crates/perry-runtime/src/gc/tests/sweep_page_tally.rs
Single-page old objects accumulate live, pinned, and dead totals. The sweep applies each tally before page-index unregister flushes.
Documentation and audit updates
docs/src/internals/garbage-collector.md, changelog.d/10220-gc-full-throughput.md, scripts/gc_runtime_root_holders.json
The changelog and GC documentation describe the three optimizations and measured results. Root-holder audit notes and hashes are updated.

Priority: ➖ Normal

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

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant FullGC
  participant Census
  participant RememberedSet
  participant OldGenSweep
  participant PageMeta
  FullGC->>Census: build block census
  Census->>RememberedSet: determine whether rebuild is empty
  RememberedSet-->>FullGC: install empty state or scan edges
  FullGC->>OldGenSweep: sweep old objects
  OldGenSweep->>PageMeta: apply batched page tally before unregister flush
Loading

Merge Risk: ⚪ Minimal · up to d6393

The documented lookup costs match the implemented paths, and no actionable merge-blocking issue remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 82 functions across 16 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: reducing the per-live-object cost of synchronous full garbage collection. It also accurately notes that the pacing retry is not included.
Description check ✅ Passed The description is detailed and covers the change summary, related issue, implementation details, test results, benchmark output, known failures, and acceptance rationale. It does not use the exact te…
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 67.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 82 functions across 16 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 3
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch gc/full-throughput
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gc/full-throughput

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
proggeramlug changed the base branch from gc/block-granular-sweep to main September 14, 2026 04:19
@proggeramlug proggeramlug reopened this Sep 14, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Rebased onto main (eb13fa188d) now that #10217 has landed: the 11 commits of this PR re-applied cleanly (git rebase --onto origin/main origin/gc/block-granular-sweep), and the PR is retargeted to main.

Measured on the quiet bench mini (M1, best of 3 interleaved rounds per engine against Node 26.5.1 and Bun 1.3.14, /usr/bin/time -l), all 50 JSON matrix rows: this PR is CPU-neutral and RSS-neutral on every row, as its description states; it is the throughput groundwork #10241's pacing relies on. The gc-ratchet gated counters are identical on all 14 probes (see the description).

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the bench-mini run did not measure this PR on its own. The arm I called "mechanism" was this PR plus #10241's commits without its two pacing commits. That arm had the same 10 misses as main with every row within noise on CPU, and it lowered records_array_8m:parse / :sparse peak RSS from 109 to 97–98 MiB — but that attribution covers #10241's non-pacing mechanism commits too, not this PR alone. The per-PR 22-row numbers in the description (±2 % CPU, RSS not worse) are the author's laptop measurement.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed through merge train #10267 (v0.5.1568). The merged main tree matches the validated train, and a fresh patch-ID audit confirms all source changes are included. The broader GC tracker #10182 remains open.

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