Skip to content

Commit 6577090

Browse files
authored
internal: separate benchmark timing and memory sampling (#4680)
1 parent 628a126 commit 6577090

8 files changed

Lines changed: 88 additions & 33 deletions

File tree

resources/benchmark/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export const maxTime = 5;
66
// The minimum sample size required to perform statistical analysis.
77
export const minSamples = 5;
88

9-
export const nodeFlags: ReadonlyArray<string> = [
9+
export const memorySamplesPerBenchmark = 10;
10+
11+
export const memoryBenchmarkNodeFlags: ReadonlyArray<string> = [
1012
'--predictable',
1113
'--no-concurrent-sweeping',
1214
'--no-minor-gc-task',

resources/benchmark/run.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'node:path';
33
import { getArguments } from './args.js';
44
import { cyan, printBenchmarkResults, red } from './output.js';
55
import { prepareBenchmarkProjects } from './projects.js';
6-
import { collectSamples } from './sampling.js';
6+
import { collectMemorySamples, collectTimingSamples } from './sampling.js';
77
import { computeStats } from './statistics.js';
88
import type { BenchmarkProject, BenchmarkResult } from './types.js';
99
import { getBenchmarkName } from './workers.js';
@@ -33,9 +33,10 @@ function runBenchmark(
3333
}
3434

3535
try {
36-
const samples = collectSamples(modulePath);
36+
const timingSamples = collectTimingSamples(modulePath);
37+
const memorySamples = collectMemorySamples(modulePath);
3738

38-
results.push(computeStats(revision, samples));
39+
results.push(computeStats(revision, timingSamples, memorySamples));
3940
process.stdout.write(' ' + cyan(i + 1) + ' tests completed.\u000D');
4041
} catch (error) {
4142
const errorMessage =

resources/benchmark/sampling.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import assert from 'node:assert';
22

3-
import { maxTime, minSamples } from './config.js';
3+
import { maxTime, memorySamplesPerBenchmark, minSamples } from './config.js';
44
import { yellow } from './output.js';
5-
import type { BenchmarkSample } from './types.js';
6-
import { sampleModule } from './workers.js';
5+
import type { BenchmarkTimingSample } from './types.js';
6+
import { sampleMemoryModule, sampleTimingModule } from './workers.js';
77

8-
export function collectSamples(modulePath: string): Array<BenchmarkSample> {
8+
export function collectTimingSamples(
9+
modulePath: string,
10+
): Array<BenchmarkTimingSample> {
911
let numOfConsequentlyRejectedSamples = 0;
10-
const samples: Array<BenchmarkSample> = [];
12+
const samples: Array<BenchmarkTimingSample> = [];
1113

1214
// If time permits, increase sample size to reduce the margin of error.
1315
const start = Date.now();
1416
while (samples.length < minSamples || (Date.now() - start) / 1e3 < maxTime) {
15-
const sample = sampleModule(modulePath);
17+
const sample = sampleTimingModule(modulePath);
1618

1719
if (sample.involuntaryContextSwitches > 0) {
1820
numOfConsequentlyRejectedSamples++;
@@ -29,7 +31,20 @@ export function collectSamples(modulePath: string): Array<BenchmarkSample> {
2931
numOfConsequentlyRejectedSamples = 0;
3032

3133
assert(sample.clocked > 0);
32-
assert(sample.memUsed > 0);
34+
samples.push(sample);
35+
}
36+
return samples;
37+
}
38+
39+
export function collectMemorySamples(modulePath: string): Array<number> {
40+
const samples: Array<number> = [];
41+
for (
42+
let sampleIndex = 0;
43+
sampleIndex < memorySamplesPerBenchmark;
44+
++sampleIndex
45+
) {
46+
const sample = sampleMemoryModule(modulePath);
47+
assert(sample > 0);
3348
samples.push(sample);
3449
}
3550
return samples;

resources/benchmark/statistics.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import assert from 'node:assert';
22

33
import { NS_PER_SEC } from './config.js';
4-
import type { BenchmarkResult, BenchmarkSample } from './types.js';
4+
import type { BenchmarkResult, BenchmarkTimingSample } from './types.js';
55

66
// T-Distribution two-tailed critical values for 95% confidence.
77
// See http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm.
@@ -18,35 +18,40 @@ const tTableInfinity = 1.96;
1818
// Computes stats on benchmark results.
1919
export function computeStats(
2020
name: string,
21-
samples: ReadonlyArray<BenchmarkSample>,
21+
timingSamples: ReadonlyArray<BenchmarkTimingSample>,
22+
memorySamples: ReadonlyArray<number>,
2223
): BenchmarkResult {
23-
assert(samples.length > 1);
24+
assert(timingSamples.length > 1);
25+
assert(memorySamples.length > 0);
2426

2527
// Compute the sample mean (estimate of the population mean).
2628
let mean = 0;
27-
let meanMemUsed = 0;
28-
for (const { clocked, memUsed } of samples) {
29+
for (const { clocked } of timingSamples) {
2930
mean += clocked;
31+
}
32+
mean /= timingSamples.length;
33+
34+
let meanMemUsed = 0;
35+
for (const memUsed of memorySamples) {
3036
meanMemUsed += memUsed;
3137
}
32-
mean /= samples.length;
33-
meanMemUsed /= samples.length;
38+
meanMemUsed /= memorySamples.length;
3439

3540
// Compute the sample variance (estimate of the population variance).
3641
let variance = 0;
37-
for (const { clocked } of samples) {
42+
for (const { clocked } of timingSamples) {
3843
variance += (clocked - mean) ** 2;
3944
}
40-
variance /= samples.length - 1;
45+
variance /= timingSamples.length - 1;
4146

4247
// Compute the sample standard deviation (estimate of the population standard deviation).
4348
const sd = Math.sqrt(variance);
4449

4550
// Compute the standard error of the mean (a.k.a. the standard deviation of the sampling distribution of the sample mean).
46-
const sem = sd / Math.sqrt(samples.length);
51+
const sem = sd / Math.sqrt(timingSamples.length);
4752

4853
// Compute the degrees of freedom.
49-
const df = samples.length - 1;
54+
const df = timingSamples.length - 1;
5055

5156
// Compute the critical value.
5257
const critical = tTable[df] ?? tTableInfinity;
@@ -62,6 +67,6 @@ export function computeStats(
6267
memPerOp: Math.floor(meanMemUsed),
6368
ops: NS_PER_SEC / mean,
6469
deviation: rme,
65-
numSamples: samples.length,
70+
numSamples: timingSamples.length,
6671
};
6772
}

resources/benchmark/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ export interface BenchmarkProject {
33
projectPath: string;
44
}
55

6-
export interface BenchmarkSample {
6+
export interface BenchmarkTimingSample {
77
clocked: number;
8-
memUsed: number;
98
involuntaryContextSwitches: number;
109
}
1110

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
loadBenchmark,
3+
readModulePath,
4+
runWorker,
5+
writeResult,
6+
} from './worker-utils.js';
7+
8+
runWorker(async () => {
9+
const benchmark = await loadBenchmark(readModulePath());
10+
await warmUp(benchmark);
11+
12+
const memBaseline = process.memoryUsage().heapUsed;
13+
for (let i = 0; i < benchmark.count; ++i) {
14+
// eslint-disable-next-line no-await-in-loop
15+
await benchmark.measure();
16+
}
17+
writeResult((process.memoryUsage().heapUsed - memBaseline) / benchmark.count);
18+
});
19+
20+
async function warmUp(benchmark) {
21+
// It looks like 7 is a magic number to reliably trigger JIT.
22+
await benchmark.measure();
23+
await benchmark.measure();
24+
await benchmark.measure();
25+
await benchmark.measure();
26+
await benchmark.measure();
27+
await benchmark.measure();
28+
await benchmark.measure();
29+
}

resources/benchmark/worker-timing.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ runWorker(async () => {
99
const benchmark = await loadBenchmark(readModulePath());
1010
await warmUp(benchmark);
1111

12-
const memBaseline = process.memoryUsage().heapUsed;
13-
1412
const resourcesStart = process.resourceUsage();
1513
const startTime = process.hrtime.bigint();
1614
for (let i = 0; i < benchmark.count; ++i) {
@@ -22,7 +20,6 @@ runWorker(async () => {
2220

2321
writeResult({
2422
clocked: timeDiff / benchmark.count,
25-
memUsed: (process.memoryUsage().heapUsed - memBaseline) / benchmark.count,
2623
involuntaryContextSwitches:
2724
resourcesEnd.involuntaryContextSwitches -
2825
resourcesStart.involuntaryContextSwitches,

resources/benchmark/workers.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import childProcess from 'node:child_process';
33

44
import { localRepoPath } from '../utils.js';
55

6-
import { nodeFlags } from './config.js';
7-
import type { BenchmarkSample } from './types.js';
6+
import { memoryBenchmarkNodeFlags } from './config.js';
7+
import type { BenchmarkTimingSample } from './types.js';
88

99
export function getBenchmarkName(modulePath: string): string {
1010
return runWorkerFile(
@@ -13,17 +13,24 @@ export function getBenchmarkName(modulePath: string): string {
1313
) as string;
1414
}
1515

16-
export function sampleModule(modulePath: string): BenchmarkSample {
16+
export function sampleTimingModule(modulePath: string): BenchmarkTimingSample {
1717
return runWorkerFile(
1818
localRepoPath('resources/benchmark/worker-timing.js'),
1919
modulePath,
20-
) as BenchmarkSample;
20+
) as BenchmarkTimingSample;
21+
}
22+
23+
export function sampleMemoryModule(modulePath: string): number {
24+
return runWorkerFile(
25+
localRepoPath('resources/benchmark/worker-memory.js'),
26+
modulePath,
27+
) as number;
2128
}
2229

2330
function runWorkerFile(workerPath: string, modulePath: string): unknown {
2431
const result = childProcess.spawnSync(
2532
process.execPath,
26-
[...nodeFlags, workerPath, modulePath],
33+
[...memoryBenchmarkNodeFlags, workerPath, modulePath],
2734
{
2835
stdio: ['inherit', 'inherit', 'inherit', 'pipe'],
2936
env: { NODE_ENV: 'production' },

0 commit comments

Comments
 (0)