Skip to content

perf(signals): narrow store writes clone by spread; overlay only for wide owned backings (#3360 part 1) - #3367

Merged
ryansolid merged 1 commit into
nextfrom
perf/store-write-floor
Sep 11, 2026
Merged

ryansolid merged 1 commit into
nextfrom
perf/store-write-floor

Conversation

@ryansolid

Copy link
Copy Markdown
Member

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 setStore each, one flush. 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:

  1. cloneRaw was 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.
  2. The 2.0.0 | slow and steady wins the race #3044 prototype overlay loses on narrow and on unowned 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.
  3. Every pending backing is registered in two weak collections (~190 ns of identity-hash + ephemeron cost per write). Not addressed here — see part two.

Change

  • scanAccessorsOnce grades the container (sc: 0 unscanned / 1 scanned / 2 plain data = Object.prototype with every own key an enumerable data property) and records the own-key count kc (bumped by set-trap writes of keys new to the container, so a {} that grows still graduates).
  • cloneRaw spreads at grade 2.
  • Overlay only when kc > 32 and the backing is owned; otherwise clone and swap. OVERLAY_MIN_KEYS sits at the measured crossover.
  • Grade-2 containers flatten and take overlay first-writes by bare assignment; a non-plain defineProperty through the draft downgrades the grade.

Numbers (prod)

rc.0 rc.7 / next this PR
steady state, 1000 stores × 1000 write+flush rounds 214 ns/write 629 ns 340 ns
reporter's 100k fresh stores, one write + flush 61 ms ~265 ms ~90 ms

Existing 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.ts pins the steady and fresh-store shapes.

Behavior note

A writable: false data 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 accessor defineProperty through the draft, growth past the threshold, 500-store batch.
  • Two #3352 clone-spy assertions loosened to ≤ 1 (spread is not a descriptor clone).
  • signals 1707/1707; prod-tier failure count identical to next (238); solid 595; web 734.
  • Size: createStore 14.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 (snapshot copies them, there is no unwrap), so the surface is purely internal. Separate PR so each half is bisectable and CodSpeed shows its effect alone.

Made with Cursor

…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-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ffcd59e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch

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

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 34572005825

Coverage remained the same at 71.842%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1007
Covered Lines: 772
Line Coverage: 76.66%
Relevant Branches: 790
Covered Branches: 519
Branch Coverage: 65.7%
Branches in Coverage %: Yes
Coverage Strength: 15.01 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Sep 11, 2026

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 6 improved benchmarks
❌ 1 regressed benchmark
✅ 151 untouched benchmarks
🆕 2 new benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

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)

Open in CodSpeed

@ryansolid
ryansolid merged commit 0f14430 into next Sep 11, 2026
6 of 7 checks passed
ryansolid added a commit that referenced this pull request Sep 11, 2026
`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>
ryansolid added a commit that referenced this pull request Sep 11, 2026
`next`'s #3367/#3368 store bytes (via #3337) under the store twins:
createStore 15.45 -> 15.72 KB (15678 B), hydrating + stores 28.50 ->
28.66 KB (28620 B). Core floor unchanged at 22,866.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Sep 11, 2026
`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>
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.

2 participants