Skip to content

Commit d64696c

Browse files
authored
internal: add runtime option to benchmark (#4697)
1 parent 2988492 commit d64696c

7 files changed

Lines changed: 197 additions & 55 deletions

File tree

resources/benchmark/args.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,40 @@ import { localRepoPath } from '../utils.js';
66
import { LOCAL } from './config.js';
77
import { bold } from './output.js';
88

9+
export type Runtime = 'node' | 'deno' | 'bun';
10+
911
export interface BenchmarkArguments {
1012
benchmarks: Array<string>;
1113
revisions: Array<string>;
14+
runtime: Runtime;
1215
}
1316

1417
export function getArguments(argv: ReadonlyArray<string>): BenchmarkArguments {
15-
const revsIndex = argv.indexOf('--revs');
16-
const revisions = revsIndex === -1 ? [] : argv.slice(revsIndex + 1);
17-
const benchmarks = revsIndex === -1 ? [...argv] : argv.slice(0, revsIndex);
18+
const runtimeIndex = argv.indexOf('--runtime');
19+
const runtimeValue =
20+
runtimeIndex === -1
21+
? inferRuntimeFromExecPath(process.execPath)
22+
: argv[runtimeIndex + 1];
23+
if (
24+
runtimeValue !== 'node' &&
25+
runtimeValue !== 'deno' &&
26+
runtimeValue !== 'bun'
27+
) {
28+
throw new Error(
29+
`Invalid --runtime value: "${runtimeValue}". Must be "node", "deno", or "bun".`,
30+
);
31+
}
32+
const runtime: Runtime = runtimeValue;
33+
34+
const filteredArgv =
35+
runtimeIndex === -1
36+
? [...argv]
37+
: [...argv.slice(0, runtimeIndex), ...argv.slice(runtimeIndex + 2)];
38+
39+
const revsIndex = filteredArgv.indexOf('--revs');
40+
const revisions = revsIndex === -1 ? [] : filteredArgv.slice(revsIndex + 1);
41+
const benchmarks =
42+
revsIndex === -1 ? [...filteredArgv] : filteredArgv.slice(0, revsIndex);
1843

1944
switch (revisions.length) {
2045
case 0:
@@ -33,7 +58,19 @@ export function getArguments(argv: ReadonlyArray<string>): BenchmarkArguments {
3358
benchmarks.push(...findAllBenchmarks());
3459
}
3560

36-
return { benchmarks, revisions };
61+
return { benchmarks, revisions, runtime };
62+
}
63+
64+
function inferRuntimeFromExecPath(execPath: string): Runtime {
65+
const executableName = path.basename(execPath).toLowerCase();
66+
67+
if (executableName.startsWith('deno')) {
68+
return 'deno';
69+
}
70+
if (executableName.startsWith('bun')) {
71+
return 'bun';
72+
}
73+
return 'node';
3774
}
3875

3976
function findAllBenchmarks(): Array<string> {

resources/benchmark/config.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,31 @@ export const memorySamplesPerBenchmark = 10;
1616
export const pairedGreenThreshold = 0.95;
1717
export const pairedYellowThreshold = 0.8;
1818

19-
export const timingBenchmarkNodeFlags: ReadonlyArray<string> = ['--expose-gc'];
19+
export const timingCommandArgsByRuntime = {
20+
node: ['--expose-gc'],
21+
bun: ['--expose-gc'],
22+
deno: [
23+
'run',
24+
'--allow-read',
25+
'--allow-env',
26+
'--allow-write',
27+
'--v8-flags=--expose-gc',
28+
],
29+
} as const;
2030

21-
export const memoryBenchmarkNodeFlags: ReadonlyArray<string> = [
22-
'--predictable',
23-
'--no-concurrent-sweeping',
24-
'--no-minor-gc-task',
25-
'--min-semi-space-size=1280', // 1.25GB
26-
'--max-semi-space-size=1280', // 1.25GB
27-
];
31+
export const memoryCommandArgsByRuntime = {
32+
node: [
33+
'--no-concurrent-sweeping',
34+
'--no-minor-gc-task',
35+
'--min-semi-space-size=1280', // 1.25GB
36+
'--max-semi-space-size=1280', // 1.25GB
37+
],
38+
bun: [],
39+
deno: [
40+
'run',
41+
'--allow-read',
42+
'--allow-env',
43+
'--allow-write',
44+
'--v8-flags=--no-concurrent-sweeping,--no-minor-gc-task,--min-semi-space-size=1280,--max-semi-space-size=1280',
45+
],
46+
} as const;

resources/benchmark/output.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type ColorFn = (value: number | string) => string;
55

66
export function printBenchmarkResults(
77
results: ReadonlyArray<BenchmarkResult>,
8+
includesMemory: boolean,
89
): void {
910
const nameMaxLen = maxBy(results, ({ name }) => name.length);
1011
const opsTop = maxBy(results, ({ ops }) => ops);
@@ -29,9 +30,7 @@ export function printBenchmarkResults(
2930
grey('\xb1') +
3031
deviationStr() +
3132
cyan('%') +
32-
grey(' x ') +
33-
memPerOpStr() +
34-
'/op' +
33+
(includesMemory ? grey(' x ') + memPerOpStr() + '/op' : '') +
3534
grey(' (' + numSamples + ' runs sampled)'),
3635
);
3736

resources/benchmark/run.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import assert from 'node:assert';
22
import path from 'node:path';
3+
import readline from 'node:readline';
34

5+
import type { Runtime } from './args.js';
46
import { getArguments } from './args.js';
57
import {
68
maxTime,
@@ -9,6 +11,7 @@ import {
911
} from './config.js';
1012
import {
1113
cyan,
14+
grey,
1215
printBenchmarkResults,
1316
printPairedComparisons,
1417
red,
@@ -28,32 +31,46 @@ import {
2831

2932
export function runBenchmarks(): void {
3033
// Get the revisions and make things happen!
31-
const { benchmarks, revisions } = getArguments(process.argv.slice(2));
34+
const { benchmarks, revisions, runtime } = getArguments(
35+
process.argv.slice(2),
36+
);
3237
const benchmarkProjects = prepareBenchmarkProjects(revisions);
3338

39+
console.log('');
40+
console.log('\u2699\uFE0F Runtime: ' + runtime);
41+
console.log('');
42+
3443
for (const benchmark of benchmarks) {
35-
runBenchmark(benchmark, benchmarkProjects);
44+
runBenchmark(benchmark, benchmarkProjects, runtime);
3645
}
3746
}
3847

3948
// Prepare all revisions and run benchmarks matching a pattern against them.
4049
function runBenchmark(
4150
benchmark: string,
4251
benchmarkProjects: ReadonlyArray<BenchmarkProject>,
52+
runtime: Runtime,
4353
): void {
44-
const memorySamples: Array<Array<number>> = [];
54+
const memorySamples: Array<Array<number> | undefined> = [];
55+
const includesMemory = runtime !== 'bun';
56+
4557
for (let i = 0; i < benchmarkProjects.length; ++i) {
4658
const modulePath = path.join(benchmarkProjects[i].projectPath, benchmark);
4759

4860
if (i === 0) {
49-
console.log('\u23F1 ' + getBenchmarkName(modulePath));
61+
console.log('\u23F1 ' + getBenchmarkName(modulePath, runtime));
62+
if (includesMemory) {
63+
writeProgress(' completed ' + cyan(0) + ' memory tests...');
64+
}
65+
}
66+
67+
if (!includesMemory) {
68+
continue;
5069
}
5170

5271
try {
53-
memorySamples[i] = collectMemorySamples(modulePath);
54-
process.stdout.write(
55-
' completed ' + cyan(i + 1) + ' memory tests...\u000D',
56-
);
72+
memorySamples[i] = collectMemorySamples(modulePath, runtime);
73+
writeProgress(' completed ' + cyan(i + 1) + ' memory tests...');
5774
} catch (error) {
5875
const errorMessage =
5976
error instanceof Error ? error.message : String(error);
@@ -63,11 +80,15 @@ function runBenchmark(
6380
return;
6481
}
6582
}
66-
process.stdout.write('\n');
83+
if (includesMemory) {
84+
endProgressLine();
85+
} else {
86+
console.log(' ' + grey('skipping memory benchmarks for bun runtime'));
87+
}
6788

6889
let timingSamples: Array<Array<number>>;
6990
try {
70-
timingSamples = collectTimingSamples(benchmark, benchmarkProjects);
91+
timingSamples = collectTimingSamples(benchmark, benchmarkProjects, runtime);
7192
} catch {
7293
console.log(' ' + red('timing samples collection failed'));
7394
return;
@@ -80,7 +101,7 @@ function runBenchmark(
80101
result = computeStats(
81102
benchmarkProjects[i].revision,
82103
timingSamples[i],
83-
memorySamples[i],
104+
includesMemory ? memorySamples[i] : undefined,
84105
);
85106
} catch (error) {
86107
const errorMessage =
@@ -96,7 +117,7 @@ function runBenchmark(
96117

97118
console.log('\n');
98119

99-
printBenchmarkResults(results);
120+
printBenchmarkResults(results, includesMemory);
100121
printPairedComparisons(
101122
getPairedComparisons(
102123
benchmarkProjects.map(({ revision }) => revision),
@@ -109,6 +130,7 @@ function runBenchmark(
109130
function collectTimingSamples(
110131
benchmark: string,
111132
benchmarkProjects: ReadonlyArray<BenchmarkProject>,
133+
runtime: Runtime,
112134
): Array<Array<number>> {
113135
const sampleGroups = benchmarkProjects.map((project) => ({
114136
revision: project.revision,
@@ -122,14 +144,15 @@ function collectTimingSamples(
122144
// pairwise revision comparison has stabilized.
123145
const start = Date.now();
124146
let round = 0;
147+
writeProgress(' completed ' + cyan(0) + ' timing rounds...');
125148
while (
126149
(Date.now() - start) / 1e3 < maxTime &&
127150
(round < minTimingSamplesPerBenchmark ||
128151
!havePairwiseComparisonsStabilized(timingSamples))
129152
) {
130153
for (const sampleGroup of shuffled(sampleGroups)) {
131154
try {
132-
const sample = sampleTimingModule(sampleGroup.modulePath);
155+
const sample = sampleTimingModule(sampleGroup.modulePath, runtime);
133156

134157
assert(sample > 0);
135158
sampleGroup.samples.push(sample);
@@ -142,13 +165,21 @@ function collectTimingSamples(
142165
}
143166

144167
++round;
145-
process.stdout.write(
146-
' completed ' + cyan(round) + ' timing rounds...\u000D',
147-
);
168+
writeProgress(' completed ' + cyan(round) + ' timing rounds...');
148169
}
149170
return timingSamples;
150171
}
151172

173+
function writeProgress(message: string): void {
174+
readline.clearLine(process.stdout, 0);
175+
readline.cursorTo(process.stdout, 0);
176+
process.stdout.write(message);
177+
}
178+
179+
function endProgressLine(): void {
180+
process.stdout.write('\n');
181+
}
182+
152183
function shuffled<T>(array: ReadonlyArray<T>): Array<T> {
153184
const shuffledArray = [...array];
154185
// Fisher-Yates shuffle: walk backward and swap each slot with a random
@@ -163,14 +194,17 @@ function shuffled<T>(array: ReadonlyArray<T>): Array<T> {
163194
return shuffledArray;
164195
}
165196

166-
export function collectMemorySamples(modulePath: string): Array<number> {
197+
export function collectMemorySamples(
198+
modulePath: string,
199+
runtime: Runtime,
200+
): Array<number> {
167201
const samples: Array<number> = [];
168202
for (
169203
let sampleIndex = 0;
170204
sampleIndex < memorySamplesPerBenchmark;
171205
++sampleIndex
172206
) {
173-
const sample = sampleMemoryModule(modulePath);
207+
const sample = sampleMemoryModule(modulePath, runtime);
174208
assert(sample > 0);
175209
samples.push(sample);
176210
}

resources/benchmark/statistics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ interface LogRatioStats {
2929
export function computeStats(
3030
name: string,
3131
timingSamples: ReadonlyArray<number>,
32-
memorySamples: ReadonlyArray<number>,
32+
memorySamples: ReadonlyArray<number> = [],
3333
): BenchmarkResult {
3434
const { mean } = computeMeanStats(timingSamples);
3535

resources/benchmark/worker-utils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export async function loadBenchmark(modulePath) {
2020
}
2121

2222
export function writeResult(result) {
23-
fs.writeFileSync(3, JSON.stringify(result));
23+
const resultFilePath = process.env.BENCHMARK_RESULT_FILE;
24+
assert(resultFilePath != null);
25+
fs.writeFileSync(resultFilePath, JSON.stringify(result));
2426
}
2527

2628
export function runWorker(main) {

0 commit comments

Comments
 (0)