Skip to content

perf: kernel-scale indexing campaign (-61% wall, deterministic output)#921

Merged
DeusData merged 17 commits into
mainfrom
perf/futile-nap-fast-exit
Jul 6, 2026
Merged

perf: kernel-scale indexing campaign (-61% wall, deterministic output)#921
DeusData merged 17 commits into
mainfrom
perf/futile-nap-fast-exit

Conversation

@DeusData

@DeusData DeusData commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Performance and determinism campaign, measured end-to-end on a linux-kernel-scale corpus (8.5M nodes / 15.5M edges):

  • Full-mode index wall clock 1015.7s -> 396.7s (-61%), throughput 8.4k -> 21.5k nodes/s.
  • Indexing output is now fully deterministic: repeated multi-threaded runs produce byte-identical node and edge sets (previously node labels and ~2k edge lines flickered per run, and parallel runs lost edges vs sequential).
  • Peak RSS 25.1 -> ~24 GB at this scale, plus ~4 GB dump-phase headroom at 2x scale (index release reordering).

Highlights per commit: extraction back-pressure futility latch; supervised-worker fast exit; C-LSP negative-lookup memo; Python instance-field overlay (fixes a sealed-registry data race / cross-file UAF); registry short-name + trait indexes; file-hash batch upsert + persist-phase profiling spans; conditional shared rust registry (lazy) + registration-loop O(1) type map + macro-expansion dedup; parallel determinism fixes (canonical collision winners, sequential canonical admission, canonical import/similarity winners); semantic vectors quantized via rotated 4-bit scalar quantization (RaBitQ-family, from-paper, dependency-free) with a permanent estimator-error gate; registry/token borrow-don't-copy; gbuf dense by-id array + dead-field removal.

Includes one deliberately-RED reproduction (repro_seq_parallel_equivalence, non-gating board) tracking the pre-existing sequential-vs-parallel pipeline divergence, and a reverted experiment (dual-ended extraction queue) kept in history with its falsifying measurement.

All suites green locally (ASan/UBSan); graph equivalence verified by sorted edge-set byte-diffs at each step.

DeusData added 17 commits July 6, 2026 14:04
Two wall-clock fixes from a linux-kernel (8.5M nodes) profiling run:

1. Extraction back-pressure futility latch: when a full collect+nap cycle ends
   still over the RSS budget, the resident floor (accumulated graph + retained
   sources) -- not in-flight transients -- holds the memory, so napping again on
   every subsequent pull reclaims nothing and only idles workers (measured: one
   full cycle per pull, ~390 s at 79% avg CPU across 63k pulls). Latch bp_futile
   after the first futile cycle, proceed with the designed soft overshoot, and
   re-arm via the cheap per-pull probe once RSS drains under budget. WARN
   mem.backpressure.futile exactly once per latch event (0->1 transition).

2. Supervised-worker fast exit: the index worker piecemeal-freed a multi-GB
   graph (and store) right before process death (measured: minutes of free()
   in the tail). In worker role, skip cbm_pipeline_free and _Exit after the
   response is written and flushed; the OS reclaims memory wholesale. In-process
   paths (tests, kill switch, degrade) free normally, keeping ASan/LSan
   meaningful.

Reproduce-first: pipeline_backpressure_futile_nap_disengages (64-file fixture >
MIN_FILES_FOR_PARALLEL so the parallel gate actually runs, CBM_MEM_BUDGET_MB=1,
deterministic nap-cycle counter; engagement guard cycles>=1 prevents a vacuous
pass). RED on the old gate: 64 cycles (one per pull) > bound cores+2. GREEN with
the latch. Suites: pipeline 216, mcp 140, subprocess 17 -- all pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…BM_PROFILE

