Comparing version 0.0.2 to 0.0.3
@@ -15,15 +15,15 @@ 'use strict'; | ||
var prototypes = require('./prototypes.js'); | ||
var timing = require('./timing.js'); | ||
// globals | ||
var log = new Log('info'); | ||
var concurrency = 10; | ||
var requestsPerSecond = 20; | ||
var secondsMeasured = 5; | ||
var server = 'localhost:80'; | ||
var globalConcurrency = 100; | ||
var globalRequestsPerSecond = 1; | ||
var agent = true; | ||
/** | ||
* Latency measurements, global variable. | ||
* A client for an HTTP connection. | ||
*/ | ||
var latency = new function() | ||
function HttpClient(url, requestsPerSecond) | ||
{ | ||
@@ -34,77 +34,31 @@ // self-reference | ||
// attributes | ||
var requests = {}; | ||
var measurements = []; | ||
var index = 0; | ||
var max = concurrency * requestsPerSecond * secondsMeasured; | ||
var total = 0; | ||
var connection; | ||
var lastCall; | ||
/** | ||
* Start the request with the given id. | ||
* Start the HTTP client. | ||
*/ | ||
self.start = function(requestId) | ||
self.start = function() | ||
{ | ||
requests[requestId] = new Date().getTime(); | ||
} | ||
/** | ||
* Compute elapsed time and add the measurement. | ||
*/ | ||
self.end = function(requestId) | ||
{ | ||
if (!(requestId in requests)) | ||
if (!requestsPerSecond) | ||
{ | ||
console.error('Message id ' + requestId + ' not found'); | ||
log.error('No requests per second selected'); | ||
return; | ||
} | ||
add(new Date().getTime() - requests[requestId]); | ||
delete requests[requestId]; | ||
var interval = Math.round(1000 / requestsPerSecond); | ||
new timing.HighResolutionTimer(interval, makeRequest); | ||
} | ||
/** | ||
* Add a new measurement, possibly removing an old one. | ||
* Make a single request to the server. | ||
*/ | ||
function add(value) | ||
function makeRequest() | ||
{ | ||
measurements.push(value); | ||
total += value; | ||
if (measurements.length > max) | ||
var id = timing.latency.start(id); | ||
var options = urlLib.parse(url); | ||
if (!agent) | ||
{ | ||
var removed = measurements.shift(); | ||
total -= removed; | ||
options.agent = false; | ||
} | ||
index++; | ||
debug('Index: ' + index); | ||
if (index > max) | ||
{ | ||
var mean = total / measurements.length; | ||
info('Mean latency: ' + mean); | ||
index = 0; | ||
} | ||
} | ||
} | ||
/** | ||
* A client for an HTTP connection. | ||
*/ | ||
function HttpClient(url) | ||
{ | ||
// self-reference | ||
var self = this; | ||
// attributes | ||
var connection; | ||
var lastCall; | ||
/** | ||
* Start the HTTP client. | ||
*/ | ||
self.start = function() | ||
{ | ||
setInterval(get, Math.round(1000 / requestsPerSecond)); | ||
} | ||
function get() | ||
{ | ||
var options = urlLib.parse(url); | ||
var get = http.get(url, connect); | ||
var get = http.get(options, getConnect(id)); | ||
get.on('error', function(error) | ||
@@ -117,12 +71,26 @@ { | ||
/** | ||
* Connect the player. | ||
* Get a function to connect the player. | ||
*/ | ||
function connect(connection) | ||
function getConnect(id) | ||
{ | ||
log.debug('HTTP client connected to %s', url); | ||
connection.setEncoding('utf8'); | ||
connection.on('data', function(chunk) | ||
return function(connection) | ||
{ | ||
log.debug('Body: %s', chunk); | ||
}); | ||
log.debug('HTTP client connected to %s with id %s', url, id); | ||
connection.setEncoding('utf8'); | ||
connection.on('data', function(chunk) | ||
{ | ||
log.debug('Body: %s', chunk); | ||
}); | ||
connection.on('error', function(error) | ||
{ | ||
log.error('Connection %s failed: %s', id, error); | ||
timing.latency.end(id); | ||
}); | ||
connection.on('end', function(result) | ||
{ | ||
log.debug('Connection %s ended', id); | ||
timing.latency.end(id); | ||
}); | ||
}; | ||
} | ||
@@ -134,3 +102,3 @@ } | ||
*/ | ||
function WebsocketClient(url) | ||
function WebsocketClient(url, requestsPerSecond) | ||
{ | ||
@@ -155,3 +123,3 @@ // self-reference | ||
client.connect(url, []); | ||
info('WebSocket client connected to ' + url); | ||
log.debug('WebSocket client connected to ' + url); | ||
} | ||
@@ -169,3 +137,3 @@ | ||
connection.on('close', function() { | ||
info('Connection closed'); | ||
log.debug('Connection closed'); | ||
}); | ||
@@ -181,3 +149,3 @@ connection.on('message', function(message) { | ||
var newCall = new Date().getTime(); | ||
latency.add(newCall - lastCall); | ||
timing.latency.add(newCall - lastCall); | ||
entry += ', latency: ' + (newCall - lastCall); | ||
@@ -213,3 +181,3 @@ lastCall = null; | ||
{ | ||
info('Starting game for ' + self.playerId); | ||
log.debug('Starting game for ' + self.playerId); | ||
setInterval(requestUpdate, Math.round(1000 / requestsPerSecond)); | ||
@@ -220,3 +188,3 @@ return; | ||
{ | ||
latency.end(message.requestId); | ||
timing.latency.end(message.requestId); | ||
} | ||
@@ -237,3 +205,3 @@ } | ||
connection.sendUTF(JSON.stringify(update)); | ||
latency.start(update.requestId); | ||
timing.latency.start(update.requestId); | ||
} | ||
@@ -244,2 +212,43 @@ } | ||
/** | ||
* Run a load test. | ||
*/ | ||
exports.loadTest = function(url, concurrency, requestsPerSecond) | ||
{ | ||
var constructor; | ||
if (url.startsWith('ws://')) | ||
{ | ||
constructor = function(url, requestsPerSecond) | ||
{ | ||
return new WebsocketClient(url, requestsPerSecond); | ||
}; | ||
} | ||
else if (url.startsWith('http')) | ||
{ | ||
constructor = function(url, requestsPerSecond) | ||
{ | ||
return new HttpClient(url, requestsPerSecond); | ||
}; | ||
} | ||
else | ||
{ | ||
return help(); | ||
} | ||
startClients(url, concurrency, requestsPerSecond, constructor); | ||
} | ||
/** | ||
* Start a number of measuring clients. | ||
*/ | ||
function startClients(url, concurrency, requestsPerSecond, constructor) | ||
{ | ||
for (var index = 0; index < concurrency; index++) | ||
{ | ||
url = url.replaceAll('$index', index); | ||
var client = constructor(url, requestsPerSecond); | ||
// start each client 100 ms after the last | ||
setTimeout(client.start, (index * 100) % 1000); | ||
} | ||
} | ||
/** | ||
* Display online help. | ||
@@ -249,74 +258,82 @@ */ | ||
{ | ||
console.log('Usage: %s %s [concurrency] [reqs/s] [seconds measured] URL'); | ||
console.log('Usage: %s %s [options] [concurrency] [req/s] URL'); | ||
console.log(' where URL can be a regular HTTP or websocket URL.'); | ||
console.log(' If you want to add an index to the URL, place a $index in it.'); | ||
console.log(' Options:'); | ||
console.log(' --noagent: send Connection: close'); | ||
console.log(' --agent: send Connection: keep-alive (default)'); | ||
} | ||
var numbersParsed = 0; | ||
/** | ||
* Process command line arguments. | ||
* Parse one argument. Returns true if there are more. | ||
*/ | ||
function processArgs(args) | ||
function parseArgument(args) | ||
{ | ||
if (args.length == 0) | ||
if (args.length <= 1) | ||
{ | ||
return help(); | ||
return false; | ||
} | ||
if (parseInt(args[0])) | ||
var argument = args[0]; | ||
if (parseInt(argument)) | ||
{ | ||
concurrency = parseInt(args[0]); | ||
args.splice(0, 1); | ||
if (parseInt(args[0])) | ||
if (numbersParsed == 0) | ||
{ | ||
requestsPerSecond = parseInt(args[0]); | ||
args.splice(0, 1); | ||
if (parseInt(args[0])) | ||
{ | ||
secondsMeasured = parseInt(args[0]); | ||
args.splice(0, 1); | ||
} | ||
globalConcurrency = parseInt(argument); | ||
} | ||
else if (numbersParsed == 1) | ||
{ | ||
globalRequestsPerSecond = parseInt(argument); | ||
} | ||
else | ||
{ | ||
log.error('Too many numeric arguments'); | ||
return false; | ||
} | ||
args.splice(0, 1); | ||
numbersParsed += 1; | ||
return true; | ||
} | ||
if (args.length != 1) | ||
if (!argument.startsWith('--')) | ||
{ | ||
return help(); | ||
console.error('Unknown argument %s', argument); | ||
return false; | ||
} | ||
var url = args[0]; | ||
var constructor; | ||
if (url.startsWith('ws://')) | ||
if (argument == '--noagent') | ||
{ | ||
constructor = function(url) | ||
{ | ||
return new WebsocketClient(url); | ||
}; | ||
agent = false; | ||
args.splice(0, 1); | ||
return true; | ||
} | ||
else if (url.startsWith('http')) | ||
if (argument == '--agent') | ||
{ | ||
constructor = function(url) | ||
{ | ||
return new HttpClient(url); | ||
}; | ||
args.splice(0, 1); | ||
return true; | ||
} | ||
else | ||
{ | ||
return help(); | ||
} | ||
startClients(url, constructor); | ||
console.error('Unknown option %s', argument); | ||
return false; | ||
} | ||
/** | ||
* Start a number of measuring clients. | ||
* Process command line arguments. | ||
*/ | ||
function startClients(url, constructor) | ||
function processArgs(args) | ||
{ | ||
for (var index = 0; index < concurrency; index++) | ||
while (parseArgument(args)) | ||
{ | ||
var url = url.replaceAll('$index', index); | ||
var client = constructor(url); | ||
// start each client 100 ms after the last | ||
setTimeout(client.start, (index * 100) % 1000); | ||
} | ||
if (args.length != 1) | ||
{ | ||
return help(); | ||
} | ||
var url = args[0]; | ||
exports.loadTest(url, globalConcurrency, globalRequestsPerSecond); | ||
} | ||
// init | ||
processArgs(process.argv.slice(2)); | ||
// start load test if invoked directly | ||
if (__filename == process.argv[1]) | ||
{ | ||
processArgs(process.argv.slice(2)); | ||
} | ||
@@ -8,3 +8,8 @@ 'use strict'; | ||
// requires | ||
var Log = require('log'); | ||
// globals | ||
var log = new Log('info'); | ||
/** | ||
@@ -34,1 +39,40 @@ * Find out if the string has the parameter at the beginning. | ||
/** | ||
* Run package tests. | ||
*/ | ||
exports.test = function() | ||
{ | ||
if (!'pepito'.startsWith('pe')) | ||
{ | ||
log.error('Failed to match using startsWith()') | ||
return false; | ||
} | ||
if ('pepito'.startsWith('po')) | ||
{ | ||
log.error('Invalid match using startsWith()') | ||
return false; | ||
} | ||
if (!'pepito'.endsWith('to')) | ||
{ | ||
log.error('Failed to match using endsWith()') | ||
return false; | ||
} | ||
if ('pepito'.startsWith('po')) | ||
{ | ||
log.error('Invalid match using endsWith()') | ||
return false; | ||
} | ||
if ('pepito'.replaceAll('p', 'c') != 'cecito') | ||
{ | ||
log.error('Invalid replaceAll().'); | ||
return false; | ||
} | ||
return true; | ||
} | ||
// run tests if invoked directly | ||
if (__filename == process.argv[1]) | ||
{ | ||
exports.test(); | ||
} | ||
{ | ||
"name": "loadtest", | ||
"version": "0.0.2", | ||
"version": "0.0.3", | ||
"description": "Load test scripts.", | ||
@@ -15,2 +15,3 @@ "homepage": "http://milliearth.org/", | ||
"websocket": "*", | ||
"microtime": "*", | ||
"log": "*" | ||
@@ -22,3 +23,6 @@ }, | ||
}, | ||
"scripts": { | ||
"test": "node test.js" | ||
}, | ||
"private": false | ||
} |
@@ -9,2 +9,5 @@ loadtest | ||
Just run: | ||
$ npm install loadtest | ||
Usage | ||
@@ -14,12 +17,15 @@ ----- | ||
Run as a script to load test a URL: | ||
$ node loadtest.js [URL] or [websocket URL] | ||
To get online help run without parameters: | ||
$ node loadtest.js | ||
$ node lib/loadtest.js [URL] or [websocket URL] | ||
To get online help, run loadtest.js without parameters: | ||
$ node lib/loadtest.js | ||
### Advanced Usage | ||
Add your own values for concurrency, requests per second and seconds measured: | ||
$ node loadtest.js [concurrency [request per second [seconds measured]]] ... | ||
Add your own values for concurrency and requests per second: | ||
$ node lib/loadtest.js [concurrency [request per second]] ... | ||
## Concurrency | ||
@@ -33,6 +39,17 @@ | ||
## Seconds Measured | ||
## --noagent | ||
How many seconds must be measured before showing the default latency. | ||
Open connections without keep-alive: send header 'Connection: Close' instead of 'Connection: Keep-alive'. | ||
Server | ||
------ | ||
loadtest bundles a test server. To run it: | ||
$ node lib/server.js [port] | ||
It will show the number of requests received per second, the latency in answering requests and the headers for selected requests. | ||
This server returns a short text 'OK' for every request, removing request processing from latency measurements. | ||
License | ||
@@ -39,0 +56,0 @@ ------- |
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
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
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
17821
10
655
65
3
3
3
+ Addedmicrotime@*
+ Addedmicrotime@3.1.1(transitive)
+ Addednode-addon-api@5.1.0(transitive)