embedder: per-type adapter tables, not per-element camelCase and scans (#261) - #264
Merged
Conversation
#261) The value adapter was recomputing pure functions of the type on every element, in both directions: - `checkNoCollisions` is correctly memoized and early-returns, but both callers built its `labels` argument with a fresh `t.fields.map(...)` first — an array allocated per record per element purely to be discarded. - `camelCase(f.label)` per field per element, and per label in both flags arms. It splits, maps and joins: three allocations each. - `t.cases.find(c => c.label === label)` per element, linear over every case. The consumer schema behind #261 has 17 of them. - `t.labels.includes(v)` per element in the enum arm. Four WeakMaps keyed on the type's identity, on the same argument the `checkedLabels` WeakSet above them already makes: type objects are stable for the lifetime of a loaded plan, so this is a one-time cost per type and the tables die with the plan. Both directions read the same tables. `checkNoCollisions` itself is untouched, so its cache-only-on-success behavior and its calling-site `where` are unchanged by construction. The variant table is first-wins on a duplicated label, which is what the `find` it replaces did — deliberately NOT cabi's `caseIndexOf`, whose duplicate handling matches the different scan IT replaced; the comment says so, so the two do not get "deduplicated" into a behavior change. Not touched: `single()` and `tagged()` allocate per variant per element, but removing that means changing the value shape the layers exchange — a contract question for #8's ledger, not a hoist. Measured on the #262 lane (deno, n=10000, interleaved before/after): `lower-ops` improved in every pair, ~9%; `lift-ops` noisier, ~10% by median paired difference. Both directions land around -80 ns/element. The gain scales with case and field count, so a wider schema sees more. Gates: check, test-conventions (goldens byte-identical), test-runtime (696 passed), conformance (0 failed, 0 stale xfails).
lannbot
enabled auto-merge
September 3, 2026 23:25
lannbot
pushed a commit
that referenced
this pull request
Sep 4, 2026
… measure #263/#264/#265 moved the boundary numbers enough that the committed baseline now misleads: the compound-element rows read ~4-5x high, and the block recording them is still headed "pre-#261 optimization" with no "after" anywhere. Adds a 2026-09-04 block alongside the 2026-08-11 one rather than overwriting it — a dated baseline is a historical record, and overwriting it destroys the before/after that makes the numbers mean anything. The new block carries the compound-element table and nothing else, on purpose. This box cannot currently reproduce the calls-per-second table: `send immediate 0` on the node-jspi lane read 780,785/s, then 1,023,625/s, then 521,044/s across three runs whose code differed only by the changes under test. Committing that would be noise with a date on it, and the README's own framing — compare the same lane across commits on one box — is precisely the use it would break. What is known instead is stated as a delta from interleaved before/after pairs (medians of paired differences, reproduced across two passes): send-sync +27%/+32%, send +22%/+28%, recv +34%/+31%. The 2026-08-11 table stays the recorded absolute baseline, labelled as understating the current tree. Stream rows are untouched for the same reason and it is stated: stream-sink at 256 KiB spans 2,900-10,800 MB/s across four interleaved runs with no consistent sign, and none of the three PRs touch the stream<u8> bulk-copy path. Also: the calls-per-second and stream tables padded lane columns to 22 characters, narrower than the longest lane name, so their headers ran together — which is why the committed baseline block is unreadable in exactly that spot. Widened to 26, matching the compound table.
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.
The second half of #261's per-element cost, after #263 fixed the layout half. #261 attributed ~20% of the boundary cost to
runtime/src/embedder/values.ts.What was per-element that shouldn't have been
Both directions (
toHostandfromHost) recomputed pure functions of the type on every element:checkNoCollisions(t, t.fields.map((f) => f.label), ...)— the function is correctly memoized on aWeakSetand early-returns, but both callers built itslabelsargument with a fresh.map()first. An array allocated per record per element, purely to be discarded by the early return.camelCase(f.label)per field per element, and per label in bothflagsarms.camelCasesplits, maps and joins — three allocations each.t.cases.find(c => c.label === label)per element, linear over every case. bench/boundary: a compound-element-type lane (#261) #262's bench schema has 16 cases; the consumer schema behind cabi: compound element types recompute layout per element and per field — ~5 µs/element for a stream<variant-of-records>; 2.6x from memoizing four pure functions #261 has 17.t.labels.includes(v)per element infromHost's enum arm.The change
Four
WeakMaps keyed on the type object's identity — record (cached labels array plus per field{label, js, type, isOption, optionInner}), variant case lookup, enum label set, flags JS names. Same justification as thecheckedLabelsWeakSetdirectly above them in the same file: type objects are stable for the lifetime of a loaded plan, so this is a one-time cost per type and the tables die with the plan.Semantics preserved
checkNoCollisionsis untouched — it still caches only on success, still throws on every call for a colliding label set, and still reports the calling site'swhere. It just gets the cached array instead of a fresh one.findit replaces did. Deliberately NOT cabi'scaseIndexOf(added in cabi: one cached layout node per type, not a walk per element per field (#261) #263), which maps a duplicate to-1because the scan it replaced asserted on exactly one match. The two sites had different pre-existing behavior; each keeps its own, and the comment says so, so nobody later "deduplicates" them into a behavior change.t.labelsdirectly, so its ordering is the source array's.Object.keysorder is observable.Deliberately not fixed here
single()andtagged()each allocate per variant per element (Object.keys, a fresh result object). That is real, but removing it means changing the value shape the CABI and embedder layers exchange — a contract question that belongs in #8's ledger, not in a hoisting PR.Measured
#262's lane, deno, n=10000, interleaved before/after (stash, rebuild bundle, measure, unstash, rebuild, measure), medians of paired differences rather than of arms — the box is noisy and single-run ratios on it are not trustworthy.
lower-ops: improved in every pair, ~9%lift-ops: noisier, ~10% by median paired differenceBoth land around −80 ns/element. The gain scales with case count and field count, so a wider schema than the bench's sees more.
Gates
just check,just test-conventions(32 passed, goldens byte-identical),just test-runtime(696 passed),just conformance(0 failed, 0 stale xfails). No contract change, no published-surface change, no version bump.