perf(signals): narrow store writes clone by spread; overlay only for wide owned backings (#3360 part 1) - #3367
Conversation
…wide owned backings Part one of #3360. Per write+commit on a one-key store: rc.7 629 ns -> 340 ns steady state; a fresh store's first write+commit 3x cheaper (the reporter's 100k-store shape ~265 -> ~90 ms). - cloneRaw spreads plain-data containers (Object.prototype, every own key an enumerable data property) instead of the descriptor walk: same result, ~1/50th the cost. The one-time accessor scan grades the container (sc 0/1/2) and records its own-key count (kc, bumped by set-trap writes of new keys so a record that grows from {} still graduates). - The #3044 prototype overlay is taken only for WIDE (>32 keys) containers over an OWNED backing. Narrow ones clone cheaper than they overlay (Object.create turns the backing into a V8 prototype and every flatten writes into that prototype); an unowned backing had to be privatize-cloned at commit anyway, so the overlay there paid create + clone + per-key copy. - Grade-2 containers flatten and take overlay first-writes by bare assignment; a non-plain defineProperty through the draft downgrades the grade so descriptor-bearing keys keep the descriptor paths. The remaining per-write cost is the weak-collection registration of each pending backing (~190 ns); that is part two. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: ffcd59e The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Coverage Report for CI Build 34572005825Coverage remained the same at 71.842%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | createStore setter: delete + set one root key (#3044 overlay) |
155.7 µs | 440.8 µs | -64.67% |
| ⚡ | batched: same writes, single commit |
3.3 ms | 1.4 ms | ×2.3 |
| ⚡ | commit boundary: flush after every 2-key setter, no subscribers (#3044) |
14.5 ms | 10.4 ms | +39.36% |
| ⚡ | projection derive: write one NESTED field (reference) |
262.3 µs | 224 µs | +17.1% |
| ⚡ | style store object, replaced × 500 |
48.6 ms | 44.8 ms | +8.54% |
| ⚡ | style store object, mutated in place × 500 |
42.8 ms | 39.8 ms | +7.42% |
| ⚡ | class store object, replaced × 500 |
45.9 ms | 43.1 ms | +6.69% |
| 🆕 | fresh stores: create + first write + first commit (reporter shape) |
N/A | 22.3 ms | N/A |
| 🆕 | steady state: owned backings (#3360) |
N/A | 11.1 ms | N/A |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/store-write-floor (ffcd59e) with next (27aee36)
`next`'s #3367/#3368 (narrow-store write floor, `$OWNER` stamp) under the A28 seams: createStore 15.15 -> 15.42 KB (15384 B), hydrating + stores 27.90 -> 28.15 KB (28112 B), hydrating 18.48 -> 18.52 KB (18484 B, layout). Core floor unchanged at 22,381. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
`next`'s #3367/#3368 (narrow-store write floor, `$OWNER` stamp) under the A28 seams: createStore 15.15 -> 15.42 KB (15384 B), hydrating + stores 27.90 -> 28.15 KB (28112 B), hydrating 18.48 -> 18.52 KB (18484 B, layout). Core floor unchanged at 22,381. Co-authored-by: Claude via Cursor <noreply@cursor.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Part one of #3360 (does not close it — part two, below, does).
Problem
#3360 reports a ~4× regression rc.0 → rc.7 on the per-write floor of stores: 100k one-key stores, one
setStoreeach, oneflush. Reproduced (prod): rc.0 ~61 ms, rc.7 ~265 ms. Both linear — it's a constant factor from the rc.1 immutable-backing model. Profiled three costs:cloneRawwas a descriptor clone (getOwnPropertyDescriptors+Object.create(proto, descs)): ~530 ns for a one-key object. A spread is ~10 ns and produces the same result for plain-data containers.Object.create(v)turns the committed backing into a V8 prototype (~200 ns), and every later flatten writes into a prototype object (~250 ns/key + validity-cell churn). On an unowned backing (every fresh store's first write) the flatten had to privatize-clone anyway, so that path paid create + descriptor clone + per-key descriptor copy — the reporter's 128 ms first flush.Change
scanAccessorsOncegrades the container (sc: 0 unscanned / 1 scanned / 2 plain data =Object.prototypewith every own key an enumerable data property) and records the own-key countkc(bumped by set-trap writes of keys new to the container, so a{}that grows still graduates).cloneRawspreads at grade 2.kc > 32and the backing is owned; otherwise clone and swap.OVERLAY_MIN_KEYSsits at the measured crossover.definePropertythrough the draft downgrades the grade.Numbers (prod)
nextExisting store benches all improved or held: batched 2-key writes 5.6k → 10.2k ops/s, selection map 3.2k → 3.9k, input burst 3.2k → 4.2k, projection root/nested (#3352) 393k/328k → 465k/459k, commit-boundary flat. New
write-floor.bench.tspins the steady and fresh-store shapes.Behavior note
A
writable: falsedata property defined through a draft used to stay non-writable across later writes only because the overlay never touched it; on the clone path the descriptor walk normalizes it to writable (R51). Enumerability is preserved either way; the test pins that and documents the normalization.Tests / gates
tests/store/write-floor.test.ts(9): symbol keys, non-enumerable keys, custom prototypes, frozen sources, non-plain and accessordefinePropertythrough the draft, growth past the threshold, 500-store batch.#3352clone-spy assertions loosened to≤ 1(spread is not a descriptor clone).next(238); solid 595; web 734.createStore14.77 → 14.92 KB, hydrating+stores 27.39 → 27.52 KB (~150 B of real code; caps bumped with notes).Part two
The remaining ~190 ns is
storeNextLookup.set(pb)+ownedRaw.add(pb)per draft. Removing them (incorrectly, to measure) gives 154 ns/write — under rc.0. The plan is to stamp owned raws with a non-enumerable owner symbol at creation and keep the WeakMap only for user-supplied objects; owned raws are never reachable by user code (snapshotcopies them, there is nounwrap), so the surface is purely internal. Separate PR so each half is bisectable and CodSpeed shows its effect alone.Made with Cursor