Repository navigation
Store each @tracked field in a TrackedValue - #21662
Draft
NullVoxPopuli-ai-agent wants to merge 1 commit into
Draft
NullVoxPopuli-ai-agent wants to merge 1 commit into
NullVoxPopuli-ai-agent wants to merge 1 commit into
Conversation
This was referenced Oct 6, 2026
The `@tracked` decorator kept each field in `trackedData`: a WeakMap of values, plus the tag registry (a WeakMap, then a Map per object) for the tag. A read or a write went through three maps. The decorator now keeps one WeakMap from the instance to a TrackedValue, which holds the value and the tag. This is the same class that the standalone `tracked(value)` form returns. - The TrackedValue takes its tag from the tag registry, so `tagFor`, `dirtyTagFor`, observers, computed chains and `notifyPropertyChange` work on the tag of the field. - A field notifies on every write, so its `equals` is a function that returns false, unless the decorator got an `equals` option. - The field setter runs the "already consumed" assertion itself, because only the decorator knows the object and the key for the message. - The TrackedValue is made at the first read or write. A first write does not run the initializer, as before. `trackedData` has no caller in the repo after this. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NullVoxPopuli-ai-agent
force-pushed
the
nvp/tracked-field-on-tracked-value
branch
from
October 7, 2026 17:20
a8f4656 to
9f82b54
Compare
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
@trackeddecorator now keeps each field in aTrackedValue, the class that the standalonetracked(value)form returns. A read or a write of a field does oneWeakMaplookup. Onmainit does three map lookups throughtrackedData.trackedData. Only one of the two must merge.trackedDatahas no caller in the repo after this PR. A separate PR can remove it.pnpm benchagainstmain, after the rebase: no change for the full run (-0.4%, not significant). Against the cell of Keep the value and the tag of a tracked field in one cell #21661: no difference.main, after the rebase: 1.9% faster in a noisy round of 8 cycles, and 3.7% faster in a quiet round of 4 cycles.1 item, 100k updatesis 11.3% to 11.8% faster. The cell of Keep the value and the tag of a tracked field in one cell #21661 is 2.7% faster than this PR in the quiet round.Rebased on
mainThis PR needed two parts of #21650, and both are merged: #21666 makes the functions of a
TrackedValueon their first use, and #21667 has the fix for the V8 deopt on the first number.The branch is now one commit on
main,9f82b544f7, and it merges alone. The numbers below are from before the rebase, with #21650 as the base.How it works
descriptorForFieldkeeps oneWeakMapfrom the instance to aTrackedValue. The getter readscell.value, and the setter writes it.TrackedValueis made at the first read or write of the field. A first read runs the initializer. A first write does not, as onmain.TrackedValuedid not fit a field in three points. This PR handles each one:TrackedValueonmaintagFor(obj, key)Object.isskips the writeequalsthat returns false, unless the decorator got anequalsoptionnotifyPropertyChangeget the tag of a field from the registry.this.items = this.itemsis how code reports a change inside a value.After the rebase, against
mainControl is
mainat9bec1cb2a8, which has the merged parts of #21650. Experiment is the one commit of this PR,9f82b544f7.pnpm bench, 50 paired rounds: tracerbench-report.pdfremoveSecondRow1is 4.2% slower.rendershows +40.3% in script time without GC, on a phase of 20 ms. With GC time included, the same phase is -0.1%. So this number comes from the GC subtraction on a small phase.rere-benchmark, 8 mirrored cycles, 16 runs for each build, 0 void runs, geometric mean of 13 benches:
mainmainbuild drifted 5.4% between the first four cycles and the last four. The result is "no clear change".1 item, 100k updatesis 11.8% faster. That bench writes one tracked field in a loop.Direct comparison with the cell of Keep the value and the tag of a tracked field in one cell #21661, in one more round with
main, Keep the value and the tag of a tracked field in one cell #21661 (withmainmerged in,e2e0b1b558), and this PR: 4 mirrored cycles, 0 void runs, and amainbuild that did not drift (-0.3%).mainTrackedValue(#21662) againstmainTrackedValueagainst the cellIn this quiet round, this PR is 3.7% faster than
main, and the cell is 6.2% faster.This PR is behind the cell in each cycle, by 0.1% to 6.7%.
The sections below are from before the rebase, when the control was Cut the allocations of a tracking frame and of a TrackedValue #21650 without this PR.
pnpm benchAgainst the cell of #21661, with both changes on top of #21650: no phase differs.
smoke-tests/benchmark-appwith agc()before the first mark, Chrome pinned to one core, 4x CPU throttle on a host with a limit of about 3 GHz, paired rounds, GC time removed.Against #21650 alone: tracerbench-report.pdf.
selectSecondRow1is 4.1% faster.swapRows1is 3.3% slower as a full phase, andrender10000Items1is 2.2% faster as a full phase.rere-benchmark
main, #21650, #21650 with the cell of #21661, and this PR ran in 4 mirrored cycles: 8 runs for each build, 5 samples per bench in each run, 0 void runs. The numbers are the geometric mean of 13 benches.1 item, 100k updates: the cell is 21.1% faster than Cut the allocations of a tracking frame and of a TrackedValue #21650, and this PR is 16.4% faster.TrackedValuecosts more than the cell on each read and write. Likely parts of that cost are theequalscall on each write and the frozen check. I did not measure the parts.Tests
metal/tests/tracked/validation_test.js: a write dirties the tag thattagForPropertygave before the first read or write,notifyPropertyChangedirties the tag that a read consumed, and a first write does not run the initializer.TrackedValuekeeps a private tag, 5 tests of that file fail: the first two new ones and three that are onmain.tsc --noEmit, ESLint, Prettier andpnpm lint:docspass.Not measured
The benches have one or two tracked fields each. They show the cost of a read and a write, but not the size of a
TrackedValuefor each field of each instance. With #21650, aTrackedValuehas 8 private fields. The cell of #21661 has 3 fields.🤖 Generated with Claude Code