Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/map-array-subsequence-fast-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/signals": patch
---

`mapArray` commits a structural change without rebuilding full-length copies of its row bookkeeping: a pass stages only the changed window, lands it into the live arrays in place with the unchanged suffix shifted by a loop, and rebuilds only the mapped array that signals the change downstream. A pass that only removes rows is matched with two pointers and no key map, and the key-chain array is sized to the window rather than to the edit's position. One-item inserts and removals on long keyed lists, every-nth removals and sparse filters no longer allocate several list-sized arrays per edit; the strong-abort contract for a mapper that suspends is unchanged, kept by ordering (verify, create, then mutate) instead of by copying.
85 changes: 67 additions & 18 deletions documentation/benchmarking-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,26 +392,51 @@ campaign that built this and what it found is the
[Store / List Delivery Lane](./performance-experiments.md#store--list-delivery-lane-2026-08-16--2026-09-18-the-octane-board)
in the journal. The mechanics:

**Where.** Fork `ryansolid/octane`, branch `solid-rc8-fixtures-rebase`
(upstream `main` + our Solid fixture commits; rebase it when upstream
moves — the Solid fixtures are ours alone, upstream still pins
2.0-beta.20). Local checkout `~/Development/octane-fork`. A second
checkout, `~/Development/octane`, holds the pre-September experiment
scaffolding and the older results directories.

**Linking `next`.** Keep a worktree of this repo on `origin/next`
(`~/Development/solid-next-bench`), `pnpm install`, `pnpm build` in
`packages/compiler` (native binary, needs cargo), then
**Where.** Fork `ryansolid/octane`, branch `bench/next-linked-oct15`
(upstream `octanejs/octane` `main` at 0.13.8, `91dae8d73`, plus our seven
Solid fixture commits — the shape passes, `board-compare.mjs`, the
`uibench/solid-reconcile` target and its manifest line). Local checkout
`~/Development/octane-fork`. Upstream pins the published rc (rc.14 on
2026-10-15) and never carries our shapes, so **rebase before every board**:
`git checkout -b bench/next-linked-<date> && git fetch origin main && git
rebase origin/main`. Upstream edits the same fixtures' `package.json` /
`vite.config.js` identically to our pin commit, so the only conflicts are
`pnpm-workspace.yaml` and `pnpm-lock.yaml` — take upstream's side for both
(`git checkout --ours`), the pin commit drops out as empty, and the lockfile
is regenerated by the install below. Older branches are kept as they were:
`bench/next-linked` and `solid-rc8-fixtures-rebase` are the 0.2.13-era
boards (Sep 9 / Sep 17). A second checkout, `~/Development/octane`, holds
the pre-September experiment scaffolding and the older results directories.

**Linking `next`.** Keep a worktree of this repo at the `next` commit under
test (`~/Development/solid-next-bench`; `git worktree add -b
bench/next-<date> ~/Development/solid-next-bench <sha>` — a worktree cannot
check out `next` itself while the main checkout is on it, hence the branch),
`pnpm install --frozen-lockfile`, `pnpm --filter @solidjs/compiler build`
(native binary, needs cargo, ~30 s), then
`pnpm turbo run build --filter=@solidjs/signals --filter=solid-js
--filter=@solidjs/web --filter=@solidjs/babel-plugin --filter=@solidjs/compiler`.
--filter=@solidjs/web --filter=@solidjs/babel-plugin` (~25 s).
In the fork's `pnpm-workspace.yaml` `overrides:` block, point `solid-js`,
`@solidjs/web`, `@solidjs/signals`, `@solidjs/compiler` and
`@solidjs/babel-plugin` at `link:../solid-next-bench/packages/<name>`,
then `pnpm install --no-frozen-lockfile`. `@solidjs/vite-plugin` stays
npm and picks the linked compiler up through the override — verify with
then `pnpm install --no-frozen-lockfile` (the fork is on pnpm 12; ~40 s
with a warm store). `@solidjs/vite-plugin` stays npm (3.0.0-next.49 drives
the compiler built from `next` at rc.15 without complaint) and picks the
linked compiler up through the override — verify with
`readlink -f node_modules/.pnpm/@solidjs+vite-plugin*/node_modules/@solidjs/compiler`.
**Keep the override and the lockfile it produces uncommitted**; the
fixture branch must install from the published rc for anyone else.
The 2026-10-15 session found the worktree deleted and the overrides
dangling — the board was unrunnable until it was recreated; check
`ls ~/Development/solid-next-bench` before trusting the link.

**Solid-vs-Solid A/B.** A second worktree at the reference commit
(`~/Development/solid-sep17-bench` at `dd908fc12`, the Sep 17 board's
`next`), built the same way. Flip the five overrides to it, reinstall,
run the suites into one results dir, flip back, run again into another;
the runner rebuilds the fixtures itself. Same machine, same Octane, same
day — this is the only read on "did `next` regress" that does not depend
on the harness staying still between boards (it did not: see gotchas).

**Running.** From the fork root:

Expand Down Expand Up @@ -441,6 +466,21 @@ for the two-sided geomean board (timed ops only). For a single suite,

**Gotchas, each learned the hard way:**

- **The machine must be idle for the whole run, and nobody but the
runner can tell.** `ps -Ao pcpu,etime,comm -r | head` before launching
and again when it finishes. The 2026-10-15 board's last two suites ran
beside a game and a Chrome renderer each holding more than half a core:
every target's uibench numbers came out 4–5× slower than the quick pass
an hour earlier and the Solid/Octane ratio there read 1.90× instead of
1.63× — load does not cancel in the ratio. The reference frameworks'
absolute drift against the previous board (React, Inferno, Svelte,
Preact; pins unchanged) is the tell: 0.97–1.05 on a clean suite, anything
else means rerun that suite.
- Upstream ≥ 0.13 batches the sub-millisecond js-framework ops (`select`,
`swap`, `select_lots`: `BATCHED_OPS` in `js-framework/run.mjs`) inside each
sample. Those ops now read 10–40× smaller than on the 0.2.13 boards for
every framework; compare them only within a run. uibench always
repeated its small cases, so its numbers are comparable across boards.
- `PLAYWRIGHT_BROWSERS_PATH` is set in the agent environment to a sandbox
cache; unset it or Chromium "doesn't exist".
- Long runs and preview servers must be tool-managed background tasks
Expand All @@ -467,13 +507,22 @@ for the two-sided geomean board (timed ops only). For a single suite,
until the fixture has been read.

**Profiling a single op.** Build the fixture unminified
(`npx vite build --minify false --outDir dist-prof`), serve it, and drive
(`npx vite build --minify false --outDir dist-prof`), serve it
(`npx vite preview --outDir dist-prof --port 5347 --strictPort`), and drive
the op from a Playwright script with CDP `Profiler` (self-time by
function) and `HeapProfiler.startSampling` (allocation by function);
interleave the A and B pages in one browser for paired comparison. The
2026-09-18 todomvc scripts (`todo-prof.mjs`, `todo-var.mjs`,
`todo-alloc.mjs`, `todo-ab.mjs`) are the template; they lived in `/tmp`
and should be re-created next to the harness if this becomes routine.
interleave the A and B pages in one browser for paired comparison. For
uibench this is `benchmarks/uibench/tools/prof.mjs` in the fork (untracked;
run from `benchmarks/uibench` so it resolves the suite's Playwright):
`node tools/prof.mjs --url=http://localhost:5347/ --case='tree/[500]/[removeLast(1)]'
--reps=200 --mode=both`. It follows `run.mjs`'s protocol (prepare → settle →
gc → run) and brackets each `__run` with two sentinel functions that spin
0.3 ms so they are always sampled; only the samples between them count, so
the `__prepare` of each rep — a full render for the removeAll cases — stays
out of the numbers. Allocation sampling is started and stopped around each
`__run` over CDP for the same reason. The 2026-09-18 todomvc scripts
(`todo-prof.mjs`, `todo-var.mjs`, `todo-alloc.mjs`, `todo-ab.mjs`) were the
template; they lived in `/tmp` and are gone.

## Cross-references

Expand Down
Loading
Loading