-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
benchmark: support for multiple http benchmarkers #8140
Changes from 8 commits
3d14782
6113cec
721a1c5
52521b9
97b334a
687a64f
3a8a9c8
ed3ad0f
7cd3daa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
'use strict'; | ||
|
||
const child_process = require('child_process'); | ||
|
||
// The port used by servers and wrk | ||
exports.PORT = process.env.PORT || 12346; | ||
|
||
function AutocannonBenchmarker() { | ||
this.name = 'autocannon'; | ||
this.autocannon_exe = process.platform === 'win32' | ||
? 'autocannon.cmd' | ||
: 'autocannon'; | ||
const result = child_process.spawnSync(this.autocannon_exe, ['-h']); | ||
this.present = !(result.error && result.error.code === 'ENOENT'); | ||
} | ||
|
||
AutocannonBenchmarker.prototype.create = function(options) { | ||
const args = ['-d', options.duration, '-c', options.connections, '-j', '-n', | ||
`http://127.0.0.1:${options.port}${options.path}` ]; | ||
const child = child_process.spawn(this.autocannon_exe, args); | ||
return child; | ||
}; | ||
|
||
AutocannonBenchmarker.prototype.processResults = function(output) { | ||
let result; | ||
try { | ||
result = JSON.parse(output); | ||
} catch (err) { | ||
// Do nothing, let next line handle this | ||
} | ||
if (!result || !result.requests || !result.requests.average) { | ||
return undefined; | ||
} else { | ||
return result.requests.average; | ||
} | ||
}; | ||
|
||
function WrkBenchmarker() { | ||
this.name = 'wrk'; | ||
this.regexp = /Requests\/sec:[ \t]+([0-9\.]+)/; | ||
const result = child_process.spawnSync('wrk', ['-h']); | ||
this.present = !(result.error && result.error.code === 'ENOENT'); | ||
} | ||
|
||
WrkBenchmarker.prototype.create = function(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('wrk', args); | ||
return child; | ||
}; | ||
|
||
WrkBenchmarker.prototype.processResults = function(output) { | ||
const match = output.match(this.regexp); | ||
const result = match && +match[1]; | ||
if (!result) { | ||
return undefined; | ||
} else { | ||
return result; | ||
} | ||
}; | ||
|
||
const http_benchmarkers = [ new WrkBenchmarker(), | ||
new AutocannonBenchmarker() ]; | ||
|
||
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: 10, | ||
benchmarker: exports.default_http_benchmarker | ||
}, options); | ||
if (!options.benchmarker) { | ||
callback(new Error('Could not locate any of the required http ' + | ||
'benchmarkers. Check benchmark/README.md for further ' + | ||
'instructions.')); | ||
return; | ||
} | ||
var 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(); | ||
|
||
var child = benchmarker.create(options); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
|
||
child.stderr.pipe(process.stderr); | ||
|
||
let stdout = ''; | ||
child.stdout.on('data', (chunk) => stdout += chunk.toString()); | ||
|
||
child.once('close', function(code) { | ||
const elapsed = process.hrtime(benchmarker_start); | ||
if (code) { | ||
var error_message = `${options.benchmarker} failed with ${code}.`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
if (stdout !== '') { | ||
error_message += ` Output: ${stdout}`; | ||
} | ||
callback(new Error(error_message), code); | ||
return; | ||
} | ||
|
||
const result = benchmarker.processResults(stdout); | ||
if (!result) { | ||
callback(new Error(`${options.benchmarker} produced strange output: ` + | ||
stdout, code)); | ||
return; | ||
} | ||
|
||
callback(null, code, options.benchmarker, result, elapsed); | ||
}); | ||
|
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,17 @@ | ||
'use strict'; | ||
|
||
const child_process = require('child_process'); | ||
|
||
// The port used by servers and wrk | ||
exports.PORT = process.env.PORT || 12346; | ||
const http_benchmarkers = require('./_http-benchmarkers.js'); | ||
|
||
exports.createBenchmark = function(fn, options) { | ||
return new Benchmark(fn, options); | ||
}; | ||
|
||
function Benchmark(fn, options) { | ||
this.name = require.main.filename.slice(__dirname.length + 1); | ||
this.options = this._parseArgs(process.argv.slice(2), options); | ||
const parsed_args = this._parseArgs(process.argv.slice(2), options); | ||
this.options = parsed_args.cli; | ||
this.extra_options = parsed_args.extra; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any reason to introduce There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we just add those to options they will be displayed when running other benchmarks, even unrelated ones. It does not look, and could be confusing. So I store those extra options elsewhere and apply them only to related benchmarks |
||
this.queue = this._queue(this.options); | ||
this.config = this.queue[0]; | ||
|
||
|
@@ -29,7 +29,7 @@ function Benchmark(fn, options) { | |
|
||
Benchmark.prototype._parseArgs = function(argv, options) { | ||
const cliOptions = Object.assign({}, options); | ||
|
||
const extraOptions = {}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: avoid noisy style changes. |
||
// Parse configuration arguments | ||
for (const arg of argv) { | ||
const match = arg.match(/^(.+?)=([\s\S]*)$/); | ||
|
@@ -38,14 +38,16 @@ Benchmark.prototype._parseArgs = function(argv, options) { | |
process.exit(1); | ||
} | ||
|
||
// Infer the type from the options object and parse accordingly | ||
const isNumber = typeof options[match[1]][0] === 'number'; | ||
const value = isNumber ? +match[2] : match[2]; | ||
|
||
cliOptions[match[1]] = [value]; | ||
if (options[match[1]]) { | ||
// Infer the type from the options object and parse accordingly | ||
const isNumber = typeof options[match[1]][0] === 'number'; | ||
const value = isNumber ? +match[2] : match[2]; | ||
cliOptions[match[1]] = [value]; | ||
} else { | ||
extraOptions[match[1]] = match[2]; | ||
} | ||
} | ||
|
||
return cliOptions; | ||
return { cli: cliOptions, extra: extraOptions }; | ||
}; | ||
|
||
Benchmark.prototype._queue = function(options) { | ||
|
@@ -88,51 +90,29 @@ Benchmark.prototype._queue = function(options) { | |
return queue; | ||
}; | ||
|
||
function hasWrk() { | ||
const result = child_process.spawnSync('wrk', ['-h']); | ||
if (result.error && result.error.code === 'ENOENT') { | ||
console.error('Couldn\'t locate `wrk` which is needed for running ' + | ||
'benchmarks. Check benchmark/README.md for further instructions.'); | ||
process.exit(1); | ||
} | ||
} | ||
// Benchmark an http server. | ||
exports.default_http_benchmarker = | ||
http_benchmarkers.default_http_benchmarker; | ||
exports.PORT = http_benchmarkers.PORT; | ||
|
||
// benchmark an http server. | ||
const WRK_REGEXP = /Requests\/sec:[ \t]+([0-9\.]+)/; | ||
Benchmark.prototype.http = function(urlPath, args, cb) { | ||
hasWrk(); | ||
Benchmark.prototype.http = function(options, cb) { | ||
const self = this; | ||
|
||
const urlFull = 'http://127.0.0.1:' + exports.PORT + urlPath; | ||
args = args.concat(urlFull); | ||
|
||
const childStart = process.hrtime(); | ||
const child = child_process.spawn('wrk', args); | ||
child.stderr.pipe(process.stderr); | ||
|
||
// Collect stdout | ||
let stdout = ''; | ||
child.stdout.on('data', (chunk) => stdout += chunk.toString()); | ||
|
||
child.once('close', function(code) { | ||
const elapsed = process.hrtime(childStart); | ||
if (cb) cb(code); | ||
|
||
if (code) { | ||
console.error('wrk failed with ' + code); | ||
process.exit(code); | ||
const http_options = Object.assign({ }, options); | ||
http_options.benchmarker = http_options.benchmarker || | ||
self.config.benchmarker || | ||
self.extra_options.benchmarker || | ||
exports.default_http_benchmarker; | ||
http_benchmarkers.run(http_options, function(error, code, used_benchmarker, | ||
result, elapsed) { | ||
if (cb) { | ||
cb(code); | ||
} | ||
|
||
// Extract requests pr second and check for odd results | ||
const match = stdout.match(WRK_REGEXP); | ||
if (!match || match.length <= 1) { | ||
console.error('wrk produced strange output:'); | ||
console.error(stdout); | ||
process.exit(1); | ||
if (error) { | ||
console.error(error); | ||
process.exit(code || 1); | ||
} | ||
|
||
// Report rate | ||
self.report(+match[1], elapsed); | ||
self.config.benchmarker = used_benchmarker; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if it's a good idea to mutate the config object. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This way user does not need to pick benchmarker - we will inject default one here if none was selected. Besides logging, nothing more is done with 'self' object from this point. |
||
self.report(result, elapsed); | ||
}); | ||
}; | ||
|
||
|
@@ -152,6 +132,9 @@ Benchmark.prototype._run = function() { | |
for (const key of Object.keys(config)) { | ||
childArgs.push(`${key}=${config[key]}`); | ||
} | ||
for (const key of Object.keys(self.extra_options)) { | ||
childArgs.push(`${key}=${self.extra_options[key]}`); | ||
} | ||
|
||
const child = child_process.fork(require.main.filename, childArgs, { | ||
env: childEnv | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use
const