Repository navigation
perf(protocol): per-ProtocolFn dispatch cache + skip ReleaseSafe locals fill (ADR-0206) - #86
Merged
Merged
Conversation
…uffer ProtocolFn now carries a DispatchCache (ADR-0206): a seqlock slot memoising the whole resolveProtocolMethod chain (per-type impl, ADR-0187 target, IRecord, Object) per receiver descriptor, keyed on protocol_generation. callProtocolFn used a fresh CallSite per call, so every protocol call re-ran the method-table scan. addProtocolImpl bumps the generation after the membership write so a fill between the two bumps cannot cache stale state. callMethodImpl's 2 KB locals buffer moves into a @setRuntimeSafety(false) wrapper so ReleaseSafe no longer 0xAA-fills it on every call (O-080). Smell-audited: 2: interior mutability via @constcast on a const ProtocolFn handle, documented as a pure memo
New suite protocol_dispatch_cache_test calls each protocol fn first, so the ProtocolFn cache is filled, then extends, re-extends, redefines the record or re-evaluates defprotocol and calls again. Expected values are the JVM's (21/21 on clojure -M); cljw --compare agrees on both backends. Smell-audited: 1: test and ledger only
…bt D-600 The DA review asked why the ~40 host-surface method_table writers need no generation bump (their rows carry protocol_name "" and never match a protocol lookup), why class_descriptors inserts cannot stale a memo (failures are never filled), and what roots the untraced cached value. These are now written beside DispatchCache. D-600 records the non-atomic generation counter (lost bump under concurrent extends, F-015) and the monomorphic per-fn slot, with the polymorphic call-site IC as its finished form. Smell-audited: 1: comments and a debt row only
addProtocolImpl bumps after the membership write (ADR-0206), so the unit test that pinned exactly one bump expects two. Smell-audited: 1: test assertion only
Owner
Author
|
Smoke (run 1, on f301c8e + the test fix in the working tree): zig_build_test_tree_walk, entrypoint_surface, repr_decode, test_reach and zig_fmt_check pass. zig_build_test_vm failed on one unit test, which pinned exactly ONE generation bump per __extend-type!. ADR-0206 adds a second bump after the membership write, so this was expected. Fixed in c243bf8; the tree_walk run, which already had the fix, passes 1415+ tests. Remaining steps are still running. |
tree_walk.zig: #83 (O-064 uninitScratch) and this branch (O-080 safety-off wrapper) both removed the ReleaseSafe 0xAA fill of the callMethod locals buffer. Kept this branch's callMethodImpl/callMethodBody split, with the buffer now taken from uninitScratch: one mechanism for per-call scratch. O-080 ledger row updated to match. data/placement.yaml regenerated. Smell-audited: 2: duplicate mechanism removed at merge, ledger amended
Append-only ledgers resolved as the union of both sides: O-080/O-081 and O-090 in .dev/optimizations.md, D-600 and D-610 in .dev/debt.yaml. Smell-audited: 0: ledger union only
BuddhiLW
added a commit
that referenced
this pull request
Oct 22, 2026
.dev/optimizations.md and .dev/debt.yaml conflicted on every parallel PR on 2026-10-22 (#85, #86 against #87), always as two sides appending rows. git's union driver resolves exactly that; check_yaml and the ledger gates still validate the result. Smell-audited: 1: merge driver for a recurring mechanical conflict
…ngle-threaded wasm32 CI's zig build -Dtarget=wasm32-wasi failed: @atomicLoad/@AtomicStore on a 64-bit Value is not available on wasm32. The single-threaded targets (ADR-0193) have no concurrent reader, so loadMethodVal/storeMethodVal use a plain access there; 64-bit hosts keep the seqlock's atomics unchanged. Verified: zig build -Dtarget=wasm32-wasi builds. Smell-audited: 1: comptime single_threaded branch, the repo's existing idiom
.dev/optimizations.md resolved as the union of both sides (this branch's O-080/O-081 and #85's O-070..O-077); from now on .gitattributes merges it by union. Smell-audited: 0: ledger union only
BuddhiLW
added a commit
that referenced
this pull request
Oct 23, 2026
tree_walk.zig: #86 split callMethodImpl/callMethodBody with a 2 KB uninitScratch locals buffer on the native stack; this branch moved the window into the threadlocal tw_scratch for recursion depth. Kept one mechanism: callMethodImpl is a plain call and callMethodBody borrows an exact-size tw_scratch window, which neither 0xaa-fills nor costs native stack. O-080 ledger row amended. data/placement.yaml regenerated. Smell-audited: 2: duplicate per-call buffer removed at merge, ledger amended
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.
Card CLJW-PERF-PROTOCOL-DISPATCH, ADR-0206. User direction: cljw faster than bb on every bench.
What
ProtocolFnholds one seqlock slot mapping(receiver descriptor, protocol_generation)to the resolved method. It covers the whole chain: per-type impl, ADR-0187 target, IRecord, Object. Before this,callProtocolFnbuilt a freshCallSiteon every call, which re-scanned the method table with string compares. Every writer bumps the generation: extend-type, extend-protocol, extend, ADR-0187 targets,addProtocolImpl(new bump, after the membership write), and type redefine.callMethodImplis now a@setRuntimeSafety(false)wrapper that owns theundefined2 KB locals buffer. ReleaseSafe no longer 0xAA-fills it on every call; that fill was 52% of cycles in this bench.callMethodBodykeeps safety on.test/clj/suites/protocol_dispatch_cache_test.clj, 8 tests and 21 assertions. Each one calls first, then changes what the call must resolve to: extend after a failed first call, re-extend, an Object default displaced by a type-specific extend, alternating receivers, record redefine per vintage, defprotocol re-eval then re-extend, native Long, reify. The JVM passes 21/21, andcljw --compareagrees on tree_walk and vm.DispatchCache. Debt D-600 records the non-atomic generation counter (a lost bump under concurrent extends, F-015) and the monomorphic slot. Its finished form is a polymorphic call-site IC in the VM.Before / after vs bb (bench/vs_bb/workloads/protocol_dispatch.clj, ReleaseSafe -Dwasm)
Host load was 16-28 because it is shared with other lings, so the instruction counts are the stable comparison.
Checks: check_core_surface, check_doc_coverage, check_placement_status and check_epipe_head pass. The smoke result is below.
Smoke: first run failed one unit test, which pinned exactly one generation bump; fixed in c243bf8. The rerun result will be added as a comment.