Skip to content

perf(runtime): monotone latches make unused-feature type probes free - #7755

Merged
proggeramlug merged 4 commits into
mainfrom
perf/registry-probe-latches
Aug 10, 2026
Merged

perf(runtime): monotone latches make unused-feature type probes free#7755
proggeramlug merged 4 commits into
mainfrom
perf/registry-probe-latches

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The finding

Symbolicated profiles of two unrelated realistic programs show 13% of runtime spent probing side-table registries for features the program never uses.

asyncpipe.ts (an async service pipeline — Map/Set, template literals, Promise.all, closures in loops):

symbol share
set::is_registered_set 3.23%
typedarray::lookup_typed_array_kind 2.45%
buffer::header::is_registered_buffer 2.40%
map::is_registered_map 2.07%
shared_sab::is_shared_sab 1.22%
symbol::is_registered_symbol 0.71%
typedarray_props::typed_array_add* 0.52%
buffer::header::is_uint8array_buf* 0.21%
total 13.0%

That program allocates no typed array, no Buffer, no SharedArrayBuffer, no Symbol. interp.ts (a tree-walking interpreter) shows the same two leaders independently — lookup_typed_ar* 3.4%, is_register* 1.9% — in a program that uses neither. Two unrelated workloads, same tax: this is a generic-path cost, not a workload quirk.

Mechanism

#7474 established the shape for Map/Set: a monotone "has anything ever been registered" flag answers for programs that use neither, with no counter to get wrong. This generalises it into registry_latch::RegistryLatch and applies it to every remaining address-keyed type probe on a generic path.

A probe checks one process-global AtomicBool before touching its table. Idle ⟹ answer "no" immediately. Previously each probe cost at minimum a _tlv_get_addr (Darwin has no local-exec TLS), a RefCell borrow and a hash — and two of them (is_registered_symbol, is_uint8array_buffer) took a process-global mutex on every miss, in every process, for every non-Symbol / non-Uint8Array value.

RegistryLatch deliberately has no disarm. Monotone is the whole safety argument: the only incorrect observation the design can produce is "idle while the table is non-empty". "Armed while every table is empty" is merely slower. There is no counter to get wrong and no unregister/probe race.

Ordering — why it is right

The binding rule, documented on the type: arm() runs BEFORE the registry mutation it advertises, in the registering thread's program order. Arming after the insert opens exactly the window this PR must not create — feature live and reachable, latch still idle, probe takes the fast path and denies a genuinely registered address. This repo already learned that lesson once: js_buffer_register_external latches EXTERNAL_BUFFERS_NONEMPTY before its insert and says why inline.

With the arm placed first:

  • Thread-local tables (BUFFER_REGISTRY, TYPED_ARRAY_REGISTRY, UINT8ARRAY_FROM_CTOR, …): only the arming thread can find their entries at all, and a thread always observes its own prior store. No window exists.
  • Process-global tables (external_buffers, SHARED_SAB_REGISTRY, SYMBOL_POINTERS): a thread can only probe an address it holds, and every route by which an address reaches another thread in this runtime passes through a synchronising edge (the SerializedValue deep-copy queue, the PENDING_THREAD_RESULTS drain — both mutex/channel mediated). The arm precedes the registration, which precedes the hand-off, so the arm is in the reader's happens-before past.

Acquire/Release is therefore stronger than today's routes require — Relaxed would be sound given those edges. It costs one instruction and removes the need to re-audit this file the next time someone publishes a heap address through a lock-free path, so the stronger ordering is what ships.

One ordering bug was found and fixed on the way in: shared_sab::alloc_shared_sab latched SHARED_SAB_NONEMPTY after its registry insert. Harmless before (the address could not escape the call that returns it), but it is the wrong pattern to leave next to a new latch that depends on the right one. It now arms first, and also arms is_registered_buffer's latch — a SAB backing reads as a registered buffer without ever entering BUFFER_REGISTRY, so the local latch has to know about it.

Probe sites latched

Found by grepping the pattern, not by following the profile — several of these never appear in either profile:

