Skip to content

Commit 3cfbdb4

Browse files
committed
add gc benchmark
1 parent 14ae278 commit 3cfbdb4

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

src/gcBench.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import v8 from "v8-natives";
2+
import { fastestTest } from "./util/benchRepeat";
3+
import { logPerfResult } from "./util/perfLogging";
4+
import { Computed, ReactiveFramework } from "./util/reactiveFramework";
5+
import { promiseDelay } from "./util/asyncUtil";
6+
7+
const cases = [
8+
collectDynamicSources,
9+
collectDynamicSourcesAfterUpdate,
10+
collectOutOfScopeConsumers,
11+
];
12+
13+
export async function gcBench(framework: ReactiveFramework) {
14+
for (const c of cases) {
15+
const iter = framework.withBuild(() => c(framework));
16+
17+
const { timing } = await fastestTest(1, async () => {
18+
for (let i = 0; i < 1; i++) {
19+
await iter();
20+
}
21+
});
22+
23+
logPerfResult({
24+
framework: framework.name,
25+
test: `gc-${c.name}`,
26+
time: timing.time.toFixed(2),
27+
gcTime: timing.gcTime?.toFixed(2),
28+
});
29+
}
30+
}
31+
32+
/**
33+
* Ensures old inactive dynamic sources get collected
34+
*/
35+
function collectDynamicSourcesAfterUpdate(framework: ReactiveFramework) {
36+
const head = framework.signal(1);
37+
let dynamic = framework.signal(0);
38+
const tail = framework.computed(() => {
39+
return dynamic.read() + head.read();
40+
});
41+
42+
const weakOldDynamic = new WeakRef(dynamic);
43+
44+
// Create connection to first dynamic
45+
tail.read();
46+
47+
// Swap dynamic for new
48+
dynamic = framework.signal(1);
49+
50+
// Trigger update to force invalidation of tail
51+
head.write(-1);
52+
53+
return async () => {
54+
let didCollect = false;
55+
56+
// Async GC loop to allow time for frameworks with scheduled cleanups
57+
for (let i = 0; i < 200; i++) {
58+
await promiseDelay();
59+
v8.collectGarbage();
60+
61+
if (weakOldDynamic.deref() === undefined) {
62+
didCollect = true;
63+
break;
64+
}
65+
}
66+
67+
console.assert(tail.read() === 0, "tail is not 0");
68+
69+
console.assert(didCollect, "Failed to collect inactive dynamic sources");
70+
};
71+
}
72+
73+
/**
74+
* Ensures old inactive dynamic sources get collected
75+
*/
76+
function collectDynamicSources(framework: ReactiveFramework) {
77+
const head = framework.signal(1);
78+
let dynamic = framework.signal(0);
79+
const tail = framework.computed(() => {
80+
return dynamic.read() + head.read();
81+
});
82+
83+
const weakOldDynamic = new WeakRef(dynamic);
84+
85+
// Create connection to first dynamic
86+
tail.read();
87+
88+
// Swap dynamic for new
89+
dynamic = framework.signal(1);
90+
91+
return async () => {
92+
let didCollect = false;
93+
94+
// Async GC loop to allow time for frameworks with scheduled cleanups
95+
for (let i = 0; i < 200; i++) {
96+
await promiseDelay();
97+
v8.collectGarbage();
98+
99+
if (weakOldDynamic.deref() === undefined) {
100+
didCollect = true;
101+
break;
102+
}
103+
}
104+
105+
console.assert(tail.read() === 1, "tail is not 1");
106+
107+
console.assert(didCollect, "Failed to collect inactive dynamic sources");
108+
};
109+
}
110+
111+
/**
112+
* Ensures out of scope (computed) consumers get collected
113+
*/
114+
function collectOutOfScopeConsumers(framework: ReactiveFramework) {
115+
const head = framework.signal(0);
116+
let tail: Computed<number> = head;
117+
const weakRefs: WeakRef<Computed<number>>[] = [];
118+
119+
for (let i = 0; i < 5; i++) {
120+
const oldTail = tail;
121+
tail = framework.computed(() => {
122+
return oldTail.read() + 1;
123+
});
124+
tail.read();
125+
weakRefs.push(new WeakRef(tail));
126+
}
127+
128+
return async () => {
129+
let activeRefCount = 0;
130+
131+
// Async GC loop to allow time for frameworks with scheduled cleanups
132+
for (let i = 0; i < 200; i++) {
133+
await promiseDelay();
134+
v8.collectGarbage();
135+
136+
activeRefCount = 0;
137+
for (const ref of weakRefs) {
138+
if (ref.deref()) {
139+
activeRefCount++;
140+
}
141+
}
142+
if (activeRefCount === 0) {
143+
break;
144+
}
145+
}
146+
147+
console.assert(head.read() === 0);
148+
149+
console.assert(
150+
activeRefCount === 0,
151+
`Found ${activeRefCount} active references when there should be none`
152+
);
153+
};
154+
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { frameworkInfo } from "./config";
55
import { logPerfResult, perfReportHeaders } from "./util/perfLogging";
66
import { molBench } from "./molBench";
77
import { kairoBench } from "./kairoBench";
8+
import { gcBench } from "./gcBench";
89

910
async function main() {
1011
logPerfResult(perfReportHeaders());
@@ -28,6 +29,10 @@ async function main() {
2829
for (const frameworkTest of frameworkInfo) {
2930
await dynamicBench(frameworkTest);
3031
}
32+
33+
for (const { framework } of frameworkInfo) {
34+
await gcBench(framework);
35+
}
3136
}
3237

3338
main();

0 commit comments

Comments
 (0)