'use strict';
const os = require('os');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const cp = require('child_process');
const NS_PER_SEC = 1e9;
const LOCAL = 'local';
const maxTime = 5;
const minSamples = 5;
if (require.main === module) {
const { benchmarks, revisions } = getArguments(process.argv.slice(2));
const benchmarkProjects = prepareBenchmarkProjects(revisions);
runBenchmarks(benchmarks, benchmarkProjects).catch((error) => {
console.error(error);
process.exit(1);
});
}
function localDir(...paths) {
return path.join(__dirname, '..', ...paths);
}
function exec(command, options = {}) {
const result = cp.execSync(command, {
encoding: 'utf-8',
stdio: ['inherit', 'pipe', 'inherit'],
...options,
});
return result?.trimEnd();
}
function prepareBenchmarkProjects(revisionList) {
const tmpDir = path.join(os.tmpdir(), 'graphql-js-benchmark');
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir);
const setupDir = path.join(tmpDir, 'setup');
fs.mkdirSync(setupDir);
return revisionList.map((revision) => {
console.log(`🍳 Preparing ${revision}...`);
const projectPath = path.join(setupDir, revision);
fs.rmSync(projectPath, { recursive: true, force: true });
fs.mkdirSync(projectPath);
fs.writeFileSync(
path.join(projectPath, 'package.json'),
'{ "private": true }',
);
exec(
'npm --quiet install --ignore-scripts ' + prepareNPMPackage(revision),
{ cwd: projectPath },
);
exec(`cp -R ${localDir('benchmark')} ${projectPath}`);
return { revision, projectPath };
});
function prepareNPMPackage(revision) {
if (revision === LOCAL) {
const repoDir = localDir();
const archivePath = path.join(tmpDir, 'graphql-local.tgz');
fs.renameSync(buildNPMArchive(repoDir), archivePath);
return archivePath;
}
const hash = exec(`git rev-parse "${revision}"`);
const archivePath = path.join(tmpDir, `graphql-${hash}.tgz`);
if (fs.existsSync(archivePath)) {
return archivePath;
}
const repoDir = path.join(tmpDir, hash);
fs.rmSync(repoDir, { recursive: true, force: true });
fs.mkdirSync(repoDir);
exec(`git archive "${hash}" | tar -xC "${repoDir}"`);
exec('npm --quiet ci --ignore-scripts', { cwd: repoDir });
fs.renameSync(buildNPMArchive(repoDir), archivePath);
fs.rmSync(repoDir, { recursive: true });
return archivePath;
}
function buildNPMArchive(repoDir) {
exec('npm --quiet run build:npm', { cwd: repoDir });
const distDir = path.join(repoDir, 'npmDist');
const archiveName = exec(`npm --quiet pack ${distDir}`, { cwd: repoDir });
return path.join(repoDir, archiveName);
}
}
async function collectSamples(modulePath) {
const samples = [];
const start = Date.now();
while (samples.length < minSamples || (Date.now() - start) / 1e3 < maxTime) {
const { clocked, memUsed } = await sampleModule(modulePath);
assert(clocked > 0);
assert(memUsed > 0);
samples.push({ clocked, memUsed });
}
return samples;
}
const tTable = {
'1': 12.706, '2': 4.303, '3': 3.182, '4': 2.776, '5': 2.571, '6': 2.447,
'7': 2.365, '8': 2.306, '9': 2.262, '10': 2.228, '11': 2.201, '12': 2.179,
'13': 2.16, '14': 2.145, '15': 2.131, '16': 2.12, '17': 2.11, '18': 2.101,
'19': 2.093, '20': 2.086, '21': 2.08, '22': 2.074, '23': 2.069, '24': 2.064,
'25': 2.06, '26': 2.056, '27': 2.052, '28': 2.048, '29': 2.045, '30': 2.042,
infinity: 1.96,
};
function computeStats(samples) {
assert(samples.length > 1);
let mean = 0;
let meanMemUsed = 0;
for (const { clocked, memUsed } of samples) {
mean += clocked;
meanMemUsed += memUsed;
}
mean /= samples.length;
meanMemUsed /= samples.length;
let variance = 0;
for (const { clocked } of samples) {
variance += (clocked - mean) ** 2;
}
variance /= samples.length - 1;
const sd = Math.sqrt(variance);
const sem = sd / Math.sqrt(samples.length);
const df = samples.length - 1;
const critical = tTable[df] || tTable.infinity;
const moe = sem * critical;
const rme = (moe / mean) * 100 || 0;
return {
memPerOp: Math.floor(meanMemUsed),
ops: NS_PER_SEC / mean,
deviation: rme,
numSamples: samples.length,
};
}
function beautifyBenchmark(results) {
const nameMaxLen = maxBy(results, ({ name }) => name.length);
const opsTop = maxBy(results, ({ ops }) => ops);
const opsMaxLen = maxBy(results, ({ ops }) => beautifyNumber(ops).length);
const memPerOpMaxLen = maxBy(
results,
({ memPerOp }) => beautifyBytes(memPerOp).length,
);
for (const result of results) {
printBench(result);
}
function printBench(bench) {
const { name, memPerOp, ops, deviation, numSamples } = bench;
console.log(
' ' +
nameStr() +
grey(' x ') +
opsStr() +
' ops/sec ' +
grey('\xb1') +
deviationStr() +
cyan('%') +
grey(' x ') +
memPerOpStr() +
'/op' +
grey(' (' + numSamples + ' runs sampled)'),
);
function nameStr() {
const nameFmt = name.padEnd(nameMaxLen);
return ops === opsTop ? green(nameFmt) : nameFmt;
}
function opsStr() {
const percent = ops / opsTop;
const colorFn = percent > 0.95 ? green : percent > 0.8 ? yellow : red;
return colorFn(beautifyNumber(ops).padStart(opsMaxLen));
}
function deviationStr() {
const colorFn = deviation > 5 ? red : deviation > 2 ? yellow : green;
return colorFn(deviation.toFixed(2));
}
function memPerOpStr() {
return beautifyBytes(memPerOp).padStart(memPerOpMaxLen);
}
}
}
function beautifyBytes(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log2(bytes) / 10);
return beautifyNumber(bytes / 2 ** (i * 10)) + ' ' + sizes[i];
}
function beautifyNumber(num) {
return Number(num.toFixed(num > 100 ? 0 : 2)).toLocaleString();
}
function maxBy(array, fn) {
return Math.max(...array.map(fn));
}
async function runBenchmarks(benchmarks, benchmarkProjects) {
for (const benchmark of benchmarks) {
const results = [];
for (let i = 0; i < benchmarkProjects.length; ++i) {
const { revision, projectPath } = benchmarkProjects[i];
const modulePath = path.join(projectPath, benchmark);
if (i === 0) {
const { name } = await sampleModule(modulePath);
console.log('⏱ ' + name);
}
try {
const samples = await collectSamples(modulePath);
results.push({
name: revision,
samples,
...computeStats(samples),
});
process.stdout.write(' ' + cyan(i + 1) + ' tests completed.\u000D');
} catch (error) {
console.log(' ' + revision + ': ' + red(String(error)));
}
}
console.log('\n');
beautifyBenchmark(results);
console.log('');
}
}
function getArguments(argv) {
const revsIndex = argv.indexOf('--revs');
const revisions = revsIndex === -1 ? [] : argv.slice(revsIndex + 1);
const benchmarks = revsIndex === -1 ? argv : argv.slice(0, revsIndex);
switch (revisions.length) {
case 0:
revisions.unshift('HEAD');
case 1: {
revisions.unshift('local');
const assumeArgv = ['benchmark', ...benchmarks, '--revs', ...revisions];
console.warn('Assuming you meant: ' + bold(assumeArgv.join(' ')));
break;
}
}
if (benchmarks.length === 0) {
benchmarks.push(...findAllBenchmarks());
}
return { benchmarks, revisions };
}
function findAllBenchmarks() {
return fs
.readdirSync(localDir('benchmark'), { withFileTypes: true })
.filter((dirent) => dirent.isFile())
.map((dirent) => dirent.name)
.filter((name) => name.endsWith('-benchmark.js'))
.map((name) => path.join('benchmark', name));
}
function bold(str) {
return '\u001b[1m' + str + '\u001b[0m';
}
function red(str) {
return '\u001b[31m' + str + '\u001b[0m';
}
function green(str) {
return '\u001b[32m' + str + '\u001b[0m';
}
function yellow(str) {
return '\u001b[33m' + str + '\u001b[0m';
}
function cyan(str) {
return '\u001b[36m' + str + '\u001b[0m';
}
function grey(str) {
return '\u001b[90m' + str + '\u001b[0m';
}
function sampleModule(modulePath) {
const sampleCode = `
const assert = require('assert');
assert(global.gc);
assert(process.send);
const module = require('${modulePath}');
clock(7, module.measure); // warm up
global.gc();
process.nextTick(() => {
const memBaseline = process.memoryUsage().heapUsed;
const clocked = clock(module.count, module.measure);
process.send({
name: module.name,
clocked: clocked / module.count,
memUsed: (process.memoryUsage().heapUsed - memBaseline) / module.count,
});
});
// Clocks the time taken to execute a test per cycle (secs).
function clock(count, fn) {
const start = process.hrtime.bigint();
for (let i = 0; i < count; ++i) {
fn();
}
return Number(process.hrtime.bigint() - start);
}
`;
return new Promise((resolve, reject) => {
const child = cp.spawn(
process.argv[0],
[
'--no-concurrent-sweeping',
'--predictable',
'--expose-gc',
'--eval',
sampleCode,
],
{
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'production' },
},
);
let message;
let error;
child.on('message', (msg) => (message = msg));
child.on('error', (e) => (error = e));
child.on('close', () => {
if (message) {
return resolve(message);
}
reject(error || new Error('Spawn process closed without error'));
});
});
}