The worker's msg=prof pass/sub-phase report is written only to its supervisor
logfile, which was deleted on every clean exit -- profiling a successful run was
impossible (the log survived only on failure). Keep it when profiling is active
and echo the path via index.supervisor.profile_log. Default behavior unchanged:
clean-run logs are still pruned when CBM_PROFILE is off.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Kernel-scale profiling put ~87% of the 361 s cross-file resolve phase inside
c_lookup_member_depth's miss cascade: in macro-heavy C nearly every member
lookup misses, identical (type_qn, member) pairs miss millions of times, and
each miss re-paid an arena_sprintf("%s.%s") (the strlen storm) plus the
O(type_count) short-name registry scan.

Fix, semantics preserved exactly:
- Per-file negative memo (open-addressing uint64 set) on (type_qn, member),
  active only at depth 0 under the shared READ-ONLY Tier-2 registry, where the
  registry-only cascades are pure functions of (type_qn, registry). The direct
  method lookup always runs first (collision/staleness guard) and the
  context-dependent scope-alias path is always evaluated; only the provably-
  NULL module-prefix/base-class/short-name cascades are skipped on a memo hit.
- Module-prefixed QN retries build into a stack buffer via cached
  module_qn_len (arena_sprintf only as truncation fallback).

Soundness: cbm_c_build_cross_registry finalizes then sets read_only=true;
cbm_registry_add_func/add_type hard-return when read_only, so no miss can
flip to a hit during resolve. Suites: c_lsp 748/0 (incl. the tier2 read-only
guards), pipeline+registry+scope+semantic 656/0. Graph equivalence verified
on fs/xfs: nodes 13341 / edges 58559 byte-identical with and without the fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
py_register_instance_field mutated the shared Tier-2 registry through direct
struct writes that bypass the read_only seal: it overwrote field-type pointers
in place and reassigned the sealed type entry's field arrays to memory owned by
the per-file arena. In the fused parallel resolve path (one sealed registry
shared by all resolve workers) that is a cross-thread data race, and it leaves
the shared registry pointing into arenas that die with their file -- a
cross-file use-after-free. The count-based seal guard could not see it because
type/func counts never change.

Python extraction emits no instance fields, so resolve-time discovery is the
ONLY source of self.x / PEP-526 fields; a plain read_only gate would silently
drop real attribute-chain CALLS edges. Fix instead routes discovery into a
per-file field overlay on PyLSPContext (arena-backed, dies with the file) and
has py_lookup_field_depth consult shared-base + overlay at the exact precedence
point of the direct field scan. Mutable per-file registries keep the original
in-place write, byte-identical.

Reproduce-first, both halves pinned:
- seal_py_shared_registry_readonly_fields: RED on the bug (field count 0->1 on
  a sealed registry), GREEN with the overlay.
- pylsp_fused_self_attr_chain_via_overlay: GREEN with the overlay, RED under a
  simulated plain gate (self.b.work() CALLS edge lost) -- proven both ways.
Suites: c_lsp 749, py_lsp 63+45 stress/scale/bench, pipeline 216, registry 54,
scope 10, type_rep 19 -- zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Rust cross-file resolver's trait-method, sole-trait-impl, and free-func
fallback loops scanned the ENTIRE type/func registry per call site on a miss.
On kernel-scale registries (~10^6 entries, macro-inflated) four tiny trait-heavy
rust files burned ~96 s EACH (~200k eval-capped call sites x full-registry
strcmp scans).

Fix: two hash indexes built at registry finalize, mirroring the existing
bucket+entries idiom -- fnv1a(bare(embedded_type)) -> chain of declaring type
indices (trait -> implementers), and fnv1a(short_name) -> chain of free-func
indices. Chains preserve ascending registry order (first-match tie-break
unchanged); allocation-free iterators fall back to the post-finalize tail and
to a full scan on unfinalized registries, so the candidate set is identical in
every mode. The four rust loops now walk the iterator and re-apply their exact
original predicates -- byte-identical results, loop bodies untouched.

Verified: graph equivalence on the kernel rust/ subtree (nodes 9654, edges
44798 identical with and without the change); registry_short_name_indexes unit
test pins iterator contents/order/dedup; suites c_lsp 750, rust_lsp 505,
registry 54, pipeline 216 -- zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
dump_and_persist_hashes upserted file hashes one statement per file --
~89k autocommit transactions (one fsync each) on the linux kernel. Switch to
the existing cbm_store_upsert_file_hash_batch (same cached upsert inside one
begin/commit, rollback on error): identical tuples and replace semantics, one
fsync. OOM fallback to the per-file path retained.