probe table call sites previous miss cost
typedarray::lookup_typed_array_kind thread-local + global kind cache 209 cache probe + TLS + hash + negative write-back
buffer::is_registered_buffer thread-local + global mutex + SAB 204 TLS + hash, then a call into is_shared_sab
symbol::is_registered_symbol process-global Mutex 59 mutex lock
buffer::is_data_view thread-local 44 TLS + hash
buffer::is_uint8array_buffer thread-local + global mutex 37 TLS + hash, then unconditional mutex lock
buffer::is_array_buffer thread-local 34 TLS + hash
buffer::is_shared_array_buffer thread-local + SAB 33 TLS + hash
regex::is_regex_pointer / is_valid_regex_ptr / is_registered_regex thread-local 59 TLS + SipHash, after the magic-check miss
buffer::buffer_ab_alias thread-local 19 TLS + hash
buffer::crypto_key_meta thread-local + global mutex 14 TLS + hash, then mutex
buffer::asymmetric_key_meta thread-local 14 TLS + hash
buffer::is_detached_buffer thread-local 12 TLS + hash
buffer::is_secret_key thread-local 12 TLS + hash

typedarray_props::typed_array_addr_from_value and typedarray::is_offheap_sidetable_alloc are fixed transitively — the latter is what puts these probes on every Date / Temporal brand check.

Correctness

Speed was the easy half. registry_latch_probes.rs covers each feature with the case where an ordering bug would hide: take the probe's idle fast path first, then register, then require the probe to find it. Typed arrays, Buffer, Uint8Array, ArrayBuffer, DataView, detached buffers, SharedArrayBuffer (including one allocated on another thread), Symbol, Map and Set all get that treatment, plus an "unregistered address misses every probe" sweep run against a heap-plausible pointer with readable bytes in front of it, so the probes that read a GcHeader are exercised on an arbitrary pointer.

The ordering rule itself is proven rather than asserted: latch_semantics models both orderings against a private latch+table pair. arm_before_publish_is_never_observably_inconsistent shows the correct order has no window; arm_after_publish_is_observably_inconsistent requires the wrong order to produce one. If that second test ever stops failing under sabotage, the rule has stopped being load-bearing and the first test is proving nothing.

Verified the suite discriminates by deleting the arm() from register_typed_array: typed_array_is_found_after_the_idle_fast_path_ran fails with left: None, right: Some(7).


Measured (quiet M1 mini, best-of-N, absolute seconds)

Two commits: the latches, then an inline split. The second exists because the first told me something. After the latches, _tlv_get_addr fell from 19.9% → 14.6% on asyncpipe and 10.8% → 5.5% on interp — the thread-local resolutions were gone — but the probes' own self-time did not move at all (lookup_typed_array_kind 0.267 s before, 0.267 s after). With the latch check behind a non-inlinable cross-module call, what was left was the call. Splitting each probe into an inlinable idle check plus an #[inline(never)] slow path removed it.

Absolute seconds

Interleaved A/B/C on the mini (round-robin so contention hits all three arms equally, best of 7). Baseline is this branch's parent, 27d5358d0, built and measured identically:

program base +latches +inline split total
asyncpipe 1.1140 1.0254 0.9531 −14.4%
interp 4.0032 3.6678 3.4169 −14.6%
iso_miss (canary) 4.4765 4.1227 3.9109 −12.6%

Protected benchmarks, same interleaved run — no regression anywhere, and the allocation-adjacent ones improve:

bench ceiling base +inline bench ceiling base +inline
churn 0.42 0.4129 0.4131 tree 1.67 1.6366 1.6414
churn_alloc 0.38 0.3771 0.3771 tree_wide 2.15 2.1210 2.1154
churn_read 0.03 0.0226 0.0222 retain 0.56 0.5325 0.5317
push_cls 0.36 0.3564 0.3566 retain_wide 1.12 1.0798 1.0793
push_num 0.15 0.1375 0.1440 fib40 0.41 0.3933 0.3929
cycles 0.20 0.1933 0.1921 deeplist 0.26 0.2459 0.2437

Every protected benchmark is inside its ceiling, and fib40 (pure integer compute, no probe on its path) is flat at 0.393 in both arms — the control that says the harness is measuring what it claims to.

