Comparing version 0.0.3 to 0.0.4
@@ -11,3 +11,3 @@ 'use strict'; | ||
var loadtest = require('./lib/loadtest.js'); | ||
var server = require('./lib/server.js'); | ||
var loadserver = require('./lib/loadserver.js'); | ||
@@ -22,3 +22,3 @@ // globals | ||
exports.loadTest = loadtest.loadTest; | ||
exports.server = server.startServer; | ||
exports.startServer = loadserver.startServer; | ||
@@ -19,11 +19,20 @@ 'use strict'; | ||
var log = new Log('info'); | ||
var globalConcurrency = 100; | ||
var globalRequestsPerSecond = 1; | ||
var agent = true; | ||
var latency; | ||
var requests = 0; | ||
// constants | ||
var DEFAULT_OPTIONS = { | ||
concurrency: 100, | ||
requestsPerSecond: 1, | ||
}; | ||
/** | ||
* A client for an HTTP connection. | ||
* Params is an object which may have: | ||
* - requestsPerSecond: how many requests to create from this client. | ||
* - maxRequests: stop sending requests after this global limit is reached. | ||
* - noAgent: if true, do not use connection keep-alive. | ||
*/ | ||
function HttpClient(url, requestsPerSecond) | ||
function HttpClient(params) | ||
{ | ||
@@ -36,2 +45,3 @@ // self-reference | ||
var lastCall; | ||
var timer; | ||
@@ -43,3 +53,3 @@ /** | ||
{ | ||
if (!requestsPerSecond) | ||
if (!params.requestsPerSecond) | ||
{ | ||
@@ -49,4 +59,4 @@ log.error('No requests per second selected'); | ||
} | ||
var interval = Math.round(1000 / requestsPerSecond); | ||
new timing.HighResolutionTimer(interval, makeRequest); | ||
var interval = Math.round(1000 / params.requestsPerSecond); | ||
timer = new timing.HighResolutionTimer(interval, makeRequest); | ||
} | ||
@@ -59,8 +69,13 @@ | ||
{ | ||
var id = timing.latency.start(id); | ||
var options = urlLib.parse(url); | ||
if (!agent) | ||
requests += 1; | ||
if (requests > params.maxRequests) | ||
{ | ||
options.agent = false; | ||
return timer.stop(); | ||
} | ||
var id = latency.start(id); | ||
var options = urlLib.parse(params.url); | ||
if (params.noAgent) | ||
{ | ||
options.noAgent = false; | ||
} | ||
var get = http.get(options, getConnect(id)); | ||
@@ -80,3 +95,3 @@ get.on('error', function(error) | ||
{ | ||
log.debug('HTTP client connected to %s with id %s', url, id); | ||
log.debug('HTTP client connected to %s with id %s', params.url, id); | ||
connection.setEncoding('utf8'); | ||
@@ -90,3 +105,3 @@ connection.on('data', function(chunk) | ||
log.error('Connection %s failed: %s', id, error); | ||
timing.latency.end(id); | ||
latency.end(id); | ||
@@ -97,3 +112,3 @@ }); | ||
log.debug('Connection %s ended', id); | ||
timing.latency.end(id); | ||
latency.end(id); | ||
}); | ||
@@ -151,3 +166,3 @@ }; | ||
var newCall = new Date().getTime(); | ||
timing.latency.add(newCall - lastCall); | ||
latency.add(newCall - lastCall); | ||
entry += ', latency: ' + (newCall - lastCall); | ||
@@ -189,3 +204,3 @@ lastCall = null; | ||
{ | ||
timing.latency.end(message.requestId); | ||
latency.end(message.requestId); | ||
} | ||
@@ -206,3 +221,3 @@ } | ||
connection.sendUTF(JSON.stringify(update)); | ||
timing.latency.start(update.requestId); | ||
latency.start(update.requestId); | ||
} | ||
@@ -214,18 +229,29 @@ } | ||
* Run a load test. | ||
* Options is an object which may have: | ||
* - url: mandatory URL to access. | ||
* - concurrency: how many concurrent clients to use. | ||
* - requestsPerSecond: how many requests per second per client. | ||
* - noAction: if true, then do not use connection keep-alive. | ||
* An optional callback will be called if/when the test finishes. | ||
*/ | ||
exports.loadTest = function(url, concurrency, requestsPerSecond) | ||
exports.loadTest = function(options, callback) | ||
{ | ||
var constructor; | ||
if (url.startsWith('ws://')) | ||
if (!options.url) | ||
{ | ||
constructor = function(url, requestsPerSecond) | ||
log.error('Missing URL in options'); | ||
return; | ||
} | ||
if (options.url.startsWith('ws://')) | ||
{ | ||
constructor = function(params) | ||
{ | ||
return new WebsocketClient(url, requestsPerSecond); | ||
return new WebsocketClient(params.url, params.requestsPerSecond); | ||
}; | ||
} | ||
else if (url.startsWith('http')) | ||
else if (options.url.startsWith('http')) | ||
{ | ||
constructor = function(url, requestsPerSecond) | ||
constructor = function(params) | ||
{ | ||
return new HttpClient(url, requestsPerSecond); | ||
return new HttpClient(params); | ||
}; | ||
@@ -237,3 +263,8 @@ } | ||
} | ||
startClients(url, concurrency, requestsPerSecond, constructor); | ||
if (callback) | ||
{ | ||
options.callback = callback; | ||
} | ||
latency = new timing.Latency(options); | ||
startClients(options, constructor); | ||
} | ||
@@ -244,8 +275,8 @@ | ||
*/ | ||
function startClients(url, concurrency, requestsPerSecond, constructor) | ||
function startClients(options, constructor) | ||
{ | ||
for (var index = 0; index < concurrency; index++) | ||
for (var index = 0; index < options.concurrency; index++) | ||
{ | ||
url = url.replaceAll('$index', index); | ||
var client = constructor(url, requestsPerSecond); | ||
options.url = options.url.replaceAll('$index', index); | ||
var client = constructor(options); | ||
// start each client 100 ms after the last | ||
@@ -272,5 +303,6 @@ setTimeout(client.start, (index * 100) % 1000); | ||
/** | ||
* Parse one argument. Returns true if there are more. | ||
* Parse one argument and change options accordingly. | ||
* Returns true if there may be more arguments to parse. | ||
*/ | ||
function parseArgument(args) | ||
function parseArgument(args, options) | ||
{ | ||
@@ -286,7 +318,7 @@ if (args.length <= 1) | ||
{ | ||
globalConcurrency = parseInt(argument); | ||
options.concurrency = parseInt(argument); | ||
} | ||
else if (numbersParsed == 1) | ||
{ | ||
globalRequestsPerSecond = parseInt(argument); | ||
options.requestsPerSecond = parseInt(argument); | ||
} | ||
@@ -304,3 +336,2 @@ else | ||
{ | ||
console.error('Unknown argument %s', argument); | ||
return false; | ||
@@ -310,3 +341,3 @@ } | ||
{ | ||
agent = false; | ||
options.noAgent = true; | ||
args.splice(0, 1); | ||
@@ -329,3 +360,4 @@ return true; | ||
{ | ||
while (parseArgument(args)) | ||
var options = DEFAULT_OPTIONS; | ||
while (parseArgument(args, options)) | ||
{ | ||
@@ -335,6 +367,14 @@ } | ||
{ | ||
if (args.length == 0) | ||
{ | ||
console.error('Missing URL'); | ||
} | ||
else | ||
{ | ||
console.error('Unknown arguments %s', JSON.stringify(args)); | ||
} | ||
return help(); | ||
} | ||
var url = args[0]; | ||
exports.loadTest(url, globalConcurrency, globalRequestsPerSecond); | ||
options.url = args[0]; | ||
exports.loadTest(options); | ||
} | ||
@@ -341,0 +381,0 @@ |
@@ -9,2 +9,3 @@ 'use strict'; | ||
// requires | ||
var async = require('async'); | ||
var Log = require('log'); | ||
@@ -40,39 +41,117 @@ | ||
/** | ||
* Run package tests. | ||
* Run tests for string prototypes. | ||
*/ | ||
exports.test = function() | ||
function testStringPrototypes(callback) | ||
{ | ||
if (!'pepito'.startsWith('pe')) | ||
{ | ||
log.error('Failed to match using startsWith()') | ||
return false; | ||
return callback('Failed to match using startsWith()') | ||
} | ||
if ('pepito'.startsWith('po')) | ||
{ | ||
log.error('Invalid match using startsWith()') | ||
return false; | ||
return callback('Invalid match using startsWith()') | ||
} | ||
if (!'pepito'.endsWith('to')) | ||
{ | ||
log.error('Failed to match using endsWith()') | ||
return false; | ||
return callback('Failed to match using endsWith()') | ||
} | ||
if ('pepito'.startsWith('po')) | ||
{ | ||
log.error('Invalid match using endsWith()') | ||
return false; | ||
return callback('Invalid match using endsWith()') | ||
} | ||
if ('pepito'.replaceAll('p', 'c') != 'cecito') | ||
{ | ||
log.error('Invalid replaceAll().'); | ||
return false; | ||
return callback('Invalid replaceAll().'); | ||
} | ||
return true; | ||
callback(null, true); | ||
} | ||
/** | ||
* Count the number of properties in an object. | ||
*/ | ||
exports.countProperties = function(object) | ||
{ | ||
var count = 0; | ||
for (var key in object) | ||
{ | ||
count++; | ||
} | ||
return count; | ||
} | ||
/** | ||
* Overwrite the given object with the original. | ||
*/ | ||
exports.overwriteObject = function(overwriter, original) | ||
{ | ||
if (!overwriter) | ||
{ | ||
return original; | ||
} | ||
if (typeof(overwriter) != 'object') | ||
{ | ||
log.error('Invalid overwriter object %s', overwriter); | ||
return original; | ||
} | ||
for (var key in overwriter) | ||
{ | ||
var value = overwriter[key]; | ||
if (value) | ||
{ | ||
original[key] = value; | ||
} | ||
} | ||
return original; | ||
} | ||
/** | ||
* Test that overwrite object works. | ||
*/ | ||
function testOverwriteObject(callback) | ||
{ | ||
var first = { | ||
a: 'a', | ||
b: 'b', | ||
}; | ||
var second = { | ||
b: 'b2', | ||
c: {d: 5}, | ||
}; | ||
exports.overwriteObject(first, second); | ||
if (exports.countProperties(second) != 3) | ||
{ | ||
return callback('Overwritten should have three properties'); | ||
} | ||
if (second.b != 'b') | ||
{ | ||
return callback('Property in second should be replaced with first'); | ||
} | ||
callback(null, true); | ||
} | ||
/** | ||
* Run package tests. | ||
*/ | ||
exports.test = function(callback) | ||
{ | ||
var tests = { | ||
stringPrototypes: testStringPrototypes, | ||
overwrite: testOverwriteObject, | ||
}; | ||
async.series(tests, callback); | ||
} | ||
// run tests if invoked directly | ||
if (__filename == process.argv[1]) | ||
{ | ||
exports.test(); | ||
exports.test(function(error, result) | ||
{ | ||
if (error) | ||
{ | ||
log.error('Tests failed: %s', error); | ||
return; | ||
} | ||
log.info('Tests succesful'); | ||
}); | ||
} | ||
@@ -10,2 +10,4 @@ 'use strict'; | ||
// requires | ||
var async = require('async'); | ||
var util = require('util'); | ||
var Log = require('log'); | ||
@@ -20,5 +22,8 @@ var microtime = require('microtime'); | ||
/** | ||
* Latency measurements, global variable. | ||
* Latency measurements. Options can be: | ||
* - seconds: how many seconds to measure before showing latency. | ||
* - maxRequests: how many requests to make, alternative to seconds. | ||
* - callback: function to call when there are measures. | ||
*/ | ||
exports.latency = new function() | ||
exports.Latency = function(options) | ||
{ | ||
@@ -31,12 +36,23 @@ // self-reference | ||
var secondsMeasured = 5; | ||
var maxRequests = null; | ||
var totalRequests = 0; | ||
var totalTime = 0; | ||
var lastShown = microtime.now(); | ||
var callback = null; | ||
/** | ||
* Initialize with seconds measured. | ||
*/ | ||
self.init = function(seconds) | ||
// init | ||
if (options) | ||
{ | ||
secondsMeasured = seconds; | ||
if (options.maxRequests) | ||
{ | ||
maxRequests = options.maxRequests; | ||
} | ||
if (options.seconds) | ||
{ | ||
secondsMeasured = options.seconds; | ||
} | ||
if (options.callback) | ||
{ | ||
callback = options.callback; | ||
} | ||
} | ||
@@ -61,3 +77,3 @@ | ||
{ | ||
log.error('Message id ' + requestId + ' not found'); | ||
error('Message id ' + requestId + ' not found'); | ||
return; | ||
@@ -76,3 +92,3 @@ } | ||
{ | ||
log.error('Please call init() with seconds to measure'); | ||
error('Please call init() with seconds to measure'); | ||
return; | ||
@@ -84,19 +100,64 @@ } | ||
log.debug('Total requests: %s', totalRequests); | ||
if (isFinished()) | ||
{ | ||
show(); | ||
} | ||
} | ||
/** | ||
* Show an error message, or send to the callback. | ||
*/ | ||
function error(message) | ||
{ | ||
if (callback) | ||
{ | ||
callback(error, null); | ||
return; | ||
} | ||
log.error(message); | ||
} | ||
/** | ||
* Check out if enough seconds have elapsed, or enough requests were received. | ||
*/ | ||
function isFinished() | ||
{ | ||
if (maxRequests) | ||
{ | ||
return (totalRequests >= maxRequests); | ||
} | ||
var secondsElapsed = (microtime.now() - lastShown) / 1000000; | ||
if (secondsElapsed >= secondsMeasured) | ||
return (secondsElapsed >= secondsMeasured); | ||
} | ||
/** | ||
* Checks if enough seconds have elapsed, or enough requests received. | ||
* Show latency for finished requests, or send to the callback. | ||
*/ | ||
function show() | ||
{ | ||
var secondsElapsed = (microtime.now() - lastShown) / 1000000; | ||
var meanTime = totalTime / totalRequests; | ||
var results = { | ||
meanLatencyMs: Math.round(meanTime / 10) / 100, | ||
rps: Math.round(totalRequests / secondsElapsed), | ||
} | ||
if (callback) | ||
{ | ||
var meanTime = totalTime / totalRequests; | ||
var rps = Math.round(totalRequests / secondsElapsed); | ||
log.info('Requests / second: %s, mean latency: %s ms', rps, Math.round(meanTime / 10) / 100); | ||
totalTime = 0; | ||
totalRequests = 0; | ||
if (secondsElapsed > 2 * secondsMeasured) | ||
{ | ||
lastShown = microtime.now(); | ||
} | ||
else | ||
{ | ||
lastShown += secondsMeasured * 1000000; | ||
} | ||
callback(null, results); | ||
} | ||
else | ||
{ | ||
log.info('Requests / second: %s, mean latency: %s ms', results.rps, results.meanLatencyMs); | ||
} | ||
totalTime = 0; | ||
totalRequests = 0; | ||
if (secondsElapsed > 2 * secondsMeasured) | ||
{ | ||
lastShown = microtime.now(); | ||
} | ||
else | ||
{ | ||
lastShown += secondsMeasured * 1000000; | ||
} | ||
} | ||
@@ -106,2 +167,55 @@ } | ||
/** | ||
* Test latency ids. | ||
*/ | ||
function testLatencyIds(callback) | ||
{ | ||
var latency = new exports.Latency(); | ||
var firstId = latency.start(); | ||
if (!firstId) | ||
{ | ||
return callback('Invalid first latency id'); | ||
} | ||
var secondId = latency.start(); | ||
if (!secondId) | ||
{ | ||
return callback('Invalid second latency id'); | ||
} | ||
if (firstId == secondId) | ||
{ | ||
return callback('Repeated latency ids'); | ||
} | ||
callback(null, true); | ||
} | ||
/** | ||
* Test latency measurements. | ||
*/ | ||
function testLatencyRequests(callback) | ||
{ | ||
var measured = false; | ||
var options = { | ||
maxRequests: 10, | ||
callback: function(error, result) | ||
{ | ||
measured = true; | ||
callback(error, result); | ||
}, | ||
}; | ||
var latency = new exports.Latency(options); | ||
for (var i = 0; i < 10; i++) | ||
{ | ||
var id = latency.start(); | ||
latency.end(id); | ||
} | ||
// give it time | ||
setTimeout(function() | ||
{ | ||
if (!measured) | ||
{ | ||
callback('Latency did not call back in 10 ms'); | ||
} | ||
}, 10); | ||
} | ||
/** | ||
* A high resolution timer. | ||
@@ -118,2 +232,3 @@ * Initialize with milliseconds to wait and the callback to call. | ||
var start = Date.now(); | ||
var active = true; | ||
@@ -125,2 +240,6 @@ /** | ||
{ | ||
if (!active) | ||
{ | ||
return false; | ||
} | ||
callback(delayMs); | ||
@@ -142,2 +261,10 @@ counter ++; | ||
/** | ||
* Stop the timer. | ||
*/ | ||
self.stop = function() | ||
{ | ||
active = false; | ||
} | ||
// start timer | ||
@@ -149,31 +276,49 @@ delayed(); | ||
/** | ||
* Run package tests. | ||
* Test a high resolution timer. | ||
*/ | ||
exports.test = function() | ||
function testTimer(callback) | ||
{ | ||
var firstId = exports.latency.start(); | ||
if (!firstId) | ||
var fired = false; | ||
var timer = new exports.HighResolutionTimer(10, function() | ||
{ | ||
log.error('Invalid first latency id'); | ||
return false; | ||
} | ||
var secondId = exports.latency.start(); | ||
if (!secondId) | ||
fired = true; | ||
callback(null, 'Timer fired'); | ||
}); | ||
timer.stop(); | ||
// give it time | ||
setTimeout(function() | ||
{ | ||
log.error('Invalid second latency id'); | ||
return false; | ||
} | ||
if (firstId == secondId) | ||
{ | ||
log.error('Repeated latency ids'); | ||
return false; | ||
} | ||
return true; | ||
if (!fired) | ||
{ | ||
callback('Timer did not fire in 20 ms'); | ||
} | ||
}, 20); | ||
} | ||
/** | ||
* Run package tests. | ||
*/ | ||
exports.test = function(callback) | ||
{ | ||
var tests = { | ||
latencyIds: testLatencyIds, | ||
latencyRequests: testLatencyRequests, | ||
timer: testTimer, | ||
}; | ||
async.series(tests, callback); | ||
} | ||
// run tests if invoked directly | ||
if (__filename == process.argv[1]) | ||
{ | ||
exports.test(); | ||
exports.test(function(error, result) | ||
{ | ||
if (error) | ||
{ | ||
log.error('Tests failed: %s', error); | ||
return; | ||
} | ||
log.info('Tests succesful'); | ||
}); | ||
} | ||
{ | ||
"name": "loadtest", | ||
"version": "0.0.3", | ||
"version": "0.0.4", | ||
"description": "Load test scripts.", | ||
@@ -16,2 +16,3 @@ "homepage": "http://milliearth.org/", | ||
"microtime": "*", | ||
"async": "*", | ||
"log": "*" | ||
@@ -18,0 +19,0 @@ }, |
@@ -1,8 +0,6 @@ | ||
loadtest | ||
======== | ||
# loadtest | ||
Runs a load test on the selected URL or websocket. Easy to extend minimally for your own ends. | ||
Runs a load test on the selected HTTP or websocket URL. The API allows for easy integration in your own tests. | ||
Installation | ||
------------ | ||
## Installation | ||
@@ -12,5 +10,6 @@ Just run: | ||
Usage | ||
----- | ||
Or add package loadtest to your package.json dependencies. | ||
## Usage | ||
Run as a script to load test a URL: | ||
@@ -30,20 +29,19 @@ | ||
## Concurrency | ||
#### Concurrency | ||
loadtest will create a simultaneous number of clients. | ||
loadtest will create a simultaneous number of clients; this parameter controls how many. | ||
## Requests Per Second | ||
#### Requests Per Second | ||
Controls the number of requests per second for each client. | ||
## --noagent | ||
#### --noagent | ||
Open connections without keep-alive: send header 'Connection: Close' instead of 'Connection: Keep-alive'. | ||
Server | ||
------ | ||
### Server | ||
loadtest bundles a test server. To run it: | ||
$ node lib/server.js [port] | ||
$ node lib/loadserver.js [port] | ||
@@ -54,5 +52,59 @@ It will show the number of requests received per second, the latency in answering requests and the headers for selected requests. | ||
License | ||
------- | ||
## API | ||
loadtest is not limited to running from the command line; it can be controlled using an API, thus allowing you to load test your application in your own tests. | ||
### Invoke Load Test | ||
To run a load test use the exported function loadTest() passing it a set of options and an optional callback: | ||
var loadtest = require('loadtest'); | ||
var options = { | ||
url: 'http://localhost:8000', | ||
maxRequests: 1000, | ||
}; | ||
loadtest.loadTest(options, function(error, result) | ||
{ | ||
if (error) | ||
{ | ||
return console.error('Got an error: %s', error); | ||
} | ||
console.log('Tests run successfully'); | ||
}); | ||
The callback will be invoked when the max number of requests is reached, or when the number of seconds has elapsed. Options are: | ||
### Options | ||
This is the set of available options. Except where noted, all options are (as their name implies) optional. | ||
#### url | ||
The URL to invoke. | ||
#### concurrency | ||
How many clients to start in parallel. | ||
#### requestsPerSecond | ||
How many requests each client will send per second. | ||
#### maxRequests | ||
A max number of requests; after they are reached the test will end. | ||
### Start Test Server | ||
To start the test server use the exported function startServer() with a port and an optional callback: | ||
var loadserver = require('loadserver'); | ||
loadserver.startServer(8000); | ||
### Complete Sample | ||
The file lib/sample.js shows a complete sample, which is also an integration test: it starts the server, send 1000 requests, waits for the callback and closes down the server. | ||
## License | ||
(The MIT License) | ||
@@ -59,0 +111,0 @@ |
42
test.js
@@ -11,2 +11,5 @@ 'use strict'; | ||
var timing = require('./lib/timing.js'); | ||
var sample = require('./lib/sample.js'); | ||
var util = require('util'); | ||
var async = require('async'); | ||
var Log = require('log'); | ||
@@ -21,15 +24,23 @@ | ||
*/ | ||
exports.test = function() | ||
exports.test = function(callback) | ||
{ | ||
if (!prototypes.test()) | ||
var run = false; | ||
var tests = { | ||
prototypes: prototypes.test, | ||
timing: timing.test, | ||
sample: sample.test, | ||
}; | ||
async.series(tests, function(error, result) | ||
{ | ||
log.error('Failure in prototypes test'); | ||
exit(1); | ||
} | ||
if (!timing.test()) | ||
run = true; | ||
callback(error, result); | ||
}); | ||
// give it time | ||
setTimeout(function() | ||
{ | ||
log.error('Failure in timing test'); | ||
exit(1); | ||
} | ||
log.notice('Tests run correctly'); | ||
if (!run) | ||
{ | ||
callback('Package tests did not call back'); | ||
} | ||
}, 2200); | ||
} | ||
@@ -40,4 +51,13 @@ | ||
{ | ||
exports.test(); | ||
exports.test(function(error, result) | ||
{ | ||
if (error) | ||
{ | ||
log.error('Failure in tests: %s', error); | ||
process.exit(1); | ||
return; | ||
} | ||
log.info('All tests successful: %s', util.inspect(result, true, 10, true)); | ||
}); | ||
} | ||
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
26667
11
1035
117
2
4
4
+ Addedasync@*
+ Addedasync@3.2.6(transitive)