Skip to content

Commit 64ab521

Browse files
authored
Defer aggregating results until after the instance returns to allow multi-instance load tests (#488)
* Expose aggregateResult * Add skipAggregateResult flag. Init histograms in aggregateResult if they are not passed in. Docs * Thin layer over aggregateResult for input validation * More manageable docs * Tests. Default opts
1 parent fd8e8c3 commit 64ab521

File tree

5 files changed

+68
-2
lines changed

5 files changed

+68
-2
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ Start autocannon against the given target.
330330
* `excludeErrorStats`: A `Boolean` which allows you to disable tracking non-2xx code responses in latency and bytes per second calculations. _OPTIONAL_ default: `false`.
331331
* `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_
332332
* `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.
333+
* `skipAggregateResult`: A `Boolean` which allows you to disable the aggregate result phase of an instance run. See [autocannon.aggregateResult](<#autocannon.aggregateResult(results[, opts])>)
333334
* `cb`: The callback which is called on completion of a benchmark. Takes the following params. _OPTIONAL_.
334335
* `err`: If there was an error encountered with the run.
335336
* `results`: The results of the run.
@@ -397,6 +398,22 @@ Print the result tables to the terminal, programmatically.
397398
* `renderResultsTable`: A truthy value to enable the rendering of the results table. default: `true`.
398399
* `renderLatencyTable`: A truthy value to enable the rendering of the latency table. default: `false`.
399400

401+
### autocannon.aggregateResult(results[, opts])
402+
403+
Aggregate the results of one or more autocannon instance runs, where the instances of autocannon have been run with the `skipAggregateResult` option.
404+
405+
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.
406+
407+
* `results`: An array of autocannon instance results, where the instances have been run with the `skipAggregateResult` option set to true. _REQUIRED_.
408+
* `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_.
409+
* `url`: _REQUIRED_
410+
* `title`: _OPTIONAL_ default: `undefined`
411+
* `socketPath`: _OPTIONAL_
412+
* `connections`: _OPTIONAL_ default: `10`.
413+
* `sampleInt`: _OPTIONAL_ default: `1`
414+
* `pipelining`: _OPTIONAL_ default: `1`
415+
* `workers`: _OPTIONAL_ default: `undefined`
416+
400417
### Autocannon events
401418

402419
Because an autocannon instance is an `EventEmitter`, it emits several events. these are below:

autocannon.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const track = require('./lib/progressTracker')
1818
const generateSubArgAliases = require('./lib/subargAliases')
1919
const { checkURL, ofURL } = require('./lib/url')
2020
const { parseHAR } = require('./lib/parseHAR')
21+
const _aggregateResult = require('./lib/aggregateResult')
22+
const validateOpts = require('./lib/validate')
2123

2224
if (typeof URL !== 'function') {
2325
console.error('autocannon requires the WHATWG URL API, but it is not available. Please upgrade to Node 6.13+.')
@@ -30,6 +32,19 @@ module.exports.track = track
3032
module.exports.start = start
3133
module.exports.printResult = printResult
3234
module.exports.parseArguments = parseArguments
35+
module.exports.aggregateResult = function aggregateResult (results, opts = {}) {
36+
if (!Array.isArray(results)) {
37+
throw new Error('"results" must be an array of results')
38+
}
39+
40+
opts = validateOpts(opts, false)
41+
42+
if (opts instanceof Error) {
43+
throw opts
44+
}
45+
46+
return _aggregateResult(results, opts)
47+
}
3348
const alias = {
3449
connections: 'c',
3550
pipelining: 'p',

lib/aggregateResult.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use strict'
22

3-
const { decodeHist, histAsObj, addPercentiles } = require('./histUtil')
3+
const { decodeHist, getHistograms, histAsObj, addPercentiles } = require('./histUtil')
44

55
function aggregateResult (results, opts, histograms) {
66
results = Array.isArray(results) ? results : [results]
7+
histograms = getHistograms(histograms)
78

89
const aggregated = results.map(r => ({
910
...r,

lib/run.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ function run (opts, tracker, cb) {
137137

138138
statusCodes.forEach((code, index) => { result[(index + 1) + 'xx'] = code })
139139

140-
const resultObj = isMainThread ? aggregateResult(result, opts, histograms) : result
140+
const resultObj = isMainThread && !opts.skipAggregateResult ? aggregateResult(result, opts, histograms) : result
141141

142142
if (opts.forever) {
143143
// we don't call callback when in forever mode, so this is the

test/aggregateResult.test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { test } = require('tap')
2+
const { startServer } = require('./helper')
3+
const autocannon = require('../autocannon')
4+
const aggregateResult = autocannon.aggregateResult
5+
const server = startServer()
6+
const url = 'http://localhost:' + server.address().port
7+
8+
test('exec separate autocannon instances with skipAggregateResult, then aggregateResult afterwards', async (t) => {
9+
t.plan(2)
10+
11+
const opts = {
12+
url,
13+
connections: 1,
14+
maxOverallRequests: 10,
15+
skipAggregateResult: true
16+
}
17+
18+
const results = await Promise.all([
19+
autocannon(opts),
20+
autocannon(opts)
21+
])
22+
23+
const aggregateResults = aggregateResult(results, opts)
24+
25+
t.equal(aggregateResults['2xx'], 20)
26+
t.equal(aggregateResults.requests.total, 20)
27+
})
28+
29+
test('aggregateResult must be passed opts with at least a URL or socketPath property', async (t) => {
30+
t.plan(2)
31+
t.throws(() => aggregateResult([]), 'url or socketPath option required')
32+
t.throws(() => aggregateResult([], {}), 'url or socketPath option required')
33+
})

0 commit comments

Comments
 (0)