Also instrument the whole post-dump tail with CBM_PROFILE spans
(persist/1_reopen .. 6_artifact_export) -- this stretch was ~60 s unattributed
on kernel-scale runs; the FTS5 backfill over all node names is now measurable
before any optimization decision.

New deterministic guard: store_file_hash_batch_equals_loop asserts the batched
path produces byte-identical file_hashes rows to the per-file loop. Suites:
pipeline 216, store_nodes 56, store_edges 25, store_search 65, store_arch 52,
store_bulk 3, store_pragmas 7, store_checkpoint 1, incremental 161 -- zero
failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Multi-threaded indexing loses edges vs single-threaded and is
run-to-run non-deterministic (fs/xfs: ST 58573 edges vs MT 58561 on the
first divergent run; rust subtree flickers 44445-44857 across runs).
Discovered while hardening perf-change equivalence checks; also the
reason raw node/edge counts are not a valid equivalence gate.

The guard indexes a corpus once single-threaded (reference) and K=4x
multi-threaded (fresh store each run) and asserts the SORTED
(source_qn|type|target_qn) edge sets are all identical. RED today;
GREEN if and only if MT output is deterministic and equals ST.

Calibration note (false-guard avoidance): three escalating synthetic C
corpora (300/500/600 files, dense cross-file calls, token-identical
bodies) stayed fully deterministic -- the similarity pass emits zero
edges on synthetic C, and the race needs real-code edge diversity. A
synthetic guard would pass on buggy code, so this repro uses the
smallest real corpus that reproduces (kernel fs/xfs) and SKIPs when the
corpus is absent. Suspected root causes documented in the test header
(per-worker edge-buffer merge, gbuf edge dedup check-then-insert,
symmetric SIMILAR_TO emission, resolve ordering). Fix deferred; this
test is its guard.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…files

Profiling the kernel showed 4 tiny trait-heavy rust files at ~90 s EACH in
cross-file resolve. Live call trees put the time not in lookups but in
REGISTRY CONSTRUCTION: cbm_pxc_filter_defs_for_file returns NULL for these
files (their path-derived module QNs never match the def_module_qn index
keys), so each one built its own registry over the ENTIRE project def set
(millions of entries at kernel scale).

Original premise corrected during implementation: only 4/87 rust files (on
the kernel rust subtree) hit the NULL-filter path; 83/87 resolve against
proper import-filtered subsets. A registry shared across ALL rust files
would hand the subset files a superset and could add edges. The fix is
therefore CONDITIONAL, byte-identical for every file by construction:

- filter NULL  -> ONE shared all_defs registry (built once in pipeline.c,
  finalized, read_only-sealed; module_qn=NULL is safe: an instrumented probe
  found 0 NULL def_module_qn across all_defs, so the per-file def_mod
  fallback never fired anyway). Kills the amplifier.
- filter SUBSET -> today's per-file subset build, unchanged.

The cross-crate Cargo manifest reaches the shared path through
cbm_pxc_get_rust_manifest() (same worker-thread thread-local the fallback
read), preserving cross-crate resolution exactly.

