Repository navigation
Keep the value and the tag of a tracked field in one cell - #21661
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
A read or a write of a tracked field went through three maps: the tag registry (a WeakMap, then a Map per object) and a WeakMap of values. Each field now has one WeakMap from the instance to a cell with the value and the tag, so a read is one lookup and consumeTag, and a write is one lookup and DIRTY_TAG. The cell takes its tag from the tag registry when it is made. Other code can ask the registry for the tag before the first read or write of the field (an observer, a computed chain, notifyPropertyChange), and that tag must stay the tag of the field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All cells have one hidden class, and V8 records which kind of value a field held so far. If the tracked fields of an app hold no number at first, the first number makes V8 throw away the optimized code of each function that reads a cell. The benchmark app has one tracked field, `selected`, which is `undefined` until the first select of a row. `--trace-deopt` showed 3 deopts with the reason "dependent field representation changed" in that phase, and the phase was 7.3% slower than main. Each cell now starts with a number and gets `undefined` right after, so the field is general before that code is optimized. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 5, 2026
Both sides added an import to tracking-test.ts. Co-Authored-By: Claude Opus 5.5 <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.
A read or a write of a
@trackedfield now does oneWeakMaplookup. Onmainit does three map lookups.main: 6.2% faster (geometric mean of 13 benches).1 item, 100k updatesis 14.9% faster.pnpm bench, against the currentmain: no significant change for the full run (+0.5%).pnpm bench:swapRows1is 6.9% slower,removeSecondRow1is 4.6% slower, andswapRows2is 3.5% slower. Each of these phases takes about 50 ms.Extracted from the spike #21656. This PR does not depend on another PR.
How it works
On
main, the getter of a tracked field callstagFor(self, key)and then reads the value:TRACKED_TAGS.get(self), aWeakMaplookuptags.get(key), aMaplookupvalues.get(self), a secondWeakMaplookupThe setter does the same through
dirtyTagFor.With this PR, each tracked field has one
WeakMapfrom the instance to a cell:consumeTag(cell.tag).DIRTY_TAG(cell.tag, true).tagForanddirtyTagForwork on the tag of the field, as onmain. Observers, computed chains andnotifyPropertyChangeget the tag from the registry, and they can ask for it before the first read or write of the field.Commits
4427f536f3Keep a tracked field's value and tag in one cellfcfd26b0eaMake the value field of a tracked cell general from the first celle2e0b1b558mergesmain. The one conflict was an import line intracking-test.ts.Against the current
mainControl is
mainat9bec1cb2a8, which has the merged parts of #21650. Experiment is this branch withmainmerged in,e2e0b1b558.rere-benchmark, with
main, this PR, and #21662 in 4 mirrored cycles: 8 runs for each build, 0 void runs, geometric mean of 13 benches.mainTrackedValue(#21662) againstmainTrackedValueagainst the cellmainbuild did not drift in this round: -0.3% between the first two cycles and the last two.1 item, 100k updatesis 14.9% faster with the cell, and 11.3% faster with theTrackedValue.TrackedValuein each cycle, by 0.1% to 6.7%.pnpm bench, 50 paired rounds: tracerbench-report.pdfclearManyItems2is 1.0% faster,swapRows1is 6.9% slower,removeSecondRow1is 4.6% slower, andswapRows2is 3.5% slower.removeSecondRow14.2% slower against the same control.The two sections below are from before the merge, with
mainatf693f240eeas the control.rere-benchmark
main, the first commit and the second commit ran in 4 mirrored cycles: 8 runs for each build, 5 samples per bench in each run. The table is for the second commit. Each number is the median of 40 samples, in ms.mainmain(geometric mean). The four cycles read -3.5%, -7.0%, -3.1%, and -6.7%.1 item, 100k updatesis faster in every round: -12.2%, -12.7%, and -17.1%.pnpm benchTracerbench compare of the second commit against
main, 50 rounds: tracerbench-report.pdf.The table shows the full run and each phase with a significant change. The full run reads +0.4%, which is not significant.
The benchmark app has one tracked field,
selectedon the state service. Each row reads it one time in a render. So this bench shows little of the gain.selectFirstRow1and the second commitThe first commit alone made
selectFirstRow17.3% slower (report). With the second commit it reads -0.5%, which is not significant.selectedisundefineduntil the first select of a row. That select stores the first number in a cell, and V8 throws away the optimized code that reads the field.--trace-deoptshows 3 deopts with the reasondependent field representation changed. All 3 are inselectFirstRow1:isSelected, the tracked getter, and one function with a minified name.mainhas none.valueand getsundefinedright after. The field is then general from the first cell on.I did not look for the cause of the two slower clear phases.
Memory
main, it gets the tag at the first read.finalGcphase ofpnpm benchshows no change.TrackedValue. In rere it is 2.7% slower than this cell on the currentmain, and it was 3.9% slower in the round before.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 the second commit,fcfd26b0ea, which is on top off693f240ee.--js-flags=--trace-deoptand no CPU throttle, one run of the benchmark app for each build. A wrapper aroundperformance.markprints each mark to the same output, so each deopt line belongs to a phase.Tests
tracking-test.ts. They check that the field and the tag registry use one tag, in each order oftagFor, read, write anddirtyTagFor. An earlier version of this change made a tag of its own in the cell, and these tests cover that bug.mainmerged in: 9526 pass, 18 skipped, 0 failed.tsc --noEmit, ESLint, Prettier andpnpm lint:docspass.🤖 Generated with Claude Code