'use strict'; const child_process = require('child_process'); const path = require('path'); const fs = require('fs'); const requirementsURL = 'https://github.com/nodejs/node/blob/master/benchmark/writing-and-running-benchmarks.md#http-benchmark-requirements'; // The port used by servers and wrk exports.PORT = Number(process.env.PORT) || 12346; class AutocannonBenchmarker { constructor() { this.name = 'autocannon'; this.executable = process.platform === 'win32' ? 'autocannon.cmd' : 'autocannon'; const result = child_process.spawnSync(this.executable, ['-h']); this.present = !(result.error && result.error.code === 'ENOENT'); } create(options) { const args = [ '-d', options.duration, '-c', options.connections, '-j', '-n', ]; for (const field in options.headers) { args.push('-H', `${field}=${options.headers[field]}`); } args.push(`http://127.0.0.1:${options.port}${options.path}`); const child = child_process.spawn(this.executable, args); return child; } processResults(output) { let result; try { result = JSON.parse(output); } catch { return undefined; } if (!result || !result.requests || !result.requests.average) { return undefined; } else { return result.requests.average; } } } class WrkBenchmarker { constructor() { this.name = 'wrk'; this.executable = 'wrk'; const result = child_process.spawnSync(this.executable, ['-h']); this.present = !(result.error && result.error.code === 'ENOENT'); } create(options) { const args = [ '-d', options.duration, '-c', options.connections, '-t', 8, `http://127.0.0.1:${options.port}${options.path}`, ]; const child = child_process.spawn(this.executable, args); return child; } processResults(output) { const throughputRe = /Requests\/sec:[ \t]+([0-9.]+)/; const match = output.match(throughputRe); const throughput = match && +match[1]; if (!isFinite(throughput)) { return undefined; } else { return throughput; } } } /** * Simple, single-threaded benchmarker for testing if the benchmark * works */ class TestDoubleBenchmarker { constructor(type) { // `type` is the type ofbenchmarker. Possible values are 'http' and 'http2'. this.name = `test-double-${type}`; this.executable = path.resolve(__dirname, '_test-double-benchmarker.js'); this.present = fs.existsSync(this.executable); this.type = type; } create(options) { const env = Object.assign({ duration: options.duration, test_url: `http://127.0.0.1:${options.port}${options.path}`, }, process.env); const child = child_process.fork(this.executable, [this.type], { silent: true, env }); return child; } processResults(output) { let result; try { result = JSON.parse(output); } catch { return undefined; } return result.throughput; } } /** * HTTP/2 Benchmarker */ class H2LoadBenchmarker { constructor() { this.name = 'h2load'; this.executable = 'h2load'; const result = child_process.spawnSync(this.executable, ['-h']); this.present = !(result.error && result.error.code === 'ENOENT'); } create(options) { const args = []; if (typeof options.requests === 'number') args.push('-n', options.requests); if (typeof options.clients === 'number') args.push('-c', options.clients); if (typeof options.threads === 'number') args.push('-t', options.threads); if (typeof options.maxConcurrentStreams === 'number') args.push('-m', options.maxConcurrentStreams); if (typeof options.initialWindowSize === 'number') args.push('-w', options.initialWindowSize); if (typeof options.sessionInitialWindowSize === 'number') args.push('-W', options.sessionInitialWindowSize); if (typeof options.rate === 'number') args.push('-r', options.rate); if (typeof options.ratePeriod === 'number') args.push(`--rate-period=${options.ratePeriod}`); if (typeof options.duration === 'number') args.push('-T', options.duration); if (typeof options.timeout === 'number') args.push('-N', options.timeout); if (typeof options.headerTableSize === 'number') args.push(`--header-table-size=${options.headerTableSize}`); if (typeof options.encoderHeaderTableSize === 'number') { args.push( `--encoder-header-table-size=${options.encoderHeaderTableSize}`); } const scheme = options.scheme || 'http'; const host = options.host || '127.0.0.1'; args.push(`${scheme}://${host}:${options.port}${options.path}`); const child = child_process.spawn(this.executable, args); return child; } processResults(output) { const rex = /(\d+(?:\.\d+)) req\/s/; return rex.exec(output)[1]; } } const http_benchmarkers = [ new WrkBenchmarker(), new AutocannonBenchmarker(), new TestDoubleBenchmarker('http'), new TestDoubleBenchmarker('http2'), new H2LoadBenchmarker(), ]; const benchmarkers = {}; http_benchmarkers.forEach((benchmarker) => { benchmarkers[benchmarker.name] = benchmarker; if (!exports.default_http_benchmarker && benchmarker.present) { exports.default_http_benchmarker = benchmarker.name; } }); exports.run = function(options, callback) { options = Object.assign({ port: exports.PORT, path: '/', connections: 100, duration: 5, benchmarker: exports.default_http_benchmarker, }, options); if (!options.benchmarker) { callback(new Error('Could not locate required http benchmarker. See ' + `${requirementsURL} for further instructions.`)); return; } const benchmarker = benchmarkers[options.benchmarker]; if (!benchmarker) { callback(new Error(`Requested benchmarker '${options.benchmarker}' ` + 'is not supported')); return; } if (!benchmarker.present) { callback(new Error(`Requested benchmarker '${options.benchmarker}' ` + 'is not installed')); return; } const benchmarker_start = process.hrtime(); const child = benchmarker.create(options); child.stderr.pipe(process.stderr); let stdout = ''; child.stdout.on('data', (chunk) => stdout += chunk.toString()); child.once('close', (code) => { const elapsed = process.hrtime(benchmarker_start); if (code) { let error_message = `${options.benchmarker} failed with ${code}.`; if (stdout !== '') { error_message += ` Output: ${stdout}`; } callback(new Error(error_message), code); return; } const result = benchmarker.processResults(stdout); if (result === undefined) { callback(new Error( `${options.benchmarker} produced strange output: ${stdout}`), code); return; } callback(null, code, options.benchmarker, result, elapsed); }); };