Skip to content

Merge train: #9827, #9835, #9837 - #9867

Merged
proggeramlug merged 11 commits into
mainfrom
train131
Sep 6, 2026
Merged

Merge train: #9827, #9835, #9837#9867
proggeramlug merged 11 commits into
mainfrom
train131

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Merge train: #9827, #9835, #9837 — the last three PRs that were blocked on conflicts.

All three conflicted on scripts/gc_runtime_root_holders.json or a ratchet fixture. The inventory conflicts are resolved with a structural merge rather than a text one, because that file cannot be merged textually:

  • its entries are JSON objects, so a line-wise union splices one entry's fields into the middle of another (that produced Expecting ',' delimiter twice while I was working these);
  • the PASS1_MARKED entry carries source pins, which have exactly one correct value per file — a union will silently restore an older branch's hash over main's, which is how a stale window pin nearly shipped in train Merge train: #9808, #9818, #9819, #9820, #9822, #9823, #9826 #9866.

The merge therefore parses both index stages as JSON, unions entries by (file, name), drops holders that landed PRs have deleted (SHAPE_CACHE_YOUNG, removed by #9756), and always recomputes the window pins from the working tree. With that, #9827 and #9835 resolved first try and #9837 then picked clean.

Validation

64/64 lint gates; perry-runtime, perry-codegen, perry-hir, perry-stdlib — all green, 0 failures.

Ralph Küpper and others added 11 commits September 6, 2026 11:13
`gc_check_trigger` runs on every `gc_malloc`, and `gc_budgeted_due_trigger`
resolved eleven raw `thread_local!` declarations one `_tlv_get_addr` call at
a time. Measured with `sample` on the compiled claude-code TUI streaming a
3300-char reply (14,578 active main-thread samples, callers resolved by an
explicit ancestor walk): `_tlv_get_addr` was 380 main-thread leaf samples,
71 of them with `gc_budgeted_due_trigger` as the immediate caller, 36 in
`old_page_account_dirty_slots`, 31 in `scan_dirty_object_slots`, 27 in
`gc_malloc_header_is_tracked`.

Sixty-seven declarations move to `crate::perry_thread_local!`.

Why they were still cold is a measurement bug in the gate, not an oversight:
`scripts/check_thread_locals.py` ratchets on raw `thread_local!` BLOCKS per
file, and a block holds any number of declarations — so `gc/policy.rs`
counted as 6 while declaring 28, and adding a `static` to a recorded block
passed silently. In the same unit as the hot side, main was 318 hot against
339 cold declarations. The gate now ratchets on declarations (385/272) and
`--self-test` gained the direction that catches it.

`ARENA_TOTAL_BYTES`, `BLOCK_POOL` and `BLOCK_POOL_BYTES` stay raw and say so:
they are read from `Arena::new`, which runs as `tls_hot::fill`'s first
provider, so a `HotKey` there re-enters `fill` — which has not yet written
the `temp_roots` field it gates on — and re-runs `ARENA`'s initializer
without bound.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
`gc_check_trigger` runs on every `gc_malloc`, and `gc_budgeted_due_trigger`
resolved eleven raw `thread_local!` declarations one `_tlv_get_addr` call at
a time. Measured with `sample` on the compiled claude-code TUI streaming a
3300-char reply (14,578 active main-thread samples, callers resolved by an
explicit ancestor walk): `_tlv_get_addr` was 380 main-thread leaf samples,
71 of them with `gc_budgeted_due_trigger` as the immediate caller, 36 in
`old_page_account_dirty_slots`, 31 in `scan_dirty_object_slots`, 27 in
`gc_malloc_header_is_tracked`.

Sixty-seven declarations move to `crate::perry_thread_local!`.

Why they were still cold is a measurement bug in the gate, not an oversight:
`scripts/check_thread_locals.py` ratchets on raw `thread_local!` BLOCKS per
file, and a block holds any number of declarations — so `gc/policy.rs`
counted as 6 while declaring 28, and adding a `static` to a recorded block
passed silently. In the same unit as the hot side, main was 318 hot against
339 cold declarations. The gate now ratchets on declarations (385/272) and
`--self-test` gained the direction that catches it.

`ARENA_TOTAL_BYTES`, `BLOCK_POOL` and `BLOCK_POOL_BYTES` stay raw and say so:
they are read from `Arena::new`, which runs as `tls_hot::fill`'s first
provider, so a `HotKey` there re-enters `fill` — which has not yet written
the `temp_roots` field it gates on — and re-runs `ARENA`'s initializer
without bound.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
Rebuilt from empty on every minor and reaching ~1,000 entries, it walked
hashbrown's growth ladder and paid a RawTable::reserve_rehash at each
power-of-two boundary: 217 leaf samples in reserve_rehash on a 3300-char
claude-code reply (1.5% of the turn), 111 under PtrHashSet::insert and the rest
under run_copied_minor_attempt and restore_surviving_dirty_coverage.

Same treatment and same justification as PREVIOUS_SURVIVOR_ESTIMATE next to it:
the count is strongly autocorrelated between adjacent cycles, over-estimating
costs only untouched reserved bytes, under-estimating falls back to ordinary
growth, and the estimate is capped so one huge cycle cannot make every later one
reserve unboundedly.

[gc-dirty-covered] reports len/capacity/presized_to per minor so the mechanism
is counted rather than assumed.

Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
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