93
94
const canGC = typeof globalThis.gc === 'function';
95
96
+// Yield to the event loop's check phase between renders. React schedules a
97
+// setImmediate per request, but a fully in-process render completes entirely
98
+// in microtasks/nextTicks, so a tight benchmark loop never lets those
99
+// immediates run. They then pile up in Node's immediate queue, each one
100
+// retaining its (otherwise finished) request graph, which inflates any memory
101
+// measurement. A real server yields to the event loop on every request, so
102
+// draining between iterations is both correct and more representative.
103
+function tick() {
104
+ return new Promise(resolve => setImmediate(resolve));
105
+}
106
+
107
+async function settledHeapUsed() {
108
+ await tick();
109
+ if (canGC) globalThis.gc();
110
+ return process.memoryUsage().heapUsed;
111
+}
112
+
113
async function runBenchmark(name, fn, iterations, warmup) {
114
if (canGC) globalThis.gc();
115
+ const heapBefore = await settledHeapUsed();
116
117
// Warmup
118
for (let i = 0; i < warmup; i++) {
119
await fn();
120
+ await tick();
121
}
122
123
// Collect GC pauses during timed iterations.
131
});
132
gcObs.observe({entryTypes: ['gc']});
133
115
- // Timed iterations
134
+ // Timed iterations. The tick between iterations is excluded from the
135
+ // timed window.
136
const times = [];
137
for (let i = 0; i < iterations; i++) {
138
const start = performance.now();
139
await fn();
140
times.push(performance.now() - start);
141
+ await tick();
142
}
143
gcObs.disconnect();
144
+ const heapAfter = await settledHeapUsed();
145
146
// Trim top/bottom 5% to remove outliers
147
const sorted = [...times].sort((a, b) => a - b);
168
iterations,
169
gcCount,
170
gcTotalMs,
171
+ heapBefore,
172
+ heapAfter,
173
};
174
}
175
187
result.gcTotalMs.toFixed(1),
188
(result.gcTotalMs / result.iterations).toFixed(2)
189
);
190
+ printHeap(result);
191
+}
192
+
193
+function printHeap(result) {
194
+ if (!canGC) return;
195
+ const mb = b => (b / 1048576).toFixed(1);
196
+ const delta = result.heapAfter - result.heapBefore;
197
+ console.log(
198
+ ' Heap: %s MB retained after run (%s%s MB vs before)',
199
+ mb(result.heapAfter),
200
+ delta >= 0 ? '+' : '',
201
+ mb(delta)
202
+ );
203
}
204
205
async function runConcurrent(name, fn, total, concurrency, warmup) {
206
if (canGC) globalThis.gc();
207
+ const heapBefore = await settledHeapUsed();
208
209
for (let i = 0; i < warmup; i++) {
210
await fn();
211
+ await tick();
212
}
213
214
let gcCount = 0;
231
while (launched < total && launched - completed < concurrency) {
232
const idx = launched++;
233
const t0 = performance.now();
195
- fn().then(() => {
196
- latencies[idx] = performance.now() - t0;
197
- completed++;
198
- if (completed === total) {
199
- resolve();
200
- } else {
201
- launch();
202
- }
203
- });
234
+ fn()
235
+ .then(() => {
236
+ latencies[idx] = performance.now() - t0;
237
+ // Drain immediates before freeing the slot (see tick()).
238
+ return tick();
239
+ })
240
+ .then(() => {
241
+ completed++;
242
+ if (completed === total) {
243
+ resolve();
244
+ } else {
245
+ launch();
246
+ }
247
+ });
248
}
249
}
250
launch();
251
});
252
const elapsed = performance.now() - start;
253
gcObs.disconnect();
254
+ const heapAfter = await settledHeapUsed();
255
256
const sorted = [...latencies].sort((a, b) => a - b);
257
const mean = sorted.reduce((s, t) => s + t, 0) / sorted.length;
266
concurrency,
267
gcCount,
268
gcTotalMs,
269
+ heapBefore,
270
+ heapAfter,
271
};
272
}
273
282
result.gcTotalMs.toFixed(1),
283
(result.gcTotalMs / result.total).toFixed(2)
284
);
285
+ printHeap(result);
286
}
287
288
// ---------------------------------------------------------------------------
355
// Warmup (unprofiled)
356
for (let i = 0; i < warmup; i++) {
357
await fn();
358
+ await tick();
359
}
360
361
// Collect GC pauses during the profiled run.
373
const session = await startProfiler();
374
for (let i = 0; i < iterations; i++) {
375
await fn();
376
+ await tick();
377
}
378
const profile = await stopProfiler(session, outputPath);
379
gcObs.disconnect();