Comparing version 0.2.0 to 0.2.1
@@ -19,1 +19,11 @@ | ||
### 2013-09-01: version 0.2.1 | ||
* Added bin/ files to avoid cross-platform issues. | ||
Parse arguments using `optimist`. | ||
Pull request #9 by https://github.com/zaphod1984 | ||
* Use native process.hrtime() instead of Microtime. | ||
Pull request #8 by to https://github.com/zaphod1984 | ||
* Changed binary testserver to testserver-loadtest, to avoid conflicts. | ||
Issue #4. Thanks to https://github.com/zaphod1984 | ||
@@ -17,2 +17,3 @@ 'use strict'; | ||
var websocket = require('./websocket.js'); | ||
var ce = require('cloneextend'); | ||
@@ -25,3 +26,3 @@ // globals | ||
noAgent: true, | ||
concurrency: 1, | ||
concurrency: 1 | ||
}; | ||
@@ -115,3 +116,3 @@ var SHOW_INTERVAL_MS = 5000; | ||
requestTimer = new timing.HighResolutionTimer(interval, self.makeRequest); | ||
} | ||
}; | ||
@@ -127,3 +128,3 @@ /** | ||
} | ||
} | ||
}; | ||
@@ -151,3 +152,3 @@ /** | ||
request.end(); | ||
} | ||
}; | ||
@@ -267,3 +268,3 @@ /** | ||
operation.start(); | ||
} | ||
}; | ||
@@ -297,3 +298,3 @@ /** | ||
showTimer = new timing.HighResolutionTimer(SHOW_INTERVAL_MS, self.latency.showPartial); | ||
} | ||
}; | ||
@@ -321,3 +322,3 @@ /** | ||
} | ||
} | ||
}; | ||
@@ -361,6 +362,7 @@ /** | ||
self.running = false; | ||
for (var index in clients) | ||
{ | ||
Object.keys(clients).forEach(function(index) { | ||
clients[index].stop(); | ||
} | ||
}); | ||
if (callback) | ||
@@ -371,133 +373,11 @@ { | ||
} | ||
} | ||
}; | ||
/** | ||
* Display online help. | ||
*/ | ||
function help() | ||
{ | ||
console.log('Usage: loadtest [options] URL'); | ||
console.log(' where URL can be a regular HTTP or websocket URL:'); | ||
console.log(' runs a load test for the given URL'); | ||
console.log('Apache ab-compatible options are:'); | ||
console.log(' -n requests Number of requests to perform'); | ||
console.log(' -c concurrency Number of multiple requests to make'); | ||
console.log(' -t timelimit Seconds to max. wait for responses'); | ||
console.log(' -C name=value Send a cookie with the given name'); | ||
console.log(' -T content-type The MIME type for the body'); | ||
console.log(' -p POST-file Send the contents of the file as POST body'); | ||
console.log(' -u PUT-file Send the contents of the file as PUT body'); | ||
console.log(' -r Do not exit on socket receive errors'); | ||
console.log('Other options are:'); | ||
console.log(' --rps Requests per second for each client'); | ||
console.log(' --noagent Do not use http agent (default)'); | ||
console.log(' --agent Use http agent (Connection: keep-alive)'); | ||
console.log(' --keepalive Use a specialized keep-alive http agent (agentkeepalive)'); | ||
console.log(' --index param Replace the value of param with an index in the URL'); | ||
console.log(' --quiet Do not log any messages'); | ||
console.log(' --debug Show debug messages'); | ||
} | ||
/** | ||
* Parse one argument and change options accordingly. | ||
* Returns true if there may be more arguments to parse. | ||
*/ | ||
function parseArgument(args, options) | ||
{ | ||
if (args.length <= 1) | ||
{ | ||
return false; | ||
} | ||
if (!args[0].startsWith('-')) | ||
{ | ||
return false; | ||
} | ||
// consume the argument | ||
var argument = args.shift(); | ||
switch(argument) | ||
{ | ||
case '-n': | ||
options.maxRequests = args.shift(); | ||
return true; | ||
case '-c': | ||
options.concurrency = args.shift(); | ||
return true; | ||
case '-t': | ||
options.maxSeconds = args.shift(); | ||
return true; | ||
case '-C': | ||
addCookie(args.shift(), options); | ||
return true; | ||
case '-T': | ||
options.contentType = args.shift(); | ||
return true; | ||
case '-p': | ||
options.method = 'POST'; | ||
options.body = fs.readFileSync(args.shift()).toString(); | ||
return true; | ||
case '-u': | ||
options.method = 'PUT'; | ||
options.body = fs.readFileSync(args.shift()).toString(); | ||
return true; | ||
case '-r': | ||
options.recover = true; | ||
return true; | ||
case '--rps': | ||
options.requestsPerSecond = parseFloat(args.shift()); | ||
return true; | ||
case '--noagent': | ||
return true; | ||
case '--agent': | ||
options.noAgent = false; | ||
return true; | ||
case '--keepalive': | ||
options.agentKeepAlive = true; | ||
return true; | ||
case '--quiet': | ||
options.quiet = true; | ||
return true; | ||
case '--debug': | ||
options.debug = true; | ||
return true; | ||
} | ||
console.error('Unknown option %s', argument); | ||
return false; | ||
} | ||
/** | ||
* Add a cookie to the options. | ||
*/ | ||
function addCookie(cookie, options) | ||
{ | ||
if (!options.cookies) | ||
{ | ||
options.cookies = []; | ||
} | ||
options.cookies.push(cookie); | ||
} | ||
/** | ||
* Process command line arguments and run | ||
*/ | ||
exports.run = function(args) | ||
exports.run = function(options) | ||
{ | ||
var options = DEFAULT_OPTIONS; | ||
while (parseArgument(args, options)) | ||
{ | ||
} | ||
if (args.length != 1) | ||
{ | ||
if (args.length == 0) | ||
{ | ||
console.error('Missing URL'); | ||
} | ||
else | ||
{ | ||
console.error('Unknown arguments %s', JSON.stringify(args)); | ||
} | ||
return help(); | ||
} | ||
options.url = args[0]; | ||
exports.loadTest(options); | ||
} | ||
exports.loadTest(ce.cloneextend(DEFAULT_OPTIONS, options)); | ||
}; | ||
@@ -504,0 +384,0 @@ // start load test if invoked directly |
@@ -94,3 +94,3 @@ 'use strict'; | ||
testing.assertEquals(result.totalRequests, 100, 'Invalid total requests', callback); | ||
testing.assert(result.meanLatencyMs > delay, 'Delay should be higher than %s, not %s', delay, result.meanLatencyMs, callback); | ||
testing.assert(result.meanLatencyMs >= delay, 'Delay should be higher than %s, not %s', delay, result.meanLatencyMs, callback); | ||
callback(null, result); | ||
@@ -97,0 +97,0 @@ }); |
@@ -150,52 +150,4 @@ 'use strict'; | ||
return server.start(callback); | ||
} | ||
}; | ||
/** | ||
* Display online help. | ||
*/ | ||
function help() | ||
{ | ||
console.log('Usage: testserver [options] [port]'); | ||
console.log(' starts a test server on the given port, default 80.'); | ||
console.log('Options are:'); | ||
console.log(' --delay Delay the response for the given milliseconds'); | ||
} | ||
/** | ||
* Process command line arguments. | ||
*/ | ||
exports.run = function(args) | ||
{ | ||
var options = {}; | ||
while (args.length > 0 && args[0].startsWith('--')) | ||
{ | ||
var arg = args.shift(); | ||
if (arg == '--delay') | ||
{ | ||
options.delay = parseInt(args.shift()); | ||
} | ||
else | ||
{ | ||
return help(); | ||
} | ||
} | ||
if (args.length > 1) | ||
{ | ||
return help(); | ||
} | ||
if (args.length == 1) | ||
{ | ||
var arg = args[0]; | ||
if (parseInt(arg)) | ||
{ | ||
options.port = parseInt(arg); | ||
} | ||
else | ||
{ | ||
return help(); | ||
} | ||
} | ||
exports.startServer(options); | ||
} | ||
// start server if invoked directly | ||
@@ -202,0 +154,0 @@ if (__filename == process.argv[1]) |
@@ -14,3 +14,2 @@ 'use strict'; | ||
var Log = require('log'); | ||
var microtime = require('microtime'); | ||
var prototypes = require('./prototypes.js'); | ||
@@ -40,4 +39,4 @@ | ||
var partialErrors = 0; | ||
var lastShown = microtime.now(); | ||
var initialTime = microtime.now(); | ||
var lastShown = getTime(); | ||
var initialTime = getTime(); | ||
var totalRequests = 0; | ||
@@ -67,5 +66,5 @@ var totalTime = 0; | ||
requestId = requestId || createId(); | ||
requests[requestId] = microtime.now(); | ||
requests[requestId] = getTime(); | ||
return requestId; | ||
} | ||
}; | ||
@@ -87,5 +86,5 @@ /** | ||
} | ||
add(microtime.now() - requests[requestId], errorCode); | ||
add(getElapsed(requests[requestId]), errorCode); | ||
delete requests[requestId]; | ||
} | ||
}; | ||
@@ -115,3 +114,3 @@ /** | ||
log.debug('Partial requests: %s', partialRequests); | ||
var rounded = Math.floor(time / 1000); | ||
var rounded = Math.floor(time); | ||
if (rounded > maxLatencyMs) | ||
@@ -138,12 +137,12 @@ { | ||
{ | ||
var elapsedSeconds = (microtime.now() - lastShown) / 1000000; | ||
var elapsedSeconds = getElapsed(lastShown) / 1000; | ||
var meanTime = partialTime / partialRequests || 0.0; | ||
var results = { | ||
meanLatencyMs: Math.round(meanTime / 10) / 100, | ||
rps: Math.round(partialRequests / elapsedSeconds), | ||
} | ||
meanLatencyMs: Math.round(meanTime / 10) * 10, | ||
rps: Math.round(partialRequests / elapsedSeconds) | ||
}; | ||
var percent = ''; | ||
if (options.maxRequests) | ||
{ | ||
var percent = ' (' + Math.round(100 * totalRequests / options.maxRequests) + '%)'; | ||
percent = ' (' + Math.round(100 * totalRequests / options.maxRequests) + '%)'; | ||
} | ||
@@ -153,3 +152,3 @@ log.info('Requests: %s%s, requests per second: %s, mean latency: %s ms', totalRequests, percent, results.rps, results.meanLatencyMs); | ||
{ | ||
var percent = Math.round(100 * 10 * totalErrors / totalRequests) / 10; | ||
percent = Math.round(100 * 10 * totalErrors / totalRequests) / 10; | ||
log.info('Errors: %s, accumulated errors: %s, %s% of total requests', partialErrors, totalErrors, percent); | ||
@@ -160,6 +159,24 @@ } | ||
partialErrors = 0; | ||
lastShown = microtime.now(); | ||
lastShown = getTime(); | ||
}; | ||
/** | ||
* Returns the current high-resolution real time in a [seconds, nanoseconds] tuple Array | ||
* @return {*} | ||
*/ | ||
function getTime() { | ||
return process.hrtime(); | ||
} | ||
/** | ||
* calculates the elapsed time between the assigned startTime and now | ||
* @param startTime | ||
* @return {Number} the elapsed time in milliseconds | ||
*/ | ||
function getElapsed(startTime) { | ||
var elapsed = process.hrtime(startTime); | ||
return elapsed[0] * 1000 + elapsed[1] / 1000000; | ||
} | ||
/** | ||
* Check out if the measures are finished. | ||
@@ -175,3 +192,3 @@ */ | ||
} | ||
var elapsedSeconds = (microtime.now() - initialTime) / 1000000; | ||
var elapsedSeconds = getElapsed(initialTime) / 1000; | ||
if (options.maxSeconds && elapsedSeconds >= maxSeconds) | ||
@@ -203,3 +220,3 @@ { | ||
{ | ||
var elapsedSeconds = (microtime.now() - initialTime) / 1000000; | ||
var elapsedSeconds = getElapsed(initialTime) / 1000; | ||
var meanTime = totalTime / totalRequests; | ||
@@ -211,7 +228,7 @@ return { | ||
rps: Math.round(totalRequests / elapsedSeconds), | ||
meanLatencyMs: Math.round(meanTime / 10) / 100, | ||
meanLatencyMs: Math.round(meanTime / 10) * 10, | ||
percentiles: self.computePercentiles(), | ||
errorCodes: errorCodes, | ||
errorCodes: errorCodes | ||
} | ||
} | ||
}; | ||
@@ -228,5 +245,6 @@ /** | ||
95: false, | ||
99: false, | ||
99: false | ||
}; | ||
var counted = 0; | ||
for (var ms = 0; ms <= maxLatencyMs; ms++) | ||
@@ -241,4 +259,4 @@ { | ||
var percent = counted / totalRequests * 100; | ||
for (var percentile in percentiles) | ||
{ | ||
Object.keys(percentiles).forEach(function(percentile) { | ||
log.debug('Checking percentile %s for %s', percentile, percent); | ||
@@ -249,6 +267,6 @@ if (!percentiles[percentile] && percent > percentile) | ||
} | ||
} | ||
}); | ||
} | ||
return percentiles; | ||
} | ||
}; | ||
@@ -265,9 +283,10 @@ /** | ||
log.info('Requests per second: %s', results.rps); | ||
log.info('Mean latency: %s ms', results.meanLatencyMs); | ||
log.info('Total time: %s s', results.totalTimeSeconds); | ||
log.info(''); | ||
log.info('Percentage of the requests served within a certain time'); | ||
for (var percentile in results.percentiles) | ||
{ | ||
Object.keys(results.percentiles).forEach(function(percentile) { | ||
log.info(' %s% %s ms', percentile, results.percentiles[percentile]); | ||
} | ||
}); | ||
log.info(' 100% %s ms (longest request)', maxLatencyMs); | ||
@@ -277,12 +296,12 @@ if (results.totalErrors) | ||
log.info(''); | ||
log.info('Total errors: %s', results.totalErrors); | ||
log.info(' 100% %s ms (longest request)', maxLatencyMs); | ||
log.info(''); | ||
for (var errorCode in results.errorCodes) | ||
{ | ||
Object.keys(results.errorCodes).forEach(function(errorCode) { | ||
var padding = ' '.repeat(4 - errorCode.length); | ||
log.info(' %s%s: %s errors', padding, errorCode, results.errorCodes[errorCode]); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
@@ -332,8 +351,9 @@ | ||
}); | ||
var id; | ||
for (var i = 0; i < 9; i++) | ||
{ | ||
var id = latency.start(); | ||
id = latency.start(); | ||
latency.end(id); | ||
} | ||
var id = latency.start(); | ||
id = latency.start(); | ||
latency.end(id, errorCode); | ||
@@ -348,3 +368,3 @@ } | ||
var options = { | ||
maxRequests: 10, | ||
maxRequests: 10 | ||
}; | ||
@@ -354,6 +374,7 @@ var latency = new exports.Latency(options, function(error, result) | ||
var percentiles = latency.getResults().percentiles; | ||
for (var percentile in percentiles) | ||
{ | ||
Object.keys(percentiles).forEach(function(percentile) { | ||
testing.assert(percentiles[percentile] !== false, 'Empty percentile for %s', percentile, callback); | ||
} | ||
}); | ||
testing.success(percentiles, callback); | ||
@@ -418,3 +439,3 @@ }); | ||
log.debug('Seconds: ' + Math.round(diff / 1000) + ', counter: ' + counter + ', drift: ' + drift); | ||
} | ||
}; | ||
@@ -427,3 +448,3 @@ /** | ||
active = false; | ||
} | ||
}; | ||
@@ -433,3 +454,3 @@ // start timer | ||
setTimeout(delayed, delayMs); | ||
} | ||
}; | ||
@@ -454,6 +475,6 @@ /** | ||
latencyPercentiles: testLatencyPercentiles, | ||
timer: testTimer, | ||
timer: testTimer | ||
}; | ||
testing.run(tests, callback); | ||
} | ||
}; | ||
@@ -460,0 +481,0 @@ // run tests if invoked directly |
{ | ||
"name": "loadtest", | ||
"version": "0.2.0", | ||
"version": "0.2.1", | ||
"description": "Run load tests for your web application. Mostly ab-compatible interface, with an option to force requests per second. Includes an API for automated load testing.", | ||
@@ -15,6 +15,7 @@ "homepage": "https://github.com/alexfernandez/loadtest", | ||
"websocket": "*", | ||
"microtime": "*", | ||
"testing": "*", | ||
"agentkeepalive": "*", | ||
"log": "*" | ||
"log": "*", | ||
"optimist": "~0.6.0", | ||
"cloneextend": "0.0.3" | ||
}, | ||
@@ -26,4 +27,4 @@ "keywords" : ["testing", "test", "load test", "load testing", "http", "performance", "black box"], | ||
"bin": { | ||
"loadtest": "lib/cli-wrapper.js", | ||
"testserver": "lib/cli-wrapper.js" | ||
"loadtest": "lib/bin/loadtest.js", | ||
"testserver-loadtest": "lib/bin/testserver.js" | ||
}, | ||
@@ -30,0 +31,0 @@ "preferGlobal": true, |
Sorry, the diff of this file is not supported yet
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
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
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
16
4
3
45811
6
1465
+ Addedcloneextend@0.0.3
+ Addedoptimist@~0.6.0
+ Addedcloneextend@0.0.3(transitive)
+ Addedminimist@0.0.10(transitive)
+ Addedoptimist@0.6.1(transitive)
+ Addedwordwrap@0.0.3(transitive)
- Removedmicrotime@*
- Removedmicrotime@3.1.1(transitive)
- Removednode-addon-api@5.1.0(transitive)