Skip to content

Commit

Permalink
Show results (#212)
Browse files Browse the repository at this point in the history
* Separate results from latency

* Rename show to result

* Move showing to result.js

* Added all info to result

* Moved second parameter

* Document await for loadTest() and result.show()

* Remove header

* Fix: send error to loadTest() callback correctly

* Test to verify that --keepalive is working (failing)

* Fix keep alive

* Use a different options object instead of modifying in place

* Store requests per second without dividing by concurrence

* Document

* Moved method down, use get by default

* Throw error when invalid method used

* Document request generator parameter

* Separate options for test

* Store requests per second option correctly in http client

* Increase testing dep

* Fix method in options

* Convert some previous traces to exceptions

* Faster tests

* Comment

* Fix testing issue: clients stopped before they have started with requestsPerSecond

* v6.2.0

* Reformat
  • Loading branch information
alexfernandez authored Aug 17, 2023
1 parent 43d1aa3 commit 8663272
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 249 deletions.
126 changes: 80 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,28 @@ thus allowing you to load test your application in your own tests.
### Invoke Load Test
To run a load test, just call the exported function `loadTest()` with a set of options and an optional callback:
To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
```javascript
import {loadTest} from 'loadtest'

const options = {
url: 'http://localhost:8000',
maxRequests: 1000,
}
const result = await loadTest(options)
result.show()
console.log('Tests run successfully')
})
```
The call returns a `Result` object that contains all info about the load test, also described below.
Call `result.show()` to display the results in the standard format on the console.
As a legacy from before promises existed,
if an optional callback is passed as second parameter then it will not behave as `async`:
the callback `function(error, result)` will be invoked when the max number of requests is reached,
or when the max number of seconds has elapsed.

```javascript
import {loadTest} from 'loadtest'
Expand All @@ -494,16 +515,51 @@ loadTest(options, function(error, result) {
if (error) {
return console.error('Got an error: %s', error)
}
result.show()
console.log('Tests run successfully')
})
```
The callback `function(error, result)` will be invoked when the max number of requests is reached,
or when the max number of seconds has elapsed.
Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
and will not call the callback.
### Result
The latency result returned at the end of the load test contains a full set of data, including:
mean latency, number of errors and percentiles.
An example follows:
```javascript
{
url: 'http://localhost:80/',
maxRequests: 1000,
maxSeconds: 0,
concurrency: 10,
agent: 'none',
requestsPerSecond: undefined,
totalRequests: 1000,
percentiles: {
'50': 7,
'90': 10,
'95': 11,
'99': 15
},
rps: 2824,
totalTimeSeconds: 0.354108,
meanLatencyMs: 7.72,
maxLatencyMs: 20,
totalErrors: 3,
errorCodes: {
'0': 1,
'500': 2
},
}
```
The `result` object also has a `result.show()` function
that displays the results on the console in the standard format.
### Options
All options but `url` are, as their name implies, optional.
Expand Down Expand Up @@ -591,6 +647,9 @@ function(params, options, client, callback) {
}
```
See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
(or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
#### `agentKeepAlive`
Use an agent with 'Connection: Keep-alive'.
Expand Down Expand Up @@ -677,6 +736,19 @@ In addition, the following three properties are added to the `result` object:
You will need to check if `error` is populated in order to determine which object to check for these properties.
The second parameter contains info about the current request:
```javascript
{
host: 'localhost',
path: '/',
method: 'GET',
statusCode: 200,
body: '<html><body>hi</body></html>',
headers: [...]
}
```
Example:
```javascript
Expand All @@ -703,7 +775,6 @@ loadTest(options, function(error) {
console.log('Tests run successfully')
})
```
In some situations request data needs to be available in the statusCallBack.
This data can be assigned to `request.labels` in the requestGenerator:
Expand Down Expand Up @@ -757,46 +828,6 @@ function contentInspector(result) {
}
},
```
### Result
The latency result passed to your callback at the end of the load test contains a full set of data, including:
mean latency, number of errors and percentiles.
An example follows:
```javascript
{
totalRequests: 1000,
percentiles: {
'50': 7,
'90': 10,
'95': 11,
'99': 15
},
rps: 2824,
totalTimeSeconds: 0.354108,
meanLatencyMs: 7.72,
maxLatencyMs: 20,
totalErrors: 3,
errorCodes: {
'0': 1,
'500': 2
}
}
```
The second parameter contains info about the current request:
```javascript
{
host: 'localhost',
path: '/',
method: 'GET',
statusCode: 200,
body: '<html><body>hi</body></html>',
headers: [...]
}
```
### Start Test Server
Expand All @@ -805,11 +836,14 @@ To start the test server use the exported function `startServer()` with a set of
```javascript
import {startServer} from 'loadtest'
const server = await startServer({port: 8000})
// do your thing
await server.close()
```
This function returns an HTTP server which can be `close()`d when it is no longer useful.
This function returns when the server is up and running,
with an HTTP server which can be `close()`d when it is no longer useful.
As a legacy from before promises existed,
if the optional callback is passed then it will not behave as `async`:
if an optional callback is passed as second parameter then it will not behave as `async`:
```
const server = startServer({port: 8000}, error => console.error(error))
Expand Down
3 changes: 1 addition & 2 deletions bin/loadtest.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import {readFile} from 'fs/promises'
import * as stdio from 'stdio'
import {loadTest} from '../lib/loadtest.js'
import {showResult} from '../lib/show.js'


const options = stdio.getopt({
Expand Down Expand Up @@ -54,7 +53,7 @@ async function processAndRun(options) {
options.url = options.args[0];
try {
const result = await loadTest(options)
showResult(options, result)
result.show()
} catch(error) {
console.error(error.message)
help()
Expand Down
31 changes: 21 additions & 10 deletions lib/httpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class HttpClient {
constructor(operation, params) {
this.operation = operation
this.params = params
this.stopped = false
this.init();
}

Expand All @@ -45,11 +46,15 @@ class HttpClient {
this.options.key = this.params.key;
}
this.options.agent = false;
if (this.params.requestsPerSecond) {
// rps for each client is total / concurrency (# of clients)
this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
}
if (this.params.agentKeepAlive) {
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive;
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
let maxSockets = 10;
if (this.params.requestsPerSecond) {
maxSockets += Math.floor(this.params.requestsPerSecond);
if (this.options.requestsPerSecond) {
maxSockets += Math.floor(this.options.requestsPerSecond);
}
this.options.agent = new KeepAlive({
maxSockets: maxSockets,
Expand All @@ -71,7 +76,7 @@ class HttpClient {
} else if (typeof this.params.body == 'function') {
this.generateMessage = this.params.body;
} else {
console.error('Unrecognized body: %s', typeof this.params.body);
throw new Error(`Unrecognized body: ${typeof this.params.body}`);
}
this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
}
Expand All @@ -81,7 +86,7 @@ class HttpClient {
} else if (typeof this.params.cookies == 'string') {
this.options.headers.Cookie = this.params.cookies;
} else {
console.error('Invalid cookies %j, please use an array or a string', this.params.cookies);
throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
}
}
addUserAgent(this.options.headers);
Expand All @@ -94,17 +99,23 @@ class HttpClient {
* Start the HTTP client.
*/
start() {
if (!this.params.requestsPerSecond) {
if (this.stopped) {
// solves testing issue: with requestsPerSecond clients are started at random,
// so sometimes they are stopped before they have even started
return
}
if (!this.options.requestsPerSecond) {
return this.makeRequest();
}
const interval = 1000 / this.params.requestsPerSecond;
const interval = 1000 / this.options.requestsPerSecond;
this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
}

/**
* Stop the HTTP client.
*/
stop() {
this.stopped = true
if (this.requestTimer) {
this.requestTimer.stop();
}
Expand Down Expand Up @@ -133,7 +144,6 @@ class HttpClient {
// adding proxy configuration
if (this.params.proxy) {
const proxy = this.params.proxy;
//console.log('using proxy server %j', proxy);
const agent = new HttpsProxyAgent(proxy);
this.options.agent = agent;
}
Expand All @@ -154,7 +164,8 @@ class HttpClient {
delete this.options.headers['Content-Length'];
}
if (typeof this.params.requestGenerator == 'function') {
request = this.params.requestGenerator(this.params, this.options, lib.request, this.getConnect(id, requestFinished, this.params.contentInspector));
const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
} else {
request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
}
Expand Down Expand Up @@ -205,7 +216,7 @@ class HttpClient {
result.instanceIndex = this.operation.instanceIndex;
}
let callback;
if (!this.params.requestsPerSecond) {
if (!this.options.requestsPerSecond) {
callback = this.makeRequest.bind(this);
}
this.operation.callback(error, result, callback);
Expand Down
20 changes: 4 additions & 16 deletions lib/latency.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as crypto from 'crypto'
import {showResult} from './show.js'
import {Result} from './result.js'


/**
Expand Down Expand Up @@ -174,19 +174,8 @@ export class Latency {
* Get final result.
*/
getResult() {
const elapsedSeconds = this.getElapsed(this.initialTime) / 1000;
const meanTime = this.totalTime / this.totalRequests;
return {
totalRequests: this.totalRequests,
totalErrors: this.totalErrors,
totalTimeSeconds: elapsedSeconds,
rps: Math.round(this.totalRequests / elapsedSeconds),
meanLatencyMs: Math.round(meanTime * 10) / 10,
maxLatencyMs: this.maxLatencyMs,
minLatencyMs: this.minLatencyMs,
percentiles: this.computePercentiles(),
errorCodes: this.errorCodes
};
const result = new Result(this.options, this)
return result
}

/**
Expand Down Expand Up @@ -226,11 +215,10 @@ export class Latency {
}
this.totalsShown = true;
const result = this.getResult();
showResult(this.options, result)
result.show()
}
}


/**
* Create a unique, random token.
*/
Expand Down
Loading

0 comments on commit 8663272

Please sign in to comment.