Skip to content

Commit 74c2cf2

Browse files
committed
Add file-meta GC coverage
1 parent 066245f commit 74c2cf2

2 files changed

Lines changed: 105 additions & 43 deletions

File tree

packages/host/app/lib/gc-card-store.ts

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ export default class CardStoreWithGarbageCollection implements CardStore {
127127
// importantly these properties are not tracked so that we are able
128128
// to deserialize an instance without glimmer rendering the inner workings of
129129
// the deserialization process.
130-
#nonTrackedCards = new Map<string, StoredInstance>();
131-
#nonTrackedCardErrors = new Map<string, CardErrorJSONAPI>();
130+
#nonTrackedInstances = new Map<string, StoredInstance>();
131+
#nonTrackedInstanceErrors = new Map<string, CardErrorJSONAPI>();
132132

133133
#cards = new TrackedMap<string, StoredInstance>();
134-
#cardErrors = new TrackedMap<string, CardErrorJSONAPI>();
134+
#instanceErrors = new TrackedMap<string, CardErrorJSONAPI>();
135135
#gcCandidates: Set<LocalId> = new Set();
136136
#referenceCount: ReferenceCount;
137137
#idResolver = new IDResolver();
@@ -299,9 +299,9 @@ export default class CardStoreWithGarbageCollection implements CardStore {
299299

300300
reset() {
301301
this.#cards.clear();
302-
this.#clearEphemeralErrors(this.#cardErrors);
303-
this.#nonTrackedCards.clear();
304-
this.#clearEphemeralErrors(this.#nonTrackedCardErrors);
302+
this.#clearEphemeralErrors(this.#instanceErrors);
303+
this.#nonTrackedInstances.clear();
304+
this.#clearEphemeralErrors(this.#nonTrackedInstanceErrors);
305305
this.#gcCandidates.clear();
306306
this.#cardDocsInFlight.clear();
307307
this.#fileMetaDocsInFlight.clear();
@@ -334,20 +334,26 @@ export default class CardStoreWithGarbageCollection implements CardStore {
334334
sweep(api: typeof CardAPI) {
335335
let dependencyGraph = this.makeDependencyGraph(api);
336336
let reachable = new Set<string>();
337-
let visited = new WeakSet<CardDef>();
337+
let visited = new WeakSet<StoredInstance>();
338338
let rootLocalIds: string[] = [];
339339

340340
for (let instance of this.#cards.values()) {
341-
if (!instance || !isCardInstance(instance)) {
341+
if (!instance || visited.has(instance)) {
342342
continue;
343343
}
344-
if (visited.has(instance)) {
344+
visited.add(instance);
345+
if (isCardInstance(instance)) {
346+
let localId = instance[localIdSymbol];
347+
if (this.hasReferences(localId)) {
348+
rootLocalIds.push(localId);
349+
}
345350
continue;
346351
}
347-
visited.add(instance);
348-
let localId = instance[localIdSymbol];
349-
if (this.hasReferences(localId)) {
350-
rootLocalIds.push(localId);
352+
if (isFileDefInstance(instance)) {
353+
let fileId = instance.id;
354+
if (fileId && this.hasReferences(fileId)) {
355+
reachable.add(fileId);
356+
}
351357
}
352358
}
353359

@@ -367,49 +373,57 @@ export default class CardStoreWithGarbageCollection implements CardStore {
367373
}
368374
}
369375

370-
visited = new WeakSet<CardDef>();
376+
visited = new WeakSet<StoredInstance>();
371377
for (let instance of this.#cards.values()) {
372-
if (!instance || !isCardInstance(instance)) {
373-
continue;
374-
}
375-
if (visited.has(instance)) {
378+
if (!instance || visited.has(instance)) {
376379
continue;
377380
}
378381
visited.add(instance);
379-
let localId = instance[localIdSymbol];
380-
if (!reachable.has(localId)) {
381-
if (this.#gcCandidates.has(localId)) {
382+
let gcId: string | undefined;
383+
let extraDeleteIds: string[] = [];
384+
if (isCardInstance(instance)) {
385+
gcId = instance[localIdSymbol];
386+
if (instance.id) {
387+
extraDeleteIds.push(instance.id);
388+
}
389+
} else if (isFileDefInstance(instance)) {
390+
gcId = instance.id;
391+
}
392+
if (!gcId) {
393+
continue; // we should alwyays have a gcId by this point, but this helps TypeScript know that
394+
}
395+
if (!reachable.has(gcId)) {
396+
if (this.#gcCandidates.has(gcId)) {
382397
destroy(instance);
383-
// brand the instance to make it easier for debugging
384398
(instance as unknown as any)[
385399
Symbol.for('__instance_detached_from_store')
386400
] = true;
387-
this.delete(localId);
388-
if (instance.id) {
389-
this.delete(instance.id);
401+
this.delete(gcId);
402+
for (let id of extraDeleteIds) {
403+
this.delete(id);
390404
}
391405
} else {
392-
this.#gcCandidates.add(localId);
406+
this.#gcCandidates.add(gcId);
393407
}
394408
} else {
395-
this.#gcCandidates.delete(localId);
409+
this.#gcCandidates.delete(gcId);
396410
}
397411
}
398412
}
399413

400414
makeTracked(remoteId: string) {
401415
remoteId = remoteId.replace(/\.json$/, '');
402-
let instance = this.#nonTrackedCards.get(remoteId);
416+
let instance = this.#nonTrackedInstances.get(remoteId);
403417
if (instance) {
404418
this.setItem(remoteId, instance);
405419
}
406-
this.#nonTrackedCards.delete(remoteId);
420+
this.#nonTrackedInstances.delete(remoteId);
407421

408-
let error = this.#nonTrackedCardErrors.get(remoteId);
422+
let error = this.#nonTrackedInstanceErrors.get(remoteId);
409423
if (error) {
410424
this.addInstanceOrError(remoteId, error);
411425
}
412-
this.#nonTrackedCardErrors.delete(remoteId);
426+
this.#nonTrackedInstanceErrors.delete(remoteId);
413427
}
414428

415429
consumersOf(api: typeof CardAPI, instance: CardDef) {
@@ -431,9 +445,9 @@ export default class CardStoreWithGarbageCollection implements CardStore {
431445
private deleteFromAll(id: string) {
432446
id = id.replace(/\.json$/, '');
433447
this.#cards.delete(id);
434-
this.#cardErrors.delete(id);
435-
this.#nonTrackedCards.delete(id);
436-
this.#nonTrackedCardErrors.delete(id);
448+
this.#instanceErrors.delete(id);
449+
this.#nonTrackedInstances.delete(id);
450+
this.#nonTrackedInstanceErrors.delete(id);
437451
}
438452

439453
private getItem(type: 'instance', id: string): StoredInstance | undefined;
@@ -465,9 +479,11 @@ export default class CardStoreWithGarbageCollection implements CardStore {
465479
item: StoredInstance | CardErrorJSONAPI | undefined;
466480
localId: string | undefined;
467481
} {
468-
let bucket = type === 'instance' ? this.#cards : this.#cardErrors;
482+
let bucket = type === 'instance' ? this.#cards : this.#instanceErrors;
469483
let silentBucket =
470-
type === 'instance' ? this.#nonTrackedCards : this.#nonTrackedCardErrors;
484+
type === 'instance'
485+
? this.#nonTrackedInstances
486+
: this.#nonTrackedInstanceErrors;
471487
let localId = isLocalId(localOrRemoteId) ? localOrRemoteId : undefined;
472488
let remoteId = !isLocalId(localOrRemoteId) ? localOrRemoteId : undefined;
473489
let item: StoredInstance | CardErrorJSONAPI | undefined;
@@ -505,10 +521,10 @@ export default class CardStoreWithGarbageCollection implements CardStore {
505521
notTracked?: true,
506522
) {
507523
id = id.replace(/\.json$/, '');
508-
let cardBucket = notTracked ? this.#nonTrackedCards : this.#cards;
524+
let cardBucket = notTracked ? this.#nonTrackedInstances : this.#cards;
509525
let errorBucket = notTracked
510-
? this.#nonTrackedCardErrors
511-
: this.#cardErrors;
526+
? this.#nonTrackedInstanceErrors
527+
: this.#instanceErrors;
512528
if (!isLocalId(id) && isCardInstance(item)) {
513529
this.#idResolver.addIdPair(item[localIdSymbol], id);
514530
} else if (!isLocalId(id)) {
@@ -569,10 +585,18 @@ export default class CardStoreWithGarbageCollection implements CardStore {
569585
}
570586
}
571587

572-
private hasReferences(localId: string): boolean {
573-
let referenceCount = this.#referenceCount.get(localId) ?? 0;
574-
for (let remoteId of this.#idResolver.getRemoteIds(localId)) {
575-
referenceCount += this.#referenceCount.get(remoteId) ?? 0;
588+
private hasReferences(id: string): boolean {
589+
let idsToCheck = new Set<string>([id]);
590+
let localId = isLocalId(id) ? id : this.#idResolver.getLocalId(id);
591+
if (localId) {
592+
idsToCheck.add(localId);
593+
for (let remoteId of this.#idResolver.getRemoteIds(localId)) {
594+
idsToCheck.add(remoteId);
595+
}
596+
}
597+
let referenceCount = 0;
598+
for (let refId of idsToCheck) {
599+
referenceCount += this.#referenceCount.get(refId) ?? 0;
576600
}
577601
return referenceCount > 0;
578602
}

packages/host/tests/integration/store-test.gts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,44 @@ module('Integration | Store', function (hooks) {
377377
);
378378
});
379379

380+
test('file-meta instance is retained when referenced', async function (assert) {
381+
await testRealm.write('hero.png', 'mock hero image');
382+
let fileUrl = `${testRealmURL}hero.png`;
383+
384+
let fileInstance = await storeService.get(fileUrl);
385+
assert.ok(fileInstance, 'file meta instance is loaded');
386+
assert.ok(
387+
(fileInstance as any).constructor?.isFileDef,
388+
'file meta instance is a FileDef',
389+
);
390+
391+
storeService.addReference(fileUrl);
392+
forceGC();
393+
394+
let peekedInstance = cardStore.get(fileUrl) as unknown;
395+
assert.strictEqual(
396+
peekedInstance,
397+
fileInstance,
398+
'file meta instance is retained after GC with reference',
399+
);
400+
});
401+
402+
test('file-meta instance is garbage collected when references drop', async function (assert) {
403+
await testRealm.write('hero.png', 'mock hero image');
404+
let fileUrl = `${testRealmURL}hero.png`;
405+
406+
await storeService.get(fileUrl);
407+
storeService.addReference(fileUrl);
408+
storeService.dropReference(fileUrl);
409+
forceGC();
410+
411+
assert.strictEqual(
412+
cardStore.get(fileUrl),
413+
undefined,
414+
'file meta instance is garbage collected after reference drop',
415+
);
416+
});
417+
380418
test('garbage collects cards that only consume each other', async function (assert) {
381419
let alpha = new PersonDef({ name: 'Alpha' });
382420
let beta = new PersonDef({ name: 'Beta' });

0 commit comments

Comments
 (0)