Skip to content

perf(store): skip per-call allocations on the store setter path - #4635

Open
brenelz wants to merge 3 commits into
solidjs:nextfrom
brenelz:perf/store-write-alloc
Open

brenelz wants to merge 3 commits into
solidjs:nextfrom
brenelz:perf/store-write-alloc

Conversation

@brenelz

@brenelz brenelz commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Every store setter call allocated at least two Sets: writeScopes = new Set() for the write scope, and a written-keys Set per record it touched in touch(). Most setters write one store and one key, so on the single-key write path these Sets were most of the per-call garbage.

  • writeScope now holds the setter's scope in a plain variable. The writeScopes Set is created only when a child proxy from another store joins the draft (draftServe). inDraft, the get trap and the shallow-view check read writeScope first.
  • t.wk now holds a bare key for a setter's first write to a record. A second key creates the Set. When a later setter in the same batch writes the record, the chain tail u can be that bare key. The three readers that walk written keys handle both forms: overlayRebuilds, the privatize branch of foldTarget, and notifyWrites (which passes the list on to reportReplacedContainers).
  • Array writes always touch the index and then length, so they still create the Set. It is seeded with .add().add() instead of an array literal, so no temporary array is allocated.

I also tried swapping pendingNotify with a spare Set instead of spreading it into an array and clearing it. I left it out: it cost 40 B minified for a 2–4% gain, because V8's Set.clear() allocates a new backing table anyway.

Numbers

Local prod builds of @solidjs/signals, next (456e6d5) and this branch loaded in one process and run interleaved, 5 rounds, median ms per iteration. The machine was under load from other jobs, so the base-vs-base column shows the noise level.

bench next this PR change base vs base
write-floor (2,000 one-key stores, one write each, then flush) 0.506 0.385 −22.7% +1.0%
input burst (200 one-key writes, each flushed) 0.427 0.326 −13.8% −5.1%
array write (500 four-item arrays, one index each) 0.164 0.153 −7% —
store root write (20,000-key record) 0.032 0.029 −10.9% −4.5%
projection root write 0.045 0.043 −4.8% 0.0%
projection nested write 0.038 0.039 −3.3% +0.7%
selection toggle (1,000 keyed readers) 0.386 0.398 +3.1% −0.5%

An earlier run on the same base gave write-floor −21.5%, input burst −8.8% and selection toggle −2.7%. The selection toggle difference is within noise.

Size

node size.mjs --no-gate --json for head and base, then node gate.mjs head.json base.json, measured after merging next at ab29345. Four scenarios that include stores grew. None is a frozen floor, so this PR raises their caps and recorded minified sizes, with a ledger line in scenarios.js for each.

scenario minified brotli cap
signals: + createStore +182 B +79 B 15.03 KB to 15.11 KB
app: hydrating + every store primitive family +192 B +64 B 32.96 KB to 33.02 KB
app: compiled CSR +191 B +62 B 26.51 KB to 26.57 KB
app: compiled hydrating +189 B +36 B 32.79 KB to 32.82 KB

Each new cap is the measured brotli size plus 10 B, rounded up to 0.01 KB, and each recorded minified size is the measured one. The base measured locally matches what next recorded in #4645. With the new caps, the gate passes.

How did you test this change?

  • pnpm build, then npx vitest run in packages/signals: 466 files, 6372 tests pass. tsc reports no errors in src or in the new test.
  • The new test, tests/store/written-key-chain-privatize.test.ts, covers the privatize branch of the fold when the written-key chain ends in a bare key. To reach that branch, the descendant has to be written first in the batch so that its fold path-copies the row. Both cases fail if writtenList drops bare keys.
  • One existing test, createStore.test.ts "wrapped nested class", fails if the joined-scope Set is never created, so the writeScopes path is covered.
  • npx vitest run in packages/web: 236 files, 1669 tests pass.
  • A local differential fuzz of multi-setter batches checked the final store against a plain-object model over 3 seeds of 3,000 trials each, with 0 mismatches on both next and this branch. The fuzz is not part of the PR.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d190aa7

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

This PR includes changesets to release 15 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
board-example Patch
canvas-example Patch
server-dbmon-example Patch
todos-server-example 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

@codspeed

codspeed Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 13.48%

3 warnings

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 3 benchmarks with a significant change ran on different hardware

Their base and head results were measured on machines with a different CPU, core count or memory, so the changes might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚠️ 3 benchmarks with a significant change ran in a different software environment

Their base and head results were measured with a different OS or toolchain, such as compiler, runtime or library versions, so the changes might come from this difference rather than from the code. Open an affected benchmark to see what changed.

See affected benchmarks

⚡ 3 improved benchmarks
✅ 197 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ steady state: owned backings (#3360) 6.5 ms 5.7 ms +15.03%
⚡ projection derive: write one NESTED field (reference) 2.4 ms 2.1 ms +14.52%
⚡ fresh stores: create + first write + first commit (reporter shape) 15.5 ms 13.9 ms +10.94%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing brenelz:perf/store-write-alloc (d190aa7) with next (ab29345)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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