Byte-identity verified empirically: single-threaded sorted edge-set diff on
the kernel rust subtree, fix off vs on -- zero difference outside
SEMANTICALLY_RELATED edges, which self-differ between two runs of the SAME
unfixed binary (the pre-existing semantic non-determinism guarded by the
repro_parallel_edge_determinism RED test). New suite test
rustlsp_shared_registry_resolves_like_per_file exercises the shared path.
Suites: rust_lsp 506, c_lsp 750, registry 54, pipeline 216, java_lsp 94,
kotlin_lsp 78, php_lsp 278, store_nodes 56 -- zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Follow-up to the conditional shared registry: the eager pipeline.c build paid
the O(all_defs) construction and a multi-million-entry registry RSS on every
run that contains rust files, even when every rust file filters to a proper
import subset and the shared registry is never consulted. Build it lazily
instead: the first worker that hits a NULL-filter rust file takes a mutex,
double-checks, builds into a dedicated arena, seals and atomically publishes;
later null-filter files reuse it and subset files never trigger it. Verified
the build fires exactly once (scale log: types=2320 funcs=8860 on the kernel
rust subtree) and the shared-path unit test still resolves identically to the
per-file build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Multi-threaded indexing produced a different graph on every run (xfs: node
labels flipping Class/Function/Macro, ~2k edge lines differing across USAGE/
CALLS/IMPORTS/SIMILAR_TO/SEMANTICALLY_RELATED) and lost edges vs repeated
runs. Five root causes, all order- or scheduling-dependence:

1. gbuf QN collisions resolved last-wins in MERGE ORDER: in C a struct, a
   function and a macro can share one name, and the QN scheme does not encode
   entity kind, so whichever entity a worker merged last overwrote the node's
   label/name/lines/props (the flickering node set that fed every pass). Both
   cbm_gbuf_upsert_node and the parallel-merge path now keep the SAME entity's
   refresh semantics but resolve cross-entity collisions by a canonical
   content rule -- lexicographically smallest (label, file_path, start_line,
   name) -- and keep the label/name secondary indexes consistent when the
   survivor changes (the old overwrite left nodes listed under their original
   label, which is how the flicker reached find_by_label consumers).
2. Semantic-edges admission raced per-node max_edges budgets on shared atomic
   counters under dynamic scheduling: scoring stays parallel and pure; pairs
   carry (func index, candidate rank); one sequential pass admits in canonical
   order. funcs[] itself is sorted by qualified name up front (the label-index
   scan order is merge order).
3. Import resolution's symbol-name fallback returned the FIRST same-named node
   in registration order; now the lexicographically-smallest-QN candidate.
4. Similarity entries inherited merge order into LSH chains + per-node cap
   truncation, and pair ownership keyed on run-varying node ids; entries are
   sorted by QN and ownership compares QNs (lsh entries carry the QN).
5. Call-neighbor context (semantic vectors + tokens) truncated MAX_CALLEES
   subsets in edge-array order; neighbor names are sorted first.

Verified: four consecutive multi-threaded kernel-xfs runs produce byte-
identical sorted node AND edge sets (13341/58598, zero diff). The repro guard
repro_parallel_edge_determinism (MT x K mutual equality) is GREEN; a new
deliberately-RED repro_seq_parallel_equivalence tracks the remaining OPEN bug
that the sequential and parallel pipelines systematically disagree (~3459
USAGE / ~1666 WRITES lines on xfs) -- separate code paths, fix deferred.
Suites: pipeline 216, c_lsp 750, rust_lsp 506, py_lsp 63, java 94, kotlin 78,
php 278, registry 54, store_nodes 56, incremental 161, semantic 32, simhash 24
(one test updated: SIMILAR_TO pair direction is now canonical by QN), mcp 140,
subprocess 17 -- zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Macro-expanded kernel rust asks the same failing (receiver, method) /
(trait, method) questions thousands of times per file; each repeat re-ran the
full registry-pure cascade (embedded-impl walk + trait-default tail) against
the shared 1.4M-entry registry -- ~63 s per trait-heavy file even after the
build-once registry and the trait indexes.

New internal/cbm/lsp/lsp_neg_memo.h: a generic, arena-backed negative-lookup
memo for LSP resolve cascades (open-addressing u64 set, site-tagged FNV pair
keys, lazy alloc, grow-by-rehash). Gated on a SEALED registry (read_only), so
a miss is a pure fact of (registry, query); the cheap direct lookup stays
before the memo check so a hash collision can never hide a real hit. Any
language wires it in a few lines -- rust first (rust_resolve_trait_method and
rust_find_sole_trait_impl, both proven to read nothing but the registry and
their query strings); the C resolver's bespoke memo can migrate, and the
audit's php/c#/java/kotlin cascades are wiring candidates.

