Skip to content

Iteration fast path, flat update frames, and flattened combinators - #21545

Closed
NullVoxPopuli-ai-agent wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:extract-iteration-fastpath
Closed

Iteration fast path, flat update frames, and flattened combinators#21545
NullVoxPopuli-ai-agent wants to merge 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:extract-iteration-fastpath

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Stacked on #21544 (its first commit is that PR; review the Iteration fast path… commit here). Full suite green locally with both commits: 9443 tests, 0 failures.

Three allocation/dispatch levers for the update path, extracted from the #21520 spike:

  1. Streaming same-order list compare — when a list's order is unchanged (the overwhelmingly common case), items match in a single streaming pass writing into a scratch item (nextInto on ArrayIterator, allocation-free) instead of allocating an IterationItem per step; on first mismatch it falls back to the general diff via a reconstructed-prefix iterator, so reorders behave exactly as today.
  2. Flat frame stack — the updating VM keeps parallel arrays indexed by depth instead of allocating an UpdatingVMFrame per block per render.
  3. Combinator flatteningcombine() flattens nested combinators and drops constants (capped at 64) so validating a combined tag is one flat loop instead of a pointer-chasing tree walk.

Measured together with #21544's subtree-skip as the difference between ~14 and ~27 fps on 8x-throttled dbmon during the spike campaign; no observable behavior change.

Related: #21520 (spike), #21543 (pools), #21544 (subtree-skip, base of this stack).

🤖 Generated with Claude Code

The UpdatingVM walks every updating opcode of every list item on every
render: cache groups (JumpIfNotModifiedOpcode) exist only at component
boundaries, so a list of plain template rows revalidates every binding
even when nothing in a row changed.

Collect each item's consumed tags in a tracking frame (via a new
frame-finalizer hook on UpdatingVMFrame) and skip the item's entire
subtree while that combined tag validates.

Trivial items opt out: for a text node or two, validating a combined
tag costs as much as updating, so collection would be pure overhead.
An item is trivial when it has <= 2 opcodes and no nested block -- a
nested block child means an arbitrarily large subtree hides behind a
small top-level count.

dbmon-style workloads (fat rows, sparse changes): ~1.6x fps at 8x CPU
throttle, ~6x (rAF-capped) at 4x. Dense-change / tiny-item workloads
and the krausest bench: neutral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Isolated same-batch measurement (today's main vs this branch — both commits, 8x throttle, mutation-anchored, zero render failures):

bench main this PR (skip + fast path)
DB Monitor 7.8 fps 18.4 fps (2.4x)
1k items, 1 update each (seq, async) 1695ms 645ms (2.6x)
1k items, 5% random (async) 274ms 126ms (2.2x)
1k items, 25% random (async) 660ms 276ms (2.4x)
Incrementing 4320ms 4559ms (+5.5%, spreads close — watching this one)
everything else neutral

Reading the increment over #21544 alone (cross-batch ratios): dbmon is carried almost entirely by the subtree-skip; the fast path's own contribution shows on the random-update benches (5%: 1.5x → 2.2x; 25%: 1.8x → 2.4x vs main), where same-order streaming compare does the most work. The +5.5% on incrementing is borderline noise but worth a re-measure before merge if it matters — happy to run a deeper sample.

🤖 Generated with Claude Code

NullVoxPopuli-ai-agent and others added 2 commits August 7, 2026 14:59
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three allocation/dispatch levers for the update path, measured together
with subtree-skipping as the difference between ~14 and ~27 fps on an
8x-throttled dbmon:

- streaming same-order list compare: when a list's order is unchanged
  (the overwhelmingly common case), items are matched in a single
  streaming pass writing into a scratch item (nextInto) instead of
  allocating an IterationItem per step, falling back to the general
  diff via a reconstructed prefix iterator on first mismatch
- flat frame stack: the updating VM keeps parallel arrays indexed by
  depth instead of allocating an UpdatingVMFrame per block per render
- combinator flattening: combine() flattens nested combinators and
  drops constants (capped) so validating a combined tag is one flat
  loop instead of a pointer-chasing tree walk

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kategengler

Copy link
Copy Markdown
Member

Converting to draft as this seems like it is not ready

@NullVoxPopuli

Copy link
Copy Markdown
Contributor

Superseded by #21557

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.

3 participants