Showing preview only (360K chars total). Download the full file or copy to clipboard to get everything.
Repository: mcollina/autocannon
Branch: master
Commit: 225e3a30a6ad
Files: 105
Total size: 335.6 KB
Directory structure:
gitextract_p81k9btq/
├── .github/
│ ├── dependabot.yml
│ ├── release-drafter.yml
│ ├── tests_checker.yml
│ └── workflows/
│ └── nodejs.yml
├── .gitignore
├── .npmignore
├── .taprc
├── LICENSE
├── README.md
├── autocannon.js
├── cluster.js
├── for-zero-x.js
├── help.txt
├── lib/
│ ├── aggregateResult.js
│ ├── defaultOptions.js
│ ├── format.js
│ ├── histUtil.js
│ ├── httpClient.js
│ ├── httpMethods.js
│ ├── httpRequestBuilder.js
│ ├── init.js
│ ├── manager.js
│ ├── multipart.js
│ ├── noop.js
│ ├── parseHAR.js
│ ├── pipelinedRequestsQueue.js
│ ├── preload/
│ │ └── autocannonDetectPort.js
│ ├── printResult.js
│ ├── progressTracker.js
│ ├── requestIterator.js
│ ├── run.js
│ ├── runTracker.js
│ ├── subargAliases.js
│ ├── url.js
│ ├── util.js
│ ├── validate.js
│ ├── worker.js
│ └── worker_threads.js
├── package.json
├── samples/
│ ├── bench-multi-url.js
│ ├── customise-individual-connection.js
│ ├── customise-verifyBody-workers.js
│ ├── customise-verifyBody.js
│ ├── helpers/
│ │ ├── on-response.js
│ │ ├── setup-request.js
│ │ └── verify-body.js
│ ├── init-context.js
│ ├── modifying-request.js
│ ├── request-context-workers.js
│ ├── request-context.js
│ ├── requests-sample.js
│ ├── track-run-workers.js
│ ├── track-run.js
│ └── using-id-replacement.js
├── server.js
└── test/
├── aggregateResult.test.js
├── argumentParsing.test.js
├── basic-auth.test.js
├── cert.pem
├── cli-ipc.test.js
├── cli.test.js
├── debug.test.js
├── envPort.test.js
├── fixtures/
│ ├── example-result.json
│ ├── httpbin-get.json
│ ├── httpbin-post.json
│ ├── httpbin-simple-get.json
│ └── multi-domains.json
├── forever.test.js
├── format.test.js
├── helper.js
├── httpClient.test.js
├── httpRequestBuilder.test.js
├── key.pem
├── keystore.pkcs12
├── onPort.test.js
├── parseHAR.test.js
├── pipelinedRequestsQueue.test.js
├── printResult-process.js
├── printResult-renderStatusCodes.test.js
├── printResult.test.js
├── progressTracker.test.js
├── progressTracker.test.stub.js
├── requestIterator.test.js
├── run.test.js
├── runAmount.test.js
├── runMultiServer.test.js
├── runMultipart.test.js
├── runRate.test.js
├── sampleInt.test.js
├── serial/
│ ├── autocannon.test.js
│ ├── run.test.js
│ ├── tap-parallel-not-ok
│ └── wasm.test.js
├── subargAliases.test.js
├── tap-parallel-ok
├── targetProcess.js
├── url.test.js
├── utils/
│ ├── has-worker-support.js
│ ├── on-response.js
│ ├── setup-client.js
│ ├── setup-request.js
│ └── verify-body.js
├── validate.test.js
└── workers.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: npm
directory: '/'
schedule:
interval: daily
open-pull-requests-limit: 10
================================================
FILE: .github/release-drafter.yml
================================================
template: |
## What’s Changed
$CHANGES
================================================
FILE: .github/tests_checker.yml
================================================
comment: 'Could you please add tests to make sure this change works as expected?',
fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java']
testDir: 'test'
================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node.js CI
on: [ push, pull_request ]
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true
jobs:
test:
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [20, 22]
steps:
- uses: actions/checkout@v3
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: node --version
- run: npm --version
- run: npm install
- run: npm test
env:
CI: true
automerge:
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]'
needs: test
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- uses: fastify/github-action-merge-dependabot@v3
with:
exclude: 'chalk;pretty-bytes'
github-token: ${{ secrets.github_token }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vim swap files
*.swp
# macOS files
.DS_Store
# lock files
package-lock.json
yarn.lock
# editor files
.vscode
.idea
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
profile-*
.devcontainer
.history
================================================
FILE: .npmignore
================================================
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
profile-*
demo.gif
server.js
*.png
appveyor.yml
.travis.yml
.github
.nyc_output
================================================
FILE: .taprc
================================================
ts: false
jsx: false
flow: false
timeout: 900
branches: 60
functions: 60
lines: 60
statements: 60
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016 Matteo Collina
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

# autocannon


An HTTP/1.1 benchmarking tool written in node, greatly inspired by [wrk][wrk]
and [wrk2][wrk2], with support for HTTP pipelining and HTTPS.
On _my_ box, *autocannon* can produce more load than `wrk` and `wrk2`, see [limitations](#limitations) for more details.
* [Installation](#install)
* [Usage](#usage)
* [API](#api)
* [Acknowledgements](#acknowledgements)
* [License](#license)
## Install
```
npm i autocannon -g
```
or if you want to use the [API](#api) or as a dependency:
```
npm i autocannon --save
```
## Usage
### Command Line
```
Usage: autocannon [opts] URL
URL is any valid HTTP or HTTPS URL.
If the PORT environment variable is set, the URL can be a path. In that case 'http://localhost:$PORT/path' will be used as the URL.
Available options:
-c/--connections NUM
The number of concurrent connections to use. default: 10.
-p/--pipelining NUM
The number of pipelined requests to use. default: 1.
-d/--duration SEC
The number of seconds to run the autocannon. default: 10.
-a/--amount NUM
The number of requests to make before exiting the benchmark. If set, duration is ignored.
-L NUM
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.
-S/--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.
-w/--workers
Number of worker threads to use to fire requests.
-W/--warmup
Use a warm up interval before starting sampling.
This enables startup processes to finish and traffic to normalize before sampling begins
use -c and -d sub args e.g. `--warmup [ -c 1 -d 3 ]`
--on-port
Start the command listed after -- on the command line. When it starts listening on a port,
start sending requests to that port. A URL is still required to send requests to
the correct path. The hostname can be omitted, `localhost` will be used by default.
If the command after -- is `node <script>`, this flag is optional and assumed to be `true`.
-m/--method METHOD
The HTTP method to use. default: 'GET'.
-t/--timeout NUM
The number of seconds before timing out and resetting a connection. default: 10
-T/--title TITLE
The title to place in the results for identification.
-b/--body BODY
The body of the request.
NOTE: This option needs to be used with the '-H/--headers' option in some frameworks
-F/--form FORM
Upload a form (multipart/form-data). The form options can be a JSON string like
'{ "field 1": { "type": "text", "value": "a text value"}, "field 2": { "type": "file", "path": "path to the file" } }'
or a path to a JSON file containing the form options.
When uploading a file the default filename value can be overridden by using the corresponding option:
'{ "field name": { "type": "file", "path": "path to the file", "options": { "filename": "myfilename" } } }'
Passing the filepath to the form can be done by using the corresponding option:
'{ "field name": { "type": "file", "path": "path to the file", "options": { "filepath": "/some/path/myfilename" } } }'
-i/--input FILE
The body of the request. See '-b/body' for more details.
-H/--headers K=V
The request headers.
--har FILE
When provided, Autocannon will use requests from the HAR file.
CAUTION: you have to specify one or more domains using URL option: only the HAR requests to the same domains will be considered.
NOTE: you can still add extra headers with -H/--headers but -m/--method, -F/--form, -i/--input -b/--body will be ignored.
-B/--bailout NUM
The number of failures before initiating a bailout.
-M/--maxConnectionRequests NUM
The max number of requests to make per connection to the server.
-O/--maxOverallRequests NUM
The max number of requests to make overall to the server.
-r/--connectionRate NUM
The max number of requests to make per second from an individual connection.
-R/--overallRate NUM
The max number of requests to make per second from all connections.
connection rate will take precedence if both are set.
NOTE: if using rate limiting and a very large rate is entered which cannot be met, Autocannon will do as many requests as possible per second.
Also, latency data will be corrected to compensate for the effects of the coordinated omission issue.
If you are not familiar with the coordinated omission issue, you should probably read [this article](http://highscalability.com/blog/2015/10/5/your-load-generator-is-probably-lying-to-you-take-the-red-pi.html) or watch this [Gil Tene's talk](https://www.youtube.com/watch?v=lJ8ydIuPFeU) on the topic.
-C/--ignoreCoordinatedOmission
Ignore the coordinated omission issue when requests should be sent at a fixed rate using 'connectionRate' or 'overallRate'.
NOTE: it is not recommended to enable this option.
When the request rate cannot be met because the server is too slow, many request latencies might be missing and Autocannon might report a misleading latency distribution.
-D/--reconnectRate NUM
The number of requests to make before resetting a connections connection to the
server.
-n/--no-progress
Don't render the progress bar. default: false.
-l/--latency
Print all the latency data. default: false.
-I/--idReplacement
Enable replacement of `[<id>]` with a randomly generated ID within the request body. e.g. `/items/[<id>]`. default: false.
-j/--json
Print the output as newline delimited JSON. This will cause the progress bar and results not to be rendered. default: false.
-f/--forever
Run the benchmark forever. Efficiently restarts the benchmark on completion. default: false.
-s/--servername
Server name for the SNI (Server Name Indication) TLS extension. Defaults to the hostname of the URL when it is not an IP address.
-x/--excludeErrorStats
Exclude error statistics (non-2xx HTTP responses) from the final latency and bytes per second averages. default: false.
-E/--expectBody EXPECTED
Ensure the body matches this value. If enabled, mismatches count towards bailout.
Enabling this option will slow down the load testing.
--renderStatusCodes
Print status codes and their respective statistics.
--cert
Path to cert chain in pem format
--key
Path to private key for specified cert in pem format
--ca
Path to trusted ca certificates for the test. This argument accepts both a single file as well as a list of files
--debug
Print connection errors to stderr.
-v/--version
Print the version number.
-V/--verbose
Print the table with results. default: true.
-h/--help
Print this menu.
```
autocannon outputs data in tables like this:
```
Running 10s test @ http://localhost:3000
10 connections
┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬──────────┐
│ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼──────────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms │ 1 ms │ 0.02 ms │ 0.16 ms │ 16.45 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴──────────┘
┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Req/Sec │ 20623 │ 20623 │ 25583 │ 26271 │ 25131.2 │ 1540.94 │ 20615 │
├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
│ Bytes/Sec │ 2.29 MB │ 2.29 MB │ 2.84 MB │ 2.92 MB │ 2.79 MB │ 171 kB │ 2.29 MB │
└───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
Req/Bytes counts sampled once per second.
251k requests in 10.05s, 27.9 MB read
```
There are two tables: one for the request latency, and one for the request volume.
The latency table lists the request times at the 2.5% percentile, the fast outliers; at 50%, the median; at 97.5%, the slow outliers; at 99%, the very slowest outliers. Here, lower means faster.
The request volume table lists the number of requests sent and the number of bytes downloaded. These values are sampled once per second. Higher values mean more requests were processed. In the above example, 2.29 MB was downloaded in 1 second in the worst case (slowest 1%). Since we only ran for 10 seconds, there are just 10 samples, the Min value and the 1% and 2.5% percentiles are all the same sample. With longer durations these numbers will differ more.
When passing the `-l` flag, a third table lists all the latency percentiles recorded by autocannon:
```
┌────────────┬──────────────┐
│ Percentile │ Latency (ms) │
├────────────┼──────────────┤
│ 0.001 │ 0 │
├────────────┼──────────────┤
│ 0.01 │ 0 │
├────────────┼──────────────┤
│ 0.1 │ 0 │
├────────────┼──────────────┤
│ 1 │ 0 │
├────────────┼──────────────┤
│ 2.5 │ 0 │
├────────────┼──────────────┤
│ 10 │ 0 │
├────────────┼──────────────┤
│ 25 │ 0 │
├────────────┼──────────────┤
│ 50 │ 0 │
├────────────┼──────────────┤
│ 75 │ 0 │
├────────────┼──────────────┤
│ 90 │ 0 │
├────────────┼──────────────┤
│ 97.5 │ 0 │
├────────────┼──────────────┤
│ 99 │ 1 │
├────────────┼──────────────┤
│ 99.9 │ 1 │
├────────────┼──────────────┤
│ 99.99 │ 3 │
├────────────┼──────────────┤
│ 99.999 │ 15 │
└────────────┴──────────────┘
```
This can give some more insight if a lot (millions) of requests were sent.
### Programmatically
```js
'use strict'
const autocannon = require('autocannon')
autocannon({
url: 'http://localhost:3000',
connections: 10, //default
pipelining: 1, // default
duration: 10 // default
}, console.log)
// async/await
async function foo () {
const result = await autocannon({
url: 'http://localhost:3000',
connections: 10, //default
pipelining: 1, // default
duration: 10 // default
})
console.log(result)
}
```
<a name="workers"></a>
#### Workers
In workers mode, `autocannon` uses instances of Node's [Worker](https://nodejs.org/dist/latest/docs/api/worker_threads.html#new-workerfilename-options) class to execute the load tests in multiple threads.
The `amount` and `connections` parameters are divided amongst the workers. If either parameter is not integer divisible by the number of `workers`, the per-worker value is rounded to the lowest integer, or set to `1`, whichever is the higher. All other parameters are applied per-worker as if the test were single-threaded.
**NOTE:** Unlike `amount` and `connections`, the "overall" parameters, `maxOverallRequests` and `overallRate`, are applied **_per worker_**. For example, if you set `connections` to `4`, `workers` to `2` and `maxOverallRequests` to `10`, each worker will receive `2` connections and a `maxOverallRequests` of `10`, resulting in `20` requests being sent.
```js
'use strict'
const autocannon = require('autocannon')
autocannon({
url: 'http://localhost:3000',
connections: 10, //default
pipelining: 1, // default
duration: 10, // default
workers: 4
}, console.log)
```
**NOTE:** When in workers mode, you need to pass in an absolute file path to all the options that accept a `function`. This is because a function passed into the main process can not be cloned and passed to the worker. So instead, it needs a file that it can `require`. The options with this behaviour are shown in the below example
```js
'use strict'
const autocannon = require('autocannon')
autocannon({
// ...
workers: 4,
setupClient: '/full/path/to/setup-client.js',
verifyBody: '/full/path/to/verify-body.js'
requests: [
{
// ...
onResponse: '/full/path/to/on-response.js'
},
{
// ...
setupRequest: '/full/path/to/setup-request.js'
}
]
}, console.log)
```
## API
### autocannon(opts[, cb])
Start autocannon against the given target.
* `opts`: Configuration options for the autocannon instance. This can have the following attributes. _REQUIRED_.
* `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_.
* `workers`: Number of worker threads to use to fire requests.
* `connections`: The number of concurrent connections. _OPTIONAL_ default: `10`.
* `duration`: The number of seconds to run the autocannon. Can be a [timestring](https://www.npmjs.com/package/timestring). _OPTIONAL_ default: `10`.
* `amount`: A `Number` stating the number of requests to make before ending the test. This overrides duration and takes precedence, so the test won't end until the number of requests needed to be completed is completed. _OPTIONAL_.
* `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.
* `timeout`: The number of seconds to wait for a response before. _OPTIONAL_ default: `10`.
* `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`.
* `bailout`: The threshold of the number of errors when making the requests to the server before this instance bail's out. This instance will take all existing results so far and aggregate them into the results. If none passed here, the instance will ignore errors and never bail out. _OPTIONAL_ default: `undefined`.
* `method`: The HTTP method to use. _OPTIONAL_ `default: 'GET'`.
* `title`: A `String` to be added to the results for identification. _OPTIONAL_ default: `undefined`.
* `body`: A `String` or a `Buffer` containing the body of the request. Insert one or more randomly generated IDs into the body by including `[<id>]` where the randomly generated ID should be inserted (Must also set idReplacement to true). This can be useful in soak testing POST endpoints where one or more fields must be unique. Leave undefined for an empty body. _OPTIONAL_ default: `undefined`.
* `form`: A `String` or an `Object` containing the multipart/form-data options or a path to the JSON file containing them
* `headers`: An `Object` containing the headers of the request. _OPTIONAL_ default: `{}`.
* `initialContext`: An object that you'd like to initialize your context with. Check out [an example of initializing context](./samples/init-context.js). _OPTIONAL_
* `setupClient`: A `Function` which will be passed the `Client` object for each connection to be made. This can be used to customise each individual connection headers and body using the API shown below. The changes you make to the client in this function will take precedence over the default `body` and `headers` you pass in here. There is an example of this in the samples folder. _OPTIONAL_ default: `function noop () {}`. When using `workers`, you need to supply a file path that default exports a function instead (Check out the [workers](#workers) section for more details).
* `verifyBody`: A `Function` which will be passed the response body for each completed request. Each request, whose `verifyBody` function does not return a truthy value, is counted in `mismatches`. This function will take precedence over the `expectBody`. There is an example of this in the samples folder. When using `workers`, you need to supply a file path that default exports a function (Check out the [workers](#workers) section for more details).
* `maxConnectionRequests`: A `Number` stating the max requests to make per connection. `amount` takes precedence if both are set. _OPTIONAL_
* `maxOverallRequests`: A `Number` stating the max requests to make overall. Can't be less than `connections`. `maxConnectionRequests` takes precedence if both are set. _OPTIONAL_
* `connectionRate`: A `Number` stating the rate of requests to make per second from each individual connection. No rate limiting by default. _OPTIONAL_
* `overallRate`: A `Number` stating the rate of requests to make per second from all connections. `connectionRate` takes precedence if both are set. No rate limiting by default. _OPTIONAL_
* `ignoreCoordinatedOmission`: A `Boolean` which disables the correction of latencies to compensate for the coordinated omission issue. Does not make sense when no rate of requests has been specified (`connectionRate` or `overallRate`). _OPTIONAL_ default: `false`.
* `reconnectRate`: A `Number` that makes the individual connections disconnect and reconnect to the server whenever it has sent that number of requests. _OPTIONAL_
* `requests`: An `Array` of `Object`s which represents the sequence of requests to make while benchmarking. Can be used in conjunction with the `body`, `headers` and `method` params above. Check the samples folder for an example of how this might be used. _OPTIONAL_. Contained objects can have these attributes:
* `body`: When present, will override `opts.body`. _OPTIONAL_
* `headers`: When present, will override `opts.headers`. _OPTIONAL_
* `method`: When present, will override `opts.method`. _OPTIONAL_
* `path`: When present, will override `opts.path`. _OPTIONAL_
* `setupRequest`: A `Function` you may provide to mutate the raw `request` object, e.g. `request.method = 'GET'`. It takes `request` (Object) and `context` (Object) parameters, and must return the modified request. When it returns a falsey value, autocannon will restart from first request. When using `workers`, you need to supply a file path that default exports a function instead (Check out [workers](#workers) section for more details) _OPTIONAL_
* `onResponse`: A `Function` you may provide to process the received response. It takes `status` (Number), `body` (String) `context` (Object) parameters and `headers` (Key-Value Object). When using `workers`, you need to supply a file path that default exports a function instead (Check out [workers](#workers) section for more details) _OPTIONAL_
* `har`: an `Object` of parsed [HAR](https://w3c.github.io/web-performance/specs/HAR/Overview.html) content. Autocannon will extra and use `entries.request`: `requests`, `method`, `form` and `body` options will be ignored. _NOTE_: you must ensure that entries are targeting the same domain as `url` option. _OPTIONAL_
* `idReplacement`: A `Boolean` which enables the replacement of `[<id>]` tags within the request body with a randomly generated ID, allowing for unique fields to be sent with requests. Check out [an example of programmatic usage](./samples/using-id-replacement.js) that can be found in the samples. _OPTIONAL_ default: `false`
* `forever`: A `Boolean` which allows you to setup an instance of autocannon that restarts indefinitely after emitting results with the `done` event. Useful for efficiently restarting your instance. To stop running forever, you must cause a `SIGINT` or call the `.stop()` function on your instance. _OPTIONAL_ default: `false`
* `servername`: A `String` identifying the server name for the SNI (Server Name Indication) TLS extension. _OPTIONAL_ default: Defaults to the hostname of the URL when it is not an IP address.
* `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.
**Returns** an instance/event emitter for tracking progress, etc. If `cb` is omitted, the return value can also be used as a Promise.
### Customizing sent requests
When running, autocannon will create as many `Client` objects as desired connections. They will run in parallel until the benchmark is over (duration or total number of requests).
Each client will loop over the `requests` array, would it contain one or several requests.
While going through available requests, the client will maintain a `context`: an object you can use in `onResponse` and `setupRequest` functions, to store and read some contextual data.
Please check the `request-context.js` file in samples.
Note that `context` object will be reset to `initialContext` (or `{}` it is not provided) when restarting to the first available request, ensuring similar runs.
### Combining connections, overallRate and amount
When combining a fixed `amount` of requests with concurrent `connections` and an `overallRate` limit, autocannon will distribute the requests and the intended rate over all connections. If the `overallRate` is not integer divisible, autocannon will configure some connection clients with a higher and some with a lower number of requests/second rate. If now the `amount` *is* integer divisible, all connection clients get the same number of requests. This means that the clients with a higher request rate will finish earlier, than the others, leading to a drop in the perceived request rate.
Example: `connections = 10, overallRate = 17, amount = 5000`
### autocannon.track(instance[, opts])
Track the progress of your autocannon, programmatically.
* `instance`: The instance of autocannon. _REQUIRED_.
* `opts`: Configuration options for tracking. This can have the following attributes. _OPTIONAL_.
* `outputStream`: The stream to output to. default: `process.stderr`.
* `renderProgressBar`: A truthy value to enable the rendering of the progress bar. default: `true`.
* `renderResultsTable`: A truthy value to enable the rendering of the results table. default: `true`.
* `renderLatencyTable`: A truthy value to enable the rendering of the advanced latency table. default: `false`.
* `progressBarString`: A `string` defining the format of the progress display output. Must be valid input for the [progress bar module](http://npm.im/progress). default: `'running [:bar] :percent'`.
Example that just prints the table of results on completion:
```js
'use strict'
const autocannon = require('autocannon')
const instance = autocannon({
url: 'http://localhost:3000'
}, console.log)
// this is used to kill the instance on CTRL-C
process.once('SIGINT', () => {
instance.stop()
})
// just render results
autocannon.track(instance, {renderProgressBar: false})
```
Check out [this example](./samples/track-run.js) to see it in use, as well.
### autocannon.printResult(resultObject[, opts])
Returns a text string containing the result tables.
* `resultObject`: The result object of autocannon. _REQUIRED_.
* `opts`: Configuration options for generating the tables. These may include the following attributes. _OPTIONAL_.
* `outputStream`: The stream to which output is directed. It is primarily used to check if the terminal supports color. default: `process.stderr`.
* `renderResultsTable`: A truthy value to enable the creation of the results table. default: `true`.
* `renderLatencyTable`: A truthy value to enable the creation of the latency table. default: `false`.
Example:
```js
"use strict";
const { stdout } = require("node:process");
const autocannon = require("autocannon");
function print(result) {
stdout.write(autocannon.printResult(result));
}
autocannon({ url: "http://localhost:3000" }, (err, result) => print(result));
```
### 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. 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
Because an autocannon instance is an `EventEmitter`, it emits several events. these are below:
* `start`: Emitted once everything has been setup in your autocannon instance and it has started. Useful for if running the instance forever.
* `tick`: Emitted every second this autocannon is running a benchmark. Useful for displaying stats, etc. Used by the `track` function. The `tick` event propagates an object containing the `counter` and `bytes` values, which can be used for extended reports.
* `done`: Emitted when the autocannon finishes a benchmark. passes the `results` as an argument to the callback.
* `response`: Emitted when the autocannons http-client gets an HTTP response from the server. This passes the following arguments to the callback:
* `client`: The `http-client` itself. Can be used to modify the headers and body the client will send to the server. API below.
* `statusCode`: The HTTP status code of the response.
* `resBytes`: The response byte length.
* `responseTime`: The time taken to get a response after initiating the request.
* `reqError`: Emitted in the case of a request error e.g. a timeout.
* `error`: Emitted if there is an error during the setup phase of autocannon.
### Results
The results object emitted by `done` and passed to the `autocannon()` callback has these properties:
* `title`: Value of the `title` option passed to `autocannon()`.
* `url`: The URL that was targeted.
* `socketPath`: The UNIX Domain Socket or Windows Named Pipe that was targeted, or `undefined`.
* `requests`: A histogram object containing statistics about the number of requests that were sent per second.
* `latency`: A histogram object containing statistics about response latency.
* `throughput`: A histogram object containing statistics about the response data throughput per second.
* `duration`: The amount of time the test took, **in seconds**.
* `errors`: The number of connection errors (including timeouts) that occurred.
* `timeouts`: The number of connection timeouts that occurred.
* `mismatches`: The number of requests with a mismatched body.
* `start`: A Date object representing when the test started.
* `finish`: A Date object representing when the test ended.
* `connections`: The amount of connections used (value of `opts.connections`).
* `pipelining`: The number of pipelined requests used per connection (value of `opts.pipelining`).
* `non2xx`: The number of non-2xx response status codes received.
* `resets`: How many times the requests pipeline was reset due to `setupRequest` returning a falsey value.
* `statusCodeStats`: Requests counter per status code (e.g. `{ "200": { "count": "500" } }`)
The histogram objects for `requests`, `latency` and `throughput` are [hdr-histogram-percentiles-obj](https://github.com/thekemkid/hdr-histogram-percentiles-obj) objects and have this shape:
* `min`: The lowest value for this statistic.
* `max`: The highest value for this statistic.
* `average`: The average (mean) value.
* `stddev`: The standard deviation.
* `p*`: The XXth percentile value for this statistic. The percentile properties are: `p2_5`, `p50`, `p75`, `p90`, `p97_5`, `p99`, `p99_9`, `p99_99`, `p99_999`.
### `Client` API
This object is passed as the first parameter of both the `setupClient` function and the `response` event from an autocannon instance. You can use this to modify the requests you are sending while benchmarking. This is also an `EventEmitter`, with the events and their params listed below.
* `client.setHeaders(headers)`: Used to modify the headers of the request this client iterator is currently on. `headers` should be an `Object`, or `undefined` if you want to remove your headers.
* `client.setBody(body)`: Used to modify the body of the request this client iterator is currently on. `body` should be a `String` or `Buffer`, or `undefined` if you want to remove the body.
* `client.setHeadersAndBody(headers, body)`: Used to modify both the headers and body this client iterator is currently on. `headers` and `body` should take the same form as above.
* `client.setRequest(request)`: Used to modify the entire request that this client iterator is currently on. Can have `headers`, `body`, `method`, or `path` as attributes. Defaults to the values passed into the autocannon instance when it was created. `Note: call this when modifying multiple request values for faster encoding`
* `client.setRequests(newRequests)`: Used to overwrite the entire requests array that was passed into the instance on initiation. `Note: call this when modifying multiple requests for faster encoding`
### `Client` events
The events a `Client` can emit are listed here:
* `headers`: Emitted when a request sent from this client has received the headers of its reply. This received an `Object` as the parameter.
* `body`: Emitted when a request sent from this client has received the body of a reply. This receives a `Buffer` as the parameter.
* `response`: Emitted when the client has received a completed response for a request it made. This is passed the following arguments:
* `statusCode`: The HTTP status code of the response.
* `resBytes`: The response byte length.
* `responseTime`: The time taken to get a response after initiating the request.
* `reset`: Emitted when the requests pipeline was reset due to `setupRequest` returning a falsey value.
Example using the autocannon events and the client API and events:
```js
'use strict'
const autocannon = require('autocannon')
const instance = autocannon({
url: 'http://localhost:3000',
setupClient: setupClient
}, (err, result) => handleResults(result))
// results passed to the callback are the same as those emitted from the done events
instance.on('done', handleResults)
instance.on('tick', () => console.log('ticking'))
instance.on('response', handleResponse)
function setupClient (client) {
client.on('body', console.log) // console.log a response body when its received
}
function handleResponse (client, statusCode, resBytes, responseTime) {
console.log(`Got response with code ${statusCode} in ${responseTime} milliseconds`)
console.log(`response: ${resBytes.toString()}`)
//update the body or headers
client.setHeaders({new: 'header'})
client.setBody('new body')
client.setHeadersAndBody({new: 'header'}, 'new body')
}
function handleResults(result) {
// ...
}
```
<a name="limitations"></a>
## Limitations
Autocannon is written in JavaScript for the Node.js runtime and it is CPU-bound.
We have verified that it yields comparable results with `wrk` when benchmarking Node.js
applications using the `http` module.
Nevertheless, it uses significantly more CPU than other tools that compiles to a binary such as `wrk`.
Autocannon can saturate the CPU, e.g. the autocannon process reaches 100%: in those cases,
we recommend using `wrk2`.
As an example, let's consider a run with 1000 connections on a server
with 4 cores with hyperthreading:
* `wrk` uses 2 threads (by default) and an auxiliary one to collect the
metrics with a total load of the CPU of 20% + 20% + 40%.
* `autocannon` uses a single thread at 80% CPU load.
Both saturates a Node.js process at around 41k req/sec, however,
`autocannon` can saturate sooner because it is single-threaded.
Note that `wrk` does not support HTTP/1.1 pipelining. As a result, `autocannon` can create
more load on the server than wrk for each open connection.
<a name="acknowledgements"></a>
## Acknowledgements
This project was kindly sponsored by [nearForm](http://nearform.com).
Logo and identity designed by Cosmic Fox Design: https://www.behance.net/cosmicfox.
[wrk][wrk] and [wrk2][wrk2] provided great inspiration.
### Chat on Gitter
If you are using autocannon or you have any questions, let us know: [Gitter](https://gitter.im/mcollina/autocannon)
### Contributors
- [Glen Keane](mailto:glenkeane.94@gmail.com) | [Github](https://github.com/GlenTiki)
- [Salman Mitha](mailto:salmanmitha@gmail.com) | [Github](https://github.com/salmanm) | [NPM](https://www.npmjs.com/~salmanm)
## License
Copyright [Matteo Collina](https://github.com/mcollina) and other contributors, Licensed under [MIT](./LICENSE).
[node-gyp]: https://github.com/nodejs/node-gyp#installation
[wrk]: https://github.com/wg/wrk
[wrk2]: https://github.com/giltene/wrk2
================================================
FILE: autocannon.js
================================================
#! /usr/bin/env node
'use strict'
const crossArgv = require('cross-argv')
const fs = require('fs')
const os = require('os')
const net = require('net')
const path = require('path')
const URL = require('url').URL
const spawn = require('child_process').spawn
const managePath = require('manage-path')
const hasAsyncHooks = require('has-async-hooks')
const subarg = require('@minimistjs/subarg')
const printResult = require('./lib/printResult')
const initJob = require('./lib/init')
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 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+.')
process.exit(1)
}
module.exports = initJob
module.exports.track = track
module.exports.start = start
module.exports.printResult = printResult
module.exports.parseArguments = parseArguments
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',
timeout: 't',
duration: 'd',
sampleInt: 'L',
amount: 'a',
json: 'j',
renderLatencyTable: ['l', 'latency'],
onPort: 'on-port',
method: 'm',
headers: ['H', 'header'],
body: 'b',
form: 'F',
servername: 's',
bailout: 'B',
input: 'i',
maxConnectionRequests: 'M',
maxOverallRequests: 'O',
connectionRate: 'r',
overallRate: 'R',
ignoreCoordinatedOmission: 'C',
reconnectRate: 'D',
renderProgressBar: 'progress',
renderStatusCodes: 'statusCodes',
title: 'T',
verbose: 'V',
version: 'v',
forever: 'f',
idReplacement: 'I',
socketPath: 'S',
excludeErrorStats: 'x',
expectBody: 'E',
workers: 'w',
warmup: 'W',
help: 'h'
}
const defaults = {
connections: 10,
timeout: 10,
pipelining: 1,
duration: 10,
sampleInt: 1000,
reconnectRate: 0,
renderLatencyTable: false,
renderProgressBar: true,
renderStatusCodes: false,
json: false,
forever: false,
method: 'GET',
idReplacement: false,
excludeErrorStats: false,
debug: false,
workers: 0,
verbose: true
}
function parseArguments (argvs) {
let argv = subarg(argvs, {
boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'renderStatusCodes', 'forever', 'idReplacement', 'excludeErrorStats', 'onPort', 'debug', 'ignoreCoordinatedOmission', 'verbose'],
alias,
default: defaults,
'--': true
})
// subarg does not convert aliases in sub arguments
argv = generateSubArgAliases(argv)
argv.url = argv._.length > 1 ? argv._ : argv._[0]
// Assume onPort if `-- node` is provided
if (argv['--'][0] === 'node') {
argv.onPort = true
}
if (argv.onPort) {
argv.spawn = argv['--']
}
// support -n to disable the progress bar and results table
if (argv.n) {
argv.renderProgressBar = false
argv.renderResultsTable = false
argv.renderStatusCodes = false
}
if (argv.version) {
console.log('autocannon', 'v' + require('./package').version)
console.log('node', process.version)
return
}
if (!checkURL(argv.url) || argv.help) {
const help = fs.readFileSync(path.join(__dirname, 'help.txt'), 'utf8')
console.error(help)
return
}
// if PORT is set (like by `0x`), target `localhost:PORT/path` by default.
// this allows doing:
// 0x --on-port 'autocannon /path' -- node server.js
if (process.env.PORT) {
argv.url = ofURL(argv.url).map(url => new URL(url, `http://localhost:${process.env.PORT}`).href)
}
// Add http:// if it's not there and this is not a /path
argv.url = ofURL(argv.url).map(url => {
if (url.indexOf('http') !== 0 && url[0] !== '/') {
url = `http://${url}`
}
return url
})
// check that the URL is valid.
ofURL(argv.url).map(url => {
try {
// If --on-port is given, it's acceptable to not have a hostname
if (argv.onPort) {
new URL(url, 'http://localhost') // eslint-disable-line no-new
} else {
new URL(url) // eslint-disable-line no-new
}
} catch (err) {
console.error(err.message)
console.error('')
console.error('When targeting a path without a hostname, the PORT environment variable must be available.')
console.error('Use a full URL or set the PORT variable.')
process.exit(1)
}
return null // to make linter happy
})
if (argv.input) {
argv.body = fs.readFileSync(argv.input, 'utf8')
}
if (argv.headers) {
if (!Array.isArray(argv.headers)) {
argv.headers = [argv.headers]
}
argv.headers = argv.headers.reduce((obj, header) => {
const colonIndex = header.indexOf(':')
const equalIndex = header.indexOf('=')
const index = Math.min(colonIndex < 0 ? Infinity : colonIndex, equalIndex < 0 ? Infinity : equalIndex)
if (Number.isFinite(index) && index > 0) {
obj[header.slice(0, index)] = header.slice(index + 1)
return obj
} else throw new Error(`An HTTP header was not correctly formatted: ${header}`)
}, {})
}
if (argv.har) {
try {
argv.har = JSON.parse(fs.readFileSync(argv.har))
// warn users about skipped HAR requests
const requestsByOrigin = parseHAR(argv.har)
const allowed = ofURL(argv.url, true).map(url => new URL(url).origin)
for (const [origin] of requestsByOrigin) {
if (!allowed.includes(origin)) {
console.error(`Warning: skipping requests to '${origin}' as the target is ${allowed.join(', ')}`)
}
}
} catch (err) {
throw new Error(`Failed to load HAR file content: ${err.message}`)
}
}
argv.tlsOptions = {}
if (argv.cert) {
try {
argv.tlsOptions.cert = fs.readFileSync(argv.cert)
} catch (err) {
throw new Error(`Failed to load cert file: ${err.message}`)
}
}
if (argv.key) {
try {
argv.tlsOptions.key = fs.readFileSync(argv.key)
} catch (err) {
throw new Error(`Failed to load key file: ${err.message}`)
}
}
if (argv.ca) {
if (typeof argv.ca === 'string') {
argv.ca = [argv.ca]
} else if (Array.isArray(argv.ca._)) {
argv.ca = argv.ca._
}
try {
argv.tlsOptions.ca = argv.ca.map(caPath => fs.readFileSync(caPath))
} catch (err) {
throw new Error(`Failed to load ca file: ${err.message}`)
}
}
// This is to distinguish down the line whether it is
// run via command-line or programmatically
argv[Symbol.for('internal')] = true
return argv
}
function start (argv) {
if (!argv) {
// we are printing the help
return
}
if (argv.onPort) {
if (!hasAsyncHooks()) {
console.error('The --on-port flag requires the async_hooks builtin module, but it is not available. Please upgrade to Node 8.1+.')
process.exit(1)
}
const { socketPath, server } = createChannel((port) => {
const url = new URL(argv.url, `http://localhost:${port}`).href
const opts = Object.assign({}, argv, {
onPort: false,
url
})
const tracker = initJob(opts, () => {
proc.kill('SIGINT')
server.close()
})
process.once('SIGINT', () => {
tracker.stop()
})
})
// manage-path always uses the $PATH variable, but we can pretend
// that it is equal to $NODE_PATH
const alterPath = managePath({ PATH: process.env.NODE_PATH })
alterPath.unshift(path.join(__dirname, 'lib/preload'))
const proc = spawn(argv.spawn[0], argv.spawn.slice(1), {
stdio: ['ignore', 'inherit', 'inherit'],
env: Object.assign({}, process.env, {
NODE_OPTIONS: ['-r', 'autocannonDetectPort'].join(' ') +
(process.env.NODE_OPTIONS ? ` ${process.env.NODE_OPTIONS}` : ''),
NODE_PATH: alterPath.get(),
AUTOCANNON_SOCKET: socketPath
})
})
} else {
// if forever is true then a promise is not returned and we need to try ... catch errors
try {
const tracker = initJob(argv)
if (tracker.then) {
tracker.catch((err) => {
console.error(err.message)
})
}
} catch (err) {
console.error(err.message)
}
}
}
function createChannel (onport) {
const pipeName = `${process.pid}.autocannon`
const socketPath = process.platform === 'win32'
? `\\\\?\\pipe\\${pipeName}`
: path.join(os.tmpdir(), pipeName)
const server = net.createServer((socket) => {
socket.once('data', (chunk) => {
const port = chunk.toString()
onport(port)
})
})
server.listen(socketPath)
server.on('close', () => {
try {
fs.unlinkSync(socketPath)
} catch (err) {}
})
return { socketPath, server }
}
if (require.main === module) {
const argv = crossArgv(process.argv.slice(2))
start(parseArguments(argv))
}
================================================
FILE: cluster.js
================================================
'use strict'
const cluster = require('cluster')
const http = require('http')
const numCPUs = Math.floor(require('os').cpus().length / 2) || 1
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`)
for (let i = 0; i < numCPUs; i++) {
cluster.fork()
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`)
})
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200)
res.end('hello world\n')
}).listen(3000)
console.log(`Worker ${process.pid} started`)
}
================================================
FILE: for-zero-x.js
================================================
'use strict'
const autocannon = require('.')
autocannon({
url: 'http://localhost:3000',
connections: 10,
duration: 10
}, console.log)
================================================
FILE: help.txt
================================================
Usage: autocannon [opts] URL
URL is any valid HTTP or HTTPS URL.
If the PORT environment variable is set, the URL can be a path. In that case 'http://localhost:$PORT/path' will be used as the URL.
Available options:
-c/--connections NUM
The number of concurrent connections to use. default: 10.
-p/--pipelining NUM
The number of pipelined requests to use. default: 1.
-d/--duration SEC
The number of seconds to run the autocannon. default: 10.
-a/--amount NUM
The number of requests to make before exiting the benchmark. If set, duration is ignored.
-L NUM
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.
-S/--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.
-w/--workers
Number of worker threads to use to fire requests.
-W/--warmup
Use a warm up interval before starting sampling.
This enables startup processes to finish and traffic to normalize before sampling begins
use -c and -d sub args e.g. `--warmup [ -c 1 -d 3 ]`
--on-port
Start the command listed after -- on the command line. When it starts listening on a port,
start sending requests to that port. A URL is still required to send requests to
the correct path. The hostname can be omitted, `localhost` will be used by default.
-m/--method METHOD
The HTTP method to use. default: 'GET'.
-t/--timeout NUM
The number of seconds before timing out and resetting a connection. default: 10
-T/--title TITLE
The title to place in the results for identification.
-b/--body BODY
The body of the request.
NOTE: This option needs to be used with the '-H/--headers' option in some frameworks
-F/--form FORM
Upload a form (multipart/form-data). The form options can be a JSON string like
'{ "field 1": { "type": "text", "value": "a text value"}, "field 2": { "type": "file", "path": "path to the file" } }'
or a path to a JSON file containing the form options.
When uploading a file the default filename value can be overridden by using the corresponding option:
'{ "field name": { "type": "file", "path": "path to the file", "options": { "filename": "myfilename" } } }'
Passing the filepath to the form can be done by using the corresponding option:
'{ "field name": { "type": "file", "path": "path to the file", "options": { "filepath": "/some/path/myfilename" } } }'
-i/--input FILE
The body of the request. See '-b/body' for more details.
-H/--headers K=V
The request headers.
--har FILE
When provided, Autocannon will use requests from the HAR file.
CAUTION: you have to specify one or more domains using URL option: only the HAR requests to the same domains will be considered.
NOTE: you can still add extra headers with -H/--headers but -m/--method, -F/--form, -i/--input -b/--body will be ignored.
-B/--bailout NUM
The number of failures before initiating a bailout.
-M/--maxConnectionRequests NUM
The max number of requests to make per connection to the server.
-O/--maxOverallRequests NUM
The max number of requests to make overall to the server.
-r/--connectionRate NUM
The max number of requests to make per second from an individual connection.
-R/--overallRate NUM
The max number of requests to make per second from all connections.
connection rate will take precedence if both are set.
NOTE: if using rate limiting and a very large rate is entered which cannot be met,
Autocannon will do as many requests as possible per second. Also, latency data will be corrected to compensate for the effects of the coordinated omission issue. If you are not familiar with the coordinated omission issue, you should probably read [this article](http://highscalability.com/blog/2015/10/5/your-load-generator-is-probably-lying-to-you-take-the-red-pi.html) or watch this [Gil Tene's talk](https://www.youtube.com/watch?v=lJ8ydIuPFeU) on the topic.
-C/--ignoreCoordinatedOmission
Ignore the coordinated omission issue when requests should be sent at a fixed rate using 'connectionRate' or 'overallRate'.
NOTE: it is not recommended to enable this option.
When the request rate cannot be met because the server is too slow, many request latencies might be missing and Autocannon might report a misleading latency distribution.
-D/--reconnectRate NUM
The number of requests to make before resetting a connections connection to the
server.
-n/--no-progress
Don't render the progress bar. default: false.
-l/--latency
Print all the latency data. default: false.
-I/--idReplacement
Enable replacement of [<id>] with a randomly generated ID within the request body. default: false.
-j/--json
Print the output as newline delimited JSON. This will cause the progress bar and results not to be rendered. default: false.
-f/--forever
Run the benchmark forever. Efficiently restarts the benchmark on completion. default: false.
-s/--servername
Server name for the SNI (Server Name Indication) TLS extension. Defaults to the hostname of the URL when it is not an IP address.
-x/--excludeErrorStats
Exclude error statistics (non-2xx HTTP responses) from the final latency and bytes per second averages. default: false.
-E/--expectBody EXPECTED
Ensure the body matches this value. If enabled, mismatches count towards bailout.
Enabling this option will slow down the load testing.
--renderStatusCodes
Print status codes and their respective statistics.
--cert
Path to cert chain in pem format
--key
Path to private key for specified cert in pem format
--ca
Path to trusted ca certificates for the test. This argument accepts both a single file as well as a list of files
--debug
Print connection errors to stderr.
-v/--version
Print the version number.
-h/--help
Print this menu.
================================================
FILE: lib/aggregateResult.js
================================================
'use strict'
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,
latencies: decodeHist(r.latencies),
requests: decodeHist(r.requests),
throughput: decodeHist(r.throughput)
})).reduce((acc, r) => {
acc.latencies.add(r.latencies)
acc.totalCompletedRequests += r.totalCompletedRequests
acc.totalRequests += r.totalRequests
acc.totalBytes += r.totalBytes
acc.samples += r.samples
acc.errors += r.errors
acc.timeouts += r.timeouts
acc.mismatches += r.mismatches
acc.non2xx += r.non2xx
acc.resets += r.resets
acc['1xx'] += r['1xx']
acc['2xx'] += r['2xx']
acc['3xx'] += r['3xx']
acc['4xx'] += r['4xx']
acc['5xx'] += r['5xx']
Object.keys(r.statusCodeStats).forEach(statusCode => {
if (!acc.statusCodeStats[statusCode]) {
acc.statusCodeStats[statusCode] = r.statusCodeStats[statusCode]
} else {
acc.statusCodeStats[statusCode].count += r.statusCodeStats[statusCode].count
}
})
return acc
})
const result = {
title: opts.title,
url: opts.url,
socketPath: opts.socketPath,
connections: opts.connections,
sampleInt: opts.sampleInt,
pipelining: opts.pipelining,
workers: opts.workers,
duration: aggregated.duration,
samples: aggregated.samples,
start: aggregated.start,
finish: aggregated.finish,
errors: aggregated.errors,
timeouts: aggregated.timeouts,
mismatches: aggregated.mismatches,
non2xx: aggregated.non2xx,
resets: aggregated.resets,
'1xx': aggregated['1xx'],
'2xx': aggregated['2xx'],
'3xx': aggregated['3xx'],
'4xx': aggregated['4xx'],
'5xx': aggregated['5xx'],
statusCodeStats: aggregated.statusCodeStats,
latency: addPercentiles(aggregated.latencies, histAsObj(aggregated.latencies)),
requests: addPercentiles(histograms.requests, histAsObj(histograms.requests, aggregated.totalCompletedRequests)),
throughput: addPercentiles(histograms.throughput, histAsObj(histograms.throughput, aggregated.totalBytes))
}
result.latency.totalCount = aggregated.latencies.totalCount
result.requests.sent = aggregated.totalRequests
if (result.requests.min >= Number.MAX_SAFE_INTEGER) result.requests.min = 0
if (result.throughput.min >= Number.MAX_SAFE_INTEGER) result.throughput.min = 0
if (result.latency.min >= Number.MAX_SAFE_INTEGER) result.latency.min = 0
return result
}
module.exports = aggregateResult
================================================
FILE: lib/defaultOptions.js
================================================
'use strict'
const defaultOptions = {
headers: {},
method: 'GET',
duration: 10,
connections: 10,
sampleInt: 1000,
pipelining: 1,
timeout: 10,
maxConnectionRequests: 0,
maxOverallRequests: 0,
connectionRate: 0,
overallRate: 0,
amount: 0,
reconnectRate: 0,
forever: false,
idReplacement: false,
requests: [{}],
servername: undefined,
excludeErrorStats: false
}
module.exports = defaultOptions
================================================
FILE: lib/format.js
================================================
'use strict'
function format (num) {
if (num < 1000) {
return '' + num
} else {
return '' + Math.round(num / 1000) + 'k'
}
}
module.exports = format
================================================
FILE: lib/histUtil.js
================================================
const hdr = require('hdr-histogram-js')
const histUtil = require('hdr-histogram-percentiles-obj')
hdr.initWebAssemblySync()
const getHistograms = ({
latencies = hdr.build({
useWebAssembly: true,
bitBucketSize: 64,
autoResize: true,
lowestDiscernibleValue: 1,
highestTrackableValue: 10000,
numberOfSignificantValueDigits: 5
}),
requests = hdr.build({
useWebAssembly: true,
bitBucketSize: 64,
autoResize: true,
lowestDiscernibleValue: 1,
highestTrackableValue: 1000000,
numberOfSignificantValueDigits: 3
}),
throughput = hdr.build({
useWebAssembly: true,
bitBucketSize: 64,
autoResize: true,
lowestDiscernibleValue: 1,
highestTrackableValue: 100000000000,
numberOfSignificantValueDigits: 3
})
} = {}) => ({
latencies,
requests,
throughput
})
function encodeHist (h) {
if (h.__custom) return null
return hdr.encodeIntoCompressedBase64(h)
}
function decodeHist (str) {
if (!str) return null
return hdr.decodeFromCompressedBase64(str)
}
exports.getHistograms = getHistograms
exports.encodeHist = encodeHist
exports.decodeHist = decodeHist
exports.histAsObj = histUtil.histAsObj
exports.addPercentiles = histUtil.addPercentiles
exports.percentiles = histUtil.percentiles
================================================
FILE: lib/httpClient.js
================================================
'use strict'
const inherits = require('util').inherits
const EE = require('events').EventEmitter
const net = require('net')
const tls = require('tls')
const retimer = require('retimer')
const HTTPParser = require('http-parser-js').HTTPParser
const RequestIterator = require('./requestIterator')
const noop = require('./noop')
const clone = require('lodash.clonedeep')
const PipelinedRequestsQueue = require('./pipelinedRequestsQueue')
function Client (opts) {
if (!(this instanceof Client)) {
return new Client(opts)
}
this.opts = clone(opts)
this.opts.setupClient = this.opts.setupClient || noop
this.opts.pipelining = this.opts.pipelining || 1
this.opts.port = this.opts.port || 80
this.opts.expectBody = this.opts.expectBody || null
this.opts.tlsOptions = this.opts.tlsOptions || {}
this.timeout = (this.opts.timeout || 10) * 1000
this.ipc = !!this.opts.socketPath
this.secure = this.opts.protocol === 'https:'
this.auth = this.opts.auth || null
if (this.secure && this.opts.port === 80) {
this.opts.port = 443
}
this.parser = new HTTPParser(HTTPParser.RESPONSE)
this.requestIterator = new RequestIterator(this.opts)
this.reqsMade = 0
// used for request limiting
this.responseMax = this.opts.responseMax
// used for rate limiting
this.reqsMadeThisSecond = 0
this.rate = this.opts.rate
// used for forcing reconnects
this.reconnectRate = this.opts.reconnectRate
this.pipelinedRequests = new PipelinedRequestsQueue()
this.destroyed = false
this.opts.setupClient(this)
const handleTimeout = () => {
this._destroyConnection()
this.timeoutTicker.reschedule(this.timeout)
this._connect()
for (let i = 0; i < this.opts.pipelining; i++) {
this.emit('timeout')
}
}
if (this.rate) {
this.rateInterval = setInterval(() => {
this.reqsMadeThisSecond = 0
if (this.paused) this._doRequest(this.cer)
this.paused = false
}, 1000)
}
this.timeoutTicker = retimer(handleTimeout, this.timeout)
this.parser[HTTPParser.kOnHeaders] = () => {}
this.parser[HTTPParser.kOnHeadersComplete] = (opts) => {
this.emit('headers', opts)
this.pipelinedRequests.setHeaders(opts)
}
this.parser[HTTPParser.kOnBody] = (body, start, len) => {
this.pipelinedRequests.addBody(body.slice(start, start + len))
this.emit('body', body)
}
this.parser[HTTPParser.kOnMessageComplete] = () => {
const resp = this.pipelinedRequests.terminateRequest()
if (!this.destroyed && this.reconnectRate && this.reqsMade % this.reconnectRate === 0) {
return this._resetConnection()
}
if (!this.destroyed) {
this.requestIterator.recordBody(resp.req, resp.headers.statusCode, resp.body, resp.headers.headers)
this.emit('response', resp.headers.statusCode, resp.bytes, resp.duration, this.rate)
this._doRequest()
const isFn = typeof this.opts.verifyBody === 'function'
if (isFn && !this.opts.verifyBody(resp.body)) {
return this.emit('mismatch', resp.body)
} else if (!isFn && this.opts.expectBody && this.opts.expectBody !== resp.body) {
return this.emit('mismatch', resp.body)
}
}
}
this._connect()
}
inherits(Client, EE)
Client.prototype._connect = function () {
if (this.secure) {
let servername
if (this.opts.servername) {
servername = this.opts.servername
} else if (!net.isIP(this.opts.hostname)) {
servername = this.opts.hostname
}
if (this.ipc) {
this.conn = tls.connect(this.opts.socketPath, { ...this.opts.tlsOptions, rejectUnauthorized: false })
} else {
this.conn = tls.connect(this.opts.port, this.opts.hostname, { ...this.opts.tlsOptions, rejectUnauthorized: false, servername })
}
} else {
if (this.ipc) {
this.conn = net.connect(this.opts.socketPath)
} else {
this.conn = net.connect(this.opts.port, this.opts.hostname)
}
}
this.conn.on('error', (error) => {
this.emit('connError', error)
if (!this.destroyed) this._connect()
})
this.conn.on('data', (chunk) => {
this.pipelinedRequests.addByteCount(chunk.length)
this.parser.execute(chunk)
})
this.conn.on('end', () => {
if (!this.destroyed) this._connect()
})
for (let i = 0; i < this.opts.pipelining; i++) {
this._doRequest()
}
}
Client.prototype._doRequest = function () {
if (!this.rate || (this.rate && this.reqsMadeThisSecond++ < this.rate)) {
if (!this.destroyed && this.responseMax && this.reqsMade >= this.responseMax) {
return this.destroy()
}
this.emit('request')
if (this.reqsMade > 0) {
this.requestIterator.nextRequest()
if (this.requestIterator.resetted) {
this.emit('reset')
}
}
this.pipelinedRequests.insertRequest(this.requestIterator.currentRequest)
this.conn.write(this.getRequestBuffer())
this.timeoutTicker.reschedule(this.timeout)
this.reqsMade++
} else {
this.paused = true
}
}
Client.prototype._resetConnection = function () {
this._destroyConnection()
this._connect()
}
Client.prototype._destroyConnection = function () {
this.conn.removeAllListeners('error')
this.conn.removeAllListeners('end')
this.conn.on('error', () => {})
this.conn.destroy()
this.pipelinedRequests.clear()
}
Client.prototype.destroy = function () {
if (!this.destroyed) {
this.destroyed = true
this.timeoutTicker.clear()
if (this.rate) clearInterval(this.rateInterval)
this.emit('done')
this._destroyConnection()
}
}
Client.prototype.getRequestBuffer = function () {
return this.requestIterator.currentRequest.requestBuffer
}
Client.prototype.setHeaders = function (newHeaders) {
this._okayToUpdateCheck()
this.requestIterator.setHeaders(newHeaders)
}
Client.prototype.setBody = function (newBody) {
this._okayToUpdateCheck()
this.requestIterator.setBody(newBody)
}
Client.prototype.setHeadersAndBody = function (newHeaders, newBody) {
this._okayToUpdateCheck()
this.requestIterator.setHeadersAndBody(newHeaders, newBody)
}
Client.prototype.setRequest = function (newRequest) {
this._okayToUpdateCheck()
this.requestIterator.setRequest(newRequest)
}
Client.prototype.setRequests = function (newRequests) {
this._okayToUpdateCheck()
this.requestIterator.setRequests(newRequests)
}
Client.prototype._okayToUpdateCheck = function () {
if (this.opts.pipelining > 1) {
throw new Error('cannot update requests when the piplining factor is greater than 1')
}
}
module.exports = Client
================================================
FILE: lib/httpMethods.js
================================================
// most http methods taken from RFC 7231
// PATCH is defined in RFC 5789
module.exports = [
'GET',
'HEAD',
'POST',
'PUT',
'DELETE',
'CONNECT',
'OPTIONS',
'TRACE',
'PATCH'
]
================================================
FILE: lib/httpRequestBuilder.js
================================================
'use strict'
const methods = require('./httpMethods')
const { getPropertyCaseInsensitive } = require('./util')
// this is a build request factory, that curries the build request function
// and sets the default for it
function requestBuilder (defaults) {
// these need to be defined per request builder creation, because of the way
// headers don't get deep copied
const builderDefaults = {
method: 'GET',
path: '/',
headers: {},
body: Buffer.alloc(0),
hostname: 'localhost',
setupRequest: reqData => reqData,
port: 80
}
defaults = Object.assign(builderDefaults, defaults)
// buildRequest takes an object, and turns it into a buffer representing the
// http request.
// second parameter is passed to setupRequest, when relevant
// will return null if setupRequest returns falsey result
return function buildRequest (reqData, context) {
// below is a hack to enable deep extending of the headers so the default
// headers object isn't overwritten by accident
reqData = reqData || {}
reqData.headers = Object.assign({}, defaults.headers, reqData.headers)
reqData = Object.assign({}, defaults, reqData)
reqData = reqData.setupRequest(reqData, context)
if (!reqData) {
return null
}
// for some reason some tests fail with method === undefined
// the reqData.method should be set to SOMETHING in this case
// cannot find reason for failure if `|| 'GET'` is taken out
const method = reqData.method
const path = reqData.path
const headers = reqData.headers
const body = reqData.body
const headersDefinedHost = getPropertyCaseInsensitive(reqData.headers, 'host')
let host = headersDefinedHost || reqData.host
if (!host) {
const hostname = reqData.hostname
const port = reqData.port
host = hostname + ':' + port
}
const baseReq = [
`${method} ${path} HTTP/1.1`
]
if (!headersDefinedHost) {
baseReq.push(`Host: ${host}`)
}
baseReq.push('Connection: keep-alive')
if (reqData.auth) {
const encodedAuth = Buffer.from(reqData.auth).toString('base64')
headers.Authorization = `Basic ${encodedAuth}`
}
if (methods.indexOf(method) < 0) {
throw new Error(`${method} HTTP method is not supported`)
}
let bodyBuf
if (typeof body === 'string') {
bodyBuf = Buffer.from(body)
} else if (typeof body === 'number') {
bodyBuf = Buffer.from(body + '')
} else if (Buffer.isBuffer(body)) {
bodyBuf = body
} else if (body && Array.isArray(body._)) {
// when the request body passed on the CLI includes brackets like for
// a JSON array, the subarg parser will oddly provide the contents as
// `body._`. Work around this specific issue.
bodyBuf = Buffer.from(`[${body._}]`)
} else if (body) {
throw new Error('body must be either a string or a buffer')
}
if (bodyBuf && bodyBuf.length > 0) {
const idCount = reqData.idReplacement
? (bodyBuf.toString().match(/\[<id>\]/g) || []).length
: 0
headers['Content-Length'] = `${bodyBuf.length + (idCount * 27)}`
}
for (const [key, header] of Object.entries(headers)) {
baseReq.push(`${key}: ${header}`)
}
let req = Buffer.from(baseReq.join('\r\n') + '\r\n\r\n', 'utf8')
if (bodyBuf && bodyBuf.length > 0) {
req = Buffer.concat([req, bodyBuf])
}
return req
}
}
module.exports = requestBuilder
================================================
FILE: lib/init.js
================================================
'use strict'
const EE = require('events').EventEmitter
const { isMainThread } = require('./worker_threads')
const initWorkers = require('./manager')
const validateOpts = require('./validate')
const noop = require('./noop')
const runTracker = require('./runTracker')
const track = require('./progressTracker')
function init (opts, cb) {
const cbPassedIn = (typeof cb === 'function')
if (!cbPassedIn && !opts.forever) {
if (opts.warmup) {
return runWithWarmup(opts)
} else {
return run(opts)
}
} else {
return _init(opts, null, cb)
}
}
function run (opts) {
const tracker = new EE()
const promise = new Promise((resolve, reject) => {
_init(opts, tracker, (err, results) => {
if (err) return reject(err)
resolve(results)
})
})
tracker.then = promise.then.bind(promise)
tracker.catch = promise.catch.bind(promise)
return tracker
}
function runWithWarmup (opts) {
const warmupOpts = {
...opts,
...opts.warmup,
warmupRunning: true,
renderResultsTable: false
}
const mainTracker = new EE()
const warmUpTracker = new EE()
const promise = new Promise((resolve, reject) => {
_init(warmupOpts, warmUpTracker, (err, warmupResults) => {
if (err) return reject(err)
_init(opts, mainTracker, (err, results) => {
if (err) return reject(err)
results.warmup = warmupResults
resolve(results)
})
})
})
mainTracker.then = promise.then.bind(promise)
mainTracker.catch = promise.catch.bind(promise)
return mainTracker
}
function _init (opts, tracker, cb) {
const cbPassedIn = (typeof cb === 'function')
cb = cb || noop
tracker = tracker || new EE()
opts = validateOpts(opts, cbPassedIn)
function _cb (err, result) {
if (err) {
return cbPassedIn ? cb(err) : setImmediate(() => tracker.emit('error', err))
}
tracker.emit('done', result)
cb(null, result)
if (!err && opts.json) {
console.log(JSON.stringify(result))
}
}
if (opts instanceof Error) {
_cb(opts)
return tracker
}
tracker.opts = opts
if (opts.workers && isMainThread) {
initWorkers(opts, tracker, _cb)
} else {
runTracker(opts, tracker, _cb)
}
// if not running via command-line and
// not rendering json, or if std isn't a tty, track progress
if (opts[Symbol.for('internal')] && (!opts.json || !process.stdout.isTTY)) track(tracker, opts)
return tracker
}
module.exports = init
================================================
FILE: lib/manager.js
================================================
'use strict'
const path = require('path')
const EE = require('events').EventEmitter
const aggregateResult = require('./aggregateResult')
const { getHistograms } = require('./histUtil')
const { Worker } = require('./worker_threads')
function initWorkers (opts, tracker, cb) {
tracker = tracker || new EE()
const workers = []
const results = []
const numWorkers = +opts.workers
const histograms = getHistograms()
const histData = {
requests: [],
throughput: []
}
let restart = true
function reset () {
workers.length = 0
results.length = 0
histograms.requests.reset()
histograms.throughput.reset()
histData.requests.length = 0
histData.throughput.length = 0
}
function startAll () {
for (const w of workers) {
w.postMessage({ cmd: 'START' })
}
setImmediate(() => { tracker.emit('start') })
}
function handleFinish () {
const result = aggregateResult(results, opts, histograms)
cb(null, result)
reset()
if (opts.forever && restart) {
setImmediate(() => {
init()
startAll()
})
}
}
function stopAll () {
for (const w of workers) {
w.postMessage({ cmd: 'STOP' })
}
reset()
}
const workerOpts = {
...opts,
...(opts.amount ? { amount: Math.max(Math.floor(opts.amount / numWorkers), 1) } : undefined),
...(opts.connections ? { connections: Math.max(Math.floor(opts.connections / numWorkers), 1) } : undefined)
}
workerOpts.a = workerOpts.amount
workerOpts.c = workerOpts.connections
function init () {
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker(path.resolve(__dirname, './worker.js'), { workerData: { opts: workerOpts } })
worker.on('message', (msg) => {
const { cmd, data, error } = msg
if (cmd === 'RESULT') {
results.push(data)
if (results.length === workers.length) {
handleFinish()
}
} else if (cmd === 'UPDATE_HIST') {
const { name, value } = data
histData[name].push(value)
if (histData[name].length === workers.length) {
const total = histData[name].reduce((acc, v) => acc + v, 0)
histData[name].length = 0
histograms[name].recordValue(total)
}
} else if (cmd === 'RESET_HIST') {
const { name } = data
histograms[name].reset()
} else if (cmd === 'ERROR') {
tracker.emit('error', error)
}
})
worker.on('error', (err) => {
console.log('Worker error:', err)
stopAll()
cb(err)
})
workers.push(worker)
}
}
init()
startAll()
tracker.stop = () => {
restart = false
stopAll()
}
return tracker
}
module.exports = initWorkers
================================================
FILE: lib/multipart.js
================================================
'use strict'
const { resolve, basename } = require('path')
const { readFileSync } = require('fs')
const FormData = require('form-data')
function getFormData (string) {
try {
return JSON.parse(string)
} catch (error) {
try {
const path = resolve(string)
const data = readFileSync(path, 'utf8')
return JSON.parse(data)
} catch (error) {
throw new Error('Invalid JSON or file where to get form data')
}
}
}
module.exports = (options) => {
const obj = typeof options === 'string' ? getFormData(options) : options
const form = new FormData()
for (const key in obj) {
const type = obj[key] && obj[key].type
switch (type) {
case 'file': {
const path = obj[key] && obj[key].path
if (!path) {
throw new Error(`Missing key 'path' in form object for key '${key}'`)
}
const opts = obj[key] && obj[key].options
const buffer = readFileSync(path)
form.append(key, buffer, Object.assign({}, {
filename: basename(path)
}, opts))
break
}
case 'text': {
const value = obj[key] && obj[key].value
if (!value) {
throw new Error(`Missing key 'value' in form object for key '${key}'`)
}
form.append(key, value)
break
}
default:
throw new Error('A \'type\' key with value \'text\' or \'file\' should be specified')
}
}
return form
}
================================================
FILE: lib/noop.js
================================================
/* istanbul ignore next */
module.exports = function noop () {}
================================================
FILE: lib/parseHAR.js
================================================
'use strict'
// given we support node v8
// eslint-disable-next-line n/no-deprecated-api
const { parse } = require('url')
function parseHAR (har) {
const requestsPerOrigin = new Map()
try {
if (!har || typeof har !== 'object' || typeof har.log !== 'object' || !Array.isArray(har.log.entries) || !har.log.entries.length) {
throw new Error('no entries found')
}
let i = 0
for (const entry of har.log.entries) {
i++
if (!entry || typeof entry !== 'object' || !entry.request || typeof entry.request !== 'object') {
throw new Error(`invalid request in entry #${i}`)
}
const { request: { method, url, headers: headerArray, postData } } = entry
// turn headers array to headers object
const headers = {}
if (!Array.isArray(headerArray)) {
throw new Error(`invalid headers array in entry #${i}`)
}
let j = 0
for (const header of headerArray) {
j++
if (!header || typeof header !== 'object' || typeof header.name !== 'string' || typeof header.value !== 'string') {
throw new Error(`invalid name or value in header #${j} of entry #${i}`)
}
const { name, value } = header
headers[name] = value
}
const { path, hash, host, protocol } = parse(url)
const origin = `${protocol}//${host}`
let requests = requestsPerOrigin.get(origin)
if (!requests) {
requests = []
requestsPerOrigin.set(origin, requests)
}
const request = {
origin,
method,
// only keep path & hash as our HttpClient will handle origin
path: `${path}${hash || ''}`,
headers
}
if (typeof postData === 'object' && typeof postData.text === 'string') {
request.body = postData.text
}
requests.push(request)
}
} catch (err) {
throw new Error(`Could not parse HAR content: ${err.message}`)
}
return requestsPerOrigin
}
exports.parseHAR = parseHAR
================================================
FILE: lib/pipelinedRequestsQueue.js
================================================
'use strict'
/**
* A queue (FIFO) to hold pipelined requests and link metadata to them as the response is received from the server.
* This facilitates the handling of responses when the HTTP requests are pipelined.
* A queue is the best structure for this because the sever reponses are provided in the same order as the requests
* Cf. https://en.wikipedia.org/wiki/HTTP_pipelining
*
* /!\ it's up to you as a user to ensure that the queue is populated if using
* the functionality. This implementation will fail silently if e.g. you try to
* call any function accessing the queue while it's empty.
*/
class PipelinedRequestsQueue {
constructor () {
this.pendingRequests = []
}
insertRequest (req) {
this.pendingRequests.push({
req,
bytes: 0,
body: '',
headers: {},
startTime: process.hrtime()
})
}
peek () {
if (this.pendingRequests.length > 0) {
return this.pendingRequests[0]
}
}
addByteCount (count) {
const req = this.peek()
if (req) {
req.bytes += count
}
}
addBody (data) {
const req = this.peek()
if (req) {
req.body += data
}
}
setHeaders (headers) {
const req = this.peek()
if (req) {
req.headers = headers
}
}
size () {
return this.pendingRequests.length
}
/** Terminates the first-in request
* This will calculate the request duration, remove it from the queue and return its data
**/
terminateRequest () {
if (this.pendingRequests.length > 0) {
const data = this.pendingRequests.shift()
const hrduration = process.hrtime(data.startTime)
data.duration = hrduration[0] * 1e3 + hrduration[1] / 1e6
return data
}
}
clear () {
this.pendingRequests = []
}
toArray () {
return this.pendingRequests
}
}
module.exports = PipelinedRequestsQueue
================================================
FILE: lib/preload/autocannonDetectPort.js
================================================
'use strict'
const onListen = require('on-net-listen')
const net = require('net')
const socket = net.connect(process.env.AUTOCANNON_SOCKET)
onListen(function (addr) {
this.destroy()
const port = Buffer.from(addr.port + '')
socket.write(port)
})
socket.unref()
================================================
FILE: lib/printResult.js
================================================
'use strict'
const Table = require('cli-table3')
const Chalk = require('chalk')
const testColorSupport = require('color-support')
const prettyBytes = require('pretty-bytes')
const { percentiles } = require('./histUtil')
const format = require('./format')
const defaults = {
// use stderr as its progressBar's default
outputStream: process.stderr,
renderResultsTable: true,
renderLatencyTable: false,
verbose: true
}
class TableWithoutColor extends Table {
constructor (opts = {}) {
super({ ...opts, style: { head: [], border: [] } })
}
}
const printResult = (result, opts) => {
opts = Object.assign({}, defaults, opts)
let strResult = ''
if (opts.verbose) {
const chalk = new Chalk.Instance(testColorSupport({ stream: opts.outputStream, alwaysReturn: true }))
const ColorSafeTable = chalk.level === 0 ? TableWithoutColor : Table
const shortLatency = new ColorSafeTable({
head: asColor(chalk.cyan, ['Stat', '2.5%', '50%', '97.5%', '99%', 'Avg', 'Stdev', 'Max'])
})
shortLatency.push(asLowRow(chalk.bold('Latency'), asMs(result.latency)))
logToLocalStr('\n' + shortLatency.toString())
const requests = new ColorSafeTable({
head: asColor(chalk.cyan, ['Stat', '1%', '2.5%', '50%', '97.5%', 'Avg', 'Stdev', 'Min'])
})
requests.push(asHighRow(chalk.bold('Req/Sec'), asNumber(result.requests)))
requests.push(asHighRow(chalk.bold('Bytes/Sec'), asBytes(result.throughput)))
logToLocalStr(requests.toString())
if (opts.renderStatusCodes === true) {
const statusCodeStats = new ColorSafeTable({
head: asColor(chalk.cyan, ['Code', 'Count'])
})
Object.keys(result.statusCodeStats).forEach(statusCode => {
const stats = result.statusCodeStats[statusCode]
const colorize = colorizeByStatusCode(chalk, statusCode)
statusCodeStats.push([colorize(statusCode), stats.count])
})
logToLocalStr(statusCodeStats.toString())
}
logToLocalStr('')
if (result.sampleInt === 1000) {
logToLocalStr('Req/Bytes counts sampled once per second.')
} else {
logToLocalStr('Req/Bytes counts sampled every ' + result.sampleInt / 1000 + ' seconds.')
}
logToLocalStr('# of samples: ' + result.samples)
logToLocalStr('')
if (opts.renderLatencyTable) {
const latencies = new ColorSafeTable({
head: asColor(chalk.cyan, ['Percentile', 'Latency (ms)'])
})
percentiles.map((perc) => {
const key = `p${perc}`.replace('.', '_')
return [
chalk.bold('' + perc),
result.latency[key]
]
}).forEach(row => {
latencies.push(row)
})
logToLocalStr(latencies.toString())
logToLocalStr('')
}
}
if (result.non2xx) {
logToLocalStr(`${result['2xx']} 2xx responses, ${result.non2xx} non 2xx responses`)
}
logToLocalStr(`${format(result.requests.sent)} requests in ${result.duration}s, ${prettyBytes(result.throughput.total)} read`)
if (result.errors) {
logToLocalStr(`${format(result.errors)} errors (${format(result.timeouts)} timeouts)`)
}
if (result.mismatches) {
logToLocalStr(`${format(result.mismatches)} requests with mismatched body`)
}
if (result.resets) {
logToLocalStr(`request pipeline was reset ${format(result.resets)} ${result.resets === 1 ? 'time' : 'times'}`)
}
function logToLocalStr (msg) {
strResult += msg + '\n'
}
return strResult
}
// create a table row for stats where low values is better
function asLowRow (name, stat) {
return [
name,
stat.p2_5,
stat.p50,
stat.p97_5,
stat.p99,
stat.average,
stat.stddev,
typeof stat.max === 'string' ? stat.max : Math.floor(stat.max * 100) / 100
]
}
// create a table row for stats where high values is better
function asHighRow (name, stat) {
return [
name,
stat.p1,
stat.p2_5,
stat.p50,
stat.p97_5,
stat.average,
stat.stddev,
typeof stat.min === 'string' ? stat.min : Math.floor(stat.min * 100) / 100
]
}
function asColor (colorise, row) {
return row.map((entry) => colorise(entry))
}
function asMs (stat) {
const result = Object.create(null)
for (const k of Object.keys(stat)) {
result[k] = `${stat[k]} ms`
}
result.max = typeof stat.max === 'string' ? stat.max : `${Math.floor(stat.max * 100) / 100} ms`
return result
}
function asNumber (stat) {
const result = Object.create(null)
for (const k of Object.keys(stat)) {
result[k] = stat[k].toLocaleString(undefined, {
// to show all digits
maximumFractionDigits: 20
})
}
return result
}
function asBytes (stat) {
const result = Object.create(stat)
for (const p of percentiles) {
const key = `p${p}`.replace('.', '_')
result[key] = prettyBytes(stat[key])
}
result.average = prettyBytes(stat.average)
result.stddev = prettyBytes(stat.stddev)
result.max = prettyBytes(stat.max)
result.min = prettyBytes(stat.min)
return result
}
function colorizeByStatusCode (chalk, statusCode) {
const codeClass = Math.floor(parseInt(statusCode) / 100) - 1
return [chalk.cyan, chalk.cyan, chalk.cyan, chalk.redBright, chalk.redBright][codeClass]
}
module.exports = printResult
================================================
FILE: lib/progressTracker.js
================================================
/**
* Prints out the test result details. It doesn't have not much business logic.
* We skip test coverage for this file
*/
/* istanbul ignore file */
'use strict'
const ProgressBar = require('progress')
const Chalk = require('chalk')
const testColorSupport = require('color-support')
const charSpinner = require('char-spinner')
const printResult = require('./printResult')
const { isMainThread } = require('./worker_threads')
const defaults = {
// use stderr as its progressBar's default
outputStream: process.stderr,
renderProgressBar: true,
renderResultsTable: true,
renderLatencyTable: false
}
function track (instance, opts) {
if (!instance) {
throw new Error('instance required for tracking')
}
opts = Object.assign({}, defaults, opts)
const chalk = new Chalk.Instance(testColorSupport({ stream: opts.outputStream, alwaysReturn: true }))
// this default needs to be set after chalk is setup, because chalk is now local to this func
const runningDescription = opts.warmupRunning ? 'warmup' : 'running'
opts.progressBarString = opts.progressBarString || `${chalk.green(runningDescription)} [:bar] :percent`
const iOpts = instance.opts
let durationProgressBar
let amountProgressBar
let addedListeners = false
let spinner
instance.on('start', () => {
if (opts.renderProgressBar && isMainThread) {
const socketPath = iOpts.socketPath ? ` (${iOpts.socketPath})` : ''
let msg = `${iOpts.connections} connections`
if (iOpts.pipelining > 1) {
msg += ` with ${iOpts.pipelining} pipelining factor`
}
if (iOpts.workers) {
msg += `\n${iOpts.workers} workers`
}
const runningType = opts.warmupRunning ? 'warmup' : 'test'
const message = iOpts.amount
? `Running ${iOpts.amount} requests ${runningType} @ ${iOpts.url}${socketPath}\n${msg}\n`
: `Running ${iOpts.duration}s ${runningType} @ ${iOpts.url}${socketPath}\n${msg}\n`
logToStream(message)
if (iOpts.workers) {
showSpinner()
} else {
if (iOpts.amount) {
amountProgressBar = trackAmount(instance, opts, iOpts)
} else {
durationProgressBar = trackDuration(instance, opts, iOpts)
}
}
addListener()
}
})
instance.on('done', (result) => {
// the code below this `if` just renders the results table...
// if the user doesn't want to render the table, we can just return early
if (opts.renderResultsTable === false) return
const tableStrResult = printResult(result, opts)
opts.outputStream.write(tableStrResult)
})
function showSpinner () {
spinner = charSpinner()
}
function hideSpinner () {
if (spinner) {
clearInterval(spinner)
spinner = null
}
}
function addListener () {
// add listeners for progress bar to instance here so they aren't
// added on restarting, causing listener leaks
if (addedListeners) {
return
}
addedListeners = true
// TODO: think about if workers can report progress every second, that
// way we could have a progress bar per worker.
if (iOpts.workers) {
// using `prependOnceListener` to make sure that we hide the spinner
// before writing anything else.
// if we print anything else to the output, the stale spinner text
// is left uncleared and clutters the output.
instance.prependOnceListener('done', hideSpinner)
return
}
// note: Attempted to curry the functions below, but that breaks the functionality
// as they use the scope/closure of the progress bar variables to allow them to be reset
if (opts.outputStream.isTTY) {
if (!iOpts.amount) { // duration progress bar
instance.on('tick', () => { durationProgressBar.tick() })
instance.on('done', () => { durationProgressBar.tick(iOpts.duration - 1) })
process.once('SIGINT', () => { durationProgressBar.tick(iOpts.duration - 1) })
} else { // amount progress bar
instance.on('response', () => { amountProgressBar.tick() })
instance.on('reqError', () => { amountProgressBar.tick() })
instance.on('done', () => { amountProgressBar.tick(iOpts.amount - 1) })
process.once('SIGINT', () => { amountProgressBar.tick(iOpts.amount - 1) })
}
}
}
function logToStream (msg) {
if (!isMainThread) return
opts.outputStream.write(msg + '\n')
}
}
function trackDuration (instance, opts, iOpts) {
// if switch needed needed to avoid
// https://github.com/mcollina/autocannon/issues/60
if (!opts.outputStream.isTTY) return
const progressBar = new ProgressBar(opts.progressBarString, {
width: 20,
incomplete: ' ',
total: iOpts.duration,
clear: true,
stream: opts.outputStream
})
progressBar.tick(0)
return progressBar
}
function trackAmount (instance, opts, iOpts) {
// if switch needed needed to avoid
// https://github.com/mcollina/autocannon/issues/60
if (!opts.outputStream.isTTY) return
const progressBar = new ProgressBar(opts.progressBarString, {
width: 20,
incomplete: ' ',
total: iOpts.amount,
clear: true,
stream: opts.outputStream
})
progressBar.tick(0)
return progressBar
}
module.exports = track
================================================
FILE: lib/requestIterator.js
================================================
'use strict'
const hyperid = require('hyperid')
const inherits = require('util').inherits
const requestBuilder = require('./httpRequestBuilder')
const clone = require('lodash.clonedeep')
const chunk = require('lodash.chunk')
const flatten = require('lodash.flatten')
const toHeaderKeyValue = (rawHeaders) => {
const tupleHeaders = chunk(rawHeaders, 2)
const headers = {}
tupleHeaders.forEach((val) => {
const currentValue = headers[val[0]]
if (!currentValue) {
headers[val[0]] = val[1]
} else {
headers[val[0]] = flatten([currentValue, val[1]])
}
})
return headers
}
function RequestIterator (opts) {
if (!(this instanceof RequestIterator)) {
return new RequestIterator(opts)
}
this.hyperid = hyperid({ urlSafe: true })
this.resetted = false
this.headers = {}
this.initialContext = opts.initialContext || {}
this.resetContext()
this.reqDefaults = opts
this.requestBuilder = requestBuilder(opts)
this.setRequests(opts.requests)
}
inherits(RequestIterator, Object)
RequestIterator.prototype.resetContext = function () {
this.context = clone(this.initialContext)
}
RequestIterator.prototype.nextRequest = function () {
this.resetted = false
++this.currentRequestIndex
// when looping over available request, clear context for a fresh start
if (this.currentRequestIndex === this.requests.length) {
this.resetContext()
this.currentRequestIndex = 0
}
this.currentRequest = this.requests[this.currentRequestIndex]
// only builds if it has dynamic setup
if (this.reqDefaults.idReplacement || typeof this.currentRequest.setupRequest === 'function') {
this.rebuildRequest()
}
return this.currentRequest
}
RequestIterator.prototype.nextRequestBuffer = function () {
// get next request
this.nextRequest()
return this.currentRequest.requestBuffer
}
RequestIterator.prototype.setRequests = function (newRequests) {
this.resetted = false
this.requests = newRequests || [{}]
this.currentRequestIndex = 0
// build all request which don't have dynamic setup, except if it's the first one
this.requests.forEach((request, i) => {
this.currentRequest = request
if (i === 0 || typeof request.setupRequest !== 'function') {
this.rebuildRequest()
}
})
this.currentRequest = this.requests[0]
}
RequestIterator.prototype.setHeaders = function (newHeaders) {
this.headers = newHeaders || {}
this.currentRequest.headers = this.headers
this.rebuildRequest()
}
RequestIterator.prototype.setBody = function (newBody) {
this.currentRequest.body = newBody || Buffer.alloc(0)
this.rebuildRequest()
}
RequestIterator.prototype.setHeadersAndBody = function (newHeaders, newBody) {
this.headers = newHeaders || {}
this.currentRequest.headers = this.headers
this.currentRequest.body = newBody || Buffer.alloc(0)
this.rebuildRequest()
}
RequestIterator.prototype.setRequest = function (newRequest) {
this.currentRequest = newRequest || {}
this.rebuildRequest()
}
RequestIterator.prototype.rebuildRequest = function () {
let data
this.resetted = false
if (this.currentRequest) {
this.currentRequest.headers = this.currentRequest.headers || this.headers
data = this.requestBuilder(this.currentRequest, this.context)
if (data) {
const hyperid = this.hyperid()
this.currentRequest.requestBuffer = this.reqDefaults.idReplacement
? Buffer.from(data.toString()
.replace(/\[<id>\]/g, hyperid)
// in the first line only (the url), replace encoded id placeholders
.replace(/^.+/, m => m.replace(/\[%3Cid%3E]/g, hyperid)))
: data
} else if (this.currentRequestIndex === 0) {
// when first request fails to build, we can not reset pipeline, or it'll never end
throw new Error('First setupRequest() failed did not returned valid request. Stopping')
} else {
this.currentRequestIndex = this.requests.length - 1
this.nextRequest()
this.resetted = true
}
}
return data
}
RequestIterator.prototype.recordBody = function (request, status, body, headers) {
if (request && typeof request.onResponse === 'function') {
request.onResponse(status, body, this.context, toHeaderKeyValue(headers || []))
}
}
module.exports = RequestIterator
================================================
FILE: lib/run.js
================================================
'use strict'
const URL = require('url')
const reInterval = require('reinterval')
const EE = require('events').EventEmitter
const Client = require('./httpClient')
const { isMainThread } = require('./worker_threads')
const { ofURL } = require('./url')
const aggregateResult = require('./aggregateResult')
const { getHistograms, encodeHist } = require('./histUtil')
const defaults = {
harRequests: new Map()
}
function run (opts, tracker, cb) {
opts = Object.assign({}, defaults, opts)
tracker = tracker || new EE()
const histograms = getHistograms(opts.histograms)
const { latencies, requests, throughput } = histograms
const statusCodes = [
0, // 1xx
0, // 2xx
0, // 3xx
0, // 4xx
0 // 5xx
]
const statusCodeStats = {}
if (opts.overallRate && (opts.overallRate < opts.connections)) opts.connections = opts.overallRate
let counter = 0
let bytes = 0
let errors = 0
let timeouts = 0
let mismatches = 0
let totalBytes = 0
let totalRequests = 0
let totalCompletedRequests = 0
let samples = 0
let resets = 0
const amount = opts.amount
let stop = false
let restart = true
let numRunning = opts.connections
let startTime = Date.now()
const includeErrorStats = !opts.excludeErrorStats
opts.url = ofURL(opts.url).map((url) => {
if (url.indexOf('http') !== 0) return 'http://' + url
return url
})
const urls = ofURL(opts.url, true).map(url => {
if (url.indexOf('http') !== 0) url = 'http://' + url
url = URL.parse(url) // eslint-disable-line n/no-deprecated-api
// copy over fields so that the client
// performs the right HTTP requests
url.pipelining = opts.pipelining
url.method = opts.method
url.body = opts.form ? opts.form.getBuffer() : opts.body
url.headers = opts.form ? Object.assign({}, opts.headers, opts.form.getHeaders()) : opts.headers
url.setupClient = opts.setupClient
url.verifyBody = opts.verifyBody
url.timeout = opts.timeout
url.origin = `${url.protocol}//${url.host}`
// only keep requests for that origin, or default to requests from options
url.requests = opts.harRequests.get(url.origin) || opts.requests
url.reconnectRate = opts.reconnectRate
url.responseMax = amount || opts.maxConnectionRequests || opts.maxOverallRequests
url.rate = opts.connectionRate || opts.overallRate
url.idReplacement = opts.idReplacement
url.socketPath = opts.socketPath
url.servername = opts.servername
url.expectBody = opts.expectBody
return url
})
let stopTimer
let clients = []
initialiseClients(clients)
if (!amount) {
stopTimer = setTimeout(() => {
stop = true
}, opts.duration * 1000)
}
const second = reInterval(tickProgressBar, 1000)
const interval = reInterval(tickInterval, opts.sampleInt)
// put the start emit in a setImmediate so trackers can be added, etc.
setImmediate(() => { tracker.emit('start') })
function tickProgressBar () {
tracker.emit('tick', { counter, bytes })
}
function tickInterval () {
totalBytes += bytes
totalCompletedRequests += counter
samples += 1
requests.recordValue(counter)
throughput.recordValue(bytes)
counter = 0
bytes = 0
if (stop) {
if (stopTimer) clearTimeout(stopTimer)
tracker.emit('tick', { counter, bytes })
second.clear()
interval.clear()
clients.forEach((client) => client.destroy())
const result = {
latencies: encodeHist(latencies),
requests: encodeHist(requests),
throughput: encodeHist(throughput),
totalCompletedRequests,
totalRequests,
totalBytes,
samples,
errors,
timeouts,
mismatches,
non2xx: statusCodes[0] + statusCodes[2] + statusCodes[3] + statusCodes[4],
statusCodeStats,
resets,
duration: Math.round((Date.now() - startTime) / 10) / 100,
start: new Date(startTime),
finish: new Date()
}
statusCodes.forEach((code, index) => { result[(index + 1) + 'xx'] = code })
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
// only place we could notify user when each round finishes
tracker.emit('done', resultObj)
} else {
latencies.destroy()
requests.destroy()
throughput.destroy()
cb(null, resultObj)
}
const restartFn = () => {
stop = false
stopTimer = setTimeout(() => {
stop = true
}, opts.duration * 1000)
errors = 0
timeouts = 0
mismatches = 0
totalBytes = 0
totalRequests = 0
totalCompletedRequests = 0
resets = 0
statusCodes.fill(0)
requests.reset()
latencies.reset()
throughput.reset()
startTime = Date.now()
// reinitialise clients
if (opts.overallRate && (opts.overallRate < opts.connections)) opts.connections = opts.overallRate
clients = []
initialiseClients(clients)
interval.reschedule(1000)
tracker.emit('start')
}
// the restart function
setImmediate(() => {
if (opts.forever && restart && isMainThread) restartFn()
})
}
}
function initialiseClients (clients) {
for (let i = 0; i < opts.connections; i++) {
const url = urls[i % urls.length]
if (!amount && !opts.maxConnectionRequests && opts.maxOverallRequests) {
url.responseMax = distributeNums(opts.maxOverallRequests, i)
}
if (amount) {
url.responseMax = distributeNums(amount, i)
if (url.responseMax === 0) {
throw Error('connections cannot be greater than amount')
}
}
if (!opts.connectionRate && opts.overallRate) {
url.rate = distributeNums(opts.overallRate, i)
}
if (opts.initialContext) {
url.initialContext = opts.initialContext
}
if (opts.tlsOptions) {
url.tlsOptions = opts.tlsOptions
}
const client = new Client(url)
client.on('response', onResponse)
client.on('connError', onError)
client.on('mismatch', onExpectMismatch)
client.on('reset', () => { resets++ })
client.on('timeout', onTimeout)
client.on('request', () => { totalRequests++ })
client.on('done', onDone)
clients.push(client)
// we will miss the initial request emits because the client emits request on construction
totalRequests += url.pipelining < url.rate ? url.rate : url.pipelining
}
function distributeNums (x, i) {
return (Math.floor(x / opts.connections) + (((i + 1) <= (x % opts.connections)) ? 1 : 0))
}
function onResponse (statusCode, resBytes, responseTime, rate) {
tracker.emit('response', this, statusCode, resBytes, responseTime)
const codeIndex = Math.floor(parseInt(statusCode) / 100) - 1
statusCodes[codeIndex] += 1
if (!statusCodeStats[statusCode]) {
statusCodeStats[statusCode] = { count: 1 }
} else {
statusCodeStats[statusCode].count++
}
// only recordValue 2xx latencies
if (codeIndex === 1 || includeErrorStats) {
if (rate && !opts.ignoreCoordinatedOmission) {
latencies.recordValueWithExpectedInterval(responseTime, Math.ceil(1 / rate))
} else {
latencies.recordValue(responseTime)
}
}
if (codeIndex === 1 || includeErrorStats) bytes += resBytes
counter++
}
function onError (error) {
for (let i = 0; i < opts.pipelining; i++) tracker.emit('reqError', error)
errors++
if (opts.debug) console.error(error)
if (opts.bailout && errors >= opts.bailout) stop = true
}
function onExpectMismatch (bpdyStr) {
for (let i = 0; i < opts.pipelining; i++) {
tracker.emit('reqMismatch', bpdyStr)
}
mismatches++
if (opts.bailout && mismatches >= opts.bailout) stop = true
}
// treat a timeout as a special type of error
function onTimeout () {
const error = new Error('request timed out')
for (let i = 0; i < opts.pipelining; i++) tracker.emit('reqError', error)
errors++
timeouts++
if (opts.bailout && errors >= opts.bailout) stop = true
}
function onDone () {
if (!--numRunning) stop = true
}
}
tracker.stop = () => {
stop = true
restart = false
}
return tracker
} // run
module.exports = run
================================================
FILE: lib/runTracker.js
================================================
'use strict'
const EE = require('events').EventEmitter
const run = require('./run')
const noop = require('./noop')
function runTracker (opts, tracker, cb) {
cb = cb || noop
tracker = tracker || new EE()
run(opts, tracker, cb)
return tracker
}
module.exports = runTracker
================================================
FILE: lib/subargAliases.js
================================================
'use strict'
const clone = require('lodash.clonedeep')
const subArgAlias = {
warmup: {
c: 'connections',
d: 'duration'
}
}
// Expects args to have already been processed by subarg
function generateSubargAliases (args) {
const aliasedArgs = clone(args)
function isParentAliasInArgs (argKey) {
return aliasedArgs[argKey]
}
function isSubargAnAlias (parentAlias, subArg) {
return parentAlias[subArg]
}
function mapAliasForSubarg (parentAlias, parentKey) {
const parentArgs = aliasedArgs[parentKey]
for (const subArg in parentArgs) {
if (isSubargAnAlias(parentAlias, subArg)) {
parentArgs[parentAlias[subArg]] = parentArgs[subArg]
}
}
}
for (const parentKey in subArgAlias) {
const parentAlias = subArgAlias[parentKey]
if (isParentAliasInArgs(parentKey)) {
mapAliasForSubarg(parentAlias, parentKey)
}
}
return aliasedArgs
}
module.exports = generateSubargAliases
================================================
FILE: lib/url.js
================================================
'use strict'
/**
* check the url is not an empty string or empty array
* @param url
*/
function checkURL (url) {
return (typeof url === 'string' && url) ||
(Array.isArray(url) && url.length > 0)
}
/**
*
* @param url
* @param asArray
* @returns
*/
function ofURL (url, asArray) {
if (Array.isArray(url)) return url
if (typeof url === 'string') {
return {
map (fn) {
if (asArray) return [fn(url)]
return fn(url)
}
}
}
throw new Error('url should only be a string or an array of string')
}
exports.checkURL = checkURL
exports.ofURL = ofURL
================================================
FILE: lib/util.js
================================================
'use strict'
const semver = require('semver')
const hasWorkerSupport = semver.gte(process.versions.node, '11.7.0')
function getPropertyCaseInsensitive (obj, key) {
for (const objKey of Object.keys(obj)) {
if (objKey.toLowerCase() === key.toLowerCase()) return obj[objKey]
}
}
module.exports = { hasWorkerSupport, getPropertyCaseInsensitive }
================================================
FILE: lib/validate.js
================================================
'use strict'
const defaultOptions = require('./defaultOptions')
const timestring = require('timestring')
const { checkURL } = require('./url')
const multipart = require('./multipart')
const { parseHAR } = require('./parseHAR')
const { hasWorkerSupport } = require('./util')
const isValidFn = (opt) => (!opt || typeof opt === 'function' || typeof opt === 'string')
const lessThanOneError = (label) => new Error(`${label} can not be less than 1`)
const greaterThanZeroError = (label) => new Error(`${label} must be greater than 0`)
const minIfPresent = (val, min) => val !== null && val < min
function safeRequire (path) {
if (typeof path === 'string') {
try {
return require(path)
} catch (err) {}
}
return path
}
function defaultOpts (opts) {
const setupClient = opts.workers ? opts.setupClient : safeRequire(opts.setupClient)
const verifyBody = opts.workers ? opts.verifyBody : safeRequire(opts.verifyBody)
const requests = opts.requests
? opts.requests.map((r) => {
const setupRequest = opts.workers ? r.setupRequest : safeRequire(r.setupRequest)
const onResponse = opts.workers ? r.onResponse : safeRequire(r.onResponse)
return {
...r,
...(setupRequest ? { setupRequest } : undefined),
...(onResponse ? { onResponse } : undefined)
}
})
: undefined
return {
...defaultOptions,
...opts,
...(setupClient ? { setupClient } : undefined),
...(verifyBody ? { verifyBody } : undefined),
...(requests ? { requests } : undefined)
}
}
module.exports = function validateOpts (opts, cbPassedIn) {
if (opts.workers && !hasWorkerSupport) return new Error('Please use node >= 11.7.0 for workers support')
// these need to be validated before defaulting
if (minIfPresent(opts.bailout, 1)) return lessThanOneError('bailout threshold')
if (minIfPresent(opts.connectionRate, 1)) return lessThanOneError('connectionRate')
if (minIfPresent(opts.overallRate, 1)) return lessThanOneError('bailout overallRate')
if (minIfPresent(opts.amount, 1)) return lessThanOneError('amount')
if (minIfPresent(opts.maxConnectionRequests, 1)) return lessThanOneError('maxConnectionRequests')
if (minIfPresent(opts.maxOverallRequests, 1)) return lessThanOneError('maxOverallRequests')
if (opts.form) {
opts.method = opts.method || 'POST'
}
// fill in defaults after
opts = defaultOpts(opts)
if (opts.json === true) {
opts.renderProgressBar = opts.renderResultsTable = opts.renderLatencyTable = false
}
if (opts.requests) {
if (opts.requests.some(r => !isValidFn(r.setupRequest))) {
return new Error('Invalid option setupRequest, please provide a function (or file path when in workers mode)')
}
if (opts.requests.some(r => !isValidFn(r.onResponse))) {
return new Error('Invalid option onResponse, please provide a function (or file path when in workers mode)')
}
}
if (!isValidFn(opts.setupClient)) {
return new Error('Invalid option setupClient, please provide a function (or file path when in workers mode)')
}
if (!isValidFn(opts.verifyBody)) {
return new Error('Invalid option verifyBody, please provide a function (or file path when in workers mode)')
}
if (!checkURL(opts.url) && !opts.socketPath) {
return new Error('url or socketPath option required')
}
if (typeof opts.duration === 'string') {
if (/[a-zA-Z]/.exec(opts.duration)) {
try {
opts.duration = timestring(opts.duration)
} catch (error) {
return error
}
} else {
opts.duration = Number(opts.duration.trim())
}
}
if (typeof opts.duration !== 'number') {
return new Error('duration entered was in an invalid format')
}
if (opts.duration < 0) {
return new Error('duration can not be less than 0')
}
opts.sampleInt = parseFloat(opts.sampleInt)
if (isNaN(opts.sampleInt)) {
return new Error('sample interval entered was in an invalid format')
}
if (opts.sampleInt < 0) {
return new Error('sample interval can not be less than 0')
}
if (opts.expectBody && opts.requests !== defaultOptions.requests) {
return new Error('expectBody cannot be used in conjunction with requests')
}
if (opts.form) {
try {
// Parse multipart upfront to make sure there's no errors
const data = multipart(opts.form)
opts.form = opts.workers ? opts.form : data // but use parsed data only if not in workers mode
} catch (error) {
return error
}
}
opts.harRequests = new Map()
if (opts.har) {
try {
opts.harRequests = parseHAR(opts.har)
} catch (error) {
return error
}
}
if (opts.connections < 1) return lessThanOneError('connections')
if (opts.pipelining < 1) return lessThanOneError('pipelining factor')
if (opts.timeout < 1) return greaterThanZeroError('timeout')
if (opts.ignoreCoordinatedOmission && !opts.connectionRate && !opts.overallRate) {
return new Error('ignoreCoordinatedOmission makes no sense without connectionRate or overallRate')
}
if (opts.forever && cbPassedIn) {
return new Error('should not use the callback parameter when the `forever` option is set to true. Use the `done` event on this event emitter')
}
if (opts.forever && opts.workers) {
return new Error('Using `forever` option isn\'t currently supported with workers')
}
return opts
}
================================================
FILE: lib/worker.js
================================================
'use strict'
const { isMainThread, parentPort, workerData } = require('worker_threads')
const multipart = require('./multipart')
const run = require('./run')
const createHist = (name) => ({
__custom: true,
recordValue: v => updateHist(name, v),
destroy: () => {},
reset: () => resetHist(name)
})
const updateHist = (name, value) => {
parentPort.postMessage({
cmd: 'UPDATE_HIST',
data: { name, value }
})
}
const resetHist = (name) => {
parentPort.postMessage({
cmd: 'RESET_HIST',
data: { name }
})
}
function runTracker (opts, cb) {
const tracker = run({
...opts,
...(opts.form ? { form: multipart(opts.form) } : undefined),
...(opts.setupClient ? { setupClient: require(opts.setupClient) } : undefined),
...(opts.verifyBody ? { verifyBody: require(opts.verifyBody) } : undefined),
requests: opts.requests
? opts.requests.map(r => ({
...r,
...(r.setupRequest ? { setupRequest: require(r.setupRequest) } : undefined),
...(r.onResponse ? { onResponse: require(r.onResponse) } : undefined)
}))
: undefined,
histograms: {
requests: createHist('requests'),
throughput: createHist('throughput')
}
}, null, cb)
tracker.on('tick', (data) => {
parentPort.postMessage({ cmd: 'TICK', data })
})
return {
stop: tracker.stop
}
}
if (!isMainThread) {
const { opts } = workerData
let tracker
parentPort.on('message', (msg) => {
const { cmd } = msg
if (cmd === 'START') {
tracker = runTracker(opts, (error, data) => {
parentPort.postMessage({ cmd: error ? 'ERROR' : 'RESULT', error, data })
parentPort.close()
})
} else if (cmd === 'STOP') {
tracker.stop()
parentPort.close()
}
})
}
================================================
FILE: lib/worker_threads.js
================================================
'use strict'
let workerThreads = {}
try {
workerThreads = require('worker_threads')
} catch (err) {
if (err) {
// we don't need the error but can't have catch block
// without err as node 8 doesn't support that
}
workerThreads = {
isMainThread: true
}
}
module.exports = workerThreads
================================================
FILE: package.json
================================================
{
"name": "autocannon",
"version": "8.0.0",
"description": "Fast HTTP benchmarking tool written in Node.js",
"main": "autocannon.js",
"bin": {
"autocannon": "autocannon.js"
},
"scripts": {
"test": "standard && tap test/serial/*.test.js test/*.test.js",
"standard:fix": "standard --fix"
},
"pre-commit": [
"test"
],
"repository": {
"type": "git",
"url": "git+https://github.com/mcollina/autocannon.git"
},
"keywords": [
"http",
"soak",
"load",
"fast",
"wrk",
"ab",
"test"
],
"author": "Matteo Collina <hello@matteocollina.com>",
"contributors": [
"Glen Keane <glenkeane.94@gmail.com>",
"Donald Robertson <donaldarobertson89@gmail.com",
"Salman Mitha <SalmanMitha@gmail.com>"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/mcollina/autocannon/issues"
},
"homepage": "https://github.com/mcollina/autocannon#readme",
"devDependencies": {
"ansi-regex": "^5.0.1",
"bl": "^6.0.0",
"busboy": "^0.3.1",
"pre-commit": "^1.1.2",
"proxyquire": "^2.1.3",
"sinon": "^15.0.0",
"split2": "^4.0.0",
"standard": "^17.0.0",
"tap": "^16.0.0",
"why-is-node-running": "^2.3.0"
},
"dependencies": {
"@minimistjs/subarg": "^1.0.0",
"chalk": "^4.1.0",
"char-spinner": "^1.0.1",
"cli-table3": "^0.6.0",
"color-support": "^1.1.1",
"cross-argv": "^2.0.0",
"form-data": "^4.0.0",
"has-async-hooks": "^1.0.0",
"hdr-histogram-js": "^3.0.0",
"hdr-histogram-percentiles-obj": "^3.0.0",
"http-parser-js": "^0.5.2",
"hyperid": "^3.0.0",
"lodash.chunk": "^4.2.0",
"lodash.clonedeep": "^4.5.0",
"lodash.flatten": "^4.4.0",
"manage-path": "^2.0.0",
"on-net-listen": "^1.1.1",
"pretty-bytes": "^5.4.1",
"progress": "^2.0.3",
"reinterval": "^1.1.0",
"retimer": "^3.0.0",
"semver": "^7.3.2",
"timestring": "^6.0.0"
}
}
================================================
FILE: samples/bench-multi-url.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
function createHandler (serverName) {
return function (req, res) {
console.log(serverName + ' received request')
res.end('hello world')
}
}
const server1 = http.createServer(createHandler('server1'))
const server2 = http.createServer(createHandler('server2'))
server1.listen(0, startBench)
server2.listen(0, startBench)
function startBench () {
const url = [
'http://localhost:' + server1.address().port,
'http://localhost:' + server2.address().port
]
// same with run the follow command in cli
// autocannon -d 10 -c 2 http://localhost:xxxx http://localhost:yyyy
autocannon({
url,
// connection number should n times of the number of server
connections: 2,
duration: 10,
requests: [
{
method: 'GET',
path: '/'
}
]
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
process.exit(1)
}
}
================================================
FILE: samples/customise-individual-connection.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
let connection = 0
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1000,
duration: 10,
setupClient
}, finishedBench)
function setupClient (client) {
client.setBody('connection number', connection++)
}
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/customise-verifyBody-workers.js
================================================
'use strict'
const http = require('http')
const path = require('path')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1000,
duration: 10,
workers: 2,
verifyBody: path.join(__dirname, 'helpers', 'verify-body')
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/customise-verifyBody.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1000,
duration: 10,
verifyBody
}, finishedBench)
function verifyBody (body) {
return body.indexOf('<html>') > -1
}
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/helpers/on-response.js
================================================
'use strict'
module.exports = (status, body, context) => {
if (status === 200) {
context.user = JSON.parse(body)
} // on error, you may abort the benchmark
}
================================================
FILE: samples/helpers/setup-request.js
================================================
'use strict'
module.exports = (req, context) => ({
...req,
path: `/user/${context.user.id}`,
body: JSON.stringify({
...context.user,
lastName: 'Doe'
})
})
================================================
FILE: samples/helpers/verify-body.js
================================================
'use strict'
module.exports = (body) => {
return true
}
================================================
FILE: samples/init-context.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
const body = []
// this route handler simply returns whatever it gets from response
req
.on('data', chunk => body.push(chunk))
.on('end', () => res.end(Buffer.concat(body)))
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1,
amount: 1,
initialContext: { user: { firstName: 'Salman' } },
requests: [
{
// use data from context
method: 'PUT',
setupRequest: (req, context) => ({
...req,
path: `/user/${context.user.id}`,
body: JSON.stringify({
...context.user,
lastName: 'Mitha'
})
})
}
]
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/modifying-request.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
const instance = autocannon({
url,
connections: 1000,
duration: 10
}, finishedBench)
let message = 0
// modify the body on future requests
instance.on('response', function (client, statusCode, returnBytes, responseTime) {
client.setBody('message ' + message++)
})
let headers = 0
// modify the headers on future requests
// this wipes any existing headers out with the new ones
instance.on('response', function (client, statusCode, returnBytes, responseTime) {
const newHeaders = {}
newHeaders[`header${headers++}`] = `headerValue${headers++}`
client.setHeaders(newHeaders)
})
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/request-context-workers.js
================================================
'use strict'
const http = require('http')
const path = require('path')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
const body = []
// this route handler simply returns whatever it gets from response
req
.on('data', chunk => body.push(chunk))
.on('end', () => res.end(Buffer.concat(body)))
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
duration: 2,
workers: 2,
requests: [
{
// let's create a new user
method: 'POST',
path: '/users',
body: JSON.stringify({ firstName: 'Jane', id: 10 }),
onResponse: path.join(__dirname, 'helpers', 'on-response')
},
{
// now we'll give them a last name
method: 'PUT',
setupRequest: path.join(__dirname, 'helpers', 'setup-request')
}
]
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/request-context.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
const body = []
// this route handler simply returns whatever it gets from response
req
.on('data', chunk => body.push(chunk))
.on('end', () => res.end(Buffer.concat(body)))
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
requests: [
{
// let's create a new user
method: 'POST',
path: '/users',
body: JSON.stringify({ firstName: 'Jane', id: 10 }),
onResponse: (status, body, context) => {
if (status === 200) {
context.user = JSON.parse(body)
} // on error, you may abort the benchmark
}
},
{
// now we'll give them a last name
method: 'PUT',
setupRequest: (req, context) => ({
...req,
path: `/user/${context.user.id}`,
body: JSON.stringify({
...context.user,
lastName: 'Doe'
})
})
}
]
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/requests-sample.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1000,
duration: 10,
headers: {
// by default we add an auth token to all requests
auth: 'A Pregenerated auth token'
},
requests: [
{
method: 'POST', // this should be a post for logging in
path: '/login',
body: 'valid login details',
// overwrite our default headers,
// so we don't add an auth token
// for this request
headers: {}
},
{
path: '/mySecretDetails'
// this will automatically add the pregenerated auth token
},
{
method: 'GET', // this should be a put for modifying secret details
path: '/mySecretDetails',
headers: { // let submit some json?
'Content-type': 'application/json; charset=utf-8'
},
// we need to stringify the json first
body: JSON.stringify({
name: 'my new name'
}),
setupRequest: reqData => {
reqData.method = 'PUT' // we are overriding the method 'GET' to 'PUT' here
return reqData
}
}
]
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/track-run-workers.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const instance = autocannon({
connections: 100,
duration: 2,
url: 'http://localhost:' + server.address().port,
workers: 2
}, finishedBench)
autocannon.track(instance)
// this is used to kill the instance on CTRL-C
process.once('SIGINT', () => {
instance.stop()
})
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/track-run.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const instance = autocannon({
url: 'http://localhost:' + server.address().port
}, finishedBench)
autocannon.track(instance)
// this is used to kill the instance on CTRL-C
process.once('SIGINT', () => {
instance.stop()
})
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: samples/using-id-replacement.js
================================================
'use strict'
const http = require('http')
const autocannon = require('../autocannon')
const server = http.createServer(handle)
server.listen(0, startBench)
function handle (req, res) {
res.end('hello world')
}
function startBench () {
const url = 'http://localhost:' + server.address().port
autocannon({
url,
connections: 1000,
duration: 10,
requests: [
{
method: 'POST',
path: '/register',
headers: {
'Content-type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
name: 'New User',
email: 'new-[<id>]@user.com' // [<id>] will be replaced with generated HyperID at run time
})
}
],
idReplacement: true
}, finishedBench)
function finishedBench (err, res) {
console.log('finished bench', err, res)
}
}
================================================
FILE: server.js
================================================
'use strict'
const http = require('http')
const https = require('https')
const fs = require('fs')
const path = require('path')
const options = {
key: fs.readFileSync(path.join(__dirname, 'test', '/key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'test', '/cert.pem')),
passphrase: 'test'
}
const server = http.createServer(handle)
const server2 = https.createServer(options, handle)
server.listen(3000)
server2.listen(3001)
function handle (req, res) {
res.end('hello world')
}
================================================
FILE: test/aggregateResult.test.js
================================================
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')
})
================================================
FILE: test/argumentParsing.test.js
================================================
'use strict'
const test = require('tap').test
const Autocannon = require('../autocannon')
const fs = require('fs')
test('parse argument', (t) => {
t.plan(4)
const args = Autocannon.parseArguments([
'-H', 'X-Http-Method-Override=GET',
'-m', 'POST',
'-b', 'the body',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, { 'X-Http-Method-Override': 'GET' })
t.equal(args.method, 'POST')
t.equal(args.body, 'the body')
})
test('parse argument with multiple headers', (t) => {
t.plan(3)
const args = Autocannon.parseArguments([
'-H', 'header1=value1',
'-H', 'header2=value2',
'-H', 'header3=value3',
'-H', 'header4=value4',
'-H', 'header5=value5',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, {
header1: 'value1',
header2: 'value2',
header3: 'value3',
header4: 'value4',
header5: 'value5'
})
t.equal(args.method, 'GET')
})
test('parse argument with multiple complex headers', (t) => {
t.plan(3)
const args = Autocannon.parseArguments([
'-H', 'header1=value1;data=asd',
'-H', 'header2=value2;data=asd',
'-H', 'header3=value3;data=asd',
'-H', 'header4=value4;data=asd',
'-H', 'header5=value5;data=asd',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, {
header1: 'value1;data=asd',
header2: 'value2;data=asd',
header3: 'value3;data=asd',
header4: 'value4;data=asd',
header5: 'value5;data=asd'
})
t.equal(args.method, 'GET')
})
test('parse argument with multiple headers in standard notation', (t) => {
t.plan(3)
const args = Autocannon.parseArguments([
'-H', 'header1: value1',
'-H', 'header2: value2',
'-H', 'header3: value3',
'-H', 'header4: value4',
'-H', 'header5: value5',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, {
header1: ' value1',
header2: ' value2',
header3: ' value3',
header4: ' value4',
header5: ' value5'
})
t.equal(args.method, 'GET')
})
test('parse argument with multiple complex headers in standard notation', (t) => {
t.plan(3)
const args = Autocannon.parseArguments([
'-H', 'header1:value1;data=asd',
'-H', 'header2:value2;data=asd',
'-H', 'header3:value3;data=asd',
'-H', 'header4:value4;data=asd',
'-H', 'header5:value5;data=asd',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, {
header1: 'value1;data=asd',
header2: 'value2;data=asd',
header3: 'value3;data=asd',
header4: 'value4;data=asd',
header5: 'value5;data=asd'
})
t.equal(args.method, 'GET')
})
test('parse argument with "=" in value header', (t) => {
t.plan(1)
const args = Autocannon.parseArguments([
'-H', 'header1=foo=bar',
'http://localhost/foo/bar'
])
t.strictSame(args.headers, {
header1: 'foo=bar'
})
})
test('parse argument ending space in value header', (t) => {
t.plan(1)
const args = Autocannon.parseArguments([
'-H', 'header1=foo=bar ',
'http://localhost/foo/bar'
])
t.strictSame(args.headers, {
header1: 'foo=bar '
})
})
test('parse argument with ":" in value header', (t) => {
t.plan(1)
const args = Autocannon.parseArguments([
'-H', 'header1=foo:bar',
'http://localhost/foo/bar'
])
t.strictSame(args.headers, {
header1: 'foo:bar'
})
})
test('parse argument not correctly formatted header', (t) => {
t.plan(1)
t.throws(() => {
Autocannon.parseArguments([
'-H', 'header1',
'http://localhost/foo/bar'
])
}, /An HTTP header was not correctly formatted/)
})
test('parse argument with multiple url', (t) => {
t.plan(2)
const args = Autocannon.parseArguments([
'localhost/foo/bar',
'http://localhost/baz/qux'
])
t.equal(args.url[0], 'http://localhost/foo/bar')
t.equal(args.url[1], 'http://localhost/baz/qux')
})
test('parse argument with input file and multiple workers', (t) => {
t.plan(3)
const inputPath = 'help.txt'
const args = Autocannon.parseArguments([
'-m', 'POST',
'-w', '2',
'-a', 10,
'-i', inputPath,
'-H', 'Content-Type=application/json',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.equal(args.method, 'POST')
t.equal(args.body, fs.readFileSync(inputPath, 'utf8'))
})
test('parse argument with cert, key and multiple ca paths', (t) => {
t.plan(5)
const certPath = 'test/cert.pem'
const keyPath = 'test/key.pem'
const caPath1 = 'help.txt'
const caPath2 = 'package.json'
const args = Autocannon.parseArguments([
'-m', 'POST',
'--cert', certPath,
'--key', keyPath,
'--ca', '[', caPath1, caPath2, ']',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.equal(args.method, 'POST')
t.same(args.tlsOptions.cert, fs.readFileSync(certPath))
t.same(args.tlsOptions.key, fs.readFileSync(keyPath))
t.same(args.tlsOptions.ca, [fs.readFileSync(caPath1), fs.readFileSync(caPath2)])
})
test('parse argument with cert, key and single ca path', (t) => {
t.plan(5)
const certPath = 'test/cert.pem'
const keyPath = 'test/key.pem'
const caPath = 'help.txt'
const args = Autocannon.parseArguments([
'-m', 'POST',
'--cert', certPath,
'--key', keyPath,
'--ca', caPath,
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.equal(args.method, 'POST')
t.same(args.tlsOptions.cert, fs.readFileSync(certPath))
t.same(args.tlsOptions.key, fs.readFileSync(keyPath))
t.same(args.tlsOptions.ca, [fs.readFileSync(caPath)])
})
================================================
FILE: test/basic-auth.test.js
================================================
'use strict'
const t = require('tap')
const split = require('split2')
const path = require('path')
const childProcess = require('child_process')
const helper = require('./helper')
const lines = [
/Running 1s test @ .*$/,
/10 connections.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 10*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
t.plan(lines.length * 2)
const server = helper.startBasicAuthServer()
const url = 'http://foo:bar@localhost:' + server.address().port
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
child.kill()
})
child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
================================================
FILE: test/cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEVzCCAz+gAwIBAgIJAJn02lrTTFjxMA0GCSqGSIb3DQEBBQUAMHoxCzAJBgNV
BAYTAklFMRIwEAYDVQQIEwl3YXRlcmZvcmQxDTALBgNVBAcTBHRlc3QxDTALBgNV
BAoTBHRlc3QxDTALBgNVBAsTBHRlc3QxDTALBgNVBAMTBHRlc3QxGzAZBgkqhkiG
9w0BCQEWDHRlc0B0ZXN0LmNvbTAeFw0xNjA2MjcxNDE4MDdaFw0xOTAzMjMxNDE4
MDdaMHoxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwl3YXRlcmZvcmQxDTALBgNVBAcT
BHRlc3QxDTALBgNVBAoTBHRlc3QxDTALBgNVBAsTBHRlc3QxDTALBgNVBAMTBHRl
c3QxGzAZBgkqhkiG9w0BCQEWDHRlc0B0ZXN0LmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAK0PhkEuPPEQNc1/96rapuEazVaa5p74QAn4PNOPIKaz
XWyLheBF78N320w6jB4eqAe3o6XMtt28iK+q+HejLZt7v+m6c7lHDtfcLSG8CEJ3
dfwR/iOfCLRlDeZyWvxouf9/s3FSAM5VqKb9kmc/Pt2+opWlX1cZvdfkg/lzSHUu
FwmuxOAONKt2dPiEvDSiSs99Kv0+jSgMmy+4D8LGyvxFCQu67bh6a2zGEEYAcAib
Rpw+Fb/AK8VYPW528SaWHRT7CcDgzdXaMfos3EWOQ/Cc0Q+MgqVfSmqTEUPXAc41
Y4Lvvl5GSHQ4lve3jIR05xenxcMIZ8BP7fJ3BfjXCxsCAwEAAaOB3zCB3DAdBgNV
HQ4EFgQUYtl9YCe7XZ4F0MvA627f+BOJoVYwgawGA1UdIwSBpDCBoYAUYtl9YCe7
XZ4F0MvA627f+BOJoVahfqR8MHoxCzAJBgNVBAYTAklFMRIwEAYDVQQIEwl3YXRl
cmZvcmQxDTALBgNVBAcTBHRlc3QxDTALBgNVBAoTBHRlc3QxDTALBgNVBAsTBHRl
c3QxDTALBgNVBAMTBHRlc3QxGzAZBgkqhkiG9w0BCQEWDHRlc0B0ZXN0LmNvbYIJ
AJn02lrTTFjxMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJN7pnlv
SascD2V0I+9wirpuuNnfUP5YCFaAjky4DREV+DRPXEL2tzTQqsEeFts8BHBH+Jz3
ytYP5NZSjuToF9czu8v3+mPCSqzdOFKruJbl/lAokLJWan8Z3qfXWZQL79C2I7Ih
hSBnH/O+jZz9FPRJ2ydR8DB0LdGVKkQFvZynPZOh7D4NKvrEgFad4p6EBFshO+8N
1ALfR/2mrJOkBHfHPVWMmy6DoXWyVijPuLaa+l2TzdQJycl6CAJw6F7tPoO75qKY
MAcIKOW5F9Zv7I3aqmoLDOaOh43FeT2JLvODe2TIaytWckoFesGadEgvAzCAXC4r
ArqQX2nVUdasOnQ=
-----END CERTIFICATE-----
================================================
FILE: test/cli-ipc.test.js
================================================
'use strict'
const t = require('tap')
const split = require('split2')
const os = require('os')
const path = require('path')
const childProcess = require('child_process')
const helper = require('./helper')
const win = process.platform === 'win32'
const lines = [
/Running 1s test @ http:\/\/example.com\/foo \([^)]*\)$/,
/10 connections.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 10*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
if (!win) {
// If not Windows we can predict exactly how many lines there will be. On
// Windows we rely on t.end() being called.
t.plan(lines.length)
}
t.autoend(false)
t.teardown(function () {
child.kill()
})
const socketPath = win
? path.join('\\\\?\\pipe', process.cwd(), 'autocannon-' + Date.now())
: path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock')
helper.startServer({ socketPath })
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', '-S', socketPath, 'example.com/foo'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
// For handling the last line on Windows
let errorLine = false
let failsafeTimer
child
.stderr
.pipe(split())
.on('data', (line) => {
let regexp = lines.shift()
const lastLine = lines.length === 0
if (regexp) {
t.ok(regexp.test(line), 'line matches ' + regexp)
if (lastLine && win) {
// We can't be sure the error line is outputted on Windows, so in case
// this really is the last line, we'll set a timer to auto-end the test
// in case there are no more lines.
failsafeTimer = setTimeout(function () {
t.end()
}, 1000)
}
} else if (!errorLine && win) {
// On Windows a few errors are expected. We'll accept a 1% error rate on
// the pipe.
errorLine = true
clearTimeout(failsafeTimer)
regexp = /^(\d+) errors \(0 timeouts\)$/
const match = line.match(regexp)
t.ok(match, 'line matches ' + regexp)
const errors = Number(match[1])
t.ok(errors / 15000 < 0.01, `should have less than 1% errors on Windows (had ${errors} errors)`)
t.end()
} else {
throw new Error('Unexpected line: ' + JSON.stringify(line))
}
})
================================================
FILE: test/cli.test.js
================================================
'use strict'
const test = require('tap').test
const split = require('split2')
const path = require('path')
const fs = require('fs')
const os = require('os')
const childProcess = require('child_process')
const helper = require('./helper')
const { hasWorkerSupport } = require('../lib/util')
test('should run benchmark against server', (t) => {
const lines = [
/Running 1s test @ .*$/,
/10 connections.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 1.*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
t.plan(lines.length * 2)
const server = helper.startServer()
const url = 'http://localhost:' + server.address().port
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
.on('end', t.end)
})
test('should parse HAR file and run requests', (t) => {
const lines = [
/Running \d+ requests test @ .*$/,
/1 connections.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 1.*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
t.plan(lines.length)
const server = helper.startServer()
const url = `http://localhost:${server.address().port}`
const harPath = path.join(os.tmpdir(), 'autocannon-test.har')
const har = helper.customizeHAR('./fixtures/httpbin-simple-get.json', 'https://httpbin.org', url)
fs.writeFileSync(harPath, JSON.stringify(har))
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-a', 4, '-c', 1, '--har', harPath, url], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp.test(line), `"${line}" matches ${regexp}`)
})
.on('end', t.end)
})
test('should throw on unknown HAR file', (t) => {
t.plan(1)
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-a', 4, '-c', 1, '--har', 'does not exist', 'http://localhost'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
const lines = []
child
.stderr
.pipe(split())
.on('data', line => lines.push(line))
.on('end', () => {
const output = lines.join('\n')
t.ok(output.includes('Error: Failed to load HAR file content: ENOENT'), `Unexpected output:\n${output}`)
t.end()
})
})
test('should throw on invalid HAR file', (t) => {
t.plan(1)
const harPath = path.join(os.tmpdir(), 'autocannon-test.har')
fs.writeFileSync(harPath, 'not valid JSON content')
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-a', 4, '-c', 1, '--har', harPath, 'http://localhost'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
const lines = []
child
.stderr
.pipe(split())
.on('data', line => lines.push(line))
.on('end', () => {
const output = lines.join('\n')
t.ok(output.includes('Error: Failed to load HAR file content: Unexpected token'), `Unexpected output:\n${output}`)
t.end()
})
})
test('should write warning about unused HAR requests', (t) => {
t.plan(1)
const server = helper.startServer()
const url = `http://localhost:${server.address().port}`
const harPath = path.join(os.tmpdir(), 'autocannon-test.har')
const har = helper.customizeHAR('./fixtures/multi-domains.json', 'https://httpbin.org', url)
fs.writeFileSync(harPath, JSON.stringify(har))
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-a', 4, '-c', 1, '--har', harPath, url], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
const lines = []
child
.stderr
.pipe(split())
.on('data', line => lines.push(line))
.on('end', () => {
const output = lines.join('\n')
t.ok(output.includes(`Warning: skipping requests to 'https://github.com' as the target is ${url}`), `Unexpected output:\n${output}`)
t.end()
})
})
test('run with workers', { skip: !hasWorkerSupport }, (t) => {
const lines = [
/Running 1s test @ .*$/,
/10 connections.*$/,
/4 workers.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/.*/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/.*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
t.plan(lines.length * 2)
const server = helper.startServer()
const url = 'http://localhost:' + server.address().port
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url, '--workers', '4'], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
.on('end', t.end)
})
test('should run handle PUT bodies', (t) => {
t.test('"number" bodies work', t => {
t.plan(2)
const server = helper.startServer()
const url = 'http://localhost:' + server.address().port
const cmd = [
path.join(__dirname, '..'),
'-d', '1',
'-m', 'PUT',
'-H', 'content-type="application/x-www-form-urlencoded"',
'-b', '1',
url
]
const child = childProcess.spawn(process.execPath, cmd, {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
const outputLines = []
child
.stderr
.pipe(split())
.on('data', (line) => {
outputLines.push(line)
})
.on('end', () => {
t.equal(
outputLines.some(l => l === 'body must be either a string or a buffer'),
false
)
t.equal(
/.* requests in ([0-9]|\.)+s, .* read/.test(outputLines.pop()),
true
)
t.end()
})
})
t.test('"string" bodies work', t => {
t.plan(2)
const server = helper.startServer()
const url = 'http://localhost:' + server.address().port
const cmd = [
path.join(__dirname, '..'),
'-d', '1',
'-m', 'PUT',
'-H', 'content-type="application/x-www-form-urlencoded"',
'-b', '"1"',
url
]
const child = childProcess.spawn(process.execPath, cmd, {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
const outputLines = []
child
.stderr
.pipe(split())
.on('data', (line) => {
outputLines.push(line)
})
.on('end', () => {
t.equal(
outputLines.some(l => l === 'body must be either a string or a buffer'),
false
)
t.equal(
/.* requests in ([0-9]|\.)+s, .* read/.test(outputLines.pop()),
true
)
t.end()
})
})
t.end()
})
================================================
FILE: test/debug.test.js
================================================
'use strict'
const test = require('tap').test
const Autocannon = require('../autocannon')
test('debug works', (t) => {
t.plan(5)
const args = Autocannon.parseArguments([
'-H', 'X-Http-Method-Override=GET',
'-m', 'POST',
'-b', 'the body',
'--debug',
'http://localhost/foo/bar'
])
t.equal(args.url, 'http://localhost/foo/bar')
t.strictSame(args.headers, { 'X-Http-Method-Override': 'GET' })
t.equal(args.method, 'POST')
t.equal(args.body, 'the body')
t.equal(args.debug, true)
})
================================================
FILE: test/envPort.test.js
================================================
'use strict'
const why = require('why-is-node-running')
const t = require('tap')
const split = require('split2')
const path = require('path')
const childProcess = require('child_process')
const helper = require('./helper')
setInterval(function () {
console.log(why())
}, 30000).unref()
const lines = [
/Running 1s test @ .*$/,
/10 connections.*$/,
/$/,
/.*/,
/$/,
/Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/,
/.*/,
/Latency.*$/,
/$/,
/.*/,
/Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/,
/.*/,
/Req\/Sec.*$/,
/$/,
/Bytes\/Sec.*$/,
/.*/,
/$/,
/Req\/Bytes counts sampled once per second.*$/,
/# of samples: 10*$/,
/$/,
/.* requests in ([0-9]|\.)+s, .* read/
]
t.plan(lines.length * 2 + 2)
const server = helper.startServer()
const port = server.address().port
const url = '/path' // no hostname
const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', url], {
cwd: __dirname,
env: Object.assign({}, process.env, {
PORT: port
}),
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
t.teardown(() => {
try {
child.kill()
} catch {}
})
child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})
.on('end', () => {
t.ok(server.autocannonConnects > 0, 'targeted the correct port')
})
const noPortChild = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), url], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})
noPortChild.on('exit', (code) => {
t.equal(code, 1, 'should exit with error when a hostless URL is passed and no PORT var is available')
})
================================================
FILE: test/fixtures/example-result.json
================================================
{
"title": "example result",
"url": "https://httpbin.org/get",
"requests": {
"average": 250,
"mean": 250,
"stddev": 117,
"min": 133,
"max": 367,
"total": 500,
"p0_001": 133,
"p0_01": 133,
"p0_1": 133,
"p1": 133,
"p2_5": 133,
"p10": 133,
"p25": 133,
"p50": 133,
"p75": 367,
"p90": 367,
"p97_5": 367,
"p99": 367,
"p99_9": 367,
"p99_99": 367,
"p99_999": 367,
"sent": 500
},
"latency": {
"average": 100.1,
"mean": 100.1,
"stddev": 84.74,
"min": 47,
"max": 424,
"p0_001": 47,
"p0_01": 47,
"p0_1": 47,
"p1": 53,
"p2_5": 55,
"p10": 58,
"p25": 59,
"p50": 67,
"p75": 107,
"p90": 126,
"p97_5": 378,
"p99": 386,
"p99_9": 424,
"p99_99": 424,
"p99_999": 424,
"totalCount": 500
},
"throughput": {
"average": 52468,
"mean": 52468,
"stddev": 24556,
"min": 27915,
"max": 77030,
"total": 104945,
"p0_001": 27919,
"p0_01": 27919,
"p0_1": 27919,
"p1": 27919,
"p2_5": 27919,
"p10": 27919,
"p25": 27919,
"p50": 27919,
"p75": 77055,
"p90": 77055,
"p97_5": 77055,
"p99": 77055,
"p99_9": 77055,
"p99_99": 77055,
"p99_999": 77055
},
"errors": 0,
"timeouts": 0,
"mismatches": 0,
"duration": 2.06,
"sampleInt": 1000,
"samples": 10,
"start": "2020-11-06T09:38:31.027Z",
"finish": "2020-11-06T09:38:33.089Z",
"connections": 40,
"pipelining": 1,
"non2xx": 0,
"resets": 0,
"1xx": 0,
"2xx": 500,
"3xx": 0,
"4xx": 0,
"5xx": 0,
"statusCodeStats": {
"200": { "count": "500" },
"302": { "count": "0" },
"401": { "count": "0" },
"403": { "count": "0" }
}
}
================================================
FILE: test/fixtures/httpbin-get.json
================================================
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "80.0.1"
},
"browser": {
"name": "Firefox",
"version": "80.0.1"
},
"pages": [
{
"startedDateTime": "2020-09-28T20:47:18.735+02:00",
"id": "page_1",
"title": "https://httpbin.org/post",
"pageTimings": {
"onContentLoad": 170,
"onLoad": 187
}
}
],
"entries": [
{
"pageref": "page_2",
"startedDateTime": "2020-09-28T22:03:15.885+02:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://httpbin.org/get",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
}
],
"cookies": [],
"queryString": [],
"headersSize": 272
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 20:03:16 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "461"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 461,
"text": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f724184-c2b2dc0aa856f9d04dac5d35\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get\"\n}\n"
},
"redirectURL": "",
"headersSize": 224,
"bodySize": 685
},
"cache": {},
"timings": {
"blocked": 184,
"dns": 8,
"connect": 85,
"ssl": 88,
"send": 0,
"wait": 86,
"receive": 0
},
"time": 451,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
},
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:46:29.772+02:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://httpbin.org/get?from=10&size=20&sort=+name",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [
{
"name": "from",
"value": "10"
},
{
"name": "size",
"value": "20"
},
{
"name": "sort",
"value": " name"
}
],
"headersSize": 299
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:46:29 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "549"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 549,
"text": "{\n \"args\": {\n \"from\": \"10\", \n \"size\": \"20\", \n \"sort\": \" name\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723d95-9675a1b77254a636c2f09fa3\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get?from=10&size=20&sort=+name\"\n}\n"
},
"redirectURL": "",
"headersSize": 224,
"bodySize": 773
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 168,
"receive": 0
},
"time": 168,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
}
]
}
}
================================================
FILE: test/fixtures/httpbin-post.json
================================================
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "80.0.1"
},
"browser": {
"name": "Firefox",
"version": "80.0.1"
},
"pages": [
{
"startedDateTime": "2020-09-28T21:41:16.913+02:00",
"id": "page_1",
"title": "httpbin.org",
"pageTimings": {
"onContentLoad": -100471,
"onLoad": -100459
}
}
],
"entries": [
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:41:16.913+02:00",
"request": {
"bodySize": 362,
"method": "POST",
"url": "https://httpbin.org/post",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "Content-Type",
"value": "multipart/form-data; boundary=---------------------------31420230025845772252453285324"
},
{
"name": "Origin",
"value": "https://httpbin.org"
},
{
"name": "Content-Length",
"value": "362"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [],
"headersSize": 426,
"postData": {
"mimeType": "multipart/form-data; boundary=---------------------------31420230025845772252453285324",
"params": [],
"text": "-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\na text value\r\n-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name=\"file\"; filename=\"blob\"\r\nContent-Type: application/octet-stream\r\n\r\nHello World!\n\r\n-----------------------------31420230025845772252453285324--\r\n"
}
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:41:16 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "766"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "https://httpbin.org"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 766,
"text": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {\n \"file\": \"Hello World!\\n\"\n }, \n \"form\": {\n \"text\": \"a text value\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Content-Length\": \"362\", \n \"Content-Type\": \"multipart/form-data; boundary=---------------------------31420230025845772252453285324\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Origin\": \"https://httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723c5c-b0e8a0609820985731015e80\"\n }, \n \"json\": null, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/post\"\n}\n"
},
"redirectURL": "",
"headersSize": 242,
"bodySize": 1008
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 89,
"receive": 0
},
"time": 89,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
},
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:43:55.877+02:00",
"request": {
"bodySize": 27,
"method": "POST",
"url": "https://httpbin.org/post",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
},
{
"name": "Origin",
"value": "https://httpbin.org"
},
{
"name": "Content-Length",
"value": "27"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [],
"headersSize": 372,
"postData": {
"mimeType": "application/x-www-form-urlencoded",
"params": [
{
"name": "text",
"value": "a text value"
},
{
"name": "number",
"value": "10"
}
],
"text": "text=a+text+value&number=10"
}
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:43:55 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "701"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "https://httpbin.org"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 701,
"text": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {\n \"number\": \"10\", \n \"text\": \"a text value\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Content-Length\": \"27\", \n \"Content-Type\": \"application/x-www-form-urlencoded\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Origin\": \"https://httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723cfb-880669c8b0b3a9628bea7db3\"\n }, \n \"json\": null, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/post\"\n}\n"
},
"redirectURL": "",
"headersSize": 242,
"bodySize": 943
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 1,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 91,
"receive": 0
},
"time": 92,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
}
]
}
}
================================================
FILE: test/fixtures/httpbin-simple-get.json
================================================
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "80.0.1"
},
"browser": {
"name": "Firefox",
"version": "80.0.1"
},
"entries": [
{
"startedDateTime": "2020-09-28T22:03:15.885+02:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://httpbin.org/get",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
}
],
"cookies": [],
"queryString": [],
"headersSize": 272
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 20:03:16 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "461"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 461,
"text": "{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f724184-c2b2dc0aa856f9d04dac5d35\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get\"\n}\n"
},
"redirectURL": "",
"headersSize": 224,
"bodySize": 685
},
"cache": {},
"timings": {
"blocked": 184,
"dns": 8,
"connect": 85,
"ssl": 88,
"send": 0,
"wait": 86,
"receive": 0
},
"time": 451,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
}
]
}
}
================================================
FILE: test/fixtures/multi-domains.json
================================================
{
"log": {
"version": "1.2",
"creator": {
"name": "Firefox",
"version": "80.0.1"
},
"browser": {
"name": "Firefox",
"version": "80.0.1"
},
"pages": [
{
"startedDateTime": "2020-09-28T21:41:16.913+02:00",
"id": "page_1",
"title": "httpbin.org",
"pageTimings": {
"onContentLoad": -100471,
"onLoad": -100459
}
}
],
"entries": [
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:41:16.913+02:00",
"request": {
"bodySize": 362,
"method": "POST",
"url": "https://httpbin.org/post",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "Content-Type",
"value": "multipart/form-data; boundary=---------------------------31420230025845772252453285324"
},
{
"name": "Origin",
"value": "https://httpbin.org"
},
{
"name": "Content-Length",
"value": "362"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [],
"headersSize": 426,
"postData": {
"mimeType": "multipart/form-data; boundary=---------------------------31420230025845772252453285324",
"params": [],
"text": "-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name=\"text\"\r\n\r\na text value\r\n-----------------------------31420230025845772252453285324\r\nContent-Disposition: form-data; name=\"file\"; filename=\"blob\"\r\nContent-Type: application/octet-stream\r\n\r\nHello World!\n\r\n-----------------------------31420230025845772252453285324--\r\n"
}
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:41:16 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "766"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "https://httpbin.org"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 766,
"text": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {\n \"file\": \"Hello World!\\n\"\n }, \n \"form\": {\n \"text\": \"a text value\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Content-Length\": \"362\", \n \"Content-Type\": \"multipart/form-data; boundary=---------------------------31420230025845772252453285324\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Origin\": \"https://httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723c5c-b0e8a0609820985731015e80\"\n }, \n \"json\": null, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/post\"\n}\n"
},
"redirectURL": "",
"headersSize": 242,
"bodySize": 1008
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 89,
"receive": 0
},
"time": 89,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
},
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:43:55.877+02:00",
"request": {
"bodySize": 27,
"method": "POST",
"url": "https://github.com",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "github.com"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://github.com/"
},
{
"name": "Content-Type",
"value": "application/x-www-form-urlencoded"
},
{
"name": "Origin",
"value": "https://github.com"
},
{
"name": "Content-Length",
"value": "27"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [],
"headersSize": 372,
"postData": {
"mimeType": "application/x-www-form-urlencoded",
"params": [
{
"name": "text",
"value": "a text value"
},
{
"name": "number",
"value": "10"
}
],
"text": "text=a+text+value&number=10"
}
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:43:55 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "701"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "https://httpbin.org"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 701,
"text": "{\n \"args\": {}, \n \"data\": \"\", \n \"files\": {}, \n \"form\": {\n \"number\": \"10\", \n \"text\": \"a text value\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Content-Length\": \"27\", \n \"Content-Type\": \"application/x-www-form-urlencoded\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Origin\": \"https://httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723cfb-880669c8b0b3a9628bea7db3\"\n }, \n \"json\": null, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/post\"\n}\n"
},
"redirectURL": "",
"headersSize": 242,
"bodySize": 943
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 1,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 91,
"receive": 0
},
"time": 92,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
},
{
"pageref": "page_1",
"startedDateTime": "2020-09-28T21:46:29.772+02:00",
"request": {
"bodySize": 0,
"method": "GET",
"url": "https://httpbin.org/get?from=10&size=20&sort=+name",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "Host",
"value": "httpbin.org"
},
{
"name": "User-Agent",
"value": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "Accept-Language",
"value": "fr,en;q=0.5"
},
{
"name": "Accept-Encoding",
"value": "gzip, deflate, br"
},
{
"name": "Referer",
"value": "https://httpbin.org/"
},
{
"name": "DNT",
"value": "1"
},
{
"name": "Connection",
"value": "keep-alive"
},
{
"name": "TE",
"value": "Trailers"
}
],
"cookies": [],
"queryString": [
{
"name": "from",
"value": "10"
},
{
"name": "size",
"value": "20"
},
{
"name": "sort",
"value": " name"
}
],
"headersSize": 299
},
"response": {
"status": 200,
"statusText": "OK",
"httpVersion": "HTTP/2",
"headers": [
{
"name": "date",
"value": "Mon, 28 Sep 2020 19:46:29 GMT"
},
{
"name": "content-type",
"value": "application/json"
},
{
"name": "content-length",
"value": "549"
},
{
"name": "server",
"value": "gunicorn/19.9.0"
},
{
"name": "access-control-allow-origin",
"value": "*"
},
{
"name": "access-control-allow-credentials",
"value": "true"
},
{
"name": "X-Firefox-Spdy",
"value": "h2"
}
],
"cookies": [],
"content": {
"mimeType": "application/json",
"size": 549,
"text": "{\n \"args\": {\n \"from\": \"10\", \n \"size\": \"20\", \n \"sort\": \" name\"\n }, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"gzip, deflate, br\", \n \"Accept-Language\": \"fr,en;q=0.5\", \n \"Dnt\": \"1\", \n \"Host\": \"httpbin.org\", \n \"Referer\": \"https://httpbin.org/\", \n \"User-Agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0\", \n \"X-Amzn-Trace-Id\": \"Root=1-5f723d95-9675a1b77254a636c2f09fa3\"\n }, \n \"origin\": \"78.192.173.27\", \n \"url\": \"https://httpbin.org/get?from=10&size=20&sort=+name\"\n}\n"
},
"redirectURL": "",
"headersSize": 224,
"bodySize": 773
},
"cache": {},
"timings": {
"blocked": 0,
"dns": 0,
"connect": 0,
"ssl": 0,
"send": 0,
"wait": 168,
"receive": 0
},
"time": 168,
"_securityState": "secure",
"serverIPAddress": "35.170.21.246",
"connection": "443"
}
]
}
}
================================================
FILE: test/forever.test.js
================================================
'use strict'
const test = require('tap').test
const initJob = require('../lib/init')
const helper = require('./helper')
const server = helper.startServer()
test('running with forever set to true and passing in a callback should cause an error to be returned in the callback', (t) => {
t.plan(2)
initJob({
url: `http://localhost:${server.address().port}`,
forever: true
}, (err, res) => {
t.ok(err, 'should be error when callback passed to run')
t.notOk(res, 'should not exist')
t.end()
})
})
test('run forever should run until .stop() is called', (t) => {
t.plan(3)
let numRuns = 0
const instance = initJob({
url: `http://localhost:${server.address().port}`,
duration: 0.5,
forever: true
})
instance.on('done', (results) => {
t.ok(results, 'should have gotten results')
if (++numRuns === 2) {
instance.stop()
setTimeout(() => {
t.ok(true, 'should have reached here without the callback being called again')
t.end()
}, 1000)
}
})
})
================================================
FILE: test/format.test.js
================================================
'use strict'
const test = require('tap').test
const format = require('../lib/format')
const pairs = {
2: 2,
'2k': 2000,
'4k': 4042,
'2300k': 2300000
}
Object.keys(pairs).forEach((expected) => {
const original = pairs[expected]
test(`format ${original} into ${expected}`, (t) => {
t.equal(expected, format(original))
t.end()
})
})
================================================
FILE: test/helper.js
================================================
'use strict'
const http = require('http')
const https = require('https')
const tls = require('tls')
const fs = require('fs')
const path = require('path')
const BusBoy = require('busboy')
function startServer (opts) {
opts = opts || {}
if (!Array.isArray(opts.responses)) {
opts.responses = []
}
const fixedStatusCode = opts.statusCode || 200
const server = http.createServer(handle)
server.autocannonConnects = 0
server.autocannonRequests = 0
server.on('connection', () => { server.autocannonConnects++ })
server.listen(opts.socketPath || 0)
function handle (req, res) {
let { statusCode, body, headers } = opts.responses[server.autocannonRequests] || {}
server.autocannonRequests++
if (!statusCode) {
statusCode = fixedStatusCode
}
if (!body) {
body = opts.body || 'hello world'
}
res.statusCode = statusCode
const reply = () => {
const bodyToWrite = typeof body === 'function' ? body(req) : body
if (headers) {
res.writeHead(statusCode, headers)
}
res.end(statusCode < 200 ? undefined : bodyToWrite)
}
if (opts.delayResponse) {
setTimeout(reply, opts.delayResponse)
} else {
reply()
}
}
server.unref()
return server
}
function startTrailerServer () {
const server = http.createServer(handle)
function handle (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain', Trailer: 'Content-MD5' })
res.write('hello ')
res.addTrailers({ 'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667' })
res.end('world')
}
server.listen(0)
server.unref()
return server
}
// this server won't reply to requests
function startTimeoutServer () {
const server = http.createServer(() => {})
server.listen(0)
server.unref()
return server
}
// this server destroys the socket on connection, should result in ECONNRESET
function startSocketDestroyingServer () {
const server = http.createServer(handle)
function handle (req, res) {
res.destroy()
server.close()
}
server.listen(0)
server.unref()
return server
}
// this server won't reply to
gitextract_p81k9btq/
├── .github/
│ ├── dependabot.yml
│ ├── release-drafter.yml
│ ├── tests_checker.yml
│ └── workflows/
│ └── nodejs.yml
├── .gitignore
├── .npmignore
├── .taprc
├── LICENSE
├── README.md
├── autocannon.js
├── cluster.js
├── for-zero-x.js
├── help.txt
├── lib/
│ ├── aggregateResult.js
│ ├── defaultOptions.js
│ ├── format.js
│ ├── histUtil.js
│ ├── httpClient.js
│ ├── httpMethods.js
│ ├── httpRequestBuilder.js
│ ├── init.js
│ ├── manager.js
│ ├── multipart.js
│ ├── noop.js
│ ├── parseHAR.js
│ ├── pipelinedRequestsQueue.js
│ ├── preload/
│ │ └── autocannonDetectPort.js
│ ├── printResult.js
│ ├── progressTracker.js
│ ├── requestIterator.js
│ ├── run.js
│ ├── runTracker.js
│ ├── subargAliases.js
│ ├── url.js
│ ├── util.js
│ ├── validate.js
│ ├── worker.js
│ └── worker_threads.js
├── package.json
├── samples/
│ ├── bench-multi-url.js
│ ├── customise-individual-connection.js
│ ├── customise-verifyBody-workers.js
│ ├── customise-verifyBody.js
│ ├── helpers/
│ │ ├── on-response.js
│ │ ├── setup-request.js
│ │ └── verify-body.js
│ ├── init-context.js
│ ├── modifying-request.js
│ ├── request-context-workers.js
│ ├── request-context.js
│ ├── requests-sample.js
│ ├── track-run-workers.js
│ ├── track-run.js
│ └── using-id-replacement.js
├── server.js
└── test/
├── aggregateResult.test.js
├── argumentParsing.test.js
├── basic-auth.test.js
├── cert.pem
├── cli-ipc.test.js
├── cli.test.js
├── debug.test.js
├── envPort.test.js
├── fixtures/
│ ├── example-result.json
│ ├── httpbin-get.json
│ ├── httpbin-post.json
│ ├── httpbin-simple-get.json
│ └── multi-domains.json
├── forever.test.js
├── format.test.js
├── helper.js
├── httpClient.test.js
├── httpRequestBuilder.test.js
├── key.pem
├── keystore.pkcs12
├── onPort.test.js
├── parseHAR.test.js
├── pipelinedRequestsQueue.test.js
├── printResult-process.js
├── printResult-renderStatusCodes.test.js
├── printResult.test.js
├── progressTracker.test.js
├── progressTracker.test.stub.js
├── requestIterator.test.js
├── run.test.js
├── runAmount.test.js
├── runMultiServer.test.js
├── runMultipart.test.js
├── runRate.test.js
├── sampleInt.test.js
├── serial/
│ ├── autocannon.test.js
│ ├── run.test.js
│ ├── tap-parallel-not-ok
│ └── wasm.test.js
├── subargAliases.test.js
├── tap-parallel-ok
├── targetProcess.js
├── url.test.js
├── utils/
│ ├── has-worker-support.js
│ ├── on-response.js
│ ├── setup-client.js
│ ├── setup-request.js
│ └── verify-body.js
├── validate.test.js
└── workers.test.js
SYMBOL INDEX (95 symbols across 39 files)
FILE: autocannon.js
constant URL (line 10) | const URL = require('url').URL
function parseArguments (line 106) | function parseArguments (argvs) {
function start (line 255) | function start (argv) {
function createChannel (line 312) | function createChannel (onport) {
FILE: lib/aggregateResult.js
function aggregateResult (line 5) | function aggregateResult (results, opts, histograms) {
FILE: lib/format.js
function format (line 3) | function format (num) {
FILE: lib/histUtil.js
function encodeHist (line 37) | function encodeHist (h) {
function decodeHist (line 43) | function decodeHist (str) {
FILE: lib/httpClient.js
function Client (line 14) | function Client (opts) {
FILE: lib/httpRequestBuilder.js
function requestBuilder (line 8) | function requestBuilder (defaults) {
FILE: lib/init.js
function init (line 12) | function init (opts, cb) {
function run (line 25) | function run (opts) {
function runWithWarmup (line 39) | function runWithWarmup (opts) {
function _init (line 63) | function _init (opts, tracker, cb) {
FILE: lib/manager.js
function initWorkers (line 9) | function initWorkers (opts, tracker, cb) {
FILE: lib/multipart.js
function getFormData (line 7) | function getFormData (string) {
FILE: lib/parseHAR.js
function parseHAR (line 7) | function parseHAR (har) {
FILE: lib/pipelinedRequestsQueue.js
class PipelinedRequestsQueue (line 13) | class PipelinedRequestsQueue {
method constructor (line 14) | constructor () {
method insertRequest (line 18) | insertRequest (req) {
method peek (line 28) | peek () {
method addByteCount (line 34) | addByteCount (count) {
method addBody (line 41) | addBody (data) {
method setHeaders (line 48) | setHeaders (headers) {
method size (line 55) | size () {
method terminateRequest (line 62) | terminateRequest () {
method clear (line 71) | clear () {
method toArray (line 75) | toArray () {
FILE: lib/printResult.js
class TableWithoutColor (line 18) | class TableWithoutColor extends Table {
method constructor (line 19) | constructor (opts = {}) {
function logToLocalStr (line 98) | function logToLocalStr (msg) {
function asLowRow (line 106) | function asLowRow (name, stat) {
function asHighRow (line 120) | function asHighRow (name, stat) {
function asColor (line 133) | function asColor (colorise, row) {
function asMs (line 137) | function asMs (stat) {
function asNumber (line 147) | function asNumber (stat) {
function asBytes (line 159) | function asBytes (stat) {
function colorizeByStatusCode (line 174) | function colorizeByStatusCode (chalk, statusCode) {
FILE: lib/progressTracker.js
function track (line 22) | function track (instance, opts) {
function trackDuration (line 137) | function trackDuration (instance, opts, iOpts) {
function trackAmount (line 154) | function trackAmount (instance, opts, iOpts) {
FILE: lib/requestIterator.js
function RequestIterator (line 24) | function RequestIterator (opts) {
FILE: lib/run.js
constant URL (line 3) | const URL = require('url')
function run (line 16) | function run (opts, tracker, cb) {
FILE: lib/runTracker.js
function runTracker (line 7) | function runTracker (opts, tracker, cb) {
FILE: lib/subargAliases.js
function generateSubargAliases (line 13) | function generateSubargAliases (args) {
FILE: lib/url.js
function checkURL (line 7) | function checkURL (url) {
function ofURL (line 18) | function ofURL (url, asArray) {
FILE: lib/util.js
function getPropertyCaseInsensitive (line 6) | function getPropertyCaseInsensitive (obj, key) {
FILE: lib/validate.js
function safeRequire (line 16) | function safeRequire (path) {
function defaultOpts (line 26) | function defaultOpts (opts) {
FILE: lib/worker.js
function runTracker (line 28) | function runTracker (opts, cb) {
FILE: samples/bench-multi-url.js
function createHandler (line 6) | function createHandler (serverName) {
function startBench (line 19) | function startBench () {
FILE: samples/customise-individual-connection.js
function handle (line 12) | function handle (req, res) {
function startBench (line 16) | function startBench () {
FILE: samples/customise-verifyBody-workers.js
function handle (line 11) | function handle (req, res) {
function startBench (line 15) | function startBench () {
FILE: samples/customise-verifyBody.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: samples/init-context.js
function handle (line 10) | function handle (req, res) {
function startBench (line 18) | function startBench () {
FILE: samples/modifying-request.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: samples/request-context-workers.js
function handle (line 11) | function handle (req, res) {
function startBench (line 19) | function startBench () {
FILE: samples/request-context.js
function handle (line 10) | function handle (req, res) {
function startBench (line 18) | function startBench () {
FILE: samples/requests-sample.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: samples/track-run-workers.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: samples/track-run.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: samples/using-id-replacement.js
function handle (line 10) | function handle (req, res) {
function startBench (line 14) | function startBench () {
FILE: server.js
function handle (line 19) | function handle (req, res) {
FILE: test/helper.js
function startServer (line 10) | function startServer (opts) {
function startTrailerServer (line 61) | function startTrailerServer () {
function startTimeoutServer (line 79) | function startTimeoutServer () {
function startSocketDestroyingServer (line 89) | function startSocketDestroyingServer () {
function startHttpsServer (line 104) | function startHttpsServer (opts = {}) {
function startTlsServer (line 125) | function startTlsServer () {
function startMultipartServer (line 166) | function startMultipartServer (opts = {}, test = () => {}) {
function startBasicAuthServer (line 204) | function startBasicAuthServer () {
function customizeHAR (line 223) | function customizeHAR (fixturePath, replaced, domain) {
function noop (line 241) | function noop () {}
FILE: test/printResult.test.js
method write (line 189) | write () {}
method write (line 249) | write () {}
FILE: test/progressTracker.test.js
method write (line 31) | write () {}
FILE: test/requestIterator.test.js
function isUrlSafe (line 211) | function isUrlSafe (string) {
FILE: test/run.test.js
method onResponse (line 765) | onResponse (status, body) {
method onResponse (line 772) | onResponse (status, body) {
method onResponse (line 968) | onResponse (status, body, context, headers) {
method onResponse (line 991) | onResponse (status, body, context, headers) {
Condensed preview — 105 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (365K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 261,
"preview": "version: 2\nupdates:\n - package-ecosystem: github-actions\n directory: '/'\n schedule:\n interval: daily\n ope"
},
{
"path": ".github/release-drafter.yml",
"chars": 48,
"preview": "template: |\n ## What’s Changed\n\n $CHANGES\n"
},
{
"path": ".github/tests_checker.yml",
"chars": 175,
"preview": "comment: 'Could you please add tests to make sure this change works as expected?',\nfileExtensions: ['.php', '.ts', '.js'"
},
{
"path": ".github/workflows/nodejs.yml",
"chars": 1276,
"preview": "name: Node.js CI\n\non: [ push, pull_request ]\n\n# This allows a subsequently queued workflow run to interrupt previous run"
},
{
"path": ".gitignore",
"chars": 2048,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
},
{
"path": ".npmignore",
"chars": 611,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscov"
},
{
"path": ".taprc",
"chars": 98,
"preview": "ts: false\njsx: false\nflow: false\ntimeout: 900\nbranches: 60\nfunctions: 60\nlines: 60\nstatements: 60\n"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Matteo Collina\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 34476,
"preview": "\n\n# autocannon\n\n\nconst fs = require('fs')\nconst os = require("
},
{
"path": "cluster.js",
"chars": 634,
"preview": "'use strict'\n\nconst cluster = require('cluster')\nconst http = require('http')\nconst numCPUs = Math.floor(require('os').c"
},
{
"path": "for-zero-x.js",
"chars": 142,
"preview": "'use strict'\n\nconst autocannon = require('.')\n\nautocannon({\n url: 'http://localhost:3000',\n connections: 10,\n duratio"
},
{
"path": "help.txt",
"chars": 6297,
"preview": "Usage: autocannon [opts] URL\n\nURL is any valid HTTP or HTTPS URL.\nIf the PORT environment variable is set, the URL can b"
},
{
"path": "lib/aggregateResult.js",
"chars": 2687,
"preview": "'use strict'\n\nconst { decodeHist, getHistograms, histAsObj, addPercentiles } = require('./histUtil')\n\nfunction aggregate"
},
{
"path": "lib/defaultOptions.js",
"chars": 429,
"preview": "'use strict'\n\nconst defaultOptions = {\n headers: {},\n method: 'GET',\n duration: 10,\n connections: 10,\n sampleInt: 1"
},
{
"path": "lib/format.js",
"chars": 165,
"preview": "'use strict'\n\nfunction format (num) {\n if (num < 1000) {\n return '' + num\n } else {\n return '' + Math.round(num "
},
{
"path": "lib/histUtil.js",
"chars": 1270,
"preview": "const hdr = require('hdr-histogram-js')\nconst histUtil = require('hdr-histogram-percentiles-obj')\n\nhdr.initWebAssemblySy"
},
{
"path": "lib/httpClient.js",
"chars": 6535,
"preview": "'use strict'\n\nconst inherits = require('util').inherits\nconst EE = require('events').EventEmitter\nconst net = require('n"
},
{
"path": "lib/httpMethods.js",
"chars": 191,
"preview": "// most http methods taken from RFC 7231\n// PATCH is defined in RFC 5789\nmodule.exports = [\n 'GET',\n 'HEAD',\n 'POST',"
},
{
"path": "lib/httpRequestBuilder.js",
"chars": 3486,
"preview": "'use strict'\n\nconst methods = require('./httpMethods')\nconst { getPropertyCaseInsensitive } = require('./util')\n\n// this"
},
{
"path": "lib/init.js",
"chars": 2469,
"preview": "'use strict'\n\nconst EE = require('events').EventEmitter\nconst { isMainThread } = require('./worker_threads')\n\nconst init"
},
{
"path": "lib/manager.js",
"chars": 2812,
"preview": "'use strict'\n\nconst path = require('path')\nconst EE = require('events').EventEmitter\nconst aggregateResult = require('./"
},
{
"path": "lib/multipart.js",
"chars": 1451,
"preview": "'use strict'\n\nconst { resolve, basename } = require('path')\nconst { readFileSync } = require('fs')\nconst FormData = requ"
},
{
"path": "lib/noop.js",
"chars": 64,
"preview": "/* istanbul ignore next */\nmodule.exports = function noop () {}\n"
},
{
"path": "lib/parseHAR.js",
"chars": 1992,
"preview": "'use strict'\n\n// given we support node v8\n// eslint-disable-next-line n/no-deprecated-api\nconst { parse } = require('url"
},
{
"path": "lib/pipelinedRequestsQueue.js",
"chars": 1870,
"preview": "'use strict'\n\n/**\n * A queue (FIFO) to hold pipelined requests and link metadata to them as the response is received fro"
},
{
"path": "lib/preload/autocannonDetectPort.js",
"chars": 270,
"preview": "'use strict'\n\nconst onListen = require('on-net-listen')\nconst net = require('net')\n\nconst socket = net.connect(process.e"
},
{
"path": "lib/printResult.js",
"chars": 5222,
"preview": "'use strict'\n\nconst Table = require('cli-table3')\nconst Chalk = require('chalk')\nconst testColorSupport = require('color"
},
{
"path": "lib/progressTracker.js",
"chars": 5269,
"preview": "/**\n * Prints out the test result details. It doesn't have not much business logic.\n * We skip test coverage for this fi"
},
{
"path": "lib/requestIterator.js",
"chars": 4278,
"preview": "'use strict'\n\nconst hyperid = require('hyperid')\nconst inherits = require('util').inherits\nconst requestBuilder = requir"
},
{
"path": "lib/run.js",
"chars": 8613,
"preview": "'use strict'\n\nconst URL = require('url')\nconst reInterval = require('reinterval')\nconst EE = require('events').EventEmit"
},
{
"path": "lib/runTracker.js",
"chars": 284,
"preview": "'use strict'\n\nconst EE = require('events').EventEmitter\nconst run = require('./run')\nconst noop = require('./noop')\n\nfun"
},
{
"path": "lib/subargAliases.js",
"chars": 958,
"preview": "'use strict'\n\nconst clone = require('lodash.clonedeep')\n\nconst subArgAlias = {\n warmup: {\n c: 'connections',\n d: "
},
{
"path": "lib/url.js",
"chars": 600,
"preview": "'use strict'\n\n/**\n * check the url is not an empty string or empty array\n * @param url\n */\nfunction checkURL (url) {\n r"
},
{
"path": "lib/util.js",
"chars": 353,
"preview": "'use strict'\n\nconst semver = require('semver')\nconst hasWorkerSupport = semver.gte(process.versions.node, '11.7.0')\n\nfun"
},
{
"path": "lib/validate.js",
"chars": 5400,
"preview": "'use strict'\n\nconst defaultOptions = require('./defaultOptions')\nconst timestring = require('timestring')\nconst { checkU"
},
{
"path": "lib/worker.js",
"chars": 1775,
"preview": "'use strict'\n\nconst { isMainThread, parentPort, workerData } = require('worker_threads')\nconst multipart = require('./mu"
},
{
"path": "lib/worker_threads.js",
"chars": 311,
"preview": "'use strict'\n\nlet workerThreads = {}\n\ntry {\n workerThreads = require('worker_threads')\n} catch (err) {\n if (err) {\n "
},
{
"path": "package.json",
"chars": 1939,
"preview": "{\n \"name\": \"autocannon\",\n \"version\": \"8.0.0\",\n \"description\": \"Fast HTTP benchmarking tool written in Node.js\",\n \"ma"
},
{
"path": "samples/bench-multi-url.js",
"chars": 1018,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nfunction createHandler (serverNa"
},
{
"path": "samples/customise-individual-connection.js",
"chars": 604,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/customise-verifyBody-workers.js",
"chars": 583,
"preview": "'use strict'\n\nconst http = require('http')\nconst path = require('path')\nconst autocannon = require('../autocannon')\n\ncon"
},
{
"path": "samples/customise-verifyBody.js",
"chars": 565,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/helpers/on-response.js",
"chars": 167,
"preview": "'use strict'\n\nmodule.exports = (status, body, context) => {\n if (status === 200) {\n context.user = JSON.parse(body)\n"
},
{
"path": "samples/helpers/setup-request.js",
"chars": 172,
"preview": "'use strict'\n\nmodule.exports = (req, context) => ({\n ...req,\n path: `/user/${context.user.id}`,\n body: JSON.stringify"
},
{
"path": "samples/helpers/verify-body.js",
"chars": 59,
"preview": "'use strict'\n\nmodule.exports = (body) => {\n return true\n}\n"
},
{
"path": "samples/init-context.js",
"chars": 1002,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/modifying-request.js",
"chars": 1019,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/request-context-workers.js",
"chars": 1062,
"preview": "'use strict'\n\nconst http = require('http')\nconst path = require('path')\nconst autocannon = require('../autocannon')\n\ncon"
},
{
"path": "samples/request-context.js",
"chars": 1269,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/requests-sample.js",
"chars": 1522,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/track-run-workers.js",
"chars": 629,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/track-run.js",
"chars": 574,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "samples/using-id-replacement.js",
"chars": 850,
"preview": "'use strict'\n\nconst http = require('http')\nconst autocannon = require('../autocannon')\n\nconst server = http.createServer"
},
{
"path": "server.js",
"chars": 496,
"preview": "'use strict'\n\nconst http = require('http')\nconst https = require('https')\nconst fs = require('fs')\nconst path = require("
},
{
"path": "test/aggregateResult.test.js",
"chars": 1002,
"preview": "const { test } = require('tap')\nconst { startServer } = require('./helper')\nconst autocannon = require('../autocannon')\n"
},
{
"path": "test/argumentParsing.test.js",
"chars": 5789,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst Autocannon = require('../autocannon')\nconst fs = require('fs')\n\ntes"
},
{
"path": "test/basic-auth.test.js",
"chars": 1178,
"preview": "'use strict'\n\nconst t = require('tap')\nconst split = require('split2')\nconst path = require('path')\nconst childProcess ="
},
{
"path": "test/cert.pem",
"chars": 1566,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEVzCCAz+gAwIBAgIJAJn02lrTTFjxMA0GCSqGSIb3DQEBBQUAMHoxCzAJBgNV\nBAYTAklFMRIwEAYDVQQIEwl3YXR"
},
{
"path": "test/cli-ipc.test.js",
"chars": 2534,
"preview": "'use strict'\n\nconst t = require('tap')\nconst split = require('split2')\nconst os = require('os')\nconst path = require('pa"
},
{
"path": "test/cli.test.js",
"chars": 8690,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst split = require('split2')\nconst path = require('path')\nconst fs = r"
},
{
"path": "test/debug.test.js",
"chars": 518,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst Autocannon = require('../autocannon')\n\ntest('debug works', (t) => {"
},
{
"path": "test/envPort.test.js",
"chars": 1780,
"preview": "'use strict'\n\nconst why = require('why-is-node-running')\nconst t = require('tap')\nconst split = require('split2')\nconst "
},
{
"path": "test/fixtures/example-result.json",
"chars": 2033,
"preview": "{\r\n \"title\": \"example result\",\r\n \"url\": \"https://httpbin.org/get\",\r\n \"requests\": {\r\n \"average\": 250,\r\n "
},
{
"path": "test/fixtures/httpbin-get.json",
"chars": 7475,
"preview": "{\n \"log\": {\n \"version\": \"1.2\",\n \"creator\": {\n \"name\": \"Firefox\",\n \"version\": \"80.0.1\"\n },\n \"brows"
},
{
"path": "test/fixtures/httpbin-post.json",
"chars": 9509,
"preview": "{\n \"log\": {\n \"version\": \"1.2\",\n \"creator\": {\n \"name\": \"Firefox\",\n \"version\": \"80.0.1\"\n },\n \"brows"
},
{
"path": "test/fixtures/httpbin-simple-get.json",
"chars": 3439,
"preview": "{\n \"log\": {\n \"version\": \"1.2\",\n \"creator\": {\n \"name\": \"Firefox\",\n \"version\": \"80.0.1\"\n },\n \"brows"
},
{
"path": "test/fixtures/multi-domains.json",
"chars": 13249,
"preview": "{\n \"log\": {\n \"version\": \"1.2\",\n \"creator\": {\n \"name\": \"Firefox\",\n \"version\": \"80.0.1\"\n },\n \"brows"
},
{
"path": "test/forever.test.js",
"chars": 1036,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst initJob = require('../lib/init')\nconst helper = require('./helper')"
},
{
"path": "test/format.test.js",
"chars": 355,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst format = require('../lib/format')\n\nconst pairs = {\n 2: 2,\n '2k': "
},
{
"path": "test/helper.js",
"chars": 5932,
"preview": "'use strict'\n\nconst http = require('http')\nconst https = require('https')\nconst tls = require('tls')\nconst fs = require("
},
{
"path": "test/httpClient.test.js",
"chars": 27619,
"preview": "'use strict'\n\nconst os = require('os')\nconst http = require('http')\nconst path = require('path')\nconst test = require('t"
},
{
"path": "test/httpRequestBuilder.test.js",
"chars": 4965,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst helper = require('./helper')\nconst server = helper.startServer()\nco"
},
{
"path": "test/key.pem",
"chars": 1751,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,081E2A4DFAF06358\n\nyfmHgUwn2RV+CGBYKJMBfLu+"
},
{
"path": "test/onPort.test.js",
"chars": 2498,
"preview": "'use strict'\n\nconst path = require('path')\nconst { test } = require('tap')\nconst spawn = require('child_process').spawn\n"
},
{
"path": "test/parseHAR.test.js",
"chars": 7627,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst { parseHAR } = require('../lib/parseHAR')\n\ntest('should throw on em"
},
{
"path": "test/pipelinedRequestsQueue.test.js",
"chars": 2146,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst PipelinedRequestsQueue = require('../lib/pipelinedRequestsQueue')\n\n"
},
{
"path": "test/printResult-process.js",
"chars": 412,
"preview": "'use strict'\r\n\r\nconst autocannon = require('../autocannon')\r\nconst exampleResult = require('./fixtures/example-result.js"
},
{
"path": "test/printResult-renderStatusCodes.test.js",
"chars": 1398,
"preview": "'use strict'\r\n\r\nconst test = require('tap').test\r\nconst split = require('split2')\r\nconst path = require('path')\r\nconst c"
},
{
"path": "test/printResult.test.js",
"chars": 5370,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst split = require('split2')\nconst path = require('path')\nconst childP"
},
{
"path": "test/progressTracker.test.js",
"chars": 880,
"preview": "'use strict'\n\nconst helper = require('./helper')\nconst test = require('tap').test\nconst { defaultMaxListeners } = requir"
},
{
"path": "test/progressTracker.test.stub.js",
"chars": 2044,
"preview": "'use strict'\n\nconst helper = require('./helper')\nconst test = require('tap').test\nconst progressTracker = require('../li"
},
{
"path": "test/requestIterator.test.js",
"chars": 19870,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst helper = require('./helper')\nconst server = helper.startServer()\nco"
},
{
"path": "test/run.test.js",
"chars": 31752,
"preview": "'use strict'\n\nconst os = require('os')\nconst path = require('path')\nconst test = require('tap').test\nconst initJob = req"
},
{
"path": "test/runAmount.test.js",
"chars": 2406,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst initJob = require('../lib/init')\nconst helper = require('./helper')"
},
{
"path": "test/runMultiServer.test.js",
"chars": 1113,
"preview": "const test = require('tap').test\nconst initJob = require('../lib/init')\nconst helper = require('./helper')\n\nconst server"
},
{
"path": "test/runMultipart.test.js",
"chars": 7417,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst { tmpdir } = require('os')\nconst { join } = require('path')\nconst {"
},
{
"path": "test/runRate.test.js",
"chars": 2527,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst initJob = require('../lib/init')\nconst helper = require('./helper')"
},
{
"path": "test/sampleInt.test.js",
"chars": 2480,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst validate = require('../lib/validate')\nconst parseArguments = requir"
},
{
"path": "test/serial/autocannon.test.js",
"chars": 4727,
"preview": "'use strict'\n\nconst childProcess = require('child_process')\nconst fs = require('fs')\nconst path = require('path')\nconst "
},
{
"path": "test/serial/run.test.js",
"chars": 371,
"preview": "'use strict'\nconst test = require('tap').test\nconst initJob = require('../../lib/init')\n\ntest('should log error on conne"
},
{
"path": "test/serial/tap-parallel-not-ok",
"chars": 0,
"preview": ""
},
{
"path": "test/serial/wasm.test.js",
"chars": 972,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst initJob = require('../../lib/init')\nconst helper = require('../help"
},
{
"path": "test/subargAliases.test.js",
"chars": 936,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst generateSubArgAliases = require('../lib/subargAliases')\n\ntest('gene"
},
{
"path": "test/tap-parallel-ok",
"chars": 0,
"preview": ""
},
{
"path": "test/targetProcess.js",
"chars": 63,
"preview": "const server = require('./helper').startServer()\n\nserver.ref()\n"
},
{
"path": "test/url.test.js",
"chars": 1589,
"preview": "'use strict'\n\nconst test = require('tap').test\n// const { ofUrl, checkUrl } = require('../lib/url')\nconst { ofURL, check"
},
{
"path": "test/utils/has-worker-support.js",
"chars": 109,
"preview": "'use strict'\n\nconst semver = require('semver')\n\nmodule.exports = semver.gte(process.versions.node, '11.7.0')\n"
},
{
"path": "test/utils/on-response.js",
"chars": 88,
"preview": "'use strict'\n\nmodule.exports = (status, body, context) => {\n context.foo = 'bar=baz'\n}\n"
},
{
"path": "test/utils/setup-client.js",
"chars": 92,
"preview": "'use strict'\n\nmodule.exports = (client) => {\n client.setHeaders({ custom: 'my-header' })\n}\n"
},
{
"path": "test/utils/setup-request.js",
"chars": 111,
"preview": "'use strict'\n\nmodule.exports = (req, context) => ({\n ...req,\n path: `/test-123?some=thing&${context.foo}`\n})\n"
},
{
"path": "test/utils/verify-body.js",
"chars": 46,
"preview": "module.exports = (body) => {\n return false\n}\n"
},
{
"path": "test/validate.test.js",
"chars": 8516,
"preview": "'use strict'\n\nconst test = require('tap').test\nconst validateOpts = require('../lib/validate')\nconst helper = require('."
},
{
"path": "test/workers.test.js",
"chars": 7078,
"preview": "'use strict'\n\nconst path = require('path')\nconst fs = require('fs')\nconst test = require('tap').test\nconst http = requir"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the mcollina/autocannon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 105 files (335.6 KB), approximately 95.8k tokens, and a symbol index with 95 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.