Skip to content

Commit 2d809e3

Browse files
stewartmcgownatscott
authored andcommitted
test(core): add GC-based tests for signal graph prevConsumer leak fix (#68681)
Uses WeakRef + global.gc() to verify that destroyed effect consumers become garbage-collectable when a non-live computed reads the same producer. The jasmine_test target is configured with node_options: --expose-gc. GC tests are skipped in browser targets via isBrowser from @angular/private/testing. Made-with: Cursor PR Close #68681
1 parent 55639ac commit 2d809e3

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

packages/core/test/signals/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_project(
1212
"//packages/core",
1313
"//packages/core/primitives/signals",
1414
"//packages/core/src/util",
15+
"//packages/private/testing",
1516
],
1617
)
1718

@@ -20,6 +21,7 @@ jasmine_test(
2021
data = [
2122
":signals_lib",
2223
],
24+
node_options = ["--expose-gc"],
2325
)
2426

2527
ng_web_test_suite(
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {computed, signal, WritableSignal} from '../../src/core';
10+
import {createWatch, SIGNAL} from '../../primitives/signals';
11+
import {isBrowser, timeout} from '@angular/private/testing';
12+
13+
describe('signal graph: destroyed consumers should be GC-eligible', () => {
14+
if (isBrowser) {
15+
it('should pass', () => {});
16+
return;
17+
}
18+
19+
function setupAndReturnRef(source: WritableSignal<number>): WeakRef<object> {
20+
const watch = createWatch(
21+
() => source(),
22+
() => {},
23+
true,
24+
);
25+
watch.run();
26+
27+
const ref = new WeakRef(watch[SIGNAL]);
28+
29+
// Non-live computed that also reads source.
30+
const derived = computed(() => source() + 1);
31+
derived();
32+
33+
// Clearing the graph edges.
34+
watch.destroy();
35+
36+
return ref;
37+
}
38+
39+
it('should GC a destroyed effect when a non-live computed reads the same producer', async () => {
40+
const source = signal(0);
41+
const ref = setupAndReturnRef(source);
42+
43+
(globalThis as any).gc();
44+
await timeout();
45+
(globalThis as any).gc();
46+
47+
expect(ref.deref()).toBeUndefined();
48+
});
49+
50+
it('should GC destroyed effects across repeated create/destroy cycles', async () => {
51+
const source = signal(0);
52+
const derived = computed(() => source() + 1);
53+
derived();
54+
55+
const refs: WeakRef<object>[] = [];
56+
for (let i = 0; i < 5; i++) {
57+
refs.push(setupAndReturnRef(source));
58+
}
59+
60+
(globalThis as any).gc();
61+
await timeout();
62+
(globalThis as any).gc();
63+
64+
for (let i = 0; i < refs.length; i++) {
65+
expect(refs[i].deref()).withContext(`cycle ${i}`).toBeUndefined();
66+
}
67+
});
68+
});

0 commit comments

Comments
 (0)