Merge train: #9827, #9835, #9837 - #9867
Merged
Merged
Conversation
`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
This was referenced Sep 6, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train: #9827, #9835, #9837 — the last three PRs that were blocked on conflicts.
All three conflicted on
scripts/gc_runtime_root_holders.jsonor a ratchet fixture. The inventory conflicts are resolved with a structural merge rather than a text one, because that file cannot be merged textually:Expecting ',' delimitertwice while I was working these);PASS1_MARKEDentry carries source pins, which have exactly one correct value per file — a union will silently restore an older branch's hash overmain'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.