Skip to content

perf(gc): seed promote-on-first-copy from a completed mark-sweep (#7598) - #7613

Merged
proggeramlug merged 4 commits into
mainfrom
perf/7592-promote-on-first-copy
Aug 8, 2026
Merged

perf(gc): seed promote-on-first-copy from a completed mark-sweep (#7598)#7613
proggeramlug merged 4 commits into
mainfrom
perf/7592-promote-on-first-copy

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Closes #7598. The promote-on-first-copy remainder of #7592.

The waste

On promote-heavy workloads every long-lived object is copied twice — Eden →
survivor by one copying minor, survivor → old by the next. json_pipeline at
500k, on main:

# kind eden_live copied promoted pause
3 minor 280,997,080 280,997,080 0 1,513 ms
4 minor 1,048,576 0 282,045,656 2,559 ms

The same 268 MB, moved twice. gc/tenuring.rs already computes the right
condition — the survival-rate lock — but it keys on prev_copied, so it needs a
previous copying minor to have filled the survivor space. It engaged for cycle
4, which is exactly why the waste is confined to cycle 3.

Route chosen: P1 from the design note — seed the decision from a completed

non-copying collection

The other two were considered and rejected on this workload:

  • P3 (size-based pretenuring) is a poor fit. These records are ~100–200 byte
    objects, so any PretenureSizeThreshold low enough to catch them catches
    almost everything, and its stated failure mode — a parse-and-discard loop over
    large buffers promoting pure garbage — is real and not bounded by anything the
    collector measures. It is a good idea whose threshold must come from a suite
    sweep, not from this workload.
  • P2 (per-age-band entry decisions) is a refinement of P1, not an
    alternative: it buys precision, and the problem here is latency.

The obstacle is that retune_after_scavenge is fed only by copying minors,
so full mark-sweeps and non-copying minor fallbacks feed the loop nothing — and
once a workload escalates to those, the loop goes blind exactly when it is
needed. But every collection on that path already walks every Eden header and
classifies it live or dead. That census answers the same question one collection
earlier.

So: at the end of any such sweep, when

  1. the surviving Eden cohort alone exceeds the desired survivor occupancy
    (compute_target_survivals(...) == 1 — the module's existing rule, only
    read from a different place), and
  2. ≥90% of the Eden bytes the sweep classified were live (the "an aging round
    would filter nothing" proof, measured instead of inferred),

engage the existing PROMOTE_LOCK so the next copying minor enters at S=1.

Exit is deliberately not this signal — it stays the existing influx-based
unlock, which is already threshold-invariant and already tested, so the seed
cannot introduce an enter/exit oscillation of its own.

Why this signal is not a fixed point of the policy reading it

This issue has produced three self-referential signals already, so here is the
argument rather than the assumption.

The failures were all the same shape: a signal suppressed by the state it is
supposed to leave.
promoted_bytes is zero by construction at S=4, so "was
the promotion rate high?" can never answer yes while S=4 holds. #7596's first
nursery cap gated from-space occupancy on a total that included from-space, so
it was a bound from-space could never cross. #7594's handoff scheduled a
non-moving full to relieve pressure only a moving cycle can relieve.

The Eden live/dead split has none of that structure, for a structural reason:
it is produced by the mark-sweep's own arena walk. Marks come from
reachability from roots; neither the mark phase nor the sweep walk reads
tenuring_survivals(). The threshold is consulted in exactly one place —
copying.rs's per-object move — and this path does not run it. So the signal is
not merely observed to survive at S=4; it cannot be a function of S at all.

Measurably, at both endpoints:

state what Eden holds at the sweep what the signal reads
S=4 — the state to leave everything that has not aged out high live fraction on a retaining workload ⇒ "lock"
S=1 — the state to stay in only Eden allocated since the last promotion the mutator's retention of recent Eden ⇒ still measurable

Measured on this workload: eden_live_bytes=279,964,968 eden_dead_bytes=896 live_pct=99 desired=1,048,576 seeds=true — taken while S was still 4.

Determinism (#7432)

#7432 forbids re-deciding S while objects are being moved, because the
copied/promoted split would then depend on root traversal order. The seed is
written at the END of a completed sweep and read at the ENTRY of a later copying
minor (CopyingNurseryCollector::new snapshots it once), so every object in a
cycle still sees exactly one threshold.

Two exclusions at the callsite keep the input deterministic too, and both are
refusals rather than tuning:

  • Budgeted cycles — whole-cycle allocate-black marks every mid-cycle birth,
    and this walk reads MARKED as live, so a churn workload's births would read as
    a ~100% live Eden. Same reason the age-bump is suppressed there.
  • Cycles that ran the conservative native-stack scan — that scan retains
    whatever the stack happens to look like a pointer to, by an amount that varies
    run to run (benchmarks/gc_ratchet/README.md measures 8.28 MB / 16% of one
    probe's reported retention). A liveness measurement taken under it is not
    sound, and feeding it to a policy would make the gated copy/promote counters
    non-deterministic.

No new env knob. The PERRY_GC_DIAG line prints the census and the verdict on
every sweep, including refusals
— a policy that silently declines is
indistinguishable from one that never ran.

Measurements

Copy census — the halving signature, per cycle

Semantic counters, PERRY_GC_TRACE=1, output hash identical on every row.
500k:

# kind old_before eden_live (main → this) copied (main → this) promoted (main → this) S
1 full 112,695,904 0 → 0 0 → 0 0 → 0
2 full 121,002,720 0 → 0 0 → 0 0 → 0
3 minor 4,440,144 280,997,080 → 280,997,080 280,997,080 → 0 0 → 280,997,080 4 → 1
4 minor 4,440,144 1,048,576 → (none) 0 → — 282,045,656 → — 1 → —

This is promotion, not cadence. Cycles 1–3 are unchanged in kind, trigger,
old_before and eden_live — cycle 3 receives the same 280,997,080 bytes to
the byte
and merely sends them somewhere else. The cycle that disappears is
baseline cycle 4, whose entire content was the second copy: its own Eden influx
was 1.0 MB and it promoted the 268 MB cycle 3 had just parked in the survivor
space. Removing it is removing the waste, not lengthening the collection
interval.

Totals:

records arm collections copied_bytes copied_objects promoted_bytes promoted_objects moved total pause total
200k main 4 113,227,216 1,657,966 114,275,776 1,670,376 227,502,992 3,008 ms
200k this 3 0 0 113,227,216 1,657,966 113,227,216 1,040 ms
500k main 4 280,997,080 4,117,015 282,045,656 4,129,425 563,042,736 5,083 ms
500k this 3 0 0 280,997,080 4,117,015 280,997,080 2,838 ms

Bytes moved: 0.498× at 200k, 0.499× at 500k. Halved, as the design note's
signature requires.

On the anti-vacuity gate. The design note asks for copied_objects > 0 && promoted_bytes > 0 on any cited run. The main arm satisfies both literally.
The changed arm cannot satisfy the first clause because eliminating that copy is
the change
; what the clause exists to catch — the #7024/#7025 shape where the
fast arm ran zero collections — is refuted directly: it ran 3 collections,
one of them a copying minor that moved 4,117,015 objects / 280,997,080 bytes
([gc-copy-minor] ran … eligible=true fallback=none). Reported as
moved_objects > 0 rather than copied_objects > 0.

Wall and RSS — pinned quiet host

perry-macos (M1, 8 GB), load ~1.6, 5 interleaved reps per size, warmup
discarded, PERRY_NO_AUTO_OPTIMIZE=1, prebuilt static archives, output
byte-identical (cmp) on every row.

records metric main this Δ
200k wall 1.85 s 1.44 s −22.2%
200k peak RSS 608,747,520 485,736,448 −20.2%
500k wall 5.12 s 3.86 s −24.6%
500k peak RSS 1,404,469,248 1,109,803,008 −21.0%

Spread within each arm was ≤0.03 s on wall and ≤0.25 MB on RSS across the 5
reps.

RSS goes DOWN, which is the opposite of what the design note expected. The
note assumed promoting earlier raises the old-gen high-water mark. It does — but
it removes a larger term: with S=4 the 268 MB cohort exists twice at once at
the peak, as Eden from-space plus survivor to-space. Promoting on first copy
means the peak holds one copy. This is the first change in this campaign whose
wall-time win does not have to be traded against RSS.

gc-ratchet — the official check, on the #7609 baseline

This is the first GC-pacing change since 2026-08-01 that could be gated properly,
so it was run as a two-arm check on the pinned host: once with a main
(d4342fff3) build, once with this branch, back to back in the same session, both
built with the same -p perry -p perry-runtime-static -p perry-stdlib-static set.
The main arm establishes what the two commits of drift since the pin
(26b9c9d59) cost on their own, so anything the second arm moves is attributable
to this change.

gc-ratchet (check) at d4342fff3  (main arm)     → gc-ratchet: OK
gc-ratchet (check) at 3aa3b091c  (this branch)  → gc-ratchet: OK

Diffing the two arms' 144-cell tables: every semantic cell is bit-identical.
heap_used_bytes, heap_total_bytes, minor_cycles, step_cycles,
copied_objects, copied_bytes, promoted_objects, promoted_bytes,
freed_bytes — all twelve probes, no difference at all. The only rows that
differ are rss_bytes, peak_rss_bytes and wall_ms, which move in both
arms and stay inside band. The largest is 12_large_live_set.peak_rss_bytes:
+0.36% on the main arm, +0.68% on this one (190,955,520 → 191,561,728, a 606 KB
difference on a 190 MB probe, band 3%). It is not a promotion effect — that
probe's promoted_bytes is identical to the byte in both arms.

12_large_live_set.wall_ms#7610's unexplained +13.58% flag, now gated:

arm baseline measured Δ
main d4342fff3 3,471 3,016 −13.11%
this branch 3,471 3,015 −13.14%

This change does not move that cell (1 ms apart). Separately, and worth
recording on #7610: the +13.58% did not reproduce in this session at all — a
main build measures 3,016 ms against the 3,471 ms the artifact pinned three
commits earlier on the same host, i.e. the regression reads as reversed. That
is a data point for #7610, not a claim by this PR.

The subject was live — and where

The ratchet result above proves no collateral; it does not prove the policy
ran, because on these probes it correctly does nothing. That distinction is the
#7024/#7025 shape, so it is settled by the diagnostic rather than assumed. The
seed prints its census and its verdict on every mark-sweep, including refusals:

12_large_live_set:  [gc-tenuring] sweep-seed eden_live_bytes=12582432
                    eden_dead_bytes=22019256 live_pct=36 desired=2097152
                    seeds=false already_locked=true
json_pipeline 500k: [gc-tenuring] sweep-seed eden_live_bytes=279964968
                    eden_dead_bytes=896 live_pct=99 desired=1048576
                    seeds=true already_locked=false

So on the largest-live-set probe the rule is evaluated and declined — by the
survival-rate half, at 36% — and on the target workload it is evaluated and
accepted. A policy that silently declines is indistinguishable from one that
never ran; this is why the refusal branch prints.

Sabotage verification

Each mutation was applied to the shipped code and the named tests re-run. All
four turn red, and the tree is green again after restoring:

mutation result
drop block_idx < resettable_general_n in keep_live_object (old-gen bytes leak into the Eden census) full_sweep_eden_census_counts_only_nursery_blocks FAILED
make seed_promote_lock_from_sweep a no-op sweep_seed_decides_before_the_first_copying_minor_snapshots_the_threshold, sweep_seed_hands_over_to_the_existing_unlock_path FAILED
drop the survival-rate condition sweep_seed_refuses_a_churn_eden, sweep_seed_rule_is_a_pure_function_of_the_census FAILED
drop the occupancy condition sweep_seed_refuses_a_small_fully_live_eden, sweep_seed_rule_is_a_pure_function_of_the_census FAILED

Gates (local — CI backlog is deep, so this is the evidence)

cargo test -p perry-runtime --no-fail-fast 1885 passed, 0 failed ·
cargo fmt --all -- --check clean · check_file_size.sh OK ·
raw_handle_debt.py 998 (baseline 998) · addr_class_inventory.py passed ·
class_id_collisions.py passed. No codegen change (the diff is four files, all
under crates/perry-runtime/src/gc/, 314 insertions and no deletions), so the
root-dominance corpus is not implicated.

What this does not do

Collateral check on GC-shaped workloads outside the ratchet

Six micro-workloads the ratchet does not contain, both arms, semantic counters
and stdout compared. tree is the workload the survival-rate lock was written
for (medium-lived cohorts that do die in the survivor space — the failure mode
this rule must not trip on); retain is the accumulator shape; churn and
push_cls are pure churn.

workload stdout collections copied_bytes promoted_bytes
tree identical 42 = 42 17,080,392 = 17,080,392 85,866,016 = 85,866,016
retain identical 7 = 7 17,808,760 = 17,808,760 152,021,368 = 152,021,368
churn identical 105 = 105 3,906,160 = 3,906,160 64 = 64
cycles identical 16 = 16 622,528 = 622,528 210,496 = 210,496
push_cls identical 105 = 105 3,906,160 = 3,906,160 64 = 64
deeplist identical 4 = 4 17,079,696 → 17,079,168 (−0.0031%) 53,579,048 = 53,579,048

deeplist's 528-byte / 3-object delta is stable across 3 runs per arm, so it is
deterministic rather than noise — and it is not a policy effect. It lands
entirely in cycle 1, a copying minor entered at S=4, while this workload's
first mark-sweep is cycle 3 — so no seed had been evaluated yet and the tenuring
state at cycle 1 is identical in both arms. It is the collector's documented
address-sensitive component in copied_objects (gc_ratchet/README.md: "a
sub-0.1% host-dependent component") showing up between two differently-laid-out
binaries. For the record, the seed is reached later on this workload and
reports live_pct=100 seeds=true already_locked=true — the copying path had
already locked, so it changes nothing.

Reproducibility of the cited numbers

The change arm was rebuilt from the committed tree (-p perry -p perry-runtime-static -p perry-stdlib-static, pinned PERRY_RUNTIME_DIR,
PERRY_NO_AUTO_OPTIMIZE=1) and json_pipeline 500k re-run against it: 3
collections, copied_bytes 0, promoted_bytes 280,997,080, output
byte-identical to the main arm. The committed source reproduces the census
above.

Summary by CodeRabbit

  • Performance

    • Improved garbage collection by promoting highly live memory directly during the next collection cycle, reducing redundant copying and memory movement.
    • Added safeguards to preserve predictable behavior for budgeted collections and conservative stack scanning.
  • Bug Fixes

    • Improved Eden memory accounting during collection, distinguishing retained and reclaimed memory more accurately.
  • Documentation

    • Added release documentation describing the promotion improvements and measured performance benefits.

Ralph Küpper added 2 commits August 8, 2026 01:27
The survival-rate lock in gc/tenuring.rs is one cycle late by construction:
it keys on prev_copied, so a previous copying minor must already have filled
the survivor space. On a one-burst workload the first copying minor therefore
always pays the wasted Eden->survivor copy (json_pipeline 500k: 268 MB copied
on cycle 3, the same 268 MB promoted on cycle 4).

Every collection that reaches the mark-sweep path -- a full, or a non-copying
minor fallback, the two blind spots of retune_after_scavenge -- already walks
every Eden header and classifies it live or dead. That census answers the same
question one collection earlier. When the surviving cohort alone exceeds the
desired survivor occupancy AND >=90% of the classified Eden bytes were live,
the existing PROMOTE_LOCK is engaged so the NEXT copying minor enters at S=1.

Exit stays the existing influx signal, so no new oscillation path. Budgeted
cycles (allocate-black marks every mid-cycle birth) and conservative-scan
cycles (unsound, run-varying liveness) are excluded at the callsite.

Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
…refusals

The promote-on-first-copy seed is a policy decision made from the sweep's Eden
census, so a plausible-but-wrong census would fire the policy on the wrong
workloads without ever crashing. One assertion per way it can be wrong: live
counted, dead counted separately, and old-gen live counted in NEITHER -- the
last is the sabotage target for the block_idx < resettable_general_n gate.

The PERRY_GC_DIAG line prints the census AND the verdict on every mark-sweep,
including refusals: a policy that silently declines is indistinguishable from
one that never ran (#7024/#7025), and it is how the ratchet probes were shown
to evaluate the rule and decline it rather than never reaching it.

Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 05547537-5151-4404-bf6e-8852c50ac485

📥 Commits

Reviewing files that changed from the base of the PR and between 1bbf4a3 and a6a0eb1.

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

📝 Walkthrough

Walkthrough

The GC now records Eden live and dead bytes during sweeping and uses eligible completed-sweep results to seed the promote-on-first-copy lock. Tests cover census accounting, policy boundaries, exclusions, threshold timing, and unlock behavior.

Changes

Eden census seeding

Layer / File(s) Summary
Eden census accounting
crates/perry-runtime/src/gc/oldgen.rs, crates/perry-runtime/src/gc/copying.rs, crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs
Sweep state and trace statistics now carry Eden live and dead bytes. General-arena survivors and reclaimed objects update the counters. Legacy and copied-minor paths initialize them to zero. Regression coverage excludes old-generation bytes from the Eden census.
Promote-lock policy
crates/perry-runtime/src/gc/tenuring.rs
Tenuring adds occupancy and 90% liveness checks for sweep seeding. Eligible data seeds threshold 1 and resets lock state without changing copying-path history. Tests cover refusal cases, boundaries, timing, and unlock behavior.
Eligible cycle wiring
crates/perry-runtime/src/gc/cycle.rs
Non-budgeted full or non-copying minor cycles with disabled conservative scanning pass sweep Eden totals to tenuring. Budgeted and conservatively scanned cycles do not seed the lock.
Release metadata
changelog.d/7613-promote-on-first-copy.md, CLAUDE.md, Cargo.toml
The changelog documents the promote-on-first-copy behavior. The documented and workspace versions change to 0.5.1349.

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

Sequence Diagram(s)

sequenceDiagram
  participant OldgenSweep
  participant Cycle
  participant Tenuring
  OldgenSweep->>Cycle: Record Eden live/dead byte totals
  Cycle->>Tenuring: Call seed_promote_lock_from_sweep
  Tenuring->>Tenuring: Apply full_seed_promotes_on_first_copy
  Tenuring-->>Cycle: Seed threshold 1 or retain current lock
Loading

Possibly related PRs

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR satisfies determinism, output, and duplicate-copy goals, but it does not implement the issue's broader allocation-site pretenuring scope or demonstrate the approximately 2.5-second build_out target. Complete allocation-site pretenuring and verify the target, or update the linked issue and acceptance criteria to define this promote-on-first-copy subset.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed The GC implementation, regression tests, diagnostics, and changelog directly support the promote-on-first-copy objective; no unrelated code changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely describes seeding promote-on-first-copy from a completed mark-sweep.
Description check ✅ Passed The description thoroughly covers the change, rationale, issue, implementation details, tests, measurements, determinism, and collateral results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/7592-promote-on-first-copy

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.

@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 `@crates/perry-runtime/src/gc/oldgen.rs`:
- Around line 1463-1465: Update the Eden live-byte accounting in the relevant
keep_live_object call sites, including the block around the shown condition and
the corresponding site around process_forwarded_object, to pass true for pinned
and ordinary objects while excluding forwarding stubs from the census. Add a
regression test covering a retained general-block forwarding stub during a
non-copying minor collection and verify it does not inflate eden_live_bytes.
🪄 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: d3641b0b-091f-422e-a9bc-8b664529b1fa

📥 Commits

Reviewing files that changed from the base of the PR and between d4342ff and 3aa3b09.

📒 Files selected for processing (5)
  • crates/perry-runtime/src/gc/copying.rs
  • crates/perry-runtime/src/gc/cycle.rs
  • crates/perry-runtime/src/gc/oldgen.rs
  • crates/perry-runtime/src/gc/tenuring.rs
  • crates/perry-runtime/src/gc/tests/incremental_sweep_reclaim.rs

Comment on lines +1463 to +1465
if block_idx < self.resettable_general_n {
self.eden_live_bytes = self.eden_live_bytes.saturating_add((*header).size as u64);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude forwarding stubs from the Eden census.

process_forwarded_object calls keep_live_object for retained general-block stubs. A non-copying minor retains every forwarding stub. These bytes therefore increase eden_live_bytes.

An unretained forwarding stub bypasses reclaim_dead_object. Its bytes do not increase eden_dead_bytes.

This asymmetric census can report a high survival rate and seed PROMOTE_LOCK for stale forwarding metadata. Count only non-forwarded Eden objects in this policy census. Add a regression test for a retained general-block forwarding stub during a non-copying minor.

Proposed fix
 unsafe fn keep_live_object(
     &mut self,
     header: *mut GcHeader,
     block_idx: usize,
     flags: u8,
     age_bump_this: bool,
     pinned: bool,
+    count_eden_live: bool,
 ) {
-    if block_idx < self.resettable_general_n {
+    if count_eden_live && block_idx < self.resettable_general_n {
         self.eden_live_bytes = self.eden_live_bytes.saturating_add((*header).size as u64);
     }
- self.keep_live_object(header, block_idx, flags, false, false);
+ self.keep_live_object(header, block_idx, flags, false, false, false);

Pass true for the pinned and ordinary-object call sites.

Also applies to: 1529-1531

🤖 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/oldgen.rs` around lines 1463 - 1465, Update the
Eden live-byte accounting in the relevant keep_live_object call sites, including
the block around the shown condition and the corresponding site around
process_forwarded_object, to pass true for pinned and ordinary objects while
excluding forwarding stubs from the census. Add a regression test covering a
retained general-block forwarding stub during a non-copying minor collection and
verify it does not inflate eden_live_bytes.

@proggeramlug
proggeramlug merged commit 9caa11a into main Aug 8, 2026
@proggeramlug
proggeramlug deleted the perf/7592-promote-on-first-copy branch August 8, 2026 00:02
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audit before merge — verified to the byte, merged as v0.5.1349

The copy census reproduces exactly on my own build and my own earlier
traces
: my v0.5.1339-era main run recorded 4 cycles / copied 113,227,216 /
promoted 114,275,776 at 200k — the agent's main-arm numbers to the digit. The
PR arm on my build: 3 cycles / copied 0 / promoted 113,227,216 — the
identical byte count, single-hop. Output hash unchanged; RSS 582 → 464 MB on my
measurements (−20%, matching the claimed −20.2%).

The count-drop deviation from the anti-vacuity signature is correctly
argued, and I checked it rather than accepting it.
My brief demanded
"bytes-copied halves while the collection count stays constant"; the count
dropped 4→3. But the eliminated cycle is main's cycle 4, whose Eden influx was
~1 MB and whose entire content was the second copy of the cohort — cycles 1–3
are unchanged in kind and trigger, and cycle 3 receives the same bytes. This is
promotion doing cadence's cleanup for free, not a cadence tune wearing
promotion's clothes. The refutation of the literal copied_objects > 0 clause
(the change's whole point is copied=0 on this workload) is handled with direct
evidence on the other probes: 12_large_live_set shows live_pct=36 seeds=false — the policy evaluated and declined, so a green ratchet is not a
never-ran green.

The RSS surprise is the best part: −20/−21% where the design note expected
growth. The explanation is mechanical and satisfying — at S=4 the 268 MB cohort
existed twice at peak (Eden from-space + survivor to-space); promoting on first
copy removes the duplicate. Best performance AND best RSS on the same change,
which is the plan's goal stated as a diff.

Sabotage re-verified: seed made a no-op → exactly the 2 claimed tests red;
restored, 1,886/0 full suite. All four lint gates + fmt clean here.

This is also the first GC-pacing change properly gated since 2026-08-01
official check on the #7609 baseline, both arms, semantic cells bit-identical,
run as designed. The self-reference proof (the census comes from reachability;
neither mark nor sweep reads the threshold; the threshold is consulted only on
the copying path, which the sweep does not run) is the standard #7594's and
#7596's fixed-point traps demanded.

Also noted for the record: the agent's process catch — a git checkout --
restore during sabotage silently discarded an uncommitted diagnostic hunk, and
it re-ran the 500k census against a rebuild of the committed tree before citing
numbers. That is the #7547 lesson (commit before destructive restore) applied
without being told.

#7592 remains open for the JSON.parse tail (~742 ms at 500k) — with
build_out now at single-hop promotion, parse is the next-largest phase.

proggeramlug pushed a commit that referenced this pull request Aug 8, 2026
The two PR audits established that the pretenure mechanism was correct
but the target was wrong: json_pipeline's minor-moved cohort (~113 MB)
is the runtime-allocated parse tree, not codegen-visible literals
(~12 MB total, ~1 MB live at minor time), and the measured 108 MB -> 0
was a confound -- the base arm predated #7613's promote-on-first-copy
seed, which on current main fires in both arms.

Removed: the born-tenured allocator entry points and their keepalive
anchors (an unused #[no_mangle] + #[used] pair is unused configuration
per the kill-policy, and un-strippable bytes per the hello-size anchor
class), the codegen consumers, and the deferred-page-registration fix
(extracted separately on perf/old-page-registration-deferral, crediting
this PR's finding).

Kept: collect_pretenure_accumulator_locals with its refusal tests, and
the explicit region_runs_once parameter on both fact-graph builders
(module main/init true, function/method/closure false) with a
graph-level test pinning both polarities -- the admission half a future
dynamic-feedback pretenurer needs.
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.

perf(gc): allocation-site pretenuring — long-lived cohorts are copied twice (Eden→survivor→old)

1 participant