From 860195b62e370858887ad6577e8c55aebb533bd2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:18:52 -0400 Subject: [PATCH 1/2] Pool the trackers of tracking frames Each tracking frame made one Tracker, one Set, and one array from that Set. Frames are strictly nested, so beginTrackFrame now takes the tracker for its depth from a pool. A tracker keeps its tags in an array. A tag keeps the index at which a tracker took it last, in a new field `slot`. A tracker has the tag if its entry at that index is the tag, so a tag that the frame consumes again costs one comparison. resetTracking clears the pooled trackers. An untrack frame takes a depth but no tracker, so the pool can have holes. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 96 ++++++++++++++++--- packages/@glimmer/validator/lib/validators.ts | 7 ++ .../@glimmer/validator/test/tracking-test.ts | 86 +++++++++++++++++ 3 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index d94ae456e6b..4596f340f5d 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -8,34 +8,80 @@ import { unwrap } from './utils'; import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; /** - * An object that that tracks @tracked properties that were consumed. + * A tag of this package. Each tag class declares `slot`: + * the index of the tag in the tracker that took it last. */ -class Tracker { - private tags = new Set(); - private last: Tag | null = null; +interface ConsumedTag extends Tag { + slot: number; +} +/** + * An object that tracks @tracked properties that were consumed. + * + * Trackers are pooled by frame depth (see `beginTrackFrame`), + * so a tracker holds no tag after its frame ends. + */ +class Tracker { + /** + * The consumed tags. `size` counts the live entries. + * + * The array never shrinks, because a write to `length` is a slow path in V8, + * and this code runs for every frame. + */ + private tags: (Tag | null)[] = []; + private size = 0; + + /** + * A tag keeps the index at which a tracker took it last. + * If this tracker has the tag at that index, the frame consumed the tag before, + * so a tag that the frame consumes again costs one comparison. + * + * The index can come from another tracker. + * The entry at that index is then another tag or no tag, and this tracker takes the tag. + * So a tag that a nested frame takes at another index, between two consumptions + * of this frame, is taken two times. A combined tag with a duplicate has the same revision. + */ add(tag: Tag) { if (tag === CONSTANT_TAG) return; - this.tags.add(tag); - if (DEBUG) { unwrap(debug.markTagAsConsumed)(tag); } - this.last = tag; + let { tags, size } = this; + + if (tags[(tag as ConsumedTag).slot] === tag) return; + + (tag as ConsumedTag).slot = size; + tags[size] = tag; + this.size = size + 1; } combine(): Tag { - let { tags } = this; + let { tags, size } = this; + let result: Tag; - if (tags.size === 0) { - return CONSTANT_TAG; - } else if (tags.size === 1) { - return this.last as Tag; + if (size === 0) { + result = CONSTANT_TAG; + } else if (size === 1) { + result = tags[0] as Tag; } else { - return combine(Array.from(this.tags)); + result = combine(tags.slice(0, size) as Tag[]); } + + this.clear(); + + return result; + } + + clear(): void { + let { tags, size } = this; + + for (let i = 0; i < size; i++) { + tags[i] = null; + } + + this.size = 0; } } @@ -56,10 +102,26 @@ let CURRENT_TRACKER: Tracker | null = null; const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; +/** + * Frames are strictly nested, so the tracker of a frame at depth `n` is free + * when that frame ends. One tracker for each depth is enough. + * + * An untrack frame takes a depth but no tracker, so the pool can have holes. + */ +const TRACKER_POOL: (Tracker | undefined)[] = []; + export function beginTrackFrame(debuggingContext?: string | false): void { + let depth = OPEN_TRACK_FRAMES.length; + OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); - CURRENT_TRACKER = new Tracker(); + let tracker = TRACKER_POOL[depth]; + + if (tracker === undefined) { + tracker = TRACKER_POOL[depth] = new Tracker(); + } + + CURRENT_TRACKER = tracker; if (DEBUG) { unwrap(debug.beginTrackingTransaction)(debuggingContext); @@ -101,6 +163,12 @@ export function resetTracking(): string | void { OPEN_TRACK_FRAMES.pop(); } + for (let tracker of TRACKER_POOL) { + if (tracker !== undefined) { + tracker.clear(); + } + } + CURRENT_TRACKER = null; if (DEBUG) { diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 66c7f1ac1cb..7de985abf97 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -114,6 +114,11 @@ class MonomorphicTagImpl { public subtag: Tag | Tag[] | null = null; private subtagBufferCache: Revision | null = null; + /** + * The index of this tag in the tracker that took it last. + */ + public slot = 0; + declare [TYPE]: T; constructor(type: T) { @@ -254,6 +259,7 @@ const VOLATILE_TAG_ID: IVOLATILE_TAG_ID = 100; export class VolatileTag implements Tag { readonly [TYPE] = VOLATILE_TAG_ID; + slot = 0; [COMPUTE](): Revision { return VOLATILE; } @@ -267,6 +273,7 @@ const CURRENT_TAG_ID: ICURRENT_TAG_ID = 101; export class CurrentTag implements Tag { readonly [TYPE] = CURRENT_TAG_ID; + slot = 0; [COMPUTE](): Revision { return $REVISION; } diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 1a0fc4c1dc0..40d8d57f18a 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -1,6 +1,7 @@ import { DEBUG } from '@glimmer/env'; import { beginTrackFrame, + beginUntrackFrame, consumeTag, createCache, createTag, @@ -10,6 +11,7 @@ import { getValue, isConst, isTracking, + resetTracking, track, trackedData, untrack, @@ -252,6 +254,90 @@ module('@glimmer/validator: tracking', () => { assert.notOk(validateTag(combined, snapshot)); }); + test('it returns the tag itself if the frame consumed one tag many times', (assert) => { + let tag = createTag(); + + beginTrackFrame(); + + consumeTag(tag); + consumeTag(tag); + + assert.strictEqual(endTrackFrame(), tag); + }); + + test('it keeps a tag that a nested frame consumed between two consumptions', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + beginTrackFrame(); + + consumeTag(tag1); + consumeTag(tag2); + + beginTrackFrame(); + consumeTag(tag1); + let inner = endTrackFrame(); + + consumeTag(tag1); + + let outer = endTrackFrame(); + + assert.strictEqual(inner, tag1); + + let snapshot = valueForTag(outer); + dirtyTag(tag1); + assert.notOk(validateTag(outer, snapshot)); + + snapshot = valueForTag(outer); + dirtyTag(tag2); + assert.notOk(validateTag(outer, snapshot)); + }); + + test('it takes a tag one time if nested frames consume it at the same index', (assert) => { + let tag = createTag(); + + beginTrackFrame(); + + for (let i = 0; i < 3; i++) { + beginTrackFrame(); + consumeTag(tag); + consumeTag(endTrackFrame()); + } + + assert.strictEqual(endTrackFrame(), tag); + }); + + test('it does not keep the tags of a frame that did not end', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + beginTrackFrame(); + consumeTag(tag1); + + resetTracking(); + + beginTrackFrame(); + consumeTag(tag2); + + assert.strictEqual(endTrackFrame(), tag2); + }); + + test('it resets after a frame that began inside untrack frames', (assert) => { + /** + * Deeper than any other test goes, + * so these depths have no tracker yet. + */ + for (let i = 0; i < 100; i++) { + beginUntrackFrame(); + } + + beginTrackFrame(); + + resetTracking(); + + assert.notOk(isTracking()); + }); + test('isTracking works within a track', (assert) => { assert.notOk(isTracking()); From d7f60e2cb4955d6c8be57671e1f7d75b04ec4091 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:35:26 -0400 Subject: [PATCH 2/2] Explain the tracker pool with diagrams The comment blocks now show the tags array of a tracker, how `slot` finds a tag that the frame has already, the nested frame that gives a duplicate, and which tracker of the pool each frame depth uses. Comments only. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 104 +++++++++++++++--- packages/@glimmer/validator/lib/validators.ts | 16 ++- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index 4596f340f5d..1ae354a32c6 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -8,8 +8,10 @@ import { unwrap } from './utils'; import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; /** - * A tag of this package. Each tag class declares `slot`: - * the index of the tag in the tracker that took it last. + * A tag with the `slot` field that `Tracker#add` reads and writes. + * + * Each tag class of this package declares the field, + * so a tag has it from its construction. */ interface ConsumedTag extends Tag { slot: number; @@ -18,28 +20,61 @@ interface ConsumedTag extends Tag { /** * An object that tracks @tracked properties that were consumed. * - * Trackers are pooled by frame depth (see `beginTrackFrame`), - * so a tracker holds no tag after its frame ends. + * A tracker collects the tags that one tracking frame consumes. + * When the frame ends, `combine()` makes one tag from them. + * + * A tracker is not made for each frame. + * `beginTrackFrame` takes it from `TRACKER_POOL`, so the next frame + * at the same depth uses the same tracker again. */ class Tracker { /** - * The consumed tags. `size` counts the live entries. + * The tags that the current frame consumed. + * Each tag is at the position of its first consumption. + * `size` is the number of tags. + * + * tags: [ a, b, c, null, null ] + * size: 3 * - * The array never shrinks, because a write to `length` is a slow path in V8, - * and this code runs for every frame. + * The entries from `size` up are `null`. + * They are left from an earlier frame that consumed more tags. + * + * The array never shrinks. + * A write to `length` is a slow path in V8, and this code runs for every frame. */ private tags: (Tag | null)[] = []; private size = 0; /** - * A tag keeps the index at which a tracker took it last. - * If this tracker has the tag at that index, the frame consumed the tag before, - * so a tag that the frame consumes again costs one comparison. + * Adds a tag to the frame, one time. + * + * A frame can consume one tag many times, + * so the tracker must find out if it has the tag already. + * It does that with no `Set`: the tag keeps the index + * at which a tracker put it last, in `tag.slot`. + * + * consume a tags: [ a ] a.slot = 0 + * consume b tags: [ a, b ] b.slot = 1 + * consume a tags[a.slot] is a, so the frame has it. No change. * - * The index can come from another tracker. - * The entry at that index is then another tag or no tag, and this tracker takes the tag. - * So a tag that a nested frame takes at another index, between two consumptions - * of this frame, is taken two times. A combined tag with a duplicate has the same revision. + * The check is `tags[tag.slot] === tag`. + * It compares the entry with the tag, so a `slot` that is out of date + * cannot hide a tag. The worst case is a tag that is in the array two times. + * + * That case needs a nested frame. + * The nested frame has its own tracker, and that tracker also writes `slot`: + * + * 1. outer frame consumes a, b outer tags: [ a, b ] b.slot = 1 + * 2. inner frame consumes b inner tags: [ b ] b.slot = 0 + * 3. outer frame consumes b outer tags[0] is a, not b. + * outer tags: [ a, b, b ] b.slot = 2 + * + * The duplicate does no harm. + * The revision of a combined tag is the highest revision of its tags, + * and a tag that is there two times does not change the highest. + * + * If the inner frame puts the tag at the index that the outer frame used, + * the check of the outer frame still passes, and there is no duplicate. */ add(tag: Tag) { if (tag === CONSTANT_TAG) return; @@ -74,6 +109,18 @@ class Tracker { return result; } + /** + * Empties the tracker for the next frame at the same depth. + * + * before: tags: [ a, b, c ] size: 3 + * after: tags: [ null, null, null ] size: 0 + * + * The entries must be `null`, and not only ignored: + * + * - an entry that stays is a match for `tags[tag.slot] === tag`, + * so the next frame would not add that tag + * - an entry that stays keeps its tag alive after the frame + */ clear(): void { let { tags, size } = this; @@ -103,10 +150,27 @@ let CURRENT_TRACKER: Tracker | null = null; const OPEN_TRACK_FRAMES: (Tracker | null)[] = []; /** - * Frames are strictly nested, so the tracker of a frame at depth `n` is free - * when that frame ends. One tracker for each depth is enough. + * The trackers, by the depth of the frame that uses them. + * The depth of a frame is the number of frames that are open around it. + * + * Frames are strictly nested: a frame ends before the frame around it ends. + * So two frames at the same depth are never open at the same time, + * and one tracker for each depth is enough. + * + * frame A depth 0 TRACKER_POOL[0] + * |- frame B depth 1 TRACKER_POOL[1] + * | `- frame C depth 2 TRACKER_POOL[2] + * `- frame D depth 1 TRACKER_POOL[1], which B used before + * + * A tracker is made the first time that a frame opens at its depth. + * After that, a frame at that depth allocates no tracker. * - * An untrack frame takes a depth but no tracker, so the pool can have holes. + * An untrack frame takes a depth but no tracker, so the pool can have holes: + * + * untrack frame depth 0 no tracker + * `- frame E depth 1 TRACKER_POOL[1] + * + * TRACKER_POOL: [ , tracker ] */ const TRACKER_POOL: (Tracker | undefined)[] = []; @@ -163,6 +227,12 @@ export function resetTracking(): string | void { OPEN_TRACK_FRAMES.pop(); } + /** + * A frame that did not end left its tags in its tracker. + * The next frame at that depth must start with no tag. + * + * The pool can have holes, see `TRACKER_POOL`. + */ for (let tracker of TRACKER_POOL) { if (tracker !== undefined) { tracker.clear(); diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 7de985abf97..01fafbfd19f 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -115,7 +115,11 @@ class MonomorphicTagImpl { private subtagBufferCache: Revision | null = null; /** - * The index of this tag in the tracker that took it last. + * The index at which a tracker put this tag last. + * `Tracker#add` uses it to find out if a tracking frame has the tag already. + * + * The start value 0 is safe: + * the tracker compares its entry at that index with the tag. */ public slot = 0; @@ -259,7 +263,12 @@ const VOLATILE_TAG_ID: IVOLATILE_TAG_ID = 100; export class VolatileTag implements Tag { readonly [TYPE] = VOLATILE_TAG_ID; + + /** + * See `slot` of `MonomorphicTagImpl`. + */ slot = 0; + [COMPUTE](): Revision { return VOLATILE; } @@ -273,7 +282,12 @@ const CURRENT_TAG_ID: ICURRENT_TAG_ID = 101; export class CurrentTag implements Tag { readonly [TYPE] = CURRENT_TAG_ID; + + /** + * See `slot` of `MonomorphicTagImpl`. + */ slot = 0; + [COMPUTE](): Revision { return $REVISION; }