Skip to content
Draft
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
8 changes: 7 additions & 1 deletion packages/@glimmer/interfaces/lib/references.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ export type ConstantReference = 0;
export type ComputeReference = 1;
export type UnboundReference = 2;
export type InvokableReference = 3;
export type CellReference = 4;
export type PropertyReference = 5;

export interface ReferenceTypes {
readonly Constant: ConstantReference;
readonly Compute: ComputeReference;
readonly Unbound: UnboundReference;
readonly Invokable: InvokableReference;
readonly Cell: CellReference;
readonly Property: PropertyReference;
}

export type ReferenceType =
| ConstantReference
| ComputeReference
| UnboundReference
| InvokableReference;
| InvokableReference
| CellReference
| PropertyReference;

declare const REFERENCE: unique symbol;
export type ReferenceSymbol = typeof REFERENCE;
Expand Down
58 changes: 27 additions & 31 deletions packages/@glimmer/reference/lib/iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import type { Nullable } from '@glimmer/interfaces';
import { getPath, toIterator } from '@glimmer/global-context';
import { EMPTY_ARRAY } from '@glimmer/util/lib/array-utils';
import { isIndexable } from '@glimmer/util/lib/collections';
import { consumeTag } from '@glimmer/validator/lib/tracking';
import { createTag, DIRTY_TAG as dirtyTag } from '@glimmer/validator/lib/validators';

import type { Reference, ReferenceEnvironment } from './reference';

import { createComputeRef, valueForRef } from './reference';

export { createIteratorItemRef } from './reference';

