Skip to content

Commit bad0a69

Browse files
benchmark: improve reproducibility (#2039)
1 parent 3cd06f1 commit bad0a69

10 files changed

+75
-43
lines changed

.eslintrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ overrides:
358358
parserOptions:
359359
sourceType: script
360360
rules:
361+
no-await-in-loop: off
361362
no-restricted-syntax: off
362363
no-console: off
363364
no-sync: off

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"testonly": "mocha --full-trace src/**/__tests__/**/*-test.js",
2929
"testonly:cover": "nyc npm run testonly",
3030
"lint": "eslint --cache --report-unused-disable-directives src resources",
31-
"benchmark": "node ./resources/benchmark.js",
31+
"benchmark": "node --predictable ./resources/benchmark.js",
3232
"prettier": "prettier --ignore-path .gitignore --write --list-different '**/*.{js,md,json,yml}'",
3333
"prettier:check": "prettier --ignore-path .gitignore --check '**/*.{js,md,json,yml}'",
3434
"check": "flow check",

resources/benchmark-fork.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// @noflow
2+
3+
'use strict';
4+
5+
const assert = require('assert');
6+
const cp = require('child_process');
7+
8+
// Clocks the time taken to execute a test per cycle (secs).
9+
function clock(count, fn) {
10+
const start = process.hrtime.bigint();
11+
for (let i = 0; i < count; ++i) {
12+
fn();
13+
}
14+
return Number(process.hrtime.bigint() - start) / count;
15+
}
16+
17+
if (require.main === module) {
18+
const modulePath = process.env.BENCHMARK_MODULE_PATH;
19+
assert(typeof modulePath === 'string');
20+
assert(process.send);
21+
const module = require(modulePath);
22+
23+
clock(7, module.measure); // warm up
24+
process.nextTick(() => {
25+
process.send({
26+
name: module.name,
27+
clocked: clock(module.count, module.measure),
28+
});
29+
});
30+
}
31+
32+
function sampleModule(modulePath) {
33+
return new Promise((resolve, reject) => {
34+
const env = { BENCHMARK_MODULE_PATH: modulePath };
35+
const child = cp.fork(__filename, { env });
36+
let message;
37+
let error;
38+
39+
child.on('message', msg => (message = msg));
40+
child.on('error', e => (error = e));
41+
child.on('close', () => {
42+
if (message) {
43+
return resolve(message);
44+
}
45+
reject(error || new Error('Forked process closed without error'));
46+
});
47+
});
48+
}
49+
50+
module.exports = { sampleModule };

resources/benchmark.js

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,15 @@ const {
1616
mkdirRecursive,
1717
readdirRecursive,
1818
} = require('./utils');
19+
const { sampleModule } = require('./benchmark-fork');
1920

2021
const NS_PER_SEC = 1e9;
2122
const LOCAL = 'local';
2223

23-
const minTime = 0.05 * NS_PER_SEC;
24-
// The maximum time a benchmark is allowed to run before finishing.
25-
const maxTime = 5 * NS_PER_SEC;
24+
// The maximum time in secounds a benchmark is allowed to run before finishing.
25+
const maxTime = 5;
2626
// The minimum sample size required to perform statistical analysis.
27-
const minSamples = 15;
28-
// The default number of times to execute a test on a benchmark's first cycle.
29-
const initCount = 10;
27+
const minSamples = 5;
3028

3129
function LOCAL_DIR(...paths) {
3230
return path.join(__dirname, '..', ...paths);
@@ -97,44 +95,19 @@ function findFiles(cwd, pattern) {
9795
return out.split('\n').filter(Boolean);
9896
}
9997

100-
function collectSamples(fn) {
101-
clock(initCount, fn); // initial warm up
102-
103-
// Cycles a benchmark until a run `count` can be established.
104-
// Resolve time span required to achieve a percent uncertainty of at most 1%.
105-
// For more information see http://spiff.rit.edu/classes/phys273/uncert/uncert.html.
106-
let count = initCount;
107-
let clocked = 0;
108-
while ((clocked = clock(count, fn)) < minTime) {
109-
// Calculate how many more iterations it will take to achieve the `minTime`.
110-
count += Math.ceil(((minTime - clocked) * count) / clocked);
111-
}
112-
113-
let elapsed = 0;
98+
async function collectSamples(modulePath) {
11499
const samples = [];
115100

116101
// If time permits, increase sample size to reduce the margin of error.
117-
while (samples.length < minSamples || elapsed < maxTime) {
118-
clocked = clock(count, fn);
102+
const start = Date.now();
103+
while (samples.length < minSamples || (Date.now() - start) / 1e3 < maxTime) {
104+
const { clocked } = await sampleModule(modulePath);
119105
assert(clocked > 0);
120-
121-
elapsed += clocked;
122-
// Compute the seconds per operation.
123-
samples.push(clocked / count);
106+
samples.push(clocked);
124107
}
125-
126108
return samples;
127109
}
128110

129-
// Clocks the time taken to execute a test per cycle (secs).
130-
function clock(count, fn) {
131-
const start = process.hrtime.bigint();
132-
for (let i = 0; i < count; ++i) {
133-
fn();
134-
}
135-
return Number(process.hrtime.bigint() - start);
136-
}
137-
138111
// T-Distribution two-tailed critical values for 95% confidence.
139112
// See http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm.
140113
const tTable = /* prettier-ignore */ {
@@ -238,7 +211,7 @@ function maxBy(array, fn) {
238211
}
239212

240213
// Prepare all revisions and run benchmarks matching a pattern against them.
241-
function prepareAndRunBenchmarks(benchmarkPatterns, revisions) {
214+
async function prepareAndRunBenchmarks(benchmarkPatterns, revisions) {
242215
const environments = revisions.map(revision => ({
243216
revision,
244217
distPath: prepareRevision(revision),
@@ -248,22 +221,24 @@ function prepareAndRunBenchmarks(benchmarkPatterns, revisions) {
248221
const results = [];
249222
for (let i = 0; i < environments.length; ++i) {
250223
const environment = environments[i];
251-
const module = require(path.join(environment.distPath, benchmark));
224+
const modulePath = path.join(environment.distPath, benchmark);
252225

253-
if (i) {
254-
console.log('⏱️ ' + module.name);
226+
if (i === 0) {
227+
const { name } = await sampleModule(modulePath);
228+
console.log('⏱️ ' + name);
255229
}
256230

257231
try {
258-
const samples = collectSamples(module.measure);
232+
const samples = await collectSamples(modulePath);
233+
259234
results.push({
260235
name: environment.revision,
261236
samples,
262237
...computeStats(samples),
263238
});
264239
process.stdout.write(' ' + cyan(i + 1) + ' tests completed.\u000D');
265240
} catch (error) {
266-
console.log(' ' + module.name + ': ' + red(String(error)));
241+
console.log(' ' + environment.revision + ': ' + red(String(error)));
267242
}
268243
}
269244
console.log('\n');

src/language/__tests__/parser-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { kitchenSinkQuery } from '../../__fixtures__';
44
import { parse } from '../parser';
55

66
export const name = 'Parse kitchen sink';
7+
export const count = 1000;
78
export function measure() {
89
parse(kitchenSinkQuery);
910
}

src/utilities/__tests__/buildASTSchema-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { buildASTSchema } from '../buildASTSchema';
88
const schemaAST = parse(bigSchemaSDL);
99

1010
export const name = 'Build Schema from AST';
11+
export const count = 10;
1112
export function measure() {
1213
buildASTSchema(schemaAST, { assumeValid: true });
1314
}

src/utilities/__tests__/buildClientSchema-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { bigSchemaIntrospectionResult } from '../../__fixtures__';
55
import { buildClientSchema } from '../buildClientSchema';
66

77
export const name = 'Build Schema from Introspection';
8+
export const count = 10;
89
export function measure() {
910
buildClientSchema(bigSchemaIntrospectionResult.data, { assumeValid: true });
1011
}

src/utilities/__tests__/introspectionFromSchema-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const queryAST = parse(getIntrospectionQuery());
1010
const schema = buildSchema(bigSchemaSDL, { assumeValid: true });
1111

1212
export const name = 'Execute Introspection Query';
13+
export const count = 10;
1314
export function measure() {
1415
execute(schema, queryAST);
1516
}

src/validation/__tests__/validateGQL-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const schema = buildSchema(bigSchemaSDL, { assumeValid: true });
99
const queryAST = parse(getIntrospectionQuery());
1010

1111
export const name = 'Validate Introspection Query';
12+
export const count = 50;
1213
export function measure() {
1314
validate(schema, queryAST);
1415
}

src/validation/__tests__/validateSDL-benchmark.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { validateSDL } from '../validate';
88
const sdlAST = parse(bigSchemaSDL);
99

1010
export const name = 'Validate SDL Document';
11+
export const count = 10;
1112
export function measure() {
1213
validateSDL(sdlAST);
1314
}

0 commit comments

Comments
 (0)