Only the full miss is memoized (zero impls and no trait default), preserving
out_impl_count/out_n fidelity for the ambiguous >=2-impl cases.

Verified: kernel rust subtree indexed before/after -- sorted node AND edge
sets byte-identical (0 diff; exact comparison, now that indexing is
deterministic). rust_lsp suite 506 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… dedup

The four kernel rust files' wall time was a CONSTANT ~63.5 s that no
resolution-side fix moved -- because it was not resolution. The shared
all_defs registry build had two lookups INSIDE its registration loop
(receiver auto-registration probe via cbm_registry_lookup_type, and the
trait-linkage scan), and before finalize the registry has no buckets: both
probes were full linear scans over the types registered so far. On the
1.4M-def shared build that is ~10^10 strcmps ~= 63 s, paid once by the
building file while the sibling null-filter files waited on the once-guard
for exactly the same wall time. This is the checklist's own
lookup-in-registration-loop pattern -- invisible on small per-file builds.

Fix: a CBMIdxMemo (new in lsp_neg_memo.h: exact-match, strcmp-verified
string-key -> index map, arena-backed, first-put-wins to mirror the linear
scans' first-match semantics) maintained alongside every add_type in
rust_populate_cross_registry; both probes become O(1).

Also rides along: a per-root-invocation macro-expansion memo. Kernel
macro_rules are self-recursive (define_sizes! re-invokes itself) and the
expansion fallback ("no pattern matched -- still expand the first rule")
made recursion non-convergent: unbounded BREADTH under the depth-8 guard
turned 2-44 source invocations into ~200k expansions (each a fresh
tree-sitter parse of the substituted body). Identical (macro, substituted
text) now expands once per chain; the memo resets at each top-level source
site so per-site attribution is untouched, and the arg re-parse dedup only
applies at recursion depth > 0. Side effect: the eval-step cap no longer
exhausts, so a few previously-truncated real calls now resolve (kernel:
+25 edges).

Verified: kernel rust subtree byte-identical before/after (sorted node and
edge sets, exact); rust_lsp suite 506 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… dead field

Three memory quick-wins from the gbuf compaction research (graph output
verified identical; all delete/incremental paths suite-covered):

1. Release ALL lookup indexes right after build_dump_nodes instead of after
   edge building: nothing later in the dump reads them, and at kernel scale
   they are ~3.8 GB (hash buckets + key strings + pointer arrays) that
   previously coexisted with every dump-side transient — the dump-phase RSS
   peak. At 2x-kernel scale this is ~7.5 GB at exactly the OOM point.

2. node_by_id was a hash table keyed on STRDUP'D DECIMAL STRINGS of the
   int64 id — ~0.44 GB of buckets and key strings, plus a snprintf + strdup +
   hash on every hot find_by_id call. Ids are dense and sequential; a plain
   grow-on-demand pointer array replaces all of it with one bounds-checked
   load.

3. The per-node and per-edge `project` field was write-only (every reader
   uses the store structs or gb->project directly; -Werror confirms) —
   8 bytes x 24M instances ~= 192 MB of dead pointers, deleted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… SQ)

The semantic pass kept four dense 768-float vectors resident per function
(ri/api/type/deco = 12.3 KB per func, ~9.4 GB transient on the linux kernel)
solely so candidate pairs could be cosine-scored after LSH. Replace the
resident floats with 4-bit quantized codes (~0.5 KB each, ~6x): new
src/semantic/rotsq.{h,c} — rotated scalar quantization from the RaBitQ
family of algorithms (Gao et al., SIGMOD 2024/2025), implemented from the
papers in plain C11 (no Eigen/library dependency): a deterministic
XXH3-seeded +-1 diagonal followed by a Fast Walsh-Hadamard rotation
(768 -> 1024 padded) spreads vector mass so per-vector scalar quantization
is near-optimal; the inner product is recovered from the codes with the
exact SQ expansion (one u8 integer dot + four multiplies) — a deterministic
pure function of the codes. Named rotsq, not rabitq: this is the family
core, not the papers' exact codebook construction.