export interface IterationItem<T, U> {
key: unknown;
value: T;
Expand Down Expand Up @@ -50,28 +50,35 @@ const IDENTITY: KeyFor = (item) => {
return item;
};

function keyForPath(path: string): KeyFor {
function pathKeyFor(path: string): KeyFor {
if (DEBUG && path[0] === '@') {
throw new Error(`invalid keypath: '${path}', valid keys: @index, @identity, or a path`);
}
return uniqueKeyFor((item) => {
return (item) => {
if (item === null || item === undefined) {
return item;
}
return getPath(item, path);
});
};
}

function makeKeyFor(key: string) {
/**
* Resolve the key strategy for a `{{#each}}` once, when the iterator reference is
* created — not on every diff. `base` is the stateless function that derives a
* key from an item; `dedup` says whether per-pass duplicate-key tracking is
* needed. `@index`/`@key` produce a unique value per position by construction, so
* they never need deduping; only identity and path keys can collide.
*/
function keyStrategy(key: string): { base: KeyFor; dedup: boolean } {
switch (key) {
case '@key':
return uniqueKeyFor(KEY);
return { base: KEY, dedup: false };
case '@index':
return uniqueKeyFor(INDEX);
return { base: INDEX, dedup: false };
case '@identity':
return uniqueKeyFor(IDENTITY);
return { base: IDENTITY, dedup: true };
default:
return keyForPath(key);
return { base: pathKeyFor(key), dedup: true };
}
}

Expand Down Expand Up @@ -147,8 +154,11 @@ function identityForNthOccurence(value: unknown, count: number) {
* and encounter an item for the nth time, we can get the _same_ key, and let
* Glimmer know that it should reuse the DOM for the previous nth occurence.
*/
function uniqueKeyFor(keyFor: KeyFor) {
let seen = new WeakMapWithPrimitives<number>();
function uniqueKeyFor(keyFor: KeyFor): KeyFor {
// Per-pass state, discarded when the iteration completes — a plain `Map`
// (which keys on objects and primitives alike) is enough; the weak-keyed
// dual-map dance is only needed for the long-lived global `IDENTITIES`.
let seen = new Map<unknown, number>();

return (value: unknown, memo: unknown) => {
let key = keyFor(value, memo);
Expand All @@ -165,11 +175,15 @@ function uniqueKeyFor(keyFor: KeyFor) {
}

export function createIteratorRef(listRef: Reference, key: string) {
// Resolve the key strategy once; only the (cheap) per-pass dedup state is
// rebuilt on each evaluation.
const { base, dedup } = keyStrategy(key);

return createComputeRef(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let iterable = valueForRef(listRef) as { [Symbol.iterator]: any } | null | false;

let keyFor = makeKeyFor(key);
let keyFor = dedup ? uniqueKeyFor(base) : base;

if (Array.isArray(iterable)) {
return new ArrayIterator(iterable, keyFor);
Expand All @@ -185,24 +199,6 @@ export function createIteratorRef(listRef: Reference, key: string) {
});
}

export function createIteratorItemRef(_value: unknown) {
let value = _value;
let tag = createTag();

return createComputeRef(
() => {
consumeTag(tag);
return value;
},
(newValue) => {
if (value !== newValue) {
value = newValue;
dirtyTag(tag);
}
}
);
}

class IteratorWrapper implements OpaqueIterator {
constructor(
private inner: IteratorDelegate,
Expand Down
143 changes: 111 additions & 32 deletions packages/@glimmer/reference/lib/reference.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
import { DEBUG } from '@glimmer/env';
import type {
CellReference,
ComputeReference,
ConstantReference,
InvokableReference,
Nullable,
PropertyReference,
Reference,
ReferenceSymbol,
ReferenceType,
UnboundReference,
} from '@glimmer/interfaces';
import type { Revision } from '@glimmer/validator/lib/validators';
import type { Tag } from '@glimmer/interfaces';
import type { DirtyableTag, Tag } from '@glimmer/interfaces';
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 { consumeTag, track } from '@glimmer/validator/lib/tracking';
import {
CONSTANT_TAG,
createTag,
DIRTY_TAG as dirtyTag,
INITIAL,
validateTag,
valueForTag,
} from '@glimmer/validator/lib/validators';
import { beginTrackFrame, consumeTag, endTrackFrame } from '@glimmer/validator/lib/tracking';

export const REFERENCE: ReferenceSymbol = Symbol('REFERENCE') as ReferenceSymbol;

const CONSTANT: ConstantReference = 0;
const COMPUTE: ComputeReference = 1;
const UNBOUND: UnboundReference = 2;
const INVOKABLE: InvokableReference = 3;
const CELL: CellReference = 4;
const PROPERTY: PropertyReference = 5;

export type { Reference as default };
export type { Reference };
Expand All @@ -45,6 +56,11 @@ class ReferenceImpl<T = unknown> implements Reference<T> {
public compute: Nullable<() => T> = null;
public update: Nullable<(val: T) => void> = null;

// For PROPERTY references: the parent reference and the property path, stored
// as data instead of being captured in getter/setter closures.
public propertyParent: Nullable<Reference> = null;
public propertyPath: Nullable<string> = null;

public debugLabel?: string;

constructor(type: ReferenceType) {
Expand Down Expand Up @@ -115,6 +131,28 @@ export function createComputeRef<T = unknown>(
return ref;
}

/**
* A `Cell` reference holds a value directly behind a single dirtyable tag. It is
* the reference used for `{{#each}}` block params (the item value and its index),
* which are created and updated by the millions when rendering large lists.
*
* Unlike a generic compute reference, a cell has no dependencies to discover: its
* value lives on the reference itself and its tag never changes. That lets
* `valueForRef` skip the `track()` frame (a `Tracker` + `Set` allocation per read)
* and lets `updateRef` mutate the value inline, so a cell needs no `compute`/
* `update` closures at all — just the reference object and its tag.
*/
export function createIteratorItemRef<T>(value: T): Reference<T> {
const ref = new ReferenceImpl<T>(CELL);
const tag = createTag();

ref.tag = tag;
ref.lastValue = value;
ref.lastRevision = valueForTag(tag);

return ref;
}

export function createReadOnlyRef(ref: Reference): Reference {
if (!isUpdatableRef(ref)) return ref;

Expand Down Expand Up @@ -144,8 +182,9 @@ export function isConstRef(_ref: Reference) {

export function isUpdatableRef(_ref: Reference) {
const ref = _ref as ReferenceImpl;
const type = ref[REFERENCE];

return ref.update !== null;
return type === CELL || type === PROPERTY || ref.update !== null;
}

export function valueForRef<T>(_ref: Reference<T>): T {
Expand All @@ -161,28 +200,72 @@ export function valueForRef<T>(_ref: Reference<T>): T {
let lastValue;

if (tag === null || !validateTag(tag, lastRevision)) {
const { compute } = ref;

const newTag = track(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
lastValue = ref.lastValue = compute!();
}, DEBUG && ref.debugLabel);

tag = ref.tag = newTag;
if (ref[REFERENCE] === CELL) {
// A cell's value is stored on the reference and gated by a fixed tag, so
// there are no dependencies to (re)discover — read the stored value and
// re-snapshot the tag without opening a tracking frame.
lastValue = ref.lastValue;
ref.lastRevision = valueForTag(tag as Tag);
} else {
// Inlined `track()`: opening the frame directly avoids allocating a thunk
// closure on every (re)compute. This is the hottest path in the VM — every
// reference read that needs evaluation passes through here.
beginTrackFrame(DEBUG && ref.debugLabel);
let newTag!: Tag;
try {
if (ref[REFERENCE] === PROPERTY) {
// A property reference reads `path` off its parent's value. Holding
// the parent + path as data (rather than getter/setter closures) is
// what lets `childRefFor` avoid two closure allocations per access.
const parent = valueForRef(ref.propertyParent as Reference);
lastValue = ref.lastValue = isDict(parent)
? (getProp(parent, ref.propertyPath as string) as T)
: undefined;
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
lastValue = ref.lastValue = ref.compute!();
}
} finally {
// Always end the frame to keep the tracking stack balanced, but commit
// the new tag/revision only on success (below) — matching `track()`'s
// semantics so a throwing getter leaves the reference's tag untouched.
newTag = endTrackFrame();
}

ref.lastRevision = valueForTag(newTag);
tag = ref.tag = newTag;
ref.lastRevision = valueForTag(newTag);
}
} else {
lastValue = ref.lastValue;
}

consumeTag(tag);
consumeTag(tag as Tag);

return lastValue as T;
}

export function updateRef(_ref: Reference, value: unknown) {
const ref = _ref as ReferenceImpl;

if (ref[REFERENCE] === CELL) {
// Equality-gated inline update — no closure indirection. Mirrors the old
// `createIteratorItemRef` setter semantics.
if (ref.lastValue !== value) {
ref.lastValue = value;
dirtyTag(ref.tag as DirtyableTag);
}
return;
}

if (ref[REFERENCE] === PROPERTY) {
// Inline `setProp` on the parent's value — mirrors the old childRefFor setter.
const parent = valueForRef(ref.propertyParent as Reference);
if (isDict(parent)) {
setProp(parent, ref.propertyPath as string, value);
}
return;
}

const update = expect(ref.update, 'called update on a non-updatable reference');

update(value);
Expand Down Expand Up @@ -216,26 +299,18 @@ export function childRefFor(_parentRef: Reference, path: string): Reference {
child = UNDEFINED_REFERENCE;
}
} else {
child = createComputeRef(
() => {
const parent = valueForRef(parentRef);

if (isDict(parent)) {
return getProp(parent, path);
}
},
(val) => {
const parent = valueForRef(parentRef);

if (isDict(parent)) {
return setProp(parent, path, val);
}
}
);
// A PROPERTY reference: `getProp`/`setProp` of `path` on the parent's value.
// Storing the parent + path as data (handled inline by valueForRef/updateRef)
// avoids allocating the getter and setter closures this used to need.
const propertyRef = new ReferenceImpl(PROPERTY);
propertyRef.propertyParent = parentRef;
propertyRef.propertyPath = path;

if (DEBUG) {
child.debugLabel = `${parentRef.debugLabel}.${path}`;
propertyRef.debugLabel = `${parentRef.debugLabel}.${path}`;
}

child = propertyRef;
}

children.set(path, child);
Expand All @@ -260,7 +335,11 @@ if (DEBUG) {
const update = isUpdatableRef(inner) ? (value: unknown): void => updateRef(inner, value) : null;
const ref = createComputeRef(() => valueForRef(inner), update);

ref[REFERENCE] = inner[REFERENCE];
// A debug alias is a genuine compute reference (it recomputes through
// `inner`); never inherit the CELL/PROPERTY types, whose fast paths assume
// the value (or parent + path) lives directly on the reference.
const innerType = inner[REFERENCE];
ref[REFERENCE] = innerType === CELL || innerType === PROPERTY ? COMPUTE : innerType;

ref.debugLabel = debugLabel;

Expand Down
Loading
Loading