Skip to content

Commit 5991c6e

Browse files
yaacovCRCopilot
andauthored
benchmark: perform timing tests in rounds to reduce variance (#4684)
Co-authored-by: Copilot <copilot@github.com>
1 parent de0f3a5 commit 5991c6e

2 files changed

Lines changed: 77 additions & 18 deletions

File tree

resources/benchmark/config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export const NS_PER_SEC = 1e9;
22
export const LOCAL = 'local';
33

4-
// The maximum time in seconds a benchmark is allowed to run before finishing.
5-
export const maxTime = 5;
4+
// The maximum total time in seconds spent collecting timing samples
5+
// across all revisions for one benchmark.
6+
export const maxTime = 60;
67
// The minimum sample size required to perform statistical analysis.
78
export const minSamples = 5;
89

resources/benchmark/run.ts

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,45 +28,103 @@ function runBenchmark(
2828
benchmark: string,
2929
benchmarkProjects: ReadonlyArray<BenchmarkProject>,
3030
): void {
31-
const results: Array<BenchmarkResult> = [];
31+
const memorySamples: Array<Array<number>> = [];
3232
for (let i = 0; i < benchmarkProjects.length; ++i) {
33-
const { revision, projectPath } = benchmarkProjects[i];
34-
const modulePath = path.join(projectPath, benchmark);
33+
const modulePath = path.join(benchmarkProjects[i].projectPath, benchmark);
3534

3635
if (i === 0) {
3736
console.log('\u23F1 ' + getBenchmarkName(modulePath));
3837
}
3938

4039
try {
41-
const timingSamples = collectTimingSamples(modulePath);
42-
const memorySamples = collectMemorySamples(modulePath);
43-
44-
results.push(computeStats(revision, timingSamples, memorySamples));
45-
process.stdout.write(' ' + cyan(i + 1) + ' tests completed.\u000D');
40+
memorySamples[i] = collectMemorySamples(modulePath);
41+
process.stdout.write(
42+
' completed ' + cyan(i + 1) + ' memory tests...\u000D',
43+
);
4644
} catch (error) {
4745
const errorMessage =
4846
error instanceof Error ? error.message : String(error);
49-
console.log(' ' + revision + ': ' + red(errorMessage));
47+
console.log(
48+
' ' + benchmarkProjects[i].revision + ': ' + red(errorMessage),
49+
);
50+
return;
5051
}
5152
}
53+
process.stdout.write('\n');
54+
55+
let timingSamples: Array<Array<number>>;
56+
try {
57+
timingSamples = collectTimingSamples(benchmark, benchmarkProjects);
58+
} catch {
59+
console.log(' ' + red('timing samples collection failed'));
60+
return;
61+
}
62+
63+
const results: Array<BenchmarkResult> = [];
64+
for (let i = 0; i < benchmarkProjects.length; ++i) {
65+
results.push(
66+
computeStats(
67+
benchmarkProjects[i].revision,
68+
timingSamples[i],
69+
memorySamples[i],
70+
),
71+
);
72+
}
73+
5274
console.log('\n');
5375

5476
printBenchmarkResults(results);
5577
console.log('');
5678
}
5779

58-
export function collectTimingSamples(modulePath: string): Array<number> {
59-
const samples: Array<number> = [];
80+
function collectTimingSamples(
81+
benchmark: string,
82+
benchmarkProjects: ReadonlyArray<BenchmarkProject>,
83+
): Array<Array<number>> {
84+
const sampleGroups = benchmarkProjects.map((project) => ({
85+
revision: project.revision,
86+
modulePath: path.join(project.projectPath, benchmark),
87+
samples: new Array<number>(),
88+
}));
6089

6190
// If time permits, increase sample size to reduce the margin of error.
6291
const start = Date.now();
63-
while (samples.length < minSamples || (Date.now() - start) / 1e3 < maxTime) {
64-
const sample = sampleTimingModule(modulePath);
92+
let round = 0;
93+
while (round < minSamples || (Date.now() - start) / 1e3 < maxTime) {
94+
for (const sampleGroup of shuffled(sampleGroups)) {
95+
try {
96+
const sample = sampleTimingModule(sampleGroup.modulePath);
6597

66-
assert(sample > 0);
67-
samples.push(sample);
98+
assert(sample > 0);
99+
sampleGroup.samples.push(sample);
100+
} catch (error) {
101+
const errorMessage =
102+
error instanceof Error ? error.message : String(error);
103+
console.log(' ' + sampleGroup.revision + ': ' + red(errorMessage));
104+
throw error;
105+
}
106+
}
107+
108+
++round;
109+
process.stdout.write(
110+
' completed ' + cyan(round) + ' timing rounds...\u000D',
111+
);
68112
}
69-
return samples;
113+
return sampleGroups.map(({ samples }) => samples);
114+
}
115+
116+
function shuffled<T>(array: ReadonlyArray<T>): Array<T> {
117+
const shuffledArray = [...array];
118+
// Fisher-Yates shuffle: walk backward and swap each slot with a random
119+
// earlier slot, including itself, to produce an unbiased permutation.
120+
for (let index = shuffledArray.length - 1; index > 0; --index) {
121+
const randomIndex = Math.floor(Math.random() * (index + 1));
122+
[shuffledArray[index], shuffledArray[randomIndex]] = [
123+
shuffledArray[randomIndex],
124+
shuffledArray[index],
125+
];
126+
}
127+
return shuffledArray;
70128
}
71129

72130
export function collectMemorySamples(modulePath: string): Array<number> {

0 commit comments

Comments
 (0)