Skip to content

fix(state): wrap fill and copyWithin through the Proxy - #188

Merged
nerdalytics merged 6 commits into
trunkfrom
fix/wrap-fill-and-copywithin
Oct 25, 2026
Merged

nerdalytics merged 6 commits into
trunkfrom
fix/wrap-fill-and-copywithin

Conversation

@nerdalytics

Copy link
Copy Markdown
Owner

Description

A reactive array handed out the native fill and copyWithin, so every element they wrote woke subscribers on its own, outside any batch. On trunk 18f5a71 an effect reading every index of [1, 2, 3] re-ran 3 times for fill(9) and 2 times for copyWithin(0, 1), where reverse() woke it once.

Changes Made

  • fill and copyWithin join the wrapped mutating method set, so a call wakes each subscriber at most once and a call that changes nothing wakes nobody (5b42960).
  • Array targets get a set trap of their own. Once arrays reached the shared object batch path, its subscriber load deoptimized and many dependencies ran 10.6% slower on medians in an interleaved A/B against 87c8823 (2dbcee2).
  • The loop-detection rule is unchanged. fill throws in a sync top-level effect only when that effect read an index the call writes, and copyWithin only when its ranges overlap. The spec now states this.
  • Review this behaviour change. An effect reading only length now re-runs once per fill call where before it did not run at all, by the spec's existing "regardless of which properties the subscriber read" rule. It is the same class of wake-count change as fix(state): intercept array mutating methods on both kinds of state #186, so it ships with the next breaking release together with that one. No version bump here.
  • The spec, root AGENTS.md and src/AGENTS.md name nine methods.

Performance Impact

  • array fill (hookless) median 79.53 ms before, 39.64 ms after; totals 1673.64 ms and 837.81 ms (default 3-cycle run, ticket 04 report).
  • An interleaved 10-cycle A/B against baseline 87c8823 shows no existing case slower beyond its own A/A spread. many dependencies reads -0.5% on medians and -1.4% on totals.
  • The subscriber lookup stays inlined in the array batch path. Calling findSubscribers instead cost +2.2% on array fill, about 0.4 points above that case's A/A spread. The margin is thin, and the performance tournament should revisit it.

Test Coverage

  • npm test 322 pass, coverage 100% lines, branches and functions, npm run check clean on 2dbcee2.
  • New assertions in tests/array-methods.test.ts, tests/property-array-mutations.test.ts, tests/infinite-loop.test.ts and tests/state-hooks.test.ts cover wake counts, batches, bounds, frozen arrays, loop behaviour and hook observation on both kinds of state.
  • One pre-existing limitation stays. fill(9) on a frozen [9, 9, 9] stays silent where native throws, because the set trap short-circuits unchanged writes. .do/inbox/2026-10-25-same-value-writes-to-frozen-targets-do-not-throw.md tracks it.

Related Issues

The change came out of .do/inbox/2026-10-25-fill-and-copywithin-not-batched.md; there is no GitHub issue. Follows #186.

Checklist

  • Tests added/updated for changes
  • Documentation updated (handbook pages wait for the release tree, per the proposal)
  • All tests passing
  • Code formatting checked
  • Performance implications considered
  • Breaking changes documented (if applicable)

…planning files

Move the wrap-array-methods-on-hookless-state and
drop-unread-hooks-slot-on-objects changes to the archive, add the
synced array-mutation main spec, the planning files of the
wrap-fill-and-copywithin change, and the inbox, ledger and review
files the previous runs wrote under .do/.
The wrap-fill-and-copywithin change alters how many wakes one fill call
causes. This case subscribes one effect that reads every index of a
hookless array and times fill calls that change every element, so the
unchanged library records the baseline. Typed perf because it measures
performance; no library behaviour changes.
A reactive array hands out the native fill and copyWithin, so each
element write wakes an every-index subscriber on its own. Add both
names to the mutating method set so they run in a batch and wake each
subscriber once after the call returns.

Cover wake counts, batches, bounds and frozen arrays on both kinds of
state, and add fill and copyWithin to the property-based array test.
A sync effect that only calls fill raises nothing and raises once it
also read a written index. copyWithin raises only when its source and
target ranges overlap. Child and microtask effects raise nothing.

On hooked state, onRead sees the method name and every property read,
and onWrite sees every property written and no other.
The main array-mutation spec purpose and requirement, the root key
behaviors and the source wrapArrayMethod row now count fill and
copyWithin.
Once fill and copyWithin run through the Proxy inside a batch, array
targets reach handleBatchFastPath, which until then sees only plain
objects. Its [SUBSCRIBERS] load then deoptimizes 36 times per 10-cycle
run (wrong map), and the case 'many dependencies', which runs after
'array fill (hookless)', gets slower: +10.6% on medians and +6.8% on
totals over three interleaved A/B runs of 10 cycles against 87c8823
(A/A spread 4.3% and 2.4%).

Control: on 87c8823, with fill wrapped by hand in batch(), the same
slowdown and the same deopt pattern appear (2.2 ms against 1.8 ms on a
fill-then-many-dependencies sequence). The cause is arrays on the
shared batch write path, not the two names added to the method set,
its Set lookup or code layout.

handleBatchArrayWrite now serves arrays through a set trap of its own,
picked by one Array.isArray per state() call, shared with hooksSlotFor.
Interleaved A/B after the change, against 87c8823: many dependencies
-0.5% on medians and -1.4% on totals, array fill -50.2%, no case
slower beyond its A/A spread. The subscriber lookup stays inlined:
calling findSubscribers instead costs 2.2% on medians and 2.4% on
totals of array fill (three interleaved 10-cycle pairs).
@nerdalytics
nerdalytics marked this pull request as ready for review October 25, 2026 18:03
@nerdalytics
nerdalytics merged commit 99313a6 into trunk Oct 25, 2026
12 checks passed
@nerdalytics
nerdalytics deleted the fix/wrap-fill-and-copywithin branch October 25, 2026 18:05
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