From bddaec1f6c519fcf57cd319e607d5a5651cd60c3 Mon Sep 17 00:00:00 2001 From: Miles Date: Sun, 2 Apr 2023 14:59:07 +0100 Subject: [PATCH 1/5] Expose aggregateResult --- autocannon.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autocannon.js b/autocannon.js index 1f1bcfe5..662403cd 100755 --- a/autocannon.js +++ b/autocannon.js @@ -18,6 +18,7 @@ const track = require('./lib/progressTracker') const generateSubArgAliases = require('./lib/subargAliases') const { checkURL, ofURL } = require('./lib/url') const { parseHAR } = require('./lib/parseHAR') +const aggregateResult = require('./lib/aggregateResult') if (typeof URL !== 'function') { console.error('autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.') @@ -30,6 +31,7 @@ module.exports.track = track module.exports.start = start module.exports.printResult = printResult module.exports.parseArguments = parseArguments +module.exports.aggregateResult = aggregateResult const alias = { connections: 'c', pipelining: 'p', From 1514a911268f3f44875962fa9fdf89c61c9c4978 Mon Sep 17 00:00:00 2001 From: Miles Date: Sat, 22 Apr 2023 15:38:55 +0100 Subject: [PATCH 2/5] Add skipAggregateResult flag. Init histograms in aggregateResult if they are not passed in. Docs --- README.md | 17 +++++++++++++++++ lib/aggregateResult.js | 3 ++- lib/run.js | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f3e8d0d..a05aaf8a 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,7 @@ Start autocannon against the given target. * `excludeErrorStats`: A `Boolean` which allows you to disable tracking non-2xx code responses in latency and bytes per second calculations. _OPTIONAL_ default: `false`. * `expectBody`: A `String` representing the expected response body. Each request whose response body is not equal to `expectBody`is counted in `mismatches`. If enabled, mismatches count towards bailout. _OPTIONAL_ * `tlsOptions`: An `Object` that is passed into `tls.connect` call ([Full list of options](https://nodejs.org/api/tls.html#tls_tls_connect_port_host_options_callback)). Note: this only applies if your URL is secure. + * `skipAggregateResult`: A `Boolean` which allows you to disable the aggregate result phase of an instance run. See [autocannon.aggregateResult](<#autocannon.aggregateResult(results[, opts])>) * `cb`: The callback which is called on completion of a benchmark. Takes the following params. _OPTIONAL_. * `err`: If there was an error encountered with the run. * `results`: The results of the run. @@ -397,6 +398,22 @@ Print the result tables to the terminal, programmatically. * `renderResultsTable`: A truthy value to enable the rendering of the results table. default: `true`. * `renderLatencyTable`: A truthy value to enable the rendering of the latency table. default: `false`. +### autocannon.aggregateResult(results[, opts]) + +Aggregate the results of one or more autocannon instance runs, where the instances of autocannon have been run with the `skipAggregateResult` option. + +This is an advanced use case, where you might be running a load test using autocannon across multiple machines and therefore need to defer aggregating the results to a later time. + +* `results`: An array of autocannon instance results, where the instances have been run with the `skipAggregateResult` option set to true. _REQUIRED_. +* `opts`: This is a subset of the options you would pass to the main autocannon API, so you could use the same options object as the one used to run the instances. _REQUIRED_. + * `title`: A `String` to be added to the results for identification. _OPTIONAL_ default: `undefined`. + * `url`: The given target. Can be HTTP or HTTPS. More than one URL is allowed, but it is recommended that the number of connections is an integer multiple of the URL. _REQUIRED_. + * `socketPath`: A path to a Unix Domain Socket or a Windows Named Pipe. A `url` is still required to send the correct Host header and path. _OPTIONAL_. + * `connections`: The number of concurrent connections. _OPTIONAL_ default: `10`. + * `sampleInt`: The number of milliseconds to elapse between taking samples. This controls the sample interval, & therefore the total number of samples, which affects statistical analyses. default: 1. + * `pipelining`: The number of [pipelined requests](https://en.wikipedia.org/wiki/HTTP_pipelining) for each connection. Will cause the `Client` API to throw when greater than 1. _OPTIONAL_ default: `1`. + * `workers`: Number of worker threads to use to fire requests. + ### Autocannon events Because an autocannon instance is an `EventEmitter`, it emits several events. these are below: diff --git a/lib/aggregateResult.js b/lib/aggregateResult.js index 0c04fa8b..f8fe83d5 100644 --- a/lib/aggregateResult.js +++ b/lib/aggregateResult.js @@ -1,9 +1,10 @@ 'use strict' -const { decodeHist, histAsObj, addPercentiles } = require('./histUtil') +const { decodeHist, getHistograms, histAsObj, addPercentiles } = require('./histUtil') function aggregateResult (results, opts, histograms) { results = Array.isArray(results) ? results : [results] + histograms = getHistograms(histograms) const aggregated = results.map(r => ({ ...r, diff --git a/lib/run.js b/lib/run.js index 7111b7d9..1331265a 100644 --- a/lib/run.js +++ b/lib/run.js @@ -137,7 +137,7 @@ function run (opts, tracker, cb) { statusCodes.forEach((code, index) => { result[(index + 1) + 'xx'] = code }) - const resultObj = isMainThread ? aggregateResult(result, opts, histograms) : result + const resultObj = isMainThread && !opts.skipAggregateResult ? aggregateResult(result, opts, histograms) : result if (opts.forever) { // we don't call callback when in forever mode, so this is the From 01e9f6959d3e9fc8ffdc1ebfb8c1743e510f531a Mon Sep 17 00:00:00 2001 From: Miles Date: Sat, 22 Apr 2023 15:50:50 +0100 Subject: [PATCH 3/5] Thin layer over aggregateResult for input validation --- autocannon.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/autocannon.js b/autocannon.js index 662403cd..ec683710 100755 --- a/autocannon.js +++ b/autocannon.js @@ -18,7 +18,8 @@ const track = require('./lib/progressTracker') const generateSubArgAliases = require('./lib/subargAliases') const { checkURL, ofURL } = require('./lib/url') const { parseHAR } = require('./lib/parseHAR') -const aggregateResult = require('./lib/aggregateResult') +const _aggregateResult = require('./lib/aggregateResult') +const validateOpts = require('./lib/validate') if (typeof URL !== 'function') { console.error('autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.') @@ -31,7 +32,19 @@ module.exports.track = track module.exports.start = start module.exports.printResult = printResult module.exports.parseArguments = parseArguments -module.exports.aggregateResult = aggregateResult +module.exports.aggregateResult = function aggregateResult (results, opts) { + if (!Array.isArray(results)) { + throw new Error('"results" must be an array of results') + } + + opts = validateOpts(opts, false) + + if (opts instanceof Error) { + throw opts + } + + return _aggregateResult(results, opts) +} const alias = { connections: 'c', pipelining: 'p', From 0282b406fd4439e37c2579d8f67fa47a62ef0e79 Mon Sep 17 00:00:00 2001 From: Miles Date: Sat, 22 Apr 2023 16:11:56 +0100 Subject: [PATCH 4/5] More manageable docs --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a05aaf8a..755b8800 100644 --- a/README.md +++ b/README.md @@ -405,14 +405,14 @@ Aggregate the results of one or more autocannon instance runs, where the instanc This is an advanced use case, where you might be running a load test using autocannon across multiple machines and therefore need to defer aggregating the results to a later time. * `results`: An array of autocannon instance results, where the instances have been run with the `skipAggregateResult` option set to true. _REQUIRED_. -* `opts`: This is a subset of the options you would pass to the main autocannon API, so you could use the same options object as the one used to run the instances. _REQUIRED_. - * `title`: A `String` to be added to the results for identification. _OPTIONAL_ default: `undefined`. - * `url`: The given target. Can be HTTP or HTTPS. More than one URL is allowed, but it is recommended that the number of connections is an integer multiple of the URL. _REQUIRED_. - * `socketPath`: A path to a Unix Domain Socket or a Windows Named Pipe. A `url` is still required to send the correct Host header and path. _OPTIONAL_. - * `connections`: The number of concurrent connections. _OPTIONAL_ default: `10`. - * `sampleInt`: The number of milliseconds to elapse between taking samples. This controls the sample interval, & therefore the total number of samples, which affects statistical analyses. default: 1. - * `pipelining`: The number of [pipelined requests](https://en.wikipedia.org/wiki/HTTP_pipelining) for each connection. Will cause the `Client` API to throw when greater than 1. _OPTIONAL_ default: `1`. - * `workers`: Number of worker threads to use to fire requests. +* `opts`: This is a subset of the options you would pass to the main autocannon API, so you could use the same options object as the one used to run the instances. See [autocannon](<#autocannon(opts[, cb])>) for full descriptions of the options. _REQUIRED_. + * `url`: _REQUIRED_ + * `title`: _OPTIONAL_ default: `undefined` + * `socketPath`: _OPTIONAL_ + * `connections`: _OPTIONAL_ default: `10`. + * `sampleInt`: _OPTIONAL_ default: `1` + * `pipelining`: _OPTIONAL_ default: `1` + * `workers`: _OPTIONAL_ default: `undefined` ### Autocannon events From 591c5ef95aab08aa85dad1e29b64051821c8e8ee Mon Sep 17 00:00:00 2001 From: Miles Date: Tue, 25 Apr 2023 22:09:05 +0100 Subject: [PATCH 5/5] Tests. Default opts --- autocannon.js | 2 +- test/aggregateResult.test.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 test/aggregateResult.test.js diff --git a/autocannon.js b/autocannon.js index ec683710..a4fb91ef 100755 --- a/autocannon.js +++ b/autocannon.js @@ -32,7 +32,7 @@ module.exports.track = track module.exports.start = start module.exports.printResult = printResult module.exports.parseArguments = parseArguments -module.exports.aggregateResult = function aggregateResult (results, opts) { +module.exports.aggregateResult = function aggregateResult (results, opts = {}) { if (!Array.isArray(results)) { throw new Error('"results" must be an array of results') } diff --git a/test/aggregateResult.test.js b/test/aggregateResult.test.js new file mode 100644 index 00000000..83cb1fcb --- /dev/null +++ b/test/aggregateResult.test.js @@ -0,0 +1,33 @@ +const { test } = require('tap') +const { startServer } = require('./helper') +const autocannon = require('../autocannon') +const aggregateResult = autocannon.aggregateResult +const server = startServer() +const url = 'http://localhost:' + server.address().port + +test('exec separate autocannon instances with skipAggregateResult, then aggregateResult afterwards', async (t) => { + t.plan(2) + + const opts = { + url, + connections: 1, + maxOverallRequests: 10, + skipAggregateResult: true + } + + const results = await Promise.all([ + autocannon(opts), + autocannon(opts) + ]) + + const aggregateResults = aggregateResult(results, opts) + + t.equal(aggregateResults['2xx'], 20) + t.equal(aggregateResults.requests.total, 20) +}) + +test('aggregateResult must be passed opts with at least a URL or socketPath property', async (t) => { + t.plan(2) + t.throws(() => aggregateResult([]), 'url or socketPath option required') + t.throws(() => aggregateResult([], {}), 'url or socketPath option required') +})