• Stars
    star
    303
  • Rank 137,655 (Top 3 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 12 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

bench-rest - benchmark REST (HTTP/HTTPS) API's. node.js client module for easy load testing / benchmarking REST API's using a simple structure/DSL can create REST flows with setup and teardown and returns (measured) metrics.

bench-rest benchmark REST API's

Node.js client module for easy load testing / benchmarking REST (HTTP/HTTPS) API's using a simple structure/DSL can create REST flows with setup and teardown and returns (measured) metrics.

Roughly bench-rest = mikeal/request + caolan/async + felixge/node-measured

Build Status Known Vulnerabilities

Contents on this page

Installation

Requires node.js >= 0.10

# If using programmatically
npm install bench-rest

# OR possibly with -g option if planning to use from command line
npm install -g bench-rest

Programmatic Usage

Simple flow performing 100 iterations with 10 concurrent connections

  var benchrest = require('bench-rest');
  var flow = 'http://localhost:8000/';  // can use as simple single GET

  // OR more powerfully define an array of REST operations with substitution
  // This does a unique PUT and then a GET for each iteration
  var flow = {
    main: [
      { put: 'http://localhost:8000/foo_#{INDEX}', json: 'mydata_#{INDEX}' },
      { get: 'http://localhost:8000/foo_#{INDEX}' }
    ]
  };

  // if the above flow will be used with the command line runner or
  // programmatically from a separate file then export it.
  module.exports = flow;

  // There are even more flow options like setup and teardown, see detailed usage

  var runOptions = {
    limit: 10,     // concurrent connections
    iterations: 100  // number of iterations to perform
  };
  benchrest(flow, runOptions)
    .on('error', function (err, ctxName) { console.error('Failed in %s with err: ', ctxName, err); })
    .on('end', function (stats, errorCount) {
      console.log('error count: ', errorCount);
      console.log('stats', stats);
    });

See Detailed Usage section below for more details

Command-line usage

# if installed with -g
bench-rest

# otherwise use from node_modules
node_modules/.bin/bench-rest

Outputs

  Usage: bench-rest [options] <flow-js-path-or-GET-URL>

  Options:

    -h, --help                   output usage information
    -V, --version                output the version number
    -n --iterations <integer>    Number of iterations to run, defaults to 1
    -a --prealloc <integer>      Max iterations to preallocate, defaults 100000
    -c --concurrency <integer>   Concurrent operations, defaults to 10
    -d --progress <integer>      Display progress bar (> 0), update every N ms, defaults 1000
    -u --user <username>         User for basic authentication, default no auth
    -p --password <password>     Password for basic authentication
    -e --evaluate <flow-string>  Evaluate flow from string, not file

  Examples:

    bench-rest -n 100 -c 100 ./examples/simple.js
    bench-rest -n 100 -c 100 -u "joe" -p "secret" /foo/flow.js
    bench-rest -n 10 -c 2 http://localhost:8000/
    bench-rest -n 10 -c 2 -e "{ head: 'http://localhost:8000/' }"

Running this

bench-rest -n 1000 -c 50 ./examples/simple.js

would output

Benchmarking 1000 iteration(s) using up to 50 concurrent connections

Using flow from: /Users/barczewskij/projects/bench-rest/examples/simple.js
 { main: [ { get: 'http://localhost:8000/' } ] }
Progress [=======================================] 100% 0.0s conc:49 1341/s

errors:  0
stats:  { totalElapsed: 894,
  main:
   { meter:
      { mean: 1240.6947890818858,
        count: 1000,
        currentRate: 1240.6947890818858,
        '1MinuteRate': 0,
        '5MinuteRate': 0,
        '15MinuteRate': 0 },
     histogram:
      { min: 4,
        max: 89,
        sum: 41603,
        variance: 242.0954864864864,
        mean: 41.603,
        stddev: 15.55941793533699,
        count: 1000,
        median: 42,
        p75: 50,
        p95: 70.94999999999993,
        p99: 81.99000000000001,
        p999: 88.99900000000002 } } }

It has one expected required parameter which is the path to a node.js file which exports a REST flow. For example:

  var flow = {
    main: [{ get: 'http://localhost:8000/' }]  // could be an array of REST operations
  };

  // if the above flow will be used with the command line runner or
  // programmatically from a separate file then export it.
  module.exports = flow;

Check for example flows in the examples directory.

See Detailed Usage for more details on creating more advanced REST flows.

Goals

  • Easy to create REST (HTTP/HTTPS) flows for benchmarking
  • Generate good concurrency (at least 8K concurrent connections for single proc on Mac OS X)
  • Obtain metrics from the runs with average, total, min, max, histogram, req/s
  • Allow iterations to vary easily using token subsitution
  • Run programmatically so can be used with CI server
  • Flow can have setup and teardown operations for startup and shutdown as well as for each iteration
  • Ability to automatically handles cookies separately for each iteration
  • Ability to automatically follows redirects for operations
  • Errors will automatically stop an iterations flow and be tracked
  • Easy use and handling of etags
  • Allows pre/post processing or verification of data
  • Provide programmatically and via cmd line the dynamic concurrency count

Detailed Usage

Advanced flow with setup/teardown and multiple steps to benchmark in each iteration

  var benchrest = require('bench-rest');
  var flow = {
    before: [],      // operations to do before anything
    beforeMain: [],  // operations to do before each iteration
    main: [  // the main flow for each iteration, #{INDEX} is unique iteration counter token
      { put: 'http://localhost:8000/foo_#{INDEX}', json: 'mydata_#{INDEX}' },
      { get: 'http://localhost:8000/foo_#{INDEX}' }
    ],
    afterMain: [{ del: 'http://localhost:8000/foo_#{INDEX}' }],   // operations to do after each iteration
    after: []        // operations to do after everything is done
  };

  module.exports = flow;

  var runOptions = {
    limit: 10,         // concurrent connections
    iterations: 1000,  // number of iterations to perform
    prealloc: 100      // only preallocate up to 100 before starting
  };
  var errors = [];
  benchrest(flow, runOptions)
    .on('error', function (err, ctxName) { console.error('Failed in %s with err: ', ctxName, err); })
    .on('progress', function (stats, percent, concurrent, ips) {
      console.log('Progress: %s complete', percent);
    })
    .on('end', function (stats, errorCount) {
      console.log('error count: ', errorCount);
      console.log('stats', stats);
    });

Returns EventEmitter

The main function from require('bench-rest') will return a node.js EventEmitter instance when called with the flow and runOptions. This event emitter will emit the following events:

  • error - emitted as an error occurs during a run. It emits parameters err and ctxName matching where the error occurred (main, before, beforeMain, after, afterMain)
  • progress - emitted periodically as iterations complete. It emits parameters stats, percentComplete, concurrent, ips. The stats is the current measured stats (discussed below). The concurrent param is the concurrent connection count at that point in time. The ips is the calculated current iterations per second rate at which the iterations are executing. The interval at which progress is output is controlled by the runOption.progress in milliseconds.
  • end - emitted when the benchmark run has finished (successfully or otherwise). It emits parameters stats and errorCount (discussed below).

Stats (metrics) and errorCount benchmark results

The stats is a measured data object and the errorCount is an count of the errors encountered. Time is reported in milliseconds. See measured for complete description of all the properties. https://github.com/felixge/node-measured

stats.totalElapsed is the elapsed time in milliseconds for the entire run including all setup and teardown operations

The stats.main will be the meter data for the main benchmark flow operations (not including the beforeMain and afterMain operations).

A couple key metrics to be aware of:

  • stats.main.meter.mean - average iterations / sec
  • stats.main.meter.count - iterations completed
  • stats.main.meter.currentRate - iterations / sec at this moment (mainly useful when monitoring progress)
  • stats.main.1MinuteRate - iterations / sec for the last minute (only relevant if more than 1 minute has passed)
  • stats.main.histogram.min - the minimum time any iteration took (milliseconds)
  • stats.main.histogram.max - the maximum time any iteration took (milliseconds)
  • stats.main.histogram.mean - the average time any iteration took (milliseconds)
  • stats.main.histogram.p95 - the amount of time that 95% of all iterations completed within (milliseconds)

The output of the above run will look something like:

error count:  0
stats {
  totalElapsed: 151,
  main:
   { meter:
      { mean: 1190.4761904761904,
        count: 100,
        currentRate: 1190.4761904761904,
        '1MinuteRate': 0,
        '5MinuteRate': 0,
        '15MinuteRate': 0 },
     histogram:
      { min: 3,
        max: 66,
        sum: 985,
        variance: 43.502525252525245,
        mean: 9.85,
        stddev: 6.595644415258091,
        count: 100,
        median: 8.5,
        p75: 11,
        p95: 17,
        p99: 65.53999999999976,
        p999: 66 } } }

Shortcuts for expressing flow

If you have very simple flow that does not need setup and teardown, then there are a few shortcuts for expressing the flow.

  • pass flow as just a string URL - it will perform a GET on this URL as the main flow, ex: var flow = 'http://localhost:8000/';
  • pass flow as just a single REST operation, ex: var flow = { head: 'http://localhost:8000/' };
  • pass flow as array of REST operations
// passing as array implies no setup/teardown and these are the main operations
var flow = [
  { put: 'http://localhost:8000/foo', json: 'mydata' },
  { get: 'http://localhost:8000/foo' }
];

Run options

The runOptions object can have the following properties which govern the benchmark run:

  • limit - required number of concurrent operations to limit at any given time
  • iterations - required number of flow iterations to perform on the main flow (as well as beforeMain and afterMain setup/teardown operations)
  • prealloc - optional max number of iterations to preallocate before starting, defaults to lesser of 100K and iterations. When using large number of iterations or large payload per iteration, it can be necessary to adjust this for optimal memory use.
  • user - optional user to be used for basic authentication
  • password - optional password to be used for basic authentication
  • progress - optional, if non-zero number is provided it enables the output of progress events each time this number of milliseconds has passed

REST Operations in the flow

The REST operations that need to be performed in either as part of the main flow or for setup and teardown are configured using the following flow properties.

Each array of opertions will be performed in series one after another unless an error is hit. The afterMain and after operations will be performed regardless of any errors encountered in the flow.

  var flow = {
    before: [],      // REST operations to perform before anything starts
    beforeMain: [],  // REST operations to perform before each iteration
    main: [],        // REST operations to perform for each iteration
    afterMain: [],   // REST operations to perform after each iteration
    after: []        // REST operations to perform after everything is finished
  };

Each operation can have the following properties:

  • one of these common REST properties get, head, put, post, patch, del (using del rather than delete since delete is a JS reserved word) with a value pointing to the URI, ex: { get: 'http://localhost:8000/foo' }
  • alternatively can specify method (use uppercase) and uri directly, ex: { method: 'GET', uri: 'http://localhost:8000/foo' }
  • json optionally provide data which will be JSON stringified and provided as body also setting content type to application/json, ex: { put: 'http://localhost:8000/foo', json: { foo: 10 } }
  • headers - optional headers to set, ex: { get: 'http://localhost:8000/foo', headers: { 'Accept-Encoding': 'gzip'}
  • any other properties/options which are valid for mikeal/request - see https://github.com/mikeal/request
  • pre/post processing - optional array as beforeHooks and afterHooks which can perform processing before and/or after an operation. See Pre/post operation processing section below for details.

Token substitution for iteration operations

To make REST flows that are independent of each other, one often wants unique URLs and unique data, so one way to make this easy is to include special tokens in the uri, json, or data.

Currently the token(s) replaced in the uri, json, or body are:

  • #{INDEX} - replaced with the zero based counter/index of the iteration

Note: for the json property the json object is JSON.stringified, tokens substituted, then JSON.parsed back to an object so that tokens will be substituted anywhere in the structure. If subsitution is not needed (no #{INDEX} in the structure, then no copy (stringify/parse) will be performed.

Pre/post operation processing

If an array of hooks is specified in an operation as beforeHooks and/or afterHooks then these synchronous operations will be done before/after the REST operation.

Built-in processing filters can be referred to by name using a string, while custom filters can be provided as a function, ex:

// This causes the HEAD operation to use a previously saved etag if found for this URI
// setting the If-None-Match header with it, and then if the HEAD request returns a failing
// status code
{ head: 'http://localhost:8000', beforeHooks: ['useEtag'], afterHooks: ['ignoreStatus'] }

The list of current built-in beforeHooks:

  • useEtag - if an etag had been previously saved for this URI with saveEtag afterHook, then set the appropriate header (for GET/HEAD, If-None-Match, otherwise If-Match). If was not previously saved or empty then no header is set.

The list of current built-in afterHooks:

  • saveEtag - afterHook which causes an etag to be saved into an object cache specific to this iteration. Stored by URI. If the etag was the result of a POST operation and a Location header was provided, then the URI at the Location will be used.
  • ignoreStatus - afterHookif an operation could possibly return an error code that you want to ignore and always continue anyway. Failing status codes are those that are greater than or equal to 400. Normal operation would be to terminate an iteration if there is a failure status code in any before, beforeMain, or main operation.
  • verify2XX - afterHook which fails if an operation's status code was not in 200-299 range. If you don't want a redirect followed, be sure to add the request option followRedirect: false. Note: by default errors are verified (greater than or equal to 400), so this would just be used when you want to make sure it is not a 3xx either.
  • startStepTimer - used in beforeHooks to start a timer for this step named step_OPIDX where OPIDX is the zero based index of the step in the flow. Be sure to call endStepTimer in afterHooks to end it. Provides detailed stats for an individual step in a flow.
  • endStepTimer - used in afterHooks to end a timer previously started with startStepTimer and included in the stats displayed at the end of the run.

To create custom beforeHook or afterHook the synchronous function needs to accept an all object and return the same or possibly modified object. To exit the flow, an exception can be thrown which will be caught and emitted. Using these beforeHooks you can modify the next request, and using the afterHooks can verify the response and/or store data for future actions.

One way to keep state for each iteration (without using external variables) is to use the all.iterCtx object which is an empty object provided for each iteration. See examples/hook.js and test/hooks-iter-ctx.mocha.js

So a verification function could be written as such

function verifyData(all) {
  if (all.err) return all; // errored so just return and it will error as normal
  assert.equal(all.response.statusCode, 200);
  assert(all.body, 'foobarbaz'); // if throws, err is caught and counted
  return all; // always return all if you want it to continue
}

Postprocess function example:

function postProcess(all) {
  // all.iterCtx obj is where you can keep data for an iteration
  all.iterCtx.location = all.response.headers.location;
  all.iterCtx.body = all.body;
  return all; // always return all if you want it to continue
}

Preprocess function example:

function preProcess(all) {
  // all.iterCtx object is where you can keep data private for an iteration
  // all.requestOptions will be used for the request, modify as needed
  all.requestOptions.uri = 'http://localhost:8000' + all.iterCtx.location;
  return all; // always return all if you want it to continue
}

The properties available on the all object are:

  • all.env.index - the zero based counter for this iteration, same as what is used for #{INDEX}
  • all.env.jar - the cookie jar
  • all.env.user - basic auth user if provided
  • all.env.password - basic auth password if provided
  • all.env.etags - object of etags saved by URI
  • all.env.stats - measured stats collection containing totalElapsed and main. If startStepTimer and endStepTimer hooks are added to individual steps then additional timers step_OPINDEX will be created for steps that have the hooks.
  • all.iterCtx - empty object created for each iteration, can be used for your private storage from beforeHooks and afterHooks
  • all.opIndex - zero based index for the operation in the array of operations, ie: first operation in the main flow will have opIndex of 0
  • all.requestOptions - the options that will be used for the request (see mikeal/request)
  • all.requestOptions.uri - the URL that will be used for the request
  • all.requestOptions.method - the method that will be used for the request
  • all.response - the response obj (only for afterHooks)
  • all.body - the response body (only for afterHooks)
  • all.err - not empty if an error has occurred
  • all.cb - the cb that will be called when done

Why create this project?

It is important to understand how well your architecture performs and with each change to the system how performance is impacted. The best way to know this is to benchmark your system with each major change.

Benchmarking also lets you:

  • understand how your system will act under load
  • how and whether multiple servers or processes will help you scale
  • whether a feature added improved or hurt performance
  • predict the need add instances or throttle load before your server reaches overload

After attempting to use the variety of load testing clients and modules for benchmarking, none really met all of my desired goals. Most clients are only able to benchmark a single operation, not a whole flow and not one with setup and teardown.

Building your own is certainly an option but it gets tedious to make all the necessary setup and error handling to achieve a simple flow and thus this project was born.

Tuning OS

Each OS may need some tweaking of the configuration to be able to generate or receive a large number of concurrent connections.

Mac OS X

The Mac OS X can be tweaked using the following parameters. The configuration allowed about 8K concurrent connections for a single process.

sysctl -a | grep maxfiles  # display maxfiles and maxfilesperproc  defaults 12288 and 10240
sudo sysctl -w kern.maxfiles=25000
sudo sysctl -w kern.maxfilesperproc=24500
sysctl -a | grep somax # display max socket setting, default 128
sudo sysctl -w kern.ipc.somaxconn=20000  # set
ulimit -S -n       # display soft max open files, default 256
ulimit -H -n       # display hard max open files, default unlimited
ulimit -S -n 20000  # set soft max open files

Key modules leveraged

Tested on Node versions

  • 4
  • 5
  • 6

Get involved

If you have input or ideas or would like to get involved, you may:

Developer Notes

We use semver for this package so any breaking changes will update the major version number.

npm test # runs tests

Versioning and publish

npm version patch # increment version and create git tag, or minor, major
git push origin master --tags # upload changes and tags to github
npm publish # publish latest version to npm

License - MIT

More Repositories

1

wait-on

wait-on is a cross-platform command line utility and Node.js API which will wait for files, ports, sockets, and http(s) resources to become available
JavaScript
1,860
star
2

redux-logic

Redux middleware for organizing all your business logic. Intercept actions and perform async processing.
JavaScript
1,807
star
3

joi-browser

joi validation bundled for the browser (deprecated) - use joi@16+
JavaScript
273
star
4

pkglink

Space saving Node.js package hard linker. pkglink locates common JavaScript/Node.js packages from your node_modules directories and hard links the package files so they share disk space.
JavaScript
214
star
5

microservices

Microservices example code using Node.js, Redis, Hapi
JavaScript
150
star
6

autoflow

autoflow (formerly react) is a javascript module to make it easier to work with asynchronous code, by reducing boilerplate code and improving error and exception handling while allowing variable and task dependencies when defining flow.
JavaScript
97
star
7

redux-logic-test

redux-logic test utilities to facilitate the testing of logic. Create mock store
JavaScript
37
star
8

redis-rstream

redis-rstream is a node.js redis read stream which streams binary or utf8 data in chunks from a redis key using an existing redis client (streams2). Tested with mranney/node_redis client.
JavaScript
33
star
9

digest-stream

Simple node.js pass-through stream (RW) which calculates the a crypto digest (sha/md5 hash) of a stream and also the length. Pipe your stream through this to get digest and length. (streams2)
JavaScript
30
star
10

markdown-it-modify-token

markdown-it plugin for modifying tokens in a markdown document. It can for example modify content or attributes for certain type of elements like links or images.
JavaScript
25
star
11

redis-wstream

redis-wstream is a node.js redis write stream which streams binary or utf8 data into a redis key using an existing redis client (streams2). Tested with mranney/node_redis client.
JavaScript
20
star
12

pass-stream

pass-stream - pass-through node.js stream which can filter/adapt and pause data
JavaScript
17
star
13

base-react-min

Minimal React.js boilerplate with an auto build environment
JavaScript
12
star
14

ensure-array

Ensure that a javascript object is an array, coerce if necessary. Move error checking out of your node.js javascript code.
JavaScript
12
star
15

autoflow-graphviz

autflow-graphviz is a plugin for autflow, the flow control rules engine, which allows react to use graphviz to generate flow diagrams for the dependencies
JavaScript
7
star
16

accum

Simple stream sink which accumulates or collects the data from a stream. Pipe your stream into this to get all the data as buffer, string, or raw array. (streams2)
JavaScript
6
star
17

hapi-bunyan-lite

Minimalistic Bunyan logging integration for Hapi - forwards Hapi log events to bunyan
JavaScript
6
star
18

ngzip

ngzip is a portable streaming stdio gzip command line utility that uses Node.js zlib. It should run anywhere Node.js runs including windows, *nix, mac.
JavaScript
5
star
19

tapper

Tapper (tapr) is a node.js tap runner which improves formatting and allows stdout and stderr mixed in with the tap output. Also optionally adds color to the output
JavaScript
4
star
20

joi-full

joi object schema validation with extensions as universal/isomorphic libarary for Node.js and bundled for the browser (babelified and bundled)
JavaScript
4
star
21

autoflow-deferred

autoflow-deferred is a plugin for react, the flow control rules engine, which adds integration with jQuery-style Deferred promises
JavaScript
4
star
22

jsconf-react

JSConf 2015 React.js training workshop
JavaScript
3
star
23

length-stream

Simple pass-through node.js stream (RW) which accumulates the length of the stream. (streams2)
JavaScript
3
star
24

git-clone-init

Create new project from clone - clone repo, rm .git, git init, sed files, git commit
Shell
3
star
25

autoflow-q

autoflow-q is a plugin for autoflow, the flow control rules engine, which adds integration with Q-style Deferred promises https://github.com/kriskowal/q
JavaScript
3
star
26

redux-util

redux utilities for easily constructing action creators and reducers
JavaScript
2
star
27

redux-logic-examples

Shell
2
star
28

function.bind.js

Polyfill old webkit and iOS browsers with Function.prototype.bind
JavaScript
2
star
29

modulize

Safe, easy method override/extension without manual alias_method chaining
Ruby
2
star
30

base-react-slim

base boilerplate react environment with auto-build env w/o testing framework
JavaScript
2
star
31

chai-stack

Light wrapper around chaijs which automatically sets chai.Assertion.includeStack = true
JavaScript
2
star
32

jsconf-react-win

JSConf 2015 React.js training workshop (Windows version)
JavaScript
2
star
33

joi-date-extensions-browser

joi-date-extensions bundled for the browser
JavaScript
1
star
34

base-react-min-win

Minimal React.js boilerplate with an auto build environment for Windows
JavaScript
1
star
35

feature-u-bookmark-notes

Simple React.js, redux, redux-logic app using feature-u to showcase feature-based development
JavaScript
1
star
36

waverider

waverider CMS - lightweight fast CMS/blog with realtime edit and preview written in javascript for node.js
JavaScript
1
star