Skip to content

One-hop tracked storage and known-tag references - #21546

Closed
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:extract-tracked-cells
Closed

One-hop tracked storage and known-tag references#21546
NullVoxPopuli-ai-agent wants to merge 1 commit into
emberjs:mainfrom
NullVoxPopuli-ai-agent:extract-tracked-cells

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Extracted from the #21520 spike; independent of the other extraction PRs. Full suite green locally: 9443 tests, 0 failures — including the notifyPropertyChange and computed-property interop this touches most directly.

One-hop tracked storage: trackedData keeps value and tag in a single cell per (field, instance) — a tracked read is one WeakMap hop + consumeTag, a write is one hop + DIRTY_TAG, replacing three map hops each way (central TRACKED_TAGS WeakMap → per-object Map, plus the separate values WeakMap). The cell registers its tag into the central registry at creation (registerTagFor), so notifyPropertyChange, computed chains, and anything else resolving tags via tagFor/dirtyTagFor observes the identical tag object the field consumes and dirties — interop is preserved by tag identity, not by parallel bookkeeping.

Known-tag references: a child reference locks onto its property's canonical tag when its first framed compute consumed exactly that tag. A single consumed tag rules out branching getters (their consumed sets vary run to run); identity with the registry's cell tag proves the read was the plain tracked-field getter on a parent that cannot change (a mutable parent's tag would have been in the frame too — constants are dropped from frames). Locked references recompute with no frame push, no tag collection, and no combine(); the getter's own consumeTag lands in the ambient frame, which is exactly what the framed path's trailing consume achieved. Anything unproven stays on the framed path forever.

Honest impact framing: neutral on the spike's benchmark suite (its apps are worker-POJO- and helper-bound); the win scales with tracked-getter density and template property reads — the common shape of real apps — and it removes per-read allocation and map traffic unconditionally.

Related: #21520 (spike), #21543 (pools), #21544/#21545 (update-path stack).

🤖 Generated with Claude Code

trackedData keeps value and tag in a single cell per (field, instance):
a tracked read is one WeakMap hop + consumeTag and a write is one hop +
DIRTY_TAG, replacing three map hops each way (central TRACKED_TAGS
WeakMap -> per-object Map, plus the separate values WeakMap). The cell
registers its tag into the central registry at creation, so
notifyPropertyChange and computed-property chains observe the identical
tag object the field itself consumes and dirties.

On top of that, a child reference locks onto its property's canonical
tag when its first framed compute consumed EXACTLY that tag: a single
tag rules out branching getters (their consumed sets vary per run), and
identity with the registry's cell tag proves a plain tracked-field read
on a parent that cannot change (a mutable parent's tag would have been
in the frame too). Locked references recompute with no frame push, no
tag collection, and no combine -- the getter's own consumeTag lands in
the ambient frame, which is exactly what the framed path's trailing
consume achieved. Anything unproven stays on the framed path.

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, 8x throttle, mutation-anchored) — regressive on walk-heavy paths; closing under the same standard that closed #21543.

bench main this PR
DB Monitor 10.6 fps (10–13) 7.1 fps (7–12) — two of three runs below main's entire range
1k items, 25% random (async) 537ms (401–663) 878ms (598–1230) — four of five runs at/above main's max
1 item, 1k sync / 100k sync 13.1ms / 42.7ms 10.1ms / 36.9ms (the promised set-path win, real but small)
everything else neutral

The sync one-shot improvement is real but tiny in absolute terms, and it's paid for with a genuine regression where ember is weakest. Likely mechanism: ReferenceImpl gained three fields (bigger objects for every ref in ref-dense trees) and every childRefFor does candidacy bookkeeping, while the locking that was supposed to pay for it rarely fires on real templates (list-item parents are mutable by design, so their refs correctly never lock). The cost is unconditional; the win is conditional — backwards.

On the spike branch these changes measured differently (its scheduler coalesces to far fewer flushes, hiding per-flush ref costs), which is exactly why the isolated-on-main standard exists. The one-hop tracked-cell storage might survive on its own without the reference-locking half — if it's wanted, it should return as a separate PR with its own isolated numbers.

🤖 Generated with Claude Code

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