diff --git a/packages/@glimmer/reference/lib/reference.ts b/packages/@glimmer/reference/lib/reference.ts index c7232d0469a..bdaa77b8524 100644 --- a/packages/@glimmer/reference/lib/reference.ts +++ b/packages/@glimmer/reference/lib/reference.ts @@ -15,6 +15,7 @@ import { expect } from '@glimmer/debug-util/lib/platform-utils'; import { getProp, setProp } from '@glimmer/global-context'; import { isDict } from '@glimmer/util/lib/collections'; import { CONSTANT_TAG, INITIAL, validateTag, valueForTag } from '@glimmer/validator/lib/validators'; +import { peekTagFor } from '@glimmer/validator/lib/meta'; import { consumeTag, track } from '@glimmer/validator/lib/tracking'; export const REFERENCE: ReferenceSymbol = Symbol('REFERENCE') as ReferenceSymbol; @@ -45,6 +46,17 @@ class ReferenceImpl implements Reference { public compute: Nullable<() => T> = null; public update: Nullable<(val: T) => void> = null; + /** + * Proven plain tracked-field read: the first framed compute consumed + * exactly the property's canonical cell tag, so the consumed set can + * never change and recomputes skip frame machinery entirely. + */ + public knownTag = false; + + /** pending known-tag candidacy; checked once after the first compute */ + public pathParent: Nullable = null; + public pathKey: Nullable = null; + public debugLabel?: string; constructor(type: ReferenceType) { @@ -163,6 +175,16 @@ export function valueForRef(_ref: Reference): T { if (tag === null || !validateTag(tag, lastRevision)) { const { compute } = ref; + if (ref.knownTag) { + // the getter's own consumeTag lands in the ambient frame, which + // is exactly what the framed path's trailing consumeTag achieved + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- knownTag implies compute + lastValue = ref.lastValue = compute!(); + ref.lastRevision = valueForTag(tag); + + return lastValue; + } + const newTag = track(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme lastValue = ref.lastValue = compute!(); @@ -171,6 +193,10 @@ export function valueForRef(_ref: Reference): T { tag = ref.tag = newTag; ref.lastRevision = valueForTag(newTag); + + if (ref.pathParent !== null) { + maybeLockKnownTag(ref, newTag); + } } else { lastValue = ref.lastValue; } @@ -180,6 +206,28 @@ export function valueForRef(_ref: Reference): T { return lastValue as T; } +/** + * A child ref locks onto its property's canonical tag when its first + * framed compute consumed EXACTLY that tag: single tag means no + * branching getter (those consume different sets per run), and + * identity with the registry's cell tag means the read was the plain + * tracked-field getter on a parent that can never change (a mutable + * parent's tag would have been in the frame too). Checked once. + */ +function maybeLockKnownTag(ref: ReferenceImpl, tag: Tag): void { + const parentRef = ref.pathParent as ReferenceImpl; + const key = ref.pathKey as string; + + ref.pathParent = null; + ref.pathKey = null; + + const parent = parentRef.lastValue; + + if (isDict(parent) && peekTagFor(parent, key) === tag) { + ref.knownTag = true; + } +} + export function updateRef(_ref: Reference, value: unknown) { const ref = _ref as ReferenceImpl; @@ -233,6 +281,9 @@ export function childRefFor(_parentRef: Reference, path: string): Reference { } ); + (child as ReferenceImpl).pathParent = parentRef; + (child as ReferenceImpl).pathKey = path; + if (DEBUG) { child.debugLabel = `${parentRef.debugLabel}.${path}`; } diff --git a/packages/@glimmer/validator/lib/meta.ts b/packages/@glimmer/validator/lib/meta.ts index 56e30cc7be1..b6a869368db 100644 --- a/packages/@glimmer/validator/lib/meta.ts +++ b/packages/@glimmer/validator/lib/meta.ts @@ -17,6 +17,32 @@ export type TagMeta = Map; const TRACKED_TAGS = new WeakMap(); +/** + * Read-only registry lookup: the canonical tag for (obj, key) if one + * exists, with no create-on-miss allocation. + */ +export function peekTagFor(obj: object, key: PropertyKey): UpdatableTag | undefined { + return TRACKED_TAGS.get(obj)?.get(key); +} + +/** + * Adopts an externally-owned tag (e.g. a tracked field's inline cell + * tag) as THE tag for (obj, key) in the central registry, so + * `tagFor`/`dirtyTagFor` consumers -- notifyPropertyChange, computed + * property chains -- observe the same tag object the field itself + * consumes and dirties. + */ +export function registerTagFor(obj: object, key: PropertyKey, tag: UpdatableTag): void { + let tags = TRACKED_TAGS.get(obj); + + if (tags === undefined) { + tags = new Map(); + TRACKED_TAGS.set(obj, tags); + } + + tags.set(key, tag); +} + export function dirtyTagFor( obj: T, key: keyof T | string | symbol, diff --git a/packages/@glimmer/validator/lib/tracked-data.ts b/packages/@glimmer/validator/lib/tracked-data.ts index 560f6d71a54..d0c4eee3e93 100644 --- a/packages/@glimmer/validator/lib/tracked-data.ts +++ b/packages/@glimmer/validator/lib/tracked-data.ts @@ -1,36 +1,80 @@ -import { dirtyTagFor, tagFor } from './meta'; +import { DEBUG } from '@glimmer/env'; +import type { UpdatableTag } from '@glimmer/interfaces'; + +import { debug } from './debug'; +import { registerTagFor } from './meta'; import { consumeTag } from './tracking'; +import { unwrap } from './utils'; +import { createUpdatableTag, DIRTY_TAG } from './validators'; export type Getter = (self: T) => T[K] | undefined; export type Setter = (self: T, value: T[K]) => void; +/** + * Value and tag live in one cell per (field, instance): a read is one + * WeakMap hop + consumeTag, a write is one hop + DIRTY_TAG. The + * previous shape went through the central tag registry + * (`TRACKED_TAGS` WeakMap -> per-object Map) plus a separate values + * WeakMap -- three map hops on every tracked read and write, which is + * the hottest path in data-heavy rendering. + */ +interface TrackedCell { + value: V; + tag: UpdatableTag; + initialized: boolean; +} + export function trackedData( key: K, initializer?: (this: T) => T[K] ): { getter: Getter; setter: Setter } { - let values = new WeakMap(); + let cells = new WeakMap>(); let hasInitializer = typeof initializer === 'function'; + function cellFor(self: T): TrackedCell { + let cell = cells.get(self); + + if (cell === undefined) { + cell = { + value: undefined, + tag: createUpdatableTag(), + initialized: !hasInitializer, + }; + cells.set(self, cell); + // one-time bridge: notifyPropertyChange / computed chains resolve + // tags through the central registry; hand them this cell's tag so + // both worlds dirty and consume the same object + registerTagFor(self, key, cell.tag); + } + + return cell; + } + function getter(self: T) { - consumeTag(tagFor(self, key)); + const cell = cellFor(self); - let value; + consumeTag(cell.tag); // If the field has never been initialized, we should initialize it - if (hasInitializer && !values.has(self)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme - value = initializer!.call(self); - values.set(self, value); - } else { - value = values.get(self); + if (!cell.initialized) { + cell.initialized = true; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by initialized + cell.value = initializer!.call(self); } - return value; + return cell.value; } function setter(self: T, value: T[K]): void { - dirtyTagFor(self, key); - values.set(self, value); + const cell = cellFor(self); + + if (DEBUG) { + unwrap(debug.assertTagNotConsumed)(cell.tag, self, key); + } + + DIRTY_TAG(cell.tag); + cell.initialized = true; + cell.value = value; } return { getter, setter };