Skip to content

test(gc): measure the grow-then-churn pacing transition and pin it as a ratchet probe (#7737 item 3) - #7783

Merged
proggeramlug merged 3 commits into
mainfrom
gc/7737-grow-then-churn-probe
Aug 10, 2026
Merged

test(gc): measure the grow-then-churn pacing transition and pin it as a ratchet probe (#7737 item 3)#7783
proggeramlug merged 3 commits into
mainfrom
gc/7737-grow-then-churn-probe

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes the last engineering item of #7737. Item 3 is what this PR does; items 1 and 2 are already fixed on main (the OBJECT_PROTOTYPES_NONEMPTY latch releases when a prune drains the registry; gc/tests/triggers.rs asserts the positive direction of the pacing recording). Item 4 — promoting gc-ratchet / gc-stress to required contexts — is a maintainer action and is untouched here.

The untested middle

The major-pacing backoff (#7726) has two well-covered ends. retain.ts-shaped growth is pure, so every escalated full reclaims almost nothing and the shift climbs to its cap of 2 and stays. tree.ts-shaped churn is always high-yield, so the shift never leaves 0. Nothing exercised a workload that is the first and then becomes the second — which is the shape a warm-cache phase followed by steady churn actually has.

14_grow_then_churn.ts is that workload: an all-live 300k-row cache that walks the shift 0 → 1 → 2 on growth alone, then push-grown array churn with the cache still live so the pacing baseline stays up.

What it measured — not what the issue predicted

#7737 reasoned the delayed reclaim would cost "up to 4× more transient RSS", since array-growth forwarding stubs are reclaimable only by a full mark-sweep. A same-binary A/B (one runtime at MAJOR_PACING_BACKOFF_SHIFT_MAX = 2, one at 0, archives rebuilt and mtime-verified between arms, stdout byte-identical):

cap 2 (shipped) cap 0 (no backoff)
escalation boundary reached 56.3 MB 14.1 MB
full mark-sweeps 3 5
copying minors / objects moved 24 / 306,715 24 / 306,715
peak RSS, 3 runs 70.5 / 70.2 / 70.5 MB 72.6 / 72.6 / 72.6 MB

The boundary moves 4×, exactly as reasoned. Peak RSS does not follow it, and does not move in the predicted direction — the shipped backoff is 2.2 MB (3%) lower, reproducibly, while running two fewer fulls for identical collector work. Footprint on this shape is set by the steady state the minors hold, not by cumulative stub debt, so the escalation boundary is never the binding constraint.

So the probe pins the measured state rather than asserting the predicted bound. A future change that makes the delayed reclaim start to bind moves rss_bytes off the pin and the ratchet reports it.

Provenance: M1 Max laptop, perry-dev, peak RSS from plain runs with no GC tracing (tracing perturbs RSS, so counters and footprint come from separate runs). Not the pinned mini that owns the artifact — the comparison transfers, the absolute bytes belong to that context.

The probe reaches its subject

The failure this suite has paid for most often is a knob that moves the workload off the path it was chosen to exercise, so this was checked rather than assumed: PERRY_GC_TRACE=1 reports backoff_shift reaching 2 with the boundary at 56.3 MB, and the harness records minor_cycles=24 with 306,715 objects moved, satisfying check's liveness rule with a collector that demonstrably ran.

The two gc-ratchet-env directives (PERRY_GC_SCAVENGE_NURSERY_MB=1, PERRY_GC_MAJOR_PACING_FLOOR_MB=1) set the absolute scale of a mechanism that is otherwise a ratio — at the shipped 16 MB / 32 MB the same three escalations need a live set in the hundreds of MB. A new test asserts both, the same discipline 13_large_eden_survivors already gets: losing either line leaves a probe that still passes, still collects, and no longer reaches the cap it exists to hold.

It is also the most stable probe in the suite — 0% spread on both retention metrics, 0.022% on RSS, against 4.7% and 5.4% for 12_large_live_set and 13_large_eden_survivors.

The re-pin is not a clean carry-over, and says so

Adding a probe changes the probe set, so the artifact must be re-pinned. The control was not green, and rather than absorbing that, the artifact notes name every cell:

probe cell pinned now
04_dead_after_deep_stack copied_objects 565 605 (+7.08%)
11_collect_at_depth heap_used_bytes 5,097,776 5,307,584 (+4.12%)
11_collect_at_depth promoted_objects / bytes 6,150 / 420,488 0 / 0
11_collect_at_depth copied_objects / bytes 0 / 0 6,139 / 419,608

Probe 11's survivors stopped being promoted on first copy and are now copied into survivor space — the #7558 shape, running the other way.

Attribution, so it is recorded as code and not drift. The control ran twice and reported the same six cells both times. A release binary was built from a8f73122d, the commit the previous artifact was pinned at, and --check from that tree against that artifact reported zero regression cells. Running one probe directly under each binary is deterministic 6-of-6 within each arm and differs between them. Two further cells (13_large_eden_survivors RSS, +7.2%) fired in the first control and not the second, so those are host variability on a sampled metric and are not claimed as a shift.

#7742 is excluded as the cause, by its own PR's evidence. #7744 measured gc_ratchet against this same artifact before it merged and reported: "the six cells it reports as REGRESSION are reported identically by origin/main itself — pre-existing drift against the pinned artifact." The same six. So the flip predates the whole-block promotion change and comes from one of the earlier collector commits — #7733 yield-adaptive major pacing, #7690 / #7682 evacuation at precise safepoints, #7721 / #7735 the loop back-edge poll, #7317 the seeded schedule, or the #7469 thread-local work.

Why it went unrecorded: 78 commits separate the two pins, twelve in the collector, and gc-ratchet is not a required context — #7737 item 4 — so none of them ever had to re-pin. It has now been observed by at least two people without being recorded, because an advisory gate cannot force the question.

It is pinned so the next change has a truthful baseline. If the promote-to-copy flip on 11_collect_at_depth was not intended by whichever change caused it, then this artifact is the wrong fix and the code is the right one — that call is the maintainer's, and the notes say so in the artifact itself.

Validation

  • run_gc_ratchet_baseline.sh --check against the new artifact on the pinned host, clean tree, all 14 probes: 0 regression cells, gc-ratchet: OK, exit 0.
  • New probe passes the Node v26.5.1 oracle byte-for-byte (correctness=pass).
  • tests/test_gc_ratchet.py: 92 passed, including the new directive assertion.
  • scripts/check_file_size.sh: exit 0.
  • Pinned on perry-macos (M1 mini), AC power, CPU-quiet gate satisfied before each phase, PERRY_NO_AUTO_OPTIMIZE=1, runtime dir pinned. This branch changes no Rust at all — a probe .ts, the README, a workflow comment, a Python test and the artifact — so the release binary is the right one for this tree.

Summary by CodeRabbit

  • New Features

    • Added a garbage-collection benchmark covering cache growth followed by allocation churn.
    • Added heap, RSS, checksum, and stability measurements for the new workload.
  • Documentation

    • Updated benchmark and preflight documentation to reflect fourteen probes.
    • Added changelog details and measurement results for the new probe.
  • Tests

    • Added coverage validating the new probe’s required runtime settings.
  • Chores

    • Incremented the workspace version to 0.5.1457.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds the fourteenth GC ratchet probe. The probe grows a retained object cache, performs array-allocation churn, verifies retained data, forces GC, and emits metrics. Documentation, preflight counts, changelog content, configuration tests, and version metadata now include the change.

Changes

GC ratchet probe

Layer / File(s) Summary
Implement grow-then-churn workload
benchmarks/gc_ratchet/probes/14_grow_then_churn.ts
Adds the retained cache, linked Row objects, ring-backed array churn, checksum validation, final GC, and heap/RSS metric output.
Integrate probe configuration and coverage
benchmarks/gc_ratchet/README.md, .github/workflows/gc-ratchet.yml, tests/test_gc_ratchet.py, changelog.d/7783-grow-then-churn-probe.md
Updates probe counts, documents source-local GC settings and measurements, records the probe change, and tests both required runtime settings.
Update release metadata
Cargo.toml, CLAUDE.md
Updates the workspace package version and documented current version to 0.5.1457.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • PerryTS/perry issue 7737 — Directly requests the grow-then-churn GC ratchet probe and related coverage.

Possibly related PRs

  • PerryTS/perry#7432 — Introduced GC ratchet probe coverage extended by probe 14.
  • PerryTS/perry#7733 — Adds a probe that exercises the major-GC pacing changes in crates/perry-runtime/src/gc/policy.rs.
  • PerryTS/perry#7739 — Updates related GC pacing and diagnostic behavior in separate runtime files.

Suggested reviewers: jdalton

Sequence Diagram(s)

sequenceDiagram
  participant Probe as 14_grow_then_churn.ts
  participant Cache as Retained cache
  participant Ring as Churn ring
  participant GC as Garbage collector
  Probe->>Cache: Build and verify retained Row graph
  Probe->>Ring: Allocate and recycle short-lived arrays
  Probe->>Cache: Traverse cache and compute liveSum
  Probe->>GC: Force collection
  Probe->>Probe: Emit checksums and heap/RSS metrics
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and relevant but omits the required template sections and does not acknowledge prohibited version and CLAUDE.md edits. Reformat the description using the required headings and explain or remove the Cargo.toml and CLAUDE.md version changes.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new GC probe and its grow-then-churn pacing purpose.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gc/7737-grow-then-churn-probe

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 force-pushed the gc/7737-grow-then-churn-probe branch from 8817352 to a1fd5a7 Compare August 10, 2026 15:51
@proggeramlug
proggeramlug marked this pull request as ready for review August 10, 2026 16:11

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

Actionable comments posted: 1

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

Inline comments:
In `@benchmarks/gc_ratchet/README.md`:
- Line 84: Update the cadence statement in the README to scope the approximately
16 MB copying-minor claim only to the default configuration arm, rather than all
probes in the matrix. Keep the surrounding probe-count description intact and
ensure the wording does not include the documented 64 MB and 1 MB nursery arms.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d7cb3151-2422-4cd3-935a-2a9088de6f63

📥 Commits

Reviewing files that changed from the base of the PR and between bf8af0e and c610b63.

📒 Files selected for processing (6)
  • .github/workflows/gc-ratchet.yml
  • benchmarks/gc_ratchet/README.md
  • benchmarks/gc_ratchet/baseline/gc-ratchet-v1.json
  • benchmarks/gc_ratchet/probes/14_grow_then_churn.ts
  • changelog.d/7783-grow-then-churn-probe.md
  • tests/test_gc_ratchet.py

## A probe may declare the collector it is a probe *of* (the large-Eden arm)

Twelve of the thirteen probes run the shipped configuration, so every copying
Twelve of the fourteen probes run the shipped configuration, so every copying

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Limit the cadence statement to the default arm.

The text says that every copying minor exercised by the matrix was approximately 16 MB. Lines 99-100 document 13_large_eden_survivors.ts with a 64 MB nursery, and 14_grow_then_churn.ts uses a 1 MB nursery. The current sentence contradicts the documented arms.

Proposed wording
-Twelve of the fourteen probes run the shipped configuration, so every copying
-minor this matrix had ever exercised was small (~16 MB) and frequent.
+Twelve of the fourteen probes run the shipped configuration (~16 MB). The
+explicit 64 MB and 1 MB arms exercise separate copying-minor cadences.
🤖 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 `@benchmarks/gc_ratchet/README.md` at line 84, Update the cadence statement in
the README to scope the approximately 16 MB copying-minor claim only to the
default configuration arm, rather than all probes in the matrix. Keep the
surrounding probe-count description intact and ensure the wording does not
include the documented 64 MB and 1 MB nursery arms.

Ralph Küpper added 3 commits August 10, 2026 19:52
… a ratchet probe (#7737 item 3)

The major-pacing backoff (#7726) had two well-covered ends and an untested
middle. retain.ts-shaped growth is pure, so the shift climbs to its cap and
stays; tree.ts-shaped churn is always high-yield, so the shift never leaves 0.
Nothing exercised a workload that is the first and then becomes the second —
the shape a warm-cache phase followed by steady churn actually has.

14_grow_then_churn.ts is that workload: an all-live 300k-row cache that walks
the shift 0 -> 1 -> 2 on growth alone, then push-grown array churn with the
cache still live so the pacing baseline stays up.

#7737 reasoned the delayed reclaim would cost "up to 4x more transient RSS".
A same-binary A/B — one runtime at MAJOR_PACING_BACKOFF_SHIFT_MAX = 2, one at
0, archives rebuilt and mtime-verified, stdout byte-identical — measured the
boundary moving 56.3 MB vs 14.1 MB (the predicted 4x) while peak RSS went the
other way: 70.5/70.2/70.5 MB shipped against 72.6/72.6/72.6 MB without the
backoff, for two fewer full mark-sweeps and identical collector work (24
copying minors, 306,715 objects moved in both). Footprint on this shape is set
by the steady state the minors hold, not by cumulative stub debt, so the
escalation boundary is never the binding constraint.

So the probe pins the measured state rather than asserting the predicted
bound. The two gc-ratchet-env directives that set its absolute scale are
asserted by a new test, the same discipline 13_large_eden_survivors gets.
…ix cells that moved

Control run twice on the pinned host before pinning: the same six cells
regressed in both (04_dead_after_deep_stack.copied_objects, and
11_collect_at_depth heap_used/copied/promoted, whose survivors stopped being
promoted on first copy). A release binary built from a8f7312 - the commit the
previous artifact was pinned at - reproduces that artifact with ZERO regression
cells, and both binaries are deterministic 6-of-6 within themselves, so the
shift is code and not drift. 78 commits separate the pins, twelve in the
collector; gc-ratchet is advisory-only (#7737 item 4), so none of them had to
re-pin. Named in the artifact notes rather than absorbed.
@proggeramlug
proggeramlug force-pushed the gc/7737-grow-then-churn-probe branch from 8fb4944 to a88e0d7 Compare August 10, 2026 18:03

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

Actionable comments posted: 1

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

Inline comments:
In `@Cargo.toml`:
- Line 318: Revert both release metadata edits: set [workspace.package].version
in Cargo.toml (318-318) back to 0.5.1456 and set Current Version in CLAUDE.md
(11-11) back to 0.5.1456; do not modify other files.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8fec8838-2756-4ca7-b4fa-d977eb52fce2

📥 Commits

Reviewing files that changed from the base of the PR and between c610b63 and a88e0d7.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CLAUDE.md
  • Cargo.toml

Comment thread Cargo.toml

[workspace.package]
version = "0.5.1456"
version = "0.5.1457"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Revert both release metadata edits.

This contributor PR must not update release metadata. The maintainer owns the version bump and must update both files together during merge or release.

  • Cargo.toml#L318-L318: revert [workspace.package].version to 0.5.1456.
  • CLAUDE.md#L11-L11: revert Current Version to 0.5.1456.
📍 Affects 2 files
  • Cargo.toml#L318-L318 (this comment)
  • CLAUDE.md#L11-L11
🤖 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 `@Cargo.toml` at line 318, Revert both release metadata edits: set
[workspace.package].version in Cargo.toml (318-318) back to 0.5.1456 and set
Current Version in CLAUDE.md (11-11) back to 0.5.1456; do not modify other
files.

Source: Learnings

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audit complete — merging.

Verified:

  • No Rust changes (probe .ts, baseline artifact, README, workflow comment, harness test) — so the binary question reduces to provenance, and the baseline's own notes carry it: pinned on perry-macos (the artifact's owner host), with the absorbed 6-cell shift named, attributed via a control (old binary vs old artifact: zero regressions), and traced to the perf(gc): whole-block in-place promotion — kill the ~243 ns/object promotion tax on a fully-live nursery #7742-era collector work.
  • 14/14 probes registered in the baseline; probe-set consistency holds.
  • Full harness suite: 92 tests, OK. Sabotage verified by me (applied, then restored): deleting one gc-ratchet-env directive from the probe fails exactly the new directive-assertion test.
  • The probe reaches its subject on an independent host: run here with its two env directives + PERRY_GC_TRACE=1, backoff_shift walks to 2 and the run records 24 minors + 3 fulls — the pinned counts exactly (deterministic counters transfer; the RSS bytes stay host-relative as the notes say).
  • Local gate green (test steps skipped — no Rust in the diff; the harness's own suite ran above).

This closes #7737 item 3 — the last engineering item. Item 4 (promoting gc-ratchet/gc-stress to required contexts) remains the maintainer/admin action already tracked there.

@proggeramlug
proggeramlug merged commit db44b31 into main Aug 10, 2026
1 of 19 checks passed
@proggeramlug
proggeramlug deleted the gc/7737-grow-then-churn-probe branch August 10, 2026 18:11
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