(The host was shared with another agent's benchmark campaign throughout. Every number above is best-of-N with the arms interleaved round-robin, so contention lands on all arms equally and the minimum converges on the uncontended time; push_num is the one benchmark whose 0.14 s scale leaves it visibly noisy, and it sits inside its 0.15 ceiling in both arms.)

Profile shares (leaf, PERRY_DEBUG_SYMBOLS=1, _big variants, two agreeing runs each)

asyncpipe_big:

symbol base +latches +inline
typedarray::lookup_typed_array_kind 1.80 / 2.05 2.28 / 2.31 — gone
buffer::header::is_registered_buffer 1.80 / 1.71 2.06 / 2.28 — gone
shared_sab::is_shared_sab 1.40 / 1.12 — gone — gone
buffer::header::is_uint8array_buffer 0.39 / 0.40 0.29 / 0.20 — gone
symbol::is_registered_symbol 0.68 / 0.73 0.93 / 0.74 0.67 / 0.75
typedarray_props::typed_array_addr_from_value 0.44 / 0.44 0.63 / 0.70 0.47 / 0.45
regex::is_regex_pointer 0.43 / 0.40 0.36 / 0.31 0.29 / 0.46
set::is_registered_set 3.51 / 3.43 3.30 / 3.36 2.78 / 2.95
map::is_registered_map 2.23 / 2.11 1.79 / 1.93 2.27 / 2.32
total 12.69 / 12.40 11.65 / 11.83 6.48 / 6.91
_tlv_get_addr (collateral) 18.18 / 19.35 14.71 / 14.57 15.45 / 16.63

interp_big:

symbol base +latches +inline
lookup_typed_array_kind 3.80 / 3.89 2.30 / 1.82 — gone
is_registered_buffer 1.92 / 1.56 2.09 / 1.85 — gone
is_shared_sab 1.07 / 0.91 — gone — gone
is_uint8array_buffer 0.28 / 0.30 0.21 / 0.22 — gone
is_registered_symbol 0.60 / 0.64 0.76 / 0.64 0.81 / 0.69
typed_array_addr_from_value 0.35 / 0.35 0.41 / 0.43 0.40 / 0.25
is_registered_set + is_registered_map 0.88 / 1.03 0.84 / 0.82 0.79 / 0.89
total 8.90 / 8.67 6.61 / 5.79 2.00 / 1.83
_tlv_get_addr 10.39 / 11.25 5.35 / 5.65 5.77 / 5.48

Every probe for a feature the program does not use is now at zero. The residual is entirely features the programs do use:

  • is_registered_set + is_registered_map — 5.2% of asyncpipe, 0.9% of interp. Both programs use Map/Set, so the perf(runtime): cache the hot thread-locals so one allocation pays one _tlv_get_addr #7474 latch is armed and the lookups are real work. A latch cannot help here; a negative cache could, but Map/Set headers are arena-allocated and recyclable, so the address-keyed memo that is sound for typed arrays (pinned, never moved) would be an ABA hazard for them. Left alone deliberately.
  • is_registered_symbol_slow — 0.7% of both, and neither program mentions Symbol. The runtime's own iterator protocol registers the well-known symbols (Promise.all and for…of both reach js_get_iteratorwell_known_symbol), so the latch is armed from startup. What is left is the process-global Mutex on every miss. That wants a different fix and is the clearest follow-up this work exposes.

Summary by CodeRabbit

  • Performance

    • Improved runtime performance for common checks involving buffers, typed arrays, symbols, regular expressions, and related data.
    • Unused feature checks now return faster with less overhead.
  • Bug Fixes

    • Improved reliability for shared buffers and detached-buffer detection, including cross-thread scenarios.
    • Preserved correct detection after runtime objects are created.
  • Documentation

    • Added release notes covering registry behavior and ordering guarantees.
  • Chores

    • Updated the package version to 0.5.1443.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The runtime adds a monotone RegistryLatch and uses it to skip unused registry probes. Buffer, typed-array, symbol, regex, and related registries arm before publication. Tests cover fast paths, cross-thread visibility, and ordering.

Changes

Registry probe latches

Layer / File(s) Summary
Latch contract
crates/perry-runtime/src/registry_latch.rs, crates/perry-runtime/src/lib.rs, Cargo.toml, CLAUDE.md
Adds RegistryLatch with Acquire reads, Release arming, monotonic state, documentation, tests, module wiring, and version updates.
Buffer registry integration
crates/perry-runtime/src/buffer/*, crates/perry-runtime/src/shared_sab.rs
Adds latches and idle fast paths for buffer-related registries. SharedArrayBuffer publication arms latches before registry insertion.
Typed-array, symbol, and regex probes
crates/perry-runtime/src/typedarray/*, crates/perry-runtime/src/symbol.rs, crates/perry-runtime/src/regex.rs
Adds registration latches and preserves active registry lookups through slow helpers.
Probe and ordering validation
crates/perry-runtime/src/registry_latch_probes.rs, changelog.d/7755-registry-probe-latches.md
Tests unregistered addresses, post-idle registration, cross-thread SharedArrayBuffer behavior, detached buffers, and arm-before-publication ordering. Provides the changelog entry.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant RegistryLatch
  participant Registry
  participant Probe
  Runtime->>RegistryLatch: arm before registration
  Runtime->>Registry: publish address or metadata
  Probe->>RegistryLatch: check latch state
  alt latch is idle
    Probe-->>Runtime: return miss
  else latch is armed
    Probe->>Registry: perform active lookup
    Registry-->>Probe: return registration result
  end
Loading

Possibly related PRs

  • PerryTS/perry#7735: Both changes add process-global atomic fast paths, but this PR targets runtime registries while that PR targets GC polling.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main performance change: monotone latches make unused-feature runtime probes fast.
Description check ✅ Passed The description thoroughly covers motivation, implementation, ordering, tests, and benchmarks, but omits the template's Related issue and checklist sections.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 perf/registry-probe-latches

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.

🧹 Nitpick comments (1)
crates/perry-runtime/src/registry_latch_probes.rs (1)

60-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Make the idle-path coverage deterministic.

These latches are process-global and never reset. Another test can arm a latch before these tests run. In that case, each scratch-address assertion still passes through the slow path. The tests then do not prove that the idle path ran.

Run each idle-path case in a fresh process, or add isolated test state with a global test lock. Assert that the relevant latch is idle before the first probe.

🤖 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/registry_latch_probes.rs` around lines 60 - 216,
Make the idle-path tests deterministic by isolating each case from
process-global latch state: run them in fresh processes or protect/reset them
with a global test lock and isolated state. Before each test’s initial
scratch-address probe, assert the relevant latch is still idle, using the
corresponding typed-array, buffer, symbol, map, or set latch accessors. Preserve
the existing allocation and recognition assertions after proving the fast path
was initially unarmed.
🤖 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.

Nitpick comments:
In `@crates/perry-runtime/src/registry_latch_probes.rs`:
- Around line 60-216: Make the idle-path tests deterministic by isolating each
case from process-global latch state: run them in fresh processes or
protect/reset them with a global test lock and isolated state. Before each
test’s initial scratch-address probe, assert the relevant latch is still idle,
using the corresponding typed-array, buffer, symbol, map, or set latch
accessors. Preserve the existing allocation and recognition assertions after
proving the fast path was initially unarmed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b923ce94-6171-4003-a93a-83ffe7d68eda

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1591a and 0ceda2a.

📒 Files selected for processing (12)
  • changelog.d/7755-registry-probe-latches.md
  • crates/perry-runtime/src/buffer/detach.rs
  • crates/perry-runtime/src/buffer/header.rs
  • crates/perry-runtime/src/buffer/mod.rs
  • crates/perry-runtime/src/lib.rs
  • crates/perry-runtime/src/regex.rs
  • crates/perry-runtime/src/registry_latch.rs
  • crates/perry-runtime/src/registry_latch_probes.rs
  • crates/perry-runtime/src/shared_sab.rs
  • crates/perry-runtime/src/symbol.rs
  • crates/perry-runtime/src/typedarray/mod.rs
  • crates/perry-runtime/src/typedarray_props.rs

Ralph Küpper added 4 commits August 10, 2026 12:21
Two unrelated realistic programs spent 13% of runtime asking side tables
whether ordinary values were typed arrays, Buffers, SharedArrayBuffer
backings or Symbols — features neither program used. Each probe cost at
minimum a `_tlv_get_addr` (Darwin has no local-exec TLS), a `RefCell`
borrow and a hash; `is_registered_symbol` and `is_uint8array_buffer` took
a process-global mutex on every miss.

Generalise the #7474 Map/Set trick into `registry_latch::RegistryLatch`: a
monotone process-global flag armed by the registration site and checked
first by the probe, so a program that never uses the feature answers from
one atomic load. The latch has no `disarm` by construction, so the only
observation it can get wrong is "idle while non-empty" — which the
arm-before-publish ordering rule rules out.

Latched: typedarray kind lookup, buffer registry (incl. the SAB fallback),
Uint8Array-from-ctor, ArrayBuffer, SharedArrayBuffer, DataView, detached
buffers, secret/crypto/asymmetric key metadata, the ArrayBuffer alias map,
Symbol pointers, and the RegExp pointer table.
…appears

The latch removed the thread-local resolutions (visible as a 19.9% -> 14.6%
drop in _tlv_get_addr on asyncpipe and 10.8% -> 5.5% on interp), but the
probes' own self-time did not move: with the latch check behind a
non-inlinable cross-module call, what was left WAS the call. Split each
latched probe into an inlinable idle check plus an #[inline(never)] slow
path, so an unused feature costs one atomic load at the call site.
@proggeramlug
proggeramlug force-pushed the perf/registry-probe-latches branch from 0ceda2a to 75e0d74 Compare August 10, 2026 10:22

@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 the release metadata changes in Cargo.toml at lines 318-318
and CLAUDE.md at lines 11-11: restore the previous [workspace.package].version
and Current Version values in both files, leaving release version updates to
maintainers.
🪄 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: 4da544f5-c2ff-4fdd-8d70-7759b28ab0c3

📥 Commits

Reviewing files that changed from the base of the PR and between 0ceda2a and 75e0d74.

⛔ 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.1442"
version = "0.5.1443"

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 contributor-owned release metadata changes.

This feature PR changes release metadata in both files. Revert both changes and let maintainers update version metadata during release.

  • Cargo.toml#L318-L318: restore the previous [workspace.package].version.
  • CLAUDE.md#L11-L11: restore the previous Current Version value.
📍 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 the release metadata changes in Cargo.toml at
lines 318-318 and CLAUDE.md at lines 11-11: restore the previous
[workspace.package].version and Current Version values in both files, leaving
release version updates to maintainers.

Sources: Coding guidelines, Learnings

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1443

The ordering proof is the part that makes this landable

A monotone latch is only safe if every registration site arms before it publishes. One missed arm() denies a genuinely registered address — a wrong answer, not a slow one. Two things make that checkable rather than reviewed:

The two-sided model. arm_before_publish_is_never_observably_inconsistent shows the correct order has no window, and arm_after_publish_is_observably_inconsistent requires the wrong order to produce one. The note that "if that second test ever stops failing under sabotage, the rule has stopped being load-bearing and the first test is proving nothing" is exactly right — a one-sided proof would pass on a latch that had quietly become decorative.

The per-feature tests take the idle fast path first, then register, then require the probe to find it — which is the only ordering in which a missing arm() shows up at all. Verified it discriminates by deleting TYPED_ARRAY_EVER_REGISTERED.arm() from register_typed_array: left: None, right: Some(7), byte-for-byte your result.

The "unregistered address misses every probe" sweep against a heap-plausible pointer with readable bytes in front of it is a good touch — the probes that read a GcHeader need an arbitrary pointer, not a null.

The design argument holds

Monotone with no disarm is the whole safety case: the only incorrect observation the design can produce is "idle while the table is non-empty", and "armed while every table is empty" is merely slower. No counter to get wrong, no unregister/probe race. That is a strictly weaker obligation than the reconciliation rules this repo has paid for elsewhere.

Shipping Acquire/Release when Relaxed would be sound given today's routes is the right call for the stated reason — it removes the need to re-audit the file the next time someone publishes a heap address through a lock-free path, and costs one instruction.

The shared_sab::alloc_shared_sab fix found on the way is the kind that matters: it armed after its insert. Harmless in isolation, but wrong to leave next to a new latch that depends on the opposite discipline. Arming is_registered_buffer's latch too — because a SAB backing reads as a registered buffer without ever entering BUFFER_REGISTRY — is the non-obvious half.

The second commit exists because the first measured honestly

After the latches, _tlv_get_addr fell 19.9% → 14.6% (asyncpipe) and 10.8% → 5.5% (interp) — but the probes' own self-time did not move at all (lookup_typed_array_kind 0.267 s → 0.267 s). Reading that as "what is left is the call" rather than declaring victory on the TLS number is the difference between a 13% claim and a real one. Hence the inlinable-idle-check / #[inline(never)]-slow-path split.

Two of these took a process-global mutex on every miss, in every process, for every non-Symbol / non-Uint8Array value (is_registered_symbol, is_uint8array_buffer). That is the sort of cost that never shows up in a microbenchmark because every microbenchmark uses the feature.

Finding the sites by grepping the pattern rather than following the profile is also right — several latched probes appear in neither profile, and those are exactly the ones that would have been left behind.

Gates 21/21.

@proggeramlug
proggeramlug merged commit 88aa492 into main Aug 10, 2026
1 of 16 checks passed
@proggeramlug
proggeramlug deleted the perf/registry-probe-latches branch August 10, 2026 10:27
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