Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions packages/@glimmer/reference/lib/reference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,6 +46,17 @@ class ReferenceImpl<T = unknown> implements Reference<T> {
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<Reference> = null;
public pathKey: Nullable<string> = null;

public debugLabel?: string;

constructor(type: ReferenceType) {
Expand Down Expand Up @@ -163,6 +175,16 @@ export function valueForRef<T>(_ref: Reference<T>): 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!();
Expand All @@ -171,6 +193,10 @@ export function valueForRef<T>(_ref: Reference<T>): T {
tag = ref.tag = newTag;

ref.lastRevision = valueForTag(newTag);

if (ref.pathParent !== null) {
maybeLockKnownTag(ref, newTag);
}
} else {
lastValue = ref.lastValue;
}
Expand All @@ -180,6 +206,28 @@ export function valueForRef<T>(_ref: Reference<T>): 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;

Expand Down Expand Up @@ -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}`;
}
Expand Down
26 changes: 26 additions & 0 deletions packages/@glimmer/validator/lib/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ export type TagMeta = Map<PropertyKey, UpdatableTag>;

const TRACKED_TAGS = new WeakMap<object, TagMeta>();

/**
* 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<T extends object>(
obj: T,
key: keyof T | string | symbol,
Expand Down
70 changes: 57 additions & 13 deletions packages/@glimmer/validator/lib/tracked-data.ts
Original file line number Diff line number Diff line change
@@ -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<T, K extends keyof T> = (self: T) => T[K] | undefined;
export type Setter<T, K extends keyof T> = (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<V> {
value: V;
tag: UpdatableTag;
initialized: boolean;
}

export function trackedData<T extends object, K extends keyof T>(
key: K,
initializer?: (this: T) => T[K]
): { getter: Getter<T, K>; setter: Setter<T, K> } {
let values = new WeakMap<T, T[K]>();
let cells = new WeakMap<T, TrackedCell<T[K] | undefined>>();
let hasInitializer = typeof initializer === 'function';

function cellFor(self: T): TrackedCell<T[K] | undefined> {
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 };
Expand Down