nodeloadlib
is a node.js library containing building blocks to programmatically create load tests for HTTP services. The components are:
- High-level load testing interface
- Distributed testing
- A scheduler which executes functions at a given rate
- Event-based loops
- Statistics classes
- HTTP-specific monitors
- Web-based reports
Add require(./dist/nodeloadlib.js)
and call runTest()
or addTest()/startTests()
:
// Add to example.js:
require('./dist/nodeloadlib');
runTest({
name: "Read",
host: 'localhost',
port: 8080,
numClients: 20,
timeLimit: 600,
successCodes: [200],
targetRps: 200,
requestGenerator: function(client) {
var url = '/data/object-' + Math.floor(Math.random()*10000);
return traceableRequest(client, 'GET', url, { 'host': 'localhost' });
}
});
This test will hit localhost:8080 with 20 concurrent connections for 10 minutes. During the test, a web server is started on localhost:8000, which displays requests per second and latency statistics. Non-200 responses will be logged to results-{timestamp}-err.log
, statistics are regularly logged to results-{timestamp}-stats.log
, and the summary page found at localhost:8000 is also written to results-{timestamp}-summary.html
.
$ node example.js ## while running, browse to http://localhost:8000 to track the test
Serving progress report on port 8000.
Opening log files.
......done.
Finishing...
Closed log files.
Shutdown report server.
Check out examples/nodeloadlib-ex.js for a example of a full read+write test.
Define these global variables before including nodeloadlib
to control its behavior:
- QUIET: set to true to disable all console output (default is false)
- HTTP_SERVER_PORT: set to the port to start the HTTP server on (default is 8000)
- DISABLE_HTTP_SERVER: set to true to not start the HTTP server (default is false)
- DISABLE_LOGS: set to true to not create the log or HTML summary files (default is false)
- TEST_CONFIG: can be "long" or "short" which changes the test reporting interval to more appropriate settings (default is "short")
High-level functions useful for quickly building up complex load tests. See api.js
.
Functions:
runTest(spec, callback, stayAliveAfterDone)
: Run a single test and callcallback
(see Test Definition below).addTest(spec)
: Add a test to be run onstartTests()
. Tests are run concurrently.addRamp(rampSpec)
: Gradually ramp up the load generated by a test (see Ramp Definition below).startTests(callback, stayAliveAfterDone)
: Run tests added byaddTest()
andaddRamp()
and callcallback
.traceableRequest(...)
: Used instead of built-in node.jshttp.Client.request()
to allows proper tracking of unique URLs.
Usage:
A "test" represents requests being sent at a fixed rate over concurrent connections. Tests are run by calling runTest()
or calling addTest()
followed by startTests()
. The parameters defining a test are detailed in Test Definition section. Issue requests using one of three methods:
-
Define
method
,path
, andrequestData
, leavingrequestGenerator
andrequestLoop
asnull
. Ifmethod
is'PUT'
or'POST'
,nodeloadlib
will sendrequestData
in the request body. -
Set
requestGenerator
to afunction(http.Client) -> http.ClientRequest
. Requests returned by this function are executed bynodeloadlib
. For example, you can GET random URLs using arequestGenerator
:addTest({ requestGenerator: function(client) { return traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000)); } });
-
Set
requestLoop
to afunction(loopFun, http.Client)
which callsloopFun({req: http.ClientRequest, res: http.ClientResponse})
after each request completes. This is the most flexibility, but the function must be sure to callloopFun()
. For example, issuePUT
requests with properIf-Match
headers using arequestLoop
:addTest({ requestLoop: function(loopFun, client) { var req = traceableRequest(client, 'GET', '/resource'); req.on('response', function(response) { if (response.statusCode != 200 && response.statusCode != 404) { loopFun({req: req, res: response}); } else { var headers = { }; if (response.headers['etag'] != null) headers['if-match'] = response.headers['etag']; req = traceableRequest(client, 'PUT', '/resource', headers, "new value"); req.on('response', function(response) { loopFun({req: req, res: response}); }); req.close(); } }); req.close(); } });
A "ramp" increases the load of a particular test over some period of time. Schedule a ramp after scheduling a test by calling addRamp()
:
var test1 = addTest({
targetRps: 100,
requestGenerator: function(client) {
return traceableRequest(client, 'GET', '/resource-' + Math.floor(Math.random()*10000));
}
});
// Add 100 requests / second using 10 concurrent connections to test1 between minutes 1 and 2
addRamp({
test: test1,
numberOfSteps: 10,
timeLimit: 60,
rpsPerStep: 10,
clientsPerStep: 1,
delay: 60
});
Start all the tests by calling startTests(...)
. The script terminates 3 seconds after the tests complete unless the parameter stayAliveAfterDone==true
.
Check out examples/nodeloadlib-ex.js for an example of a full read+write test.
Test Definition: The following object defines the parameters and defaults for a test, which is used by addTest()
or runTest()
:
var TEST_DEFAULTS = {
name: 'Debug test', // A descriptive name for the test
host: 'localhost', // host and port specify where to connect
port: 8080, //
requestGenerator: null, // Specify one of: requestGenerator, requestLoop, or (method, path, requestData)
requestLoop: null, // - A requestGenerator is a function that takes a http.Client param
method: 'GET', // and returns a http.ClientRequest.
path: '/', // - A requestLoop is a function that takes two params (loopFun, http.Client).
requestData: null, // It should call loopFun({req: http.ClientRequest, res: http.ClientResponse})
// after each operation to schedule the next iteration of requestLoop.
// - (method, path, requestData) specify a single URL to test
numClients: 10, // Maximum number of concurrent executions of request loop
numRequests: Infinity, // Maximum number of iterations of request loop
timeLimit: 120, // Maximum duration of test in seconds
targetRps: Infinity, // Number of times per second to execute request loop
delay: 0, // Seconds before starting test
successCodes: null, // List of success HTTP response codes. Failures are logged to the error log.
stats: ['latency', 'result-codes'], // Specify list of: latency, result-codes, uniques, concurrency. Note that "uniques"
// only shows up in summary report and requests must be made with traceableRequest().
// Not doing so will result in reporting only 2 uniques.
latencyConf: {percentiles: [.95,.99]}, // Set latencyConf.percentiles to percentiles to report for the 'latency' stat
reportInterval: 2, // Seconds between each progress report
reportFun: null, // Function called each reportInterval that takes a param, stats, which is a map of
// { 'latency': Reportable(Histogram), 'result-codes': Reportable(ResultsCounter},
// 'uniques': Reportable(Uniques), 'concurrency': Reportable(Peak) }
}
Ramp Definition: The following object defines the parameters and defaults for a ramp, which is used by addRamp()
:
var RAMP_DEFAULTS = {
test: null, // The test to ramp up, returned from from addTest()
numberOfSteps: 10, // Number of steps in ramp
timeLimit: 10, // The total number of seconds to ramp up
rpsPerStep: 10, // The rps to add to the test at each step
clientsPerStep: 1, // The number of connections to add to the test at each step.
delay: 0 // Number of seconds to wait before ramping up.
}
Functions to distributing tests across multiple slave nodeload
instances. See remote.js
Functions:
remoteTest(spec)
: Return a test to be scheduled withremoteStart(...)
(spec
uses same format asaddTest(spec)
).remoteStart(master, slaves, tests, callback, stayAliveAfterDone)
: Run tests on specified slaves.remoteStartFile(master, slaves, filename, callback, stayAliveAfterDone)
: Execute a.js
file on specified slaves.
Usage:
First, simply start nodeloadlib.js
on each slave instances.
$ node dist/nodeloadlib.js # Run on each slave machine
Then, create tests in using remoteTest(spec)
with the same spec
fields in the Test Definition section above. Pass the created tests as a list to remoteStart(...)
to execute them on slave nodeload
instances. master
must be the "host:port"
of the nodeload
which is executing remoteStart(...)
. It will receive and aggregates statistics from the slaves, so the address should be reachable by the slaves. Or, use master=null
to disable reports from the slaves.
// This script must be run on master:8000, which will aggregate results. Each slave
// will GET http://internal-service:8080/ at 100 rps.
var t1 = remoteTest({
name: "Distributed test",
host: 'internal-service',
port: 8080,
timeLimit: 20,
targetRps: 100
});
remoteStart('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], [t1]);
Alternatively, an existing nodeload
load test script file can be used:
// The file /path/to/load-test.js should contain valid javascript and can use any nodeloadlib functions
remoteStartFile('master:8000', ['slave1:8000', 'slave2:8000', 'slave3:8000'], '/path/to/load-test.js');
When the remote tests complete, the master instance will call the callback
parameter if non-null. It then automatically terminates after 3 seconds unless the parameter stayAliveAfterDone==true
.
The SCHEDULER
object allows a function to be called at a desired rate and concurrency level.
Functions:
SCHEDULER.schedule(spec)
: Schedule a function to be executed (see the Schedule Definition below)SCHEDULER.startAll(callback)
: Start running all the scheduled functions and execute callback when they completeSCHEDULER.startSchedule(callback)
: Start a single scheduled function and execute callback when it completesfunLoop(fun)
: Wrap functions that do not perform IO so they can be used with SCHEDULER
Usage:
Call SCHEDULER.schedule(spec)
to add a job. spec.fun
must be a function(loopFun, args)
and call loopFun(results)
when it completes. Call SCHEDULER.startAll()
to start running all scheduled jobs.
If spec.argGenerator
is non-null, it is called spec.concurrency
times on startup. One return value is passed as the second parameter to each concurrent execution of spec.fun
. If null, the value of spec.args
is passed to all executions of spec.fun
instead.
A scheduled job finishes after its target duration or it has been called the maximum number of times. SCHEDULER
stops all running jobs once all monitored jobs finish. For example, 1 monitored job is scheduled for 5 seconds, and 2 unmonitored jobs are scheduled with no time limits. SCHEDULER
will start all 3 jobs when SCHEDULER.startAll()
is called, and stop all 3 jobs 5 seconds later. Unmonitored jobs are useful for running side processes such as statistics gathering and reporting.
Example:
var t = 1;
SCHEDULER.schedule({
fun: funLoop(function(i) { sys.puts("Thread " + i) }),
argGenerator: function() { return t++; },
concurrency: 5,
rps: 10,
duration: 10
});
SCHEDULER.startAll(function() { sys.puts("Done.") });
Job Definition: The following object defines the parameters and defaults for a job run by SCHEDULER
:
var JOB_DEFAULTS = {
fun: null, // A function to execute which accepts the parameters (loopFun, args).
// The value of args is the return value of argGenerator() or the args
// parameter if argGenerator is null. The function must call
// loopFun(results) when it completes.
argGenerator: null, // A function which is called once when the job is started. The return
// value is passed to fun as the "args" parameter. This is useful when
// concurrency > 1, and each "thread" should have its own args.
args: null, // If argGenerator is NOT specified, then this is passed to the fun as "args".
concurrency: 1, // Number of concurrent calls of fun()
rps: Infinity, // Target number of time per second to call fun()
duration: Infinity, // Maximum duration of this job in seconds
numberOfTimes: Infinity, // Maximum number of times to call fun()
delay: 0, // Seconds to wait before calling fun() for the first time
monitored: true // Does this job need to finish in order for SCHEDULER.startAll() to end?
};
The ConditionalLoop
class provides a generic way to write a loop where each iteration is scheduled using process.nextTick()
. This allows many long running "loops" to be executed concurrently by node.js
.
Functions:
ConditionalLoop(fun, args, conditions, delay):
Defines a loop (see Loop Definition below)ConditionalLoop.start(callback):
Starts executing and callcallback
on terminationConditionalLoop.stop():
Terminate the looptimeLimit(seconds)
,maxExecutions(numberOfTimes)
: useful ConditionalLoop conditionsrpsLoop(rps, fun)
: Wrap afunction(loopFun, args)
so ConditionalLoop calls it a set ratefunLoop(fun)
: Wrap a non-IO performingfunction(args)
so it can be used with a ConditionalLoop
Usage:
Create a ConditionalLoop
instance and call ConditionalLoop.start()
to execute the loop. A function given to ConditionalLoop
must be a function(loopFun, args)
which ends by calling loopFun()
.
The conditions
parameter is a list of functions. When any function returns false
, the loop terminates. For example, the functions timeLimit(seconds)
and maxExecutions(numberOfTimes)
are conditions that limit the duration and number of iterations of a loop respectively.
The loop also terminates if ConditionalLoop.stop()
is called.
Example:
var fun = function(loopFun, startTime) {
sys.puts("It's been " + (new Date() - startTime) / 1000 + " seconds");
loopFun();
};
var stopOnFriday = function() {
return (new Date()).getDay() < 5;
}
var loop = new ConditionalLoop(rpsLoop(1, fun), new Date(), [stopOnFriday, timeLimit(604800 /*1 week*/)], 1);
loop.start(function() { sys.puts("It's Friday!") });
Loop Definition:
The ConditionalLoop
constructor arguments are:
fun: Function that takes parameters (loopFun, args) and calls loopFun() after each iteration
args: The args parameter to pass to fun
conditions: A list of functions representing termination conditions. Terminate when any function returns `false`.
delay: Seconds to wait before starting the first iteration
Implementations of various statistics.
Classes:
Histogram(numBuckets)
: A histogram of integers. If most of the items are between 0 andnumBuckets
, calculating percentiles and stddev is fast.Accumulator
: Calculates the sum of the numbers put in.ResultsCounter
: Tracks results which are be limited to a small set of possible choices. Tracks the total number of results, number of results by value, and results added per second.Uniques
: Tracks the number of unique items added.Peak
: Tracks the max of the numbers put in.Rate
: Tracks the rate at which items are added.LogFile
: Outputs to a file on disk.NullLog
: Ignores all items put in.Reportable
: Wraps any other statistic to store an interval and cumulative version of it.
Functions:
randomString(length)
: Returns a random string of ASCII characters between 32 and 126 of the requested length.nextGaussian(mean, stddev)
: Returns a normally distributed number using the provided mean and standard deviation.nextPareto(min, max, shape)
: Returns a Pareto distributed number betweenmin
andmax
inclusive using the provided shape.roundRobin(list)
: Returns a copy of the list with aget()
method.get()
returns list entries round robin.
Usage:
All of the statistics classes support the methods:
.length
: The total number of itemsput()
into this object.put(item)
: Include an item in the statistic.get()
: Get a specific value from the object, which varies depending on the object.clear()
: Clear out all items.summary()
: Get a object containing a summary of the object, which varies depending on the object. The fields returned are used to generate the trends of the HTML report graphs.
In addition, these other methods are supported:
Histogram.mean()
: Calculate the mean of the numbers in the histogram.Histogram.percentile(percentile)
: Calculate the givenpercentile
, between 0 and 1, of the numbers in the histogram.Histogram.stddev()
: Standard deviation of the numbers in the histogram.LogFile.open()
: Open the file.LogFile.close()
: Close the file.Reportable.next()
: clear out the interval statistic for the next window.
Refer to the Statistics
section near line 910 of nodeloadlib.js for the return value of the get()
and summary()
functions for the different classes.
A collection of wrappers for requestLoop
functions that record statistics for HTTP requests. These functions can be run scheduled with SCHEDULER
or run with a ConditionalLoop
.
Functions:
monitorLatenciesLoop(latencies, fun)
: Callfun()
and put the execution duration inlatencies
, which should be aHistogram
.monitorResultsLoop(results, fun)
: Callfun()
and put the HTTP response code inresults
, which should be aResultsCounter
.monitorByteReceivedLoop(bytesReceived, fun)
: Callfun()
and put the number of bytes received inbytesReceived
, usually anAccumulator
.monitorConcurrencyLoop(concurrency, fun)
: Callfun()
and put the number of "threads" currently executing it intoconcurrency
, usually aPeak
.monitorRateLoop(rate, fun)
: Callfun()
and notifyrate
, which should be aRate
, that it was called.monitorHttpFailuresLoop(successCodes, fun, log)
: Callfun()
and put the HTTP request and response intolog
, which should be aLogFile
, for every request that does not return an HTTP status code included in the listsuccessCodes
.monitorUniqueUrlsLoop(uniqs, fun)
: Callfun()
and put the HTTP request path intouniqs
, which should be aUniques
.loopWrapper(fun, start, finish)
: Create a custom loop wrapper by specifying a functions to execute before and after callingfun()
.
Usage:
All of these wrappers return a function(loopFun, args)
which can be used by SCHEDULER
and ConditionalLoop
. The underlying function should have the same signature and execute an HTTP request. It must call loopFun({req: http.ClientRequest, res: http.ClientResponse})
when it completes the request.
Example:
// Issue GET requests to random objects at localhost:8080/data/obj-{0-1000} for 1 minute and
// track the number of unique URLs
var uniq = new Reportable(Uniques, 'Uniques');
var loop = monitorUniqueUrlsLoop(uniq, function(loopFun, client) {
var req = traceableRequest(client, 'GET', '/data/obj-' + Math.floor(Math.random()*1000));
req.on('response', function(response) {
sys.puts('ah')
loopFun({req: req, res: response});
});
req.close();
});
SCHEDULER.schedule({
fun: loop,
args: http.createClient(8080, 'localhost'),
duration: 60
}).start(function() {
sys.puts(JSON.stringify(uniq.summary()));
});
Functions for manipulating the report that is available during the test at http://localhost:8000/ and that is written to results-{timestamp}-summary.html
. Note that the directory flot/
must be present for the charts to display properly.
Functions:
HTTP_REPORT.setText(text)
: Sets the text at the top of the report.HTTP_REPORT.puts(text)
: Appends the linetext
to the text at the top of the report.HTTP_REPORT.addChart(name)
: Adds a chart with the titlename
to the report and returns aChart
object. SeeChart.put(data)
below.HTTP_REPORT.removeChart(name)
: Removes the chart with titlename
from the report.HTTP_REPORT.clear()
: Clears the text and removes all charts from the report.Chart.put(data)
: Add the data, which is a map of { 'trend-1': value, 'trend-2': value, ... }, to the chart, which tracks the values for each trend over time.SUMMARY_HTML_REFRESH_PERIOD
: Change this global variable to set the auto-refresh period of the HTML page in milliseconds.
Usage:
An HTTP server is started on port HTTP_SERVER_PORT
, which defaults to 8000, unless DISABLE_HTTP_SERVER=true
when nodeloadlib
is included. Likewise, the file results-{timestamp}-summary.html
is written to the current directory when the test ends unless DISABLE_LOGS=true
when nodeloadlib
is included.
A chart is automatically added to HTTP_REPORT
for each statistic requested in a test created by addTest()
or runTest()
, and for each Reportable
object created with parameter addToHttpReport=true
. Call HTTP_REPORT.addChart()
to add additional charts to the report. Add data points to it manually by calling put()
on the returned object.
The progress page automatically issues an AJAX request to refresh the text and chart data every SUMMARY_HTML_REFRESH_PERIOD
milliseconds.
Some handy features worth mentioning.
-
Examine and add to stats to the HTML page:
addTest().stats and runTest().stats are maps:
{ 'latency': Reportable(Histogram), 'result-codes': Reportable(ResultsCounter}, 'uniques': Reportable(Uniques), 'concurrency': Reportable(Peak) }
Put
Reportable
instances to this map to have it automatically updated each reporting interval. Create theReportable
withaddToHttpReport=true
to add a chart for it on the HTML status page. Or, setReport.disableIntervalReporting=true
to only updateReportable.cumulative
and notReportable.interval
each reporting interval. -
Post-process statistics:
Use a
startTests()
callback to examine the final statistics intest.stats[name].cumulative
at test completion.// GET random URLs of the form localhost:8080/data/object-#### for 10 seconds, then // print out all the URLs that were hit. var t = addTest({ timeLimit: 10, targetRps: 10, stats: ['uniques'], requestGenerator: function(client) { return traceableRequest(client, 'GET', '/data/object-' + Math.floor(Math.random()*100));; } }); function printAllUrls() { qputs(JSON.stringify(t.stats['uniques'].cumulative)); } startTests(printAllUrls);
-
Out-of-the-box file server:
Just start
nodeloadlib.js
and it will serve files in the current directory.$ node dist/nodeloadlib.js $ curl -i localhost:8000/nodeloadlib.js # executed in a separate terminal HTTP/1.1 200 OK Content-Length: 50763 Connection: keep-alive var sys = require('sys'); var http = require('http'); ...
-
Run arbitrary Javascript:
POST any valid Javascript to
/remote
to have iteval()
'd.$ node dist/nodeloadlib.js Serving progress report on port 8000. Opening log files. Starting remote test: sys.puts("hello!") hello! $ curl -i -d 'sys.puts("hello!")' localhost:8000/remote # executed in a separate terminal