Repository navigation
Skip unchanged blocks in the updating VM - #21669
Draft
NullVoxPopuli-ai-agent wants to merge 3 commits into
Draft
NullVoxPopuli-ai-agent wants to merge 3 commits into
NullVoxPopuli-ai-agent wants to merge 3 commits into
Conversation
Every block opcode (try, list, list item) now records the combined tag of
what its render or last update consumed, the way a component cache group
does. On update, a block whose tag still validates is skipped as a whole,
and its tag is consumed into the parent frame so parents stay correct.
Before, an unchanged row in a {{#each}} cost one validation per dynamic
reference: on the js-framework-benchmark row that is five validateTag
calls and five megamorphic opcode evaluations per row per update. Now it
costs one validation, and the five opcodes never run.
The append VM opens the frame before a block's opcode is constructed,
because the list block reads its iterable in the constructor, and closes it
when the block exits. On the updating side the frame is closed when the
block's updating frame finishes. When a block re-renders after a thrown
assertion, the append VM closes the frame on exit and the updating VM pops
the frame without closing it again.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wPae8QnFbKHKn5xbgaQVq
A guard that fails is dropped on the spot, with no tracking frame: a block that changed is likely to change again, and a frame on every miss made all-rows-change workloads slower. A dropped guard comes back after eight unguarded updates, so a block that changed once and then stayed still is skipped again, while a block that changes on every update pays for one frame in every eight. A re-render brings the guard back at once. An unguarded block costs what it did before guards existed: one frame push. A re-render of a dropped block opens its own tracking frame, because the append VM closes one when the block exits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dropped guard comes back after 8 updates. Each test runs a block, a nested block, or a list through 20 updates of one kind and then changes something else, so the block is skipped, dropped, and guarded again while the test checks the output. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The updating VM skips a block whose inputs did not change. A list with 1,000 rows where a few rows change now pays for those rows, and one tag check for each of the others.
main(geometric mean of 13 benches). The list benches with one render for each update are 33% to 45% faster.pnpm bench: script time for the full run is 2.7% lower. Update of each 10th row is 39% to 41% faster. Append, remove and swap are 9% to 26% faster.clearItems2is 3.0% slower, and two fan-out benches read 8% to 10% slower.This PR replaces #21660, which had the same change on top of #21650. This version needs no other PR. Extracted from the spike #21656, and first proposed as #21612.
Commits
dfe3529fe0Skip unchanged blocks in the updating VM with a per-block tagfdb505e907Drop a missed guard, and bring it back laterd7a316d71eTest blocks through more updates than a guard needs to come backHow it works
{{#if}}, an{{#each}}item, and each other block that can render again) keeps the combined tag of everything it consumed in its last render or update.alwaysRevalidategoes past the guards.rere-benchmark
main, this PR, and #21660 ran in 4 mirrored cycles: 8 runs for each build, 5 samples per bench in each run, 0 void runs. Each number is the median of 40 samples, in ms.mainmain(geometric mean). The four cycles read -12.1%, -13.2%, -12.2%, and -7.8%.pnpm benchTracerbench compare against
main, 50 rounds: tracerbench-report.pdf.The table shows only the phases with a significant change.
clearItems1is slower only as a full phase. The script time is the same, so the extra time is after the render task. I did not find the cause.gc()is 27% faster.How this was measured
pnpm benchsettings: tracerbench compare onsmoke-tests/benchmark-app, headless, fidelity 50. Chrome and tracerbench ran pinned to one CPU core.gc()at the top ofrunBenchmark(), before the first mark, and a measuredfinalGcphase that callsgc()afterclearItems4.f693f240ee. Experiment is this PR atd7a316d71e, which is on top off693f240ee.Tests
block-guards-test.ts. Each one runs a block, a nested block, or a list through 20 updates of one kind and then changes something else, so the block is skipped, dropped, and guarded again while the test checks the output.tsc --noEmit, ESLint and Prettier pass.🤖 Generated with Claude Code