Skip to content

fix(gc): route the census thread-locals through hot TLS - #9781

Closed
proggeramlug wants to merge 2 commits into
PerryTS:mainfrom
proggeramlug:fix/census-hot-tls
Closed

fix(gc): route the census thread-locals through hot TLS#9781
proggeramlug wants to merge 2 commits into
PerryTS:mainfrom
proggeramlug:fix/census-hot-tls

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

The break

scripts/check_thread_locals.py fails on main, which takes the self-test-checkers
job of the TLS Budget workflow down — on main itself (run 33945210479) and on every open
PR, including unrelated ones:

thread-local policy check FAILED:

  crates/perry-runtime/src/gc/census.rs: 2 raw `thread_local!` block(s), none allowed.
    Use `crate::perry_thread_local!` — same syntax, same `.with()` at every call site,
    and the address lands in this thread's hot cache instead of costing a
    `_tlv_get_addr` call (#7469).

In that workflow the expensive tls-budget job passes (13m19s); only this 12-second
hermetic checker fails.

Why it is a real finding, not just a ratchet

gc::census is compiled unconditionally — crates/perry-runtime/src/gc/mod.rs:247 declares
pub(crate) mod census; with no cfg — so both blocks are in shipping builds and pay
_tlv_get_addr per access on Darwin, which is exactly the cost #7469's policy exists to
prevent. (The census is runtime-gated by PERRY_GC_CENSUS, but that gate is read through
ARMED, one of these very thread-locals.)

The file's third thread_local! is #[cfg(test)] and is out of scope by construction, as
the checker's own header documents — hence "2 raw blocks", not 3.

This is the same treatment 12efed122 ("perf(runtime): route four recent thread_local
declarations through hot TLS") gave four other recent declarations; census.rs was missed.

The change

Both blocks become crate::perry_thread_local!. The macro takes the same syntax — multiple
statics per block, doc comments, const { … } initialisers — and yields the same .with()
API. All nine call sites of ARMED / SEQ / LABEL / PASS1_MARKED already go through
.with() (verified: zero non-.with() uses), so nothing else changes.

Verification

Checker, run locally against the tree:

before:  thread-local policy check FAILED: ... 2 raw `thread_local!` block(s)   rc=1
after:   thread-local policy OK: 304 hot declarations, 122 raw blocks in
         87 recorded cold files, capacity 768                                   rc=0

Compile, on linux-x86_64 — cargo check -p perry-runtime --all-targets:

ASSERT converted: 2 (expect 2)
RC=0
real check of perry-runtime: 1        # a genuine `Checking perry-runtime v…`, not a cache hit
census.rs diagnostics: 0
Finished `dev` profile in 30.68s

Note: with RUSTFLAGS=-D warnings this branch does not build, but for an unrelated
reason — the three clashing_extern_declarations errors fixed by #9776, none of which
come from census.rs. That is why the compile check above is run without -D warnings;
once #9776 lands, both hold together.

Scope

One of several independent breakages on main, which is currently red across six workflows.
Also open from this sweep: #9776 (warnings), #9777 (cargo-test), #9780
(check + ext-link).

https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2

Summary by CodeRabbit

  • Performance

    • Reduced runtime overhead for garbage-collection census tracking, particularly on Apple platforms.
  • Reliability

    • Restored automated checks that help detect thread-local storage regressions before release.

Ralph Küpper added 2 commits September 5, 2026 07:16
`scripts/check_thread_locals.py` fails on main, taking the
`self-test-checkers` job down on every PR:

    thread-local policy check FAILED:
      crates/perry-runtime/src/gc/census.rs: 2 raw `thread_local!` block(s),
      none allowed.

`gc::census` is compiled unconditionally (`pub(crate) mod census;`), so its
two ungated `thread_local!` blocks are in shipping builds and pay
`_tlv_get_addr` per access on Darwin — exactly what PerryTS#7469's policy exists to
prevent. The third block in the file is `#[cfg(test)]` and is out of scope by
construction.

Converted to `crate::perry_thread_local!`, which is the same syntax and the
same `.with()` at every call site — all nine uses of ARMED/SEQ/LABEL/
PASS1_MARKED already go through `.with()`, so nothing else changes. This is
the same treatment 12efed1 gave the four other recent declarations; census
was missed.

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

coderabbitai Bot commented Sep 5, 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: Team

Run ID: cc6681ff-9db8-49c0-a048-ea9467f859e2

📥 Commits

Reviewing files that changed from the base of the PR and between 12efed1 and 4367aad.

📒 Files selected for processing (2)
  • changelog.d/9781-census-hot-tls.md
  • crates/perry-runtime/src/gc/census.rs

📝 Walkthrough

Walkthrough

The census module replaces two standard thread-local declarations with crate::perry_thread_local!. The changelog records the Darwin _tlv_get_addr optimization and the restored self-test-checkers job.

Changes

Census thread-local declarations

Layer / File(s) Summary
Replace census thread-local macros
crates/perry-runtime/src/gc/census.rs, changelog.d/9781-census-hot-tls.md
The ARMED, SEQ, LABEL, and PASS1_MARKED declarations now use crate::perry_thread_local!. The changelog documents the optimization and restored checker job.

Estimated code review effort: 1 (Trivial) | ~5 minutes

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #9774, which fixes the same gc/census.rs raw thread_local! breakage and does it more completely — converting the blocks makes gc_runtime_root_holders.py enumerate them for the first time, and #9774 gives PASS1_MARKED an honest verdict (untraced_in_nonmoving_window) plus a gate that verifies the window against the holder's own source. My independent reproduction agrees with the diagnosis, so nothing is lost here.

https://claude.ai/code/session_01YPfnmWZmSpSWpmnoXvH8z2

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