Producers build each dense vector in a worker-local buffer, encode, and
discard; the int8 qvec export is unchanged. LSH signatures are computed
from DEQUANTIZED codes against hyperplanes in the rotated basis — random
hyperplanes are basis-agnostic, so no dense originals are retained and the
phase structure is untouched.

Estimator quality gated by sem_rotsq_ip_error_bounds (seeded vectors:
mean error < 1%, max < 4% of unit scale, ASan-green). Effect on output:
multi-threaded determinism preserved (xfs: two runs byte-identical);
SEMANTICALLY_RELATED edges shift once as scores move within ~1% of the
0.75 threshold (xfs: ~130 -> 203; sampled new pairs are high quality —
alloc/free, read-verify and collapse/insert families).

Suites: semantic 33 (incl. the new estimator gate), simhash 24,
pipeline 216 — zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…rned

Two borrow-don't-copy wins from the layer-duplication analysis (a hot string
was resident 4-5x across extraction arenas, gbuf, registries, defs and
tokens). Graph output verified byte-identical.

1. Resolution registry (registry.c): every registered definition strdup'd its
   qualified name TWICE (exact-map key + by_name array item) and its label
   once (~8.5M label strdups). The by_name items now borrow the exact map's
   single owned key (same lifetime, freed in one place) and labels intern
   into a small per-registry pool (<= ~30 distinct). ~280 MB + ~17M
   allocations on the kernel.

2. Semantic tokens (pass_semantic_edges.c): all_tokens held one strdup per
   token INSTANCE — identical tokens ("xfs", "error", ...) duplicated across
   hundreds of thousands of functions. Each tokenize worker now interns into
   a per-worker pool (key == value == the single owned copy; no contention,
   content-identical strings so downstream corpus/tfidf output is unchanged);
   all_tokens slots borrow. Ownership moves from per-func frees to pool
   frees at pass end.

Suites: semantic 33, simhash 24, pipeline 216, registry 54, incremental 161.
xfs: two MT runs byte-identical, and byte-identical to the pre-change graph.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Phase-aligned RSS attribution located the global peak (24.4 GB at t=47s,
~14 GB transient above the retained floor) INSIDE parallel extraction: the
size-descending queue put the kernel's 12 largest files into 12 workers
simultaneously, each holding a multi-MB source + its 10-50x TSTree + arena.
Every post-extraction optimization sat below this waterline.

Replace the single biggest-first cursor with a dual-ended claim queue over
the same size-sorted list: big_workers = clamp(workers/6, 1, 3) claim from
the BIG end while the rest rush the small end, so at most big_workers giant
working-sets coexist -- a structural bound, not a scheduling heuristic (a
static interleave cannot bound concurrency under dynamic pulls: smalls
finish in ms and workers converge back onto the giants). Giants still start
at t=0, preserving the LPT no-straggler property; workers never idle while
either end has files. Per-file claim flags (atomic exchange) make the
crossover race-free; end-of-queue rescans are O(n) bytes once.

Intra-file parallel parsing was researched and ruled out: tree-sitter has
no such concept (incremental != parallel; one parser per thread is the
model), and data-parallel parsing theory (PAPAGENO through SLE 2025 cyclic
OPG work) requires operator-precedence grammars with local parsability --
none of the 159 vendored GLR grammars qualify, and C's preprocessor makes
byte-chunking unsound.

Output is schedule-independent since the determinism fix and verified so:
xfs sorted edge set byte-identical to the previous commit. pipeline 216
passed. Worst case (all-giant repo) unchanged by design.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…parses"

This reverts commit 0475a880e5735ef5cb0b0d99532e552096e5a2d5.

Measured on the linux kernel: peak RSS unchanged (23.95 vs 24.4 GB, within
run noise) at +22 s wall -- the extraction peak turned out to be monotonic
retained-tree/source accumulation, which is schedule-independent, not the
concurrent giant transients this queue bounded. Net negative as measured;
the retention-side fix is parked for post-release.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData merged commit 617e9c5 into main Jul 6, 2026
9 of 11 checks passed
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