From 8610111ebe2afac947d0b3316a96546cf2061628 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/5] Reuse the combined tag of a tracking frame endTrackFrame takes the tag that the same frame produced the last time it ran. If the frame consumed the same tags again, the result is that tag: it keeps its memoized revision, and the frame allocates no tag and no array. getValue passes the tag of its cache. Split out of #21650. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 30 ++++- packages/@glimmer/validator/lib/validators.ts | 17 +++ .../@glimmer/validator/test/tracking-test.ts | 127 ++++++++++++++++++ 3 files changed, 169 insertions(+), 5 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index 1ae354a32c6..7313e55d28d 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -5,7 +5,14 @@ import type { Revision } from './validators'; import { debug } from './debug'; import { unwrap } from './utils'; -import { combine, CONSTANT_TAG, isConstTag, validateTag, valueForTag } from './validators'; +import { + combine, + CONSTANT_TAG, + isCombinationOf, + isConstTag, + validateTag, + valueForTag, +} from './validators'; /** * A tag with the `slot` field that `Tracker#add` reads and writes. @@ -92,7 +99,12 @@ class Tracker { this.size = size + 1; } - combine(): Tag { + /** + * `previous` is the tag that this frame produced the last time it ran. + * If the frame consumed the same tags again, the result is `previous`, + * which keeps its memoized revision and needs no allocation. + */ + combine(previous: Tag | undefined): Tag { let { tags, size } = this; let result: Tag; @@ -100,6 +112,8 @@ class Tracker { result = CONSTANT_TAG; } else if (size === 1) { result = tags[0] as Tag; + } else if (previous !== undefined && isCombinationOf(previous, tags, size)) { + result = previous; } else { result = combine(tags.slice(0, size) as Tag[]); } @@ -192,7 +206,13 @@ export function beginTrackFrame(debuggingContext?: string | false): void { } } -export function endTrackFrame(): Tag { +/** + * Closes the current frame and returns its combined tag. + * + * Pass the tag that the same frame produced the last time it ran. + * If the frame consumed the same tags again, that tag is the result. + */ +export function endTrackFrame(previous?: Tag): Tag { let current = CURRENT_TRACKER; if (DEBUG) { @@ -205,7 +225,7 @@ export function endTrackFrame(): Tag { CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; - return unwrap(current).combine(); + return unwrap(current).combine(previous); } export function beginUntrackFrame(): void { @@ -313,7 +333,7 @@ export function getValue(cache: Cache): T | undefined { try { cache[LAST_VALUE] = fn(); } finally { - tag = endTrackFrame(); + tag = endTrackFrame(tag); cache[TAG] = tag; cache[SNAPSHOT] = valueForTag(tag); consumeTag(tag); diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 9745468c398..5fb0702b3f1 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -298,6 +298,23 @@ export const CURRENT_TAG = new CurrentTag(); export const combine = MonomorphicTagImpl.combine; +/** + * Whether `tag` is the combination of the first `size` entries of `tags`, in order. + */ +export function isCombinationOf(tag: Tag, tags: (Tag | null)[], size: number): boolean { + if (tag[TYPE] !== COMBINATOR_TAG_ID) return false; + + let subtags = (tag as MonomorphicTagImpl).subtag as Tag[]; + + if (subtags.length !== size) return false; + + for (let i = 0; i < size; i++) { + if (subtags[i] !== tags[i]) return false; + } + + return true; +} + // Warm let tag1 = createUpdatableTag(); diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 40d8d57f18a..d83159ad46f 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -307,6 +307,60 @@ module('@glimmer/validator: tracking', () => { assert.strictEqual(endTrackFrame(), tag); }); + test('it returns the previous tag if the frame consumed the same tags', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + + beginTrackFrame(); + consumeTag(tag1); + consumeTag(tag2); + let first = endTrackFrame(); + + beginTrackFrame(); + consumeTag(tag1); + consumeTag(tag2); + let second = endTrackFrame(first); + + assert.strictEqual(second, first); + }); + + test('it returns a new tag if the frame consumed other tags', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let tag3 = createTag(); + + beginTrackFrame(); + consumeTag(tag1); + consumeTag(tag2); + let first = endTrackFrame(); + + beginTrackFrame(); + consumeTag(tag1); + consumeTag(tag3); + let second = endTrackFrame(first); + + assert.notStrictEqual(second, first); + + let snapshot = valueForTag(second); + dirtyTag(tag2); + assert.ok(validateTag(second, snapshot)); + + dirtyTag(tag3); + assert.notOk(validateTag(second, snapshot)); + + beginTrackFrame(); + consumeTag(tag1); + consumeTag(tag3); + consumeTag(tag2); + let third = endTrackFrame(second); + + assert.notStrictEqual(third, second); + + snapshot = valueForTag(third); + dirtyTag(tag2); + assert.notOk(validateTag(third, snapshot)); + }); + test('it does not keep the tags of a frame that did not end', (assert) => { let tag1 = createTag(); let tag2 = createTag(); @@ -448,6 +502,79 @@ module('@glimmer/validator: tracking', () => { assert.deepEqual(getValue(outerCache), [3, 2], 'memoized result returned correctly'); }); + test('it tracks the new tags if the tags change between two runs', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let tag3 = createTag(); + let useThird = false; + let count = 0; + + let cache = createCache(() => { + consumeTag(tag1); + consumeTag(useThird ? tag3 : tag2); + + return ++count; + }); + + assert.strictEqual(getValue(cache), 1); + + dirtyTag(tag2); + assert.strictEqual(getValue(cache), 2, 'the cache ran again with the same tags'); + + dirtyTag(tag2); + assert.strictEqual(getValue(cache), 3, 'the reused tag is still dirtied by its tags'); + + useThird = true; + dirtyTag(tag1); + assert.strictEqual(getValue(cache), 4); + + dirtyTag(tag2); + assert.strictEqual(getValue(cache), 4, 'the cache does not depend on the old tag'); + + dirtyTag(tag3); + assert.strictEqual(getValue(cache), 5, 'the cache depends on the new tag'); + }); + + test('an outer cache runs again if a tag of a reused inner tag changes', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let tag3 = createTag(); + let innerCount = 0; + let outerCount = 0; + + let inner = createCache(() => { + consumeTag(tag1); + consumeTag(tag2); + + return ++innerCount; + }); + + let outer = createCache(() => { + consumeTag(tag3); + getValue(inner); + + return ++outerCount; + }); + + assert.strictEqual(getValue(outer), 1); + + dirtyTag(tag1); + assert.strictEqual(getValue(outer), 2, 'the inner cache ran again with the same tags'); + assert.strictEqual(innerCount, 2); + + dirtyTag(tag2); + assert.strictEqual(getValue(outer), 3, 'the outer cache follows the reused inner tag'); + assert.strictEqual(innerCount, 3); + + dirtyTag(tag3); + assert.strictEqual(getValue(outer), 4); + assert.strictEqual(innerCount, 3, 'the inner cache did not run again'); + + dirtyTag(tag1); + assert.strictEqual(getValue(outer), 5, 'the outer cache still follows the inner tag'); + assert.strictEqual(innerCount, 4); + }); + test('isTracking works within a memoized function and untrack frame', (assert) => { assert.notOk(isTracking()); From 4048c0cca39562ef65ca0b0ba553ab267b7f0ac2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:40:32 -0400 Subject: [PATCH 2/5] Keep the reuse check out of Tracker#combine With the check in `combine`, V8 did not inline the method into the end of the frame, and each small frame paid for a call. Against main, a frame with one tag was 4% to 7% slower, and `kairo: avoidable propagation` was 13% slower. The check is now in a function of its own, `combineOrReuse`, which only frames with two or more tags call. No case is more than 2% slower than main, and `kairo: mux` and `batch` keep their gain. Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/@glimmer/validator/lib/tracking.ts | 31 +++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index 7313e55d28d..d68912e94bf 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -24,6 +24,30 @@ interface ConsumedTag extends Tag { slot: number; } +/** + * Makes the tag of a frame that consumed two or more tags. + * + * `previous` is the tag that the frame produced the last time it ran. + * If the frame consumed the same tags again, in the same order, + * the result is `previous`. + * That tag keeps its memoized revision, and nothing is allocated. + * + * 1. last run: consumed [ a, b, c ] result: tag X. + * 2. this run: consumed [ a, b, c ] result: tag X again. + * 3. or: consumed [ a, c ] result: a new tag. + * + * This code is not in `Tracker#combine`, to keep that method small. + * V8 puts a small `combine` inline into the end of the frame. + * With this code in it, a frame that consumed one tag was 6% slower. + */ +function combineOrReuse(previous: Tag | undefined, tags: (Tag | null)[], size: number): Tag { + if (previous !== undefined && isCombinationOf(previous, tags, size)) { + return previous; + } + + return combine(tags.slice(0, size) as Tag[]); +} + /** * An object that tracks @tracked properties that were consumed. * @@ -101,8 +125,7 @@ class Tracker { /** * `previous` is the tag that this frame produced the last time it ran. - * If the frame consumed the same tags again, the result is `previous`, - * which keeps its memoized revision and needs no allocation. + * See `combineOrReuse`. */ combine(previous: Tag | undefined): Tag { let { tags, size } = this; @@ -112,10 +135,8 @@ class Tracker { result = CONSTANT_TAG; } else if (size === 1) { result = tags[0] as Tag; - } else if (previous !== undefined && isCombinationOf(previous, tags, size)) { - result = previous; } else { - result = combine(tags.slice(0, size) as Tag[]); + result = combineOrReuse(previous, tags, size); } this.clear(); From 6ab63a155e174cd41bddd76355d2487af7a147c2 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:44:46 -0400 Subject: [PATCH 3/5] Reuse the combined tag in the frame of a cache only Only `getValue` passes the last tag, so only a cache can reuse it. The reuse is now in code that only `getValue` calls: `endCacheFrame` and `Tracker#combineForCache`. `endTrackFrame` and `Tracker#combine` are the code of main again, with no argument. The frames of the render VM and of the curly component manager end there, so this change cannot make them slower. The two tests that passed a tag to `endTrackFrame` are gone. The tests of `createCache` cover the reuse. Co-Authored-By: Claude Opus 5.5 --- packages/@glimmer/validator/lib/tracking.ts | 59 +++++++++++++++---- .../@glimmer/validator/test/tracking-test.ts | 54 ----------------- 2 files changed, 49 insertions(+), 64 deletions(-) diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index d68912e94bf..1daf6eb249a 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -36,8 +36,8 @@ interface ConsumedTag extends Tag { * 2. this run: consumed [ a, b, c ] result: tag X again. * 3. or: consumed [ a, c ] result: a new tag. * - * This code is not in `Tracker#combine`, to keep that method small. - * V8 puts a small `combine` inline into the end of the frame. + * This code is not in `Tracker#combineForCache`, to keep that method small. + * V8 puts a small method inline into the end of the frame. * With this code in it, a frame that consumed one tag was 6% slower. */ function combineOrReuse(previous: Tag | undefined, tags: (Tag | null)[], size: number): Tag { @@ -123,11 +123,30 @@ class Tracker { this.size = size + 1; } + combine(): Tag { + let { tags, size } = this; + let result: Tag; + + if (size === 0) { + result = CONSTANT_TAG; + } else if (size === 1) { + result = tags[0] as Tag; + } else { + result = combine(tags.slice(0, size) as Tag[]); + } + + this.clear(); + + return result; + } + /** - * `previous` is the tag that this frame produced the last time it ran. + * `combine()` for the frame of a cache. + * + * `previous` is the tag that the cache has from its last run. * See `combineOrReuse`. */ - combine(previous: Tag | undefined): Tag { + combineForCache(previous: Tag | undefined): Tag { let { tags, size } = this; let result: Tag; @@ -227,13 +246,33 @@ export function beginTrackFrame(debuggingContext?: string | false): void { } } +export function endTrackFrame(): Tag { + let current = CURRENT_TRACKER; + + if (DEBUG) { + if (OPEN_TRACK_FRAMES.length === 0) { + throw new Error('attempted to close a tracking frame, but one was not open'); + } + + unwrap(debug.endTrackingTransaction)(); + } + + CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; + + return unwrap(current).combine(); +} + /** - * Closes the current frame and returns its combined tag. + * `endTrackFrame()` for the frame of a cache. + * + * `previous` is the tag that the cache has from its last run. + * If the cache consumed the same tags again, that tag is the result. * - * Pass the tag that the same frame produced the last time it ran. - * If the frame consumed the same tags again, that tag is the result. + * Only `getValue` calls this. + * The frames of the render VM end in `endTrackFrame`, + * which has no code for the reuse. */ -export function endTrackFrame(previous?: Tag): Tag { +function endCacheFrame(previous: Tag | undefined): Tag { let current = CURRENT_TRACKER; if (DEBUG) { @@ -246,7 +285,7 @@ export function endTrackFrame(previous?: Tag): Tag { CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop() || null; - return unwrap(current).combine(previous); + return unwrap(current).combineForCache(previous); } export function beginUntrackFrame(): void { @@ -354,7 +393,7 @@ export function getValue(cache: Cache): T | undefined { try { cache[LAST_VALUE] = fn(); } finally { - tag = endTrackFrame(tag); + tag = endCacheFrame(tag); cache[TAG] = tag; cache[SNAPSHOT] = valueForTag(tag); consumeTag(tag); diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index d83159ad46f..6c6a4b2eac7 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -307,60 +307,6 @@ module('@glimmer/validator: tracking', () => { assert.strictEqual(endTrackFrame(), tag); }); - test('it returns the previous tag if the frame consumed the same tags', (assert) => { - let tag1 = createTag(); - let tag2 = createTag(); - - beginTrackFrame(); - consumeTag(tag1); - consumeTag(tag2); - let first = endTrackFrame(); - - beginTrackFrame(); - consumeTag(tag1); - consumeTag(tag2); - let second = endTrackFrame(first); - - assert.strictEqual(second, first); - }); - - test('it returns a new tag if the frame consumed other tags', (assert) => { - let tag1 = createTag(); - let tag2 = createTag(); - let tag3 = createTag(); - - beginTrackFrame(); - consumeTag(tag1); - consumeTag(tag2); - let first = endTrackFrame(); - - beginTrackFrame(); - consumeTag(tag1); - consumeTag(tag3); - let second = endTrackFrame(first); - - assert.notStrictEqual(second, first); - - let snapshot = valueForTag(second); - dirtyTag(tag2); - assert.ok(validateTag(second, snapshot)); - - dirtyTag(tag3); - assert.notOk(validateTag(second, snapshot)); - - beginTrackFrame(); - consumeTag(tag1); - consumeTag(tag3); - consumeTag(tag2); - let third = endTrackFrame(second); - - assert.notStrictEqual(third, second); - - snapshot = valueForTag(third); - dirtyTag(tag2); - assert.notOk(validateTag(third, snapshot)); - }); - test('it does not keep the tags of a frame that did not end', (assert) => { let tag1 = createTag(); let tag2 = createTag(); From e0ff7fb88dfe67b892bed0501b175ce7b74e085b Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:47:27 -0400 Subject: [PATCH 4/5] Test that a cache keeps its tag The tests of this change passed with the code of main too, because they only checked that a cache still follows its tags. The new tests read the tag of a cache through an outer frame. One of them fails on main: a cache that consumes the same tags again must keep its tag. Two more need a new tag: for other tags, and for the same tags in another order. Those two fail if the check says "same tags" for every combined tag. Co-Authored-By: Claude Opus 5.5 --- .../@glimmer/validator/test/tracking-test.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 6c6a4b2eac7..8097c0550a2 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -448,6 +448,95 @@ module('@glimmer/validator: tracking', () => { assert.deepEqual(getValue(outerCache), [3, 2], 'memoized result returned correctly'); }); + /** + * The tag that a cache has after a read. + * + * The outer frame consumes only the tag of the cache, + * and a frame with one tag returns that tag. + */ + function tagOf(cache: ReturnType) { + beginTrackFrame(); + getValue(cache); + + return endTrackFrame(); + } + + test('it keeps its tag if it consumes the same tags in the next run', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let count = 0; + + let cache = createCache(() => { + consumeTag(tag1); + consumeTag(tag2); + + return ++count; + }); + + let first = tagOf(cache); + + dirtyTag(tag1); + + let second = tagOf(cache); + + assert.strictEqual(count, 2, 'the cache ran again'); + assert.strictEqual(second, first, 'the cache has the tag of its first run'); + }); + + test('it gets a new tag if it consumes other tags in the next run', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let tag3 = createTag(); + let useThird = false; + let count = 0; + + let cache = createCache(() => { + consumeTag(tag1); + consumeTag(useThird ? tag3 : tag2); + + return ++count; + }); + + let first = tagOf(cache); + + useThird = true; + dirtyTag(tag1); + + let second = tagOf(cache); + + assert.strictEqual(count, 2, 'the cache ran again'); + assert.notStrictEqual(second, first); + }); + + test('it gets a new tag if it consumes the same tags in another order', (assert) => { + let tag1 = createTag(); + let tag2 = createTag(); + let swap = false; + let count = 0; + + let cache = createCache(() => { + consumeTag(swap ? tag2 : tag1); + consumeTag(swap ? tag1 : tag2); + + return ++count; + }); + + let first = tagOf(cache); + + swap = true; + dirtyTag(tag1); + + let second = tagOf(cache); + + assert.strictEqual(count, 2, 'the cache ran again'); + assert.notStrictEqual(second, first); + + let snapshot = valueForTag(second); + + dirtyTag(tag2); + assert.notOk(validateTag(second, snapshot), 'the new tag follows both tags'); + }); + test('it tracks the new tags if the tags change between two runs', (assert) => { let tag1 = createTag(); let tag2 = createTag(); From 12bc8d460dbaac247df4fc853009a09c2c2e541d Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:18:04 -0400 Subject: [PATCH 5/5] Use steps in the tests of the cache tag Each cache now records a step with a text that says what it reads, for example "cache reads tag1 and tag3". A test checks the steps after each read, in place of a counter that went from 1 to 5. A read that must not run the cache checks for no step. Co-Authored-By: Claude Opus 5.5 --- .../@glimmer/validator/test/tracking-test.ts | 107 +++++++++++------- 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 8097c0550a2..eedb07297d5 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -464,22 +464,23 @@ module('@glimmer/validator: tracking', () => { test('it keeps its tag if it consumes the same tags in the next run', (assert) => { let tag1 = createTag(); let tag2 = createTag(); - let count = 0; let cache = createCache(() => { + assert.step('cache reads tag1 and tag2'); + consumeTag(tag1); consumeTag(tag2); - - return ++count; }); let first = tagOf(cache); + assert.verifySteps(['cache reads tag1 and tag2']); + dirtyTag(tag1); let second = tagOf(cache); - assert.strictEqual(count, 2, 'the cache ran again'); + assert.verifySteps(['cache reads tag1 and tag2'], 'the cache ran again'); assert.strictEqual(second, first, 'the cache has the tag of its first run'); }); @@ -488,23 +489,29 @@ module('@glimmer/validator: tracking', () => { let tag2 = createTag(); let tag3 = createTag(); let useThird = false; - let count = 0; let cache = createCache(() => { consumeTag(tag1); - consumeTag(useThird ? tag3 : tag2); - return ++count; + if (useThird) { + assert.step('cache reads tag1 and tag3'); + consumeTag(tag3); + } else { + assert.step('cache reads tag1 and tag2'); + consumeTag(tag2); + } }); let first = tagOf(cache); + assert.verifySteps(['cache reads tag1 and tag2']); + useThird = true; dirtyTag(tag1); let second = tagOf(cache); - assert.strictEqual(count, 2, 'the cache ran again'); + assert.verifySteps(['cache reads tag1 and tag3'], 'the cache ran again'); assert.notStrictEqual(second, first); }); @@ -512,23 +519,29 @@ module('@glimmer/validator: tracking', () => { let tag1 = createTag(); let tag2 = createTag(); let swap = false; - let count = 0; let cache = createCache(() => { - consumeTag(swap ? tag2 : tag1); - consumeTag(swap ? tag1 : tag2); - - return ++count; + if (swap) { + assert.step('cache reads tag2, then tag1'); + consumeTag(tag2); + consumeTag(tag1); + } else { + assert.step('cache reads tag1, then tag2'); + consumeTag(tag1); + consumeTag(tag2); + } }); let first = tagOf(cache); + assert.verifySteps(['cache reads tag1, then tag2']); + swap = true; dirtyTag(tag1); let second = tagOf(cache); - assert.strictEqual(count, 2, 'the cache ran again'); + assert.verifySteps(['cache reads tag2, then tag1'], 'the cache ran again'); assert.notStrictEqual(second, first); let snapshot = valueForTag(second); @@ -542,72 +555,90 @@ module('@glimmer/validator: tracking', () => { let tag2 = createTag(); let tag3 = createTag(); let useThird = false; - let count = 0; let cache = createCache(() => { consumeTag(tag1); - consumeTag(useThird ? tag3 : tag2); - return ++count; + if (useThird) { + assert.step('cache reads tag1 and tag3'); + consumeTag(tag3); + } else { + assert.step('cache reads tag1 and tag2'); + consumeTag(tag2); + } }); - assert.strictEqual(getValue(cache), 1); + getValue(cache); + assert.verifySteps(['cache reads tag1 and tag2']); dirtyTag(tag2); - assert.strictEqual(getValue(cache), 2, 'the cache ran again with the same tags'); + getValue(cache); + assert.verifySteps(['cache reads tag1 and tag2'], 'the cache ran again with the same tags'); dirtyTag(tag2); - assert.strictEqual(getValue(cache), 3, 'the reused tag is still dirtied by its tags'); + getValue(cache); + assert.verifySteps(['cache reads tag1 and tag2'], 'the reused tag still follows its tags'); useThird = true; dirtyTag(tag1); - assert.strictEqual(getValue(cache), 4); + getValue(cache); + assert.verifySteps(['cache reads tag1 and tag3']); dirtyTag(tag2); - assert.strictEqual(getValue(cache), 4, 'the cache does not depend on the old tag'); + getValue(cache); + assert.verifySteps([], 'the cache does not depend on the old tag'); dirtyTag(tag3); - assert.strictEqual(getValue(cache), 5, 'the cache depends on the new tag'); + getValue(cache); + assert.verifySteps(['cache reads tag1 and tag3'], 'the cache depends on the new tag'); }); test('an outer cache runs again if a tag of a reused inner tag changes', (assert) => { let tag1 = createTag(); let tag2 = createTag(); let tag3 = createTag(); - let innerCount = 0; - let outerCount = 0; let inner = createCache(() => { + assert.step('inner reads tag1 and tag2'); + consumeTag(tag1); consumeTag(tag2); - - return ++innerCount; }); let outer = createCache(() => { + assert.step('outer reads tag3 and inner'); + consumeTag(tag3); getValue(inner); - - return ++outerCount; }); - assert.strictEqual(getValue(outer), 1); + getValue(outer); + assert.verifySteps(['outer reads tag3 and inner', 'inner reads tag1 and tag2']); dirtyTag(tag1); - assert.strictEqual(getValue(outer), 2, 'the inner cache ran again with the same tags'); - assert.strictEqual(innerCount, 2); + getValue(outer); + assert.verifySteps( + ['outer reads tag3 and inner', 'inner reads tag1 and tag2'], + 'the inner cache ran again with the same tags' + ); dirtyTag(tag2); - assert.strictEqual(getValue(outer), 3, 'the outer cache follows the reused inner tag'); - assert.strictEqual(innerCount, 3); + getValue(outer); + assert.verifySteps( + ['outer reads tag3 and inner', 'inner reads tag1 and tag2'], + 'the outer cache follows the reused inner tag' + ); dirtyTag(tag3); - assert.strictEqual(getValue(outer), 4); - assert.strictEqual(innerCount, 3, 'the inner cache did not run again'); + getValue(outer); + assert.verifySteps(['outer reads tag3 and inner'], 'the inner cache did not run again'); dirtyTag(tag1); - assert.strictEqual(getValue(outer), 5, 'the outer cache still follows the inner tag'); - assert.strictEqual(innerCount, 4); + getValue(outer); + assert.verifySteps( + ['outer reads tag3 and inner', 'inner reads tag1 and tag2'], + 'the outer cache still follows the inner tag' + ); }); test('isTracking works within a memoized function and untrack frame', (assert) => {