Skip to content

perf(signals): mapArray small-move fast path for keyed reorders - #3227

Draft
ryansolid wants to merge 3 commits into
nextfrom
keyed-small-moves
Draft

perf(signals): mapArray small-move fast path for keyed reorders#3227
ryansolid wants to merge 3 commits into
nextfrom
keyed-small-moves

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

  • Adds a bounded small-move pre-pass (trySmallMove) to mapArray's keyed update path: rotations, swaps, and small displacements (≤16 displaced rows) commit as targeted moves instead of full-window rebuilds. Ineligible shapes fall through to the untouched algorithm after a few probes.
  • Scoped to plain identity-keyed mode only — custom key functions and index-tracking modes never enter the fast path.
  • Extracted into its own function so the main updateKeyedMap loop stays within JIT inlining budgets (validated: no reverse/shuffle regression).

Validation

  • New mapArray-smallmove.test.ts suite pins semantics at 1000-row scale: rotate, displace, swap, remove, replacement, duplicates, mixed shapes, and custom-key exclusion.
  • js-framework-benchmark: swap1k −6.5%, no regressions across the main suite (create, replace, append, remove, select, clear).
  • Reorder matrix (rotate/displace/reverse/shuffle): delta-cost wins on small moves; full-path shapes unchanged.

Notes

  • Changeset included (maparray-small-move-fast-path.md, patch — prerelease mode).
  • Renderer-agnostic: benefits all keyed For consumers (DOM and universal); does not touch reconcileArrays.

Made with Cursor

@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 21152c5

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

coveralls commented Sep 2, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 33668852541

Coverage remained the same at 71.564%

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: 1004
Covered Lines: 768
Line Coverage: 76.49%
Relevant Branches: 793
Covered Branches: 518
Branch Coverage: 65.32%
Branches in Coverage %: Yes
Coverage Strength: 14.6 hits per line

💛 - Coveralls

@codspeed-hq

codspeed-hq Bot commented Sep 2, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 54.86%

⚠️ 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

❌ 1 regressed benchmark
✅ 135 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
merge 75.7 µs 167.6 µs -54.86%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing keyed-small-moves (21152c5) with next (87d5338)

Open in CodSpeed

ryansolid and others added 3 commits September 2, 2026 11:38
…a cost

Per-op decomposition of the octane board's stable jfb-reorder deficits
(rotate 1.9x, displace3-8 2.1-2.7x, removefirst 3-4x vs octane) profiled
to updateKeyedMap: the keyed diff pays O(newLen) regardless of delta —
window key-map, four full-length staged arrays, element-copied prefix and
suffix — ~50-140µs/op on 1000 rows against ~20µs of actual DOM work.

The fast path (trySmallMove, own function — inlining it deoptimized the
GENERAL path via the JIT function-size budget, same lesson as the uibench
reconcile regression):

- PHASE 1, scan only: two-pointer walk records aligned RUNS, realigning
  at boundaries with bounded lookahead (interleaved splices stack shift
  offsets past single-step). A compare BUDGET makes hopeless shapes
  (reverse/shuffle) bail almost immediately with zero allocation.
- PHASE 2, commit on success: slice() the live arrays (native memcpy
  keeps the fresh-identity contract downstream change propagation relies
  on), copy only shifted runs, patch displaced pairs (O(k^2), k<=32),
  dispose leftover sources (dif<0). Unmatched destinations (replacements,
  insertions) bail with nothing staged.
- Gated to LARGE trimmed windows: the trims already make plain removals
  window-cheap; the fast path would only re-walk what they proved.

Same-load A/B on 1000 rows: rotatef 140->13µs, rotateb 142->14µs, swap
94->8µs, displace3 54->9µs; removefirst 6.5µs and reverse at parity.
Row-signal/custom-key modes adopt through the same compare() rule with
per-run setSignal parity to the general path's step-3.

New suite pins the semantics: identity preservation across rotate/
displace/swap at jfb scale (1000 rows), replacement/mixed/duplicate
windows land the general path correctly, custom-key moves match by key
and adopt fresh objects, scrambles beyond the bound and length changes
stay correct. signals 1497 / web 726 / solid 576 green.

Co-authored-by: Cursor <cursoragent@cursor.com>
Size golf: source-level dedup (shared eq/scan/place helpers) bought
almost nothing — brotli already compresses repetition — so the real cut
is UNIQUE LOGIC: the fast path now serves only the plain identity-keyed
mode (no row signals, no index accessors — the hot For shape); other
modes keep the general path. That deletes the compare/keyFn plumbing and
half of phase 2: added cost drops 0.62 -> ~0.45 kB brotli in mapArray-
bearing bundles, zero elsewhere.

The pure-identity hot loop also got FASTER: rotate 10.5 -> 5.8µs, swap
6.2 -> 4.0µs, displace3 7.0 -> 4.6µs, removefirst 3.4µs, reverse at
pristine parity. Also removes a latent double-push in the del branch
(leftover half-edit; engaged only at deep lookahead distances).

All 32 map suites green; full signals 1497 green.

Co-authored-by: Cursor <cursoragent@cursor.com>
trySmallMove rides mapArray's keyed path, so every For-bearing bundle
pays 430-490 B brotli (A/B-verified against the branch base; non-For
bundles pay zero). Buys delta-cost keyed reorders — jfb swap1k −6.5%
with the main suite clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid marked this pull request as draft September 2, 2026 18:52
@ryansolid

Copy link
Copy Markdown
Member Author

Parking this (not shipping in RC.6): it optimizes the reactive half of a double-reconcile (mapArray diffs items, reconcileArrays re-derives the same moves against the DOM), and we've decided to pursue deleting the double-reconcile instead — a unified keyed For that owns row structure and DOM placement in one persistent structure (Octane-shaped, pull-based, no message seam). Keeping this branch as the validated fallback-tier reference and the performance floor the unified approach has to beat: swap1k −6.5%, delta-cost small reorders, main suite clean.

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