loadtest 5.1.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +1 -1
- package/README.md +80 -45
- package/bin/loadtest.js +26 -154
- package/bin/testserver.js +5 -12
- package/index.js +6 -12
- package/lib/baseClient.js +5 -22
- package/lib/config.js +9 -24
- package/lib/headers.js +3 -64
- package/lib/hrtimer.js +2 -55
- package/lib/httpClient.js +19 -64
- package/lib/latency.js +25 -147
- package/lib/loadtest.js +19 -150
- package/lib/options.js +156 -0
- package/lib/testserver.js +15 -65
- package/lib/websocket.js +8 -54
- package/package.json +8 -8
- package/sample/post-file.js +13 -0
- package/sample/request-generator.js +2 -4
- package/test/all.js +33 -0
- package/test/body-generator.js +41 -0
- package/test/headers.js +51 -0
- package/test/hrtimer.js +39 -0
- package/test/httpClient.js +22 -0
- package/{lib → test}/integration.js +13 -35
- package/test/latency.js +81 -0
- package/test/loadtest.js +87 -0
- package/{lib → test}/request-generator.js +9 -21
- package/test/testserver.js +24 -0
- package/test/websocket.js +23 -0
- package/test.js +0 -36
- /package/{test → sample}/.loadtestrc +0 -0
package/lib/headers.js
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Support for custom headers.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
// requires
|
|
9
|
-
const testing = require('testing');
|
|
10
1
|
|
|
11
2
|
|
|
12
3
|
/**
|
|
13
4
|
* Add all raw headers given to the given array.
|
|
14
5
|
*/
|
|
15
|
-
|
|
6
|
+
export function addHeaders(rawHeaders, headers) {
|
|
16
7
|
if (Array.isArray(rawHeaders)) {
|
|
17
8
|
rawHeaders.forEach(function(header) {
|
|
18
9
|
addHeader(header, headers);
|
|
@@ -22,7 +13,7 @@ exports.addHeaders = function(rawHeaders, headers) {
|
|
|
22
13
|
} else {
|
|
23
14
|
console.error('Invalid header structure %j, it should be an array');
|
|
24
15
|
}
|
|
25
|
-
}
|
|
16
|
+
}
|
|
26
17
|
|
|
27
18
|
/**
|
|
28
19
|
* Add a single header to an array.
|
|
@@ -37,64 +28,12 @@ function addHeader(rawHeader, headers) {
|
|
|
37
28
|
headers[key.toLowerCase()] = value;
|
|
38
29
|
}
|
|
39
30
|
|
|
40
|
-
function testAddHeaders(callback) {
|
|
41
|
-
const tests = [ {
|
|
42
|
-
raw: 'k:v',
|
|
43
|
-
headers: { 'k': 'v' }
|
|
44
|
-
}, {
|
|
45
|
-
raw: ['k:v', 'k:v2'],
|
|
46
|
-
headers: { 'k': 'v2' }
|
|
47
|
-
}, {
|
|
48
|
-
raw: ['k:v', 'k2:v2'],
|
|
49
|
-
headers: { 'k': 'v', 'k2': 'v2' }
|
|
50
|
-
}, {
|
|
51
|
-
raw: 'K:v',
|
|
52
|
-
headers: { 'k': 'v' }
|
|
53
|
-
}, {
|
|
54
|
-
raw: 'k:v:w',
|
|
55
|
-
headers: { 'k': 'v:w' }
|
|
56
|
-
}
|
|
57
|
-
];
|
|
58
|
-
tests.forEach(function(test) {
|
|
59
|
-
const headers = {};
|
|
60
|
-
exports.addHeaders(test.raw, headers);
|
|
61
|
-
testing.assertEquals(headers, test.headers, 'Wrong headers', callback);
|
|
62
|
-
});
|
|
63
|
-
testing.success(callback);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
31
|
/**
|
|
67
32
|
* Add a user-agent header if not present.
|
|
68
33
|
*/
|
|
69
|
-
|
|
34
|
+
export function addUserAgent(headers) {
|
|
70
35
|
if(!headers['user-agent']) {
|
|
71
36
|
headers['user-agent'] = 'node.js loadtest bot';
|
|
72
37
|
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
function testAddUserAgent(callback) {
|
|
76
|
-
const headers = {'k': 'v', 'q': 'r' };
|
|
77
|
-
exports.addUserAgent(headers);
|
|
78
|
-
testing.assertEquals(Object.keys(headers).length, 3, 'Did not add user agent', callback);
|
|
79
|
-
const userAgent = headers['user-agent'];
|
|
80
|
-
testing.assert(userAgent.includes('bot'), 'Invalid user agent', callback);
|
|
81
|
-
exports.addUserAgent(headers);
|
|
82
|
-
testing.assertEquals(Object.keys(headers).length, 3, 'Should not add user agent', callback);
|
|
83
|
-
testing.success(callback);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Run all tests.
|
|
88
|
-
*/
|
|
89
|
-
exports.test = function(callback) {
|
|
90
|
-
testing.run([
|
|
91
|
-
testAddHeaders,
|
|
92
|
-
testAddUserAgent,
|
|
93
|
-
], callback);
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// run tests if invoked directly
|
|
97
|
-
if (__filename == process.argv[1]) {
|
|
98
|
-
exports.test(testing.show);
|
|
99
38
|
}
|
|
100
39
|
|
package/lib/hrtimer.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Measure latency for a load test.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// requires
|
|
10
|
-
const testing = require('testing');
|
|
11
|
-
const Log = require('log');
|
|
12
|
-
|
|
13
|
-
// globals
|
|
14
|
-
const log = new Log('info');
|
|
15
1
|
|
|
16
2
|
|
|
17
3
|
/**
|
|
@@ -19,7 +5,7 @@ const log = new Log('info');
|
|
|
19
5
|
* - delayMs: miliseconds to wait before calls. Can be fractional.
|
|
20
6
|
* - callback: function to call every time.
|
|
21
7
|
*/
|
|
22
|
-
class HighResolutionTimer {
|
|
8
|
+
export class HighResolutionTimer {
|
|
23
9
|
|
|
24
10
|
/**
|
|
25
11
|
* Create a timer with the given delay.
|
|
@@ -58,7 +44,7 @@ class HighResolutionTimer {
|
|
|
58
44
|
traceDrift() {
|
|
59
45
|
const diff = Date.now() - this.start;
|
|
60
46
|
const drift = diff / this.delayMs - this.counter;
|
|
61
|
-
|
|
47
|
+
console.debug('Seconds: ' + Math.round(diff / 1000) + ', counter: ' + this.counter + ', drift: ' + drift);
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
stop() {
|
|
@@ -77,42 +63,3 @@ class HighResolutionTimer {
|
|
|
77
63
|
}
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
/**
|
|
81
|
-
* Test a high resolution timer.
|
|
82
|
-
*/
|
|
83
|
-
function testTimerStop(callback) {
|
|
84
|
-
const timer = new HighResolutionTimer(10, callback);
|
|
85
|
-
setImmediate(() => timer.stop())
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function testTimerRun(callback) {
|
|
89
|
-
let run = 0
|
|
90
|
-
const timer = new HighResolutionTimer(100, () => run++)
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
testing.equals(run, 3, callback)
|
|
93
|
-
timer.stop()
|
|
94
|
-
testing.success(callback)
|
|
95
|
-
}, 250)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Run package tests.
|
|
100
|
-
*/
|
|
101
|
-
function test(callback) {
|
|
102
|
-
const tests = [
|
|
103
|
-
testTimerStop,
|
|
104
|
-
testTimerRun,
|
|
105
|
-
];
|
|
106
|
-
testing.run(tests, callback);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
module.exports = {
|
|
110
|
-
HighResolutionTimer,
|
|
111
|
-
test,
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// run tests if invoked directly
|
|
115
|
-
if (__filename == process.argv[1]) {
|
|
116
|
-
test(testing.show);
|
|
117
|
-
}
|
|
118
|
-
|
package/lib/httpClient.js
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const testing = require('testing');
|
|
11
|
-
const urlLib = require('url');
|
|
12
|
-
const http = require('http');
|
|
13
|
-
const https = require('https');
|
|
14
|
-
const qs = require('querystring');
|
|
15
|
-
const websocket = require('websocket');
|
|
16
|
-
const Log = require('log');
|
|
17
|
-
const {HighResolutionTimer} = require('./hrtimer.js');
|
|
18
|
-
const headers = require('./headers.js');
|
|
19
|
-
|
|
20
|
-
// globals
|
|
21
|
-
const log = new Log('info');
|
|
1
|
+
import * as urlLib from 'url'
|
|
2
|
+
import * as http from 'http'
|
|
3
|
+
import * as https from 'https'
|
|
4
|
+
import * as querystring from 'querystring'
|
|
5
|
+
import * as websocket from 'websocket'
|
|
6
|
+
import {HighResolutionTimer} from './hrtimer.js'
|
|
7
|
+
import {addUserAgent} from './headers.js'
|
|
8
|
+
import * as agentkeepalive from 'agentkeepalive'
|
|
9
|
+
import * as HttpsProxyAgent from 'https-proxy-agent'
|
|
22
10
|
|
|
23
11
|
|
|
24
12
|
/**
|
|
25
13
|
* Create a new HTTP client.
|
|
26
14
|
* Seem parameters below.
|
|
27
15
|
*/
|
|
28
|
-
|
|
16
|
+
export function create(operation, params) {
|
|
29
17
|
return new HttpClient(operation, params);
|
|
30
|
-
}
|
|
18
|
+
}
|
|
31
19
|
|
|
32
20
|
/**
|
|
33
21
|
* A client for an HTTP connection.
|
|
@@ -58,7 +46,7 @@ class HttpClient {
|
|
|
58
46
|
}
|
|
59
47
|
this.options.agent = false;
|
|
60
48
|
if (this.params.agentKeepAlive) {
|
|
61
|
-
const KeepAlive = (this.options.protocol == 'https:') ?
|
|
49
|
+
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive;
|
|
62
50
|
let maxSockets = 10;
|
|
63
51
|
if (this.params.requestsPerSecond) {
|
|
64
52
|
maxSockets += Math.floor(this.params.requestsPerSecond);
|
|
@@ -74,19 +62,16 @@ class HttpClient {
|
|
|
74
62
|
}
|
|
75
63
|
if (this.params.body) {
|
|
76
64
|
if (typeof this.params.body == 'string') {
|
|
77
|
-
log.debug('Received string body');
|
|
78
65
|
this.generateMessage = () => this.params.body;
|
|
79
66
|
} else if (typeof this.params.body == 'object') {
|
|
80
|
-
log.debug('Received JSON body');
|
|
81
67
|
if (this.params.contentType === 'application/x-www-form-urlencoded') {
|
|
82
|
-
this.params.body =
|
|
68
|
+
this.params.body = querystring.stringify(this.params.body);
|
|
83
69
|
}
|
|
84
70
|
this.generateMessage = () => this.params.body;
|
|
85
71
|
} else if (typeof this.params.body == 'function') {
|
|
86
|
-
log.debug('Received function body');
|
|
87
72
|
this.generateMessage = this.params.body;
|
|
88
73
|
} else {
|
|
89
|
-
|
|
74
|
+
console.error('Unrecognized body: %s', typeof this.params.body);
|
|
90
75
|
}
|
|
91
76
|
this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
|
|
92
77
|
}
|
|
@@ -99,11 +84,10 @@ class HttpClient {
|
|
|
99
84
|
console.error('Invalid cookies %j, please use an array or a string', this.params.cookies);
|
|
100
85
|
}
|
|
101
86
|
}
|
|
102
|
-
|
|
87
|
+
addUserAgent(this.options.headers);
|
|
103
88
|
if (this.params.secureProtocol) {
|
|
104
89
|
this.options.secureProtocol = this.params.secureProtocol;
|
|
105
90
|
}
|
|
106
|
-
log.debug('Options: %j', this.options);
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
/**
|
|
@@ -145,7 +129,6 @@ class HttpClient {
|
|
|
145
129
|
if (this.options.protocol == 'ws:') {
|
|
146
130
|
lib = websocket;
|
|
147
131
|
}
|
|
148
|
-
const HttpsProxyAgent = require('https-proxy-agent');
|
|
149
132
|
|
|
150
133
|
// adding proxy configuration
|
|
151
134
|
if (this.params.proxy) {
|
|
@@ -178,7 +161,7 @@ class HttpClient {
|
|
|
178
161
|
if (this.params.timeout) {
|
|
179
162
|
const timeout = parseInt(this.params.timeout);
|
|
180
163
|
if (!timeout) {
|
|
181
|
-
|
|
164
|
+
console.error('Invalid timeout %s', this.params.timeout);
|
|
182
165
|
}
|
|
183
166
|
request.setTimeout(timeout, () => {
|
|
184
167
|
requestFinished('Connection timed out');
|
|
@@ -200,7 +183,6 @@ class HttpClient {
|
|
|
200
183
|
return (error, result) => {
|
|
201
184
|
let errorCode = null;
|
|
202
185
|
if (error) {
|
|
203
|
-
log.debug('Connection %s failed: %s', id, error);
|
|
204
186
|
if (result) {
|
|
205
187
|
errorCode = result.statusCode;
|
|
206
188
|
if (result.customErrorCode !== undefined) {
|
|
@@ -209,8 +191,6 @@ class HttpClient {
|
|
|
209
191
|
} else {
|
|
210
192
|
errorCode = '-1';
|
|
211
193
|
}
|
|
212
|
-
} else {
|
|
213
|
-
log.debug('Connection %s ended', id);
|
|
214
194
|
}
|
|
215
195
|
|
|
216
196
|
const elapsed = this.operation.latency.end(id, errorCode);
|
|
@@ -238,10 +218,8 @@ class HttpClient {
|
|
|
238
218
|
getConnect(id, callback, contentInspector) {
|
|
239
219
|
let body = '';
|
|
240
220
|
return connection => {
|
|
241
|
-
log.debug('HTTP client connected to %s with id %s', this.params.url, id);
|
|
242
221
|
connection.setEncoding('utf8');
|
|
243
222
|
connection.on('data', chunk => {
|
|
244
|
-
log.debug('Body: %s', chunk);
|
|
245
223
|
body += chunk;
|
|
246
224
|
});
|
|
247
225
|
connection.on('error', error => {
|
|
@@ -257,6 +235,9 @@ class HttpClient {
|
|
|
257
235
|
body: body,
|
|
258
236
|
headers: connection.headers,
|
|
259
237
|
};
|
|
238
|
+
if (connection.req.labels) {
|
|
239
|
+
result.labels = connection.req.labels
|
|
240
|
+
}
|
|
260
241
|
if (contentInspector) {
|
|
261
242
|
contentInspector(result)
|
|
262
243
|
}
|
|
@@ -272,29 +253,3 @@ class HttpClient {
|
|
|
272
253
|
}
|
|
273
254
|
}
|
|
274
255
|
|
|
275
|
-
function testHttpClient(callback) {
|
|
276
|
-
const options = {
|
|
277
|
-
url: 'http://localhost:7357/',
|
|
278
|
-
maxSeconds: 0.1,
|
|
279
|
-
concurrency: 1,
|
|
280
|
-
quiet: true,
|
|
281
|
-
};
|
|
282
|
-
exports.create({}, options);
|
|
283
|
-
testing.success(callback);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Run all tests.
|
|
289
|
-
*/
|
|
290
|
-
exports.test = function (callback) {
|
|
291
|
-
testing.run([
|
|
292
|
-
testHttpClient,
|
|
293
|
-
], callback);
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
// run tests if invoked directly
|
|
297
|
-
if (__filename == process.argv[1]) {
|
|
298
|
-
exports.test(testing.show);
|
|
299
|
-
}
|
|
300
|
-
|
package/lib/latency.js
CHANGED
|
@@ -1,30 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Measure latency for a load test.
|
|
5
|
-
* (C) 2013 Alex Fernández.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// requires
|
|
10
|
-
const testing = require('testing');
|
|
11
|
-
const util = require('util');
|
|
12
|
-
const crypto = require('crypto');
|
|
13
|
-
const Log = require('log');
|
|
14
|
-
|
|
15
|
-
// globals
|
|
16
|
-
const log = new Log('info');
|
|
1
|
+
import * as crypto from 'crypto'
|
|
17
2
|
|
|
18
3
|
|
|
19
4
|
/**
|
|
20
5
|
* Latency measurements. Options can be:
|
|
21
6
|
* - maxRequests: max number of requests to measure before stopping.
|
|
22
7
|
* - maxSeconds: max seconds, alternative to max requests.
|
|
23
|
-
* - quiet: do not log messages.
|
|
24
8
|
* An optional callback(error, results) will be called with an error,
|
|
25
9
|
* or the results after max is reached.
|
|
26
10
|
*/
|
|
27
|
-
class Latency {
|
|
11
|
+
export class Latency {
|
|
28
12
|
constructor(options, callback) {
|
|
29
13
|
this.options = options
|
|
30
14
|
this.callback = callback
|
|
@@ -45,12 +29,6 @@ class Latency {
|
|
|
45
29
|
this.totalsShown = false;
|
|
46
30
|
this.requestIndex = 0;
|
|
47
31
|
this.requestIdToIndex = {};
|
|
48
|
-
if (options.quiet) {
|
|
49
|
-
log.level = Log.NOTICE;
|
|
50
|
-
}
|
|
51
|
-
if (options.debug) {
|
|
52
|
-
log.level = Log.DEBUG;
|
|
53
|
-
}
|
|
54
32
|
}
|
|
55
33
|
|
|
56
34
|
/**
|
|
@@ -77,7 +55,6 @@ class Latency {
|
|
|
77
55
|
*/
|
|
78
56
|
end(requestId, errorCode) {
|
|
79
57
|
if (!(requestId in this.requests)) {
|
|
80
|
-
log.debug('Message id ' + requestId + ' not found');
|
|
81
58
|
return -2;
|
|
82
59
|
}
|
|
83
60
|
if (!this.running) {
|
|
@@ -94,7 +71,6 @@ class Latency {
|
|
|
94
71
|
* Accepts an optional error code signaling an error.
|
|
95
72
|
*/
|
|
96
73
|
add(time, errorCode) {
|
|
97
|
-
log.debug('New value: %s', time);
|
|
98
74
|
this.partialTime += time;
|
|
99
75
|
this.partialRequests++;
|
|
100
76
|
this.totalTime += time;
|
|
@@ -108,7 +84,6 @@ class Latency {
|
|
|
108
84
|
}
|
|
109
85
|
this.errorCodes[errorCode] += 1;
|
|
110
86
|
}
|
|
111
|
-
log.debug('Partial requests: %s', this.partialRequests);
|
|
112
87
|
const rounded = Math.floor(time);
|
|
113
88
|
if (rounded > this.maxLatencyMs) {
|
|
114
89
|
this.maxLatencyMs = rounded;
|
|
@@ -117,7 +92,6 @@ class Latency {
|
|
|
117
92
|
this.minLatencyMs = rounded;
|
|
118
93
|
}
|
|
119
94
|
if (!this.histogramMs[rounded]) {
|
|
120
|
-
log.debug('Initializing histogram for %s', rounded);
|
|
121
95
|
this.histogramMs[rounded] = 0;
|
|
122
96
|
}
|
|
123
97
|
this.histogramMs[rounded] += 1;
|
|
@@ -140,10 +114,10 @@ class Latency {
|
|
|
140
114
|
if (this.options.maxRequests) {
|
|
141
115
|
percent = ' (' + Math.round(100 * this.totalRequests / this.options.maxRequests) + '%)';
|
|
142
116
|
}
|
|
143
|
-
|
|
117
|
+
console.info('Requests: %s%s, requests per second: %s, mean latency: %s ms', this.totalRequests, percent, results.rps, results.meanLatencyMs);
|
|
144
118
|
if (this.totalErrors) {
|
|
145
119
|
percent = Math.round(100 * 10 * this.totalErrors / this.totalRequests) / 10;
|
|
146
|
-
|
|
120
|
+
console.info('Errors: %s, accumulated errors: %s, %s% of total requests', this.partialErrors, this.totalErrors, percent);
|
|
147
121
|
}
|
|
148
122
|
this.partialTime = 0;
|
|
149
123
|
this.partialRequests = 0;
|
|
@@ -173,14 +147,11 @@ class Latency {
|
|
|
173
147
|
* Check out if the measures are finished.
|
|
174
148
|
*/
|
|
175
149
|
isFinished() {
|
|
176
|
-
log.debug('Total requests %s, max requests: %s', this.totalRequests, this.options.maxRequests);
|
|
177
150
|
if (this.options.maxRequests && this.totalRequests >= this.options.maxRequests) {
|
|
178
|
-
log.debug('Max requests reached: %s', this.totalRequests);
|
|
179
151
|
return true;
|
|
180
152
|
}
|
|
181
153
|
const elapsedSeconds = this.getElapsed(this.initialTime) / 1000;
|
|
182
154
|
if (this.options.maxSeconds && elapsedSeconds >= this.options.maxSeconds) {
|
|
183
|
-
log.debug('Max seconds reached: %s', this.totalRequests);
|
|
184
155
|
return true;
|
|
185
156
|
}
|
|
186
157
|
return false;
|
|
@@ -194,7 +165,6 @@ class Latency {
|
|
|
194
165
|
if (this.callback) {
|
|
195
166
|
return this.callback(null, this.getResults());
|
|
196
167
|
}
|
|
197
|
-
this.show();
|
|
198
168
|
}
|
|
199
169
|
|
|
200
170
|
/**
|
|
@@ -220,7 +190,6 @@ class Latency {
|
|
|
220
190
|
* Compute the percentiles.
|
|
221
191
|
*/
|
|
222
192
|
computePercentiles() {
|
|
223
|
-
log.debug('Histogram: %s', util.inspect(this.histogramMs));
|
|
224
193
|
const percentiles = {
|
|
225
194
|
50: false,
|
|
226
195
|
90: false,
|
|
@@ -233,12 +202,10 @@ class Latency {
|
|
|
233
202
|
if (!this.histogramMs[ms]) {
|
|
234
203
|
continue;
|
|
235
204
|
}
|
|
236
|
-
log.debug('Histogram for %s: %s', ms, this.histogramMs[ms]);
|
|
237
205
|
counted += this.histogramMs[ms];
|
|
238
206
|
const percent = counted / this.totalRequests * 100;
|
|
239
207
|
|
|
240
208
|
Object.keys(percentiles).forEach(percentile => {
|
|
241
|
-
log.debug('Checking percentile %s for %s', percentile, percent);
|
|
242
209
|
if (!percentiles[percentile] && percent > percentile) {
|
|
243
210
|
percentiles[percentile] = ms;
|
|
244
211
|
}
|
|
@@ -256,43 +223,43 @@ class Latency {
|
|
|
256
223
|
}
|
|
257
224
|
this.totalsShown = true;
|
|
258
225
|
const results = this.getResults();
|
|
259
|
-
|
|
260
|
-
|
|
226
|
+
console.info('');
|
|
227
|
+
console.info('Target URL: %s', this.options.url);
|
|
261
228
|
if (this.options.maxRequests) {
|
|
262
|
-
|
|
229
|
+
console.info('Max requests: %s', this.options.maxRequests);
|
|
263
230
|
} else if (this.options.maxSeconds) {
|
|
264
|
-
|
|
231
|
+
console.info('Max time (s): %s', this.options.maxSeconds);
|
|
265
232
|
}
|
|
266
|
-
|
|
233
|
+
console.info('Concurrency level: %s', this.options.concurrency);
|
|
267
234
|
let agent = 'none';
|
|
268
235
|
if (this.options.agentKeepAlive) {
|
|
269
236
|
agent = 'keepalive';
|
|
270
237
|
}
|
|
271
|
-
|
|
238
|
+
console.info('Agent: %s', agent);
|
|
272
239
|
if (this.options.requestsPerSecond) {
|
|
273
|
-
|
|
240
|
+
console.info('Requests per second: %s', this.options.requestsPerSecond * this.options.concurrency);
|
|
274
241
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
242
|
+
console.info('');
|
|
243
|
+
console.info('Completed requests: %s', results.totalRequests);
|
|
244
|
+
console.info('Total errors: %s', results.totalErrors);
|
|
245
|
+
console.info('Total time: %s s', results.totalTimeSeconds);
|
|
246
|
+
console.info('Requests per second: %s', results.rps);
|
|
247
|
+
console.info('Mean latency: %s ms', results.meanLatencyMs);
|
|
248
|
+
console.info('');
|
|
249
|
+
console.info('Percentage of the requests served within a certain time');
|
|
283
250
|
|
|
284
251
|
Object.keys(results.percentiles).forEach(percentile => {
|
|
285
|
-
|
|
252
|
+
console.info(' %s% %s ms', percentile, results.percentiles[percentile]);
|
|
286
253
|
});
|
|
287
254
|
|
|
288
|
-
|
|
255
|
+
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
289
256
|
if (results.totalErrors) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
257
|
+
console.info('');
|
|
258
|
+
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
259
|
+
console.info('');
|
|
293
260
|
Object.keys(results.errorCodes).forEach(errorCode => {
|
|
294
261
|
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
295
|
-
|
|
262
|
+
console.info(' %s%s: %s errors', padding, errorCode, results.errorCodes[errorCode]);
|
|
296
263
|
});
|
|
297
264
|
}
|
|
298
265
|
}
|
|
@@ -308,92 +275,3 @@ function createId() {
|
|
|
308
275
|
return hash.update(value).digest('hex').toLowerCase();
|
|
309
276
|
}
|
|
310
277
|
|
|
311
|
-
/**
|
|
312
|
-
* Test latency ids.
|
|
313
|
-
*/
|
|
314
|
-
function testLatencyIds(callback) {
|
|
315
|
-
const latency = new Latency({});
|
|
316
|
-
const firstId = latency.start();
|
|
317
|
-
testing.assert(firstId, 'Invalid first latency id %s', firstId, callback);
|
|
318
|
-
const secondId = latency.start();
|
|
319
|
-
testing.assert(secondId, 'Invalid second latency id', callback);
|
|
320
|
-
testing.assert(firstId != secondId, 'Repeated latency ids', callback);
|
|
321
|
-
testing.success(callback);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Test latency measurements.
|
|
326
|
-
*/
|
|
327
|
-
function testLatencyRequests(callback) {
|
|
328
|
-
const options = {
|
|
329
|
-
maxRequests: 10,
|
|
330
|
-
};
|
|
331
|
-
const errorCode = '500';
|
|
332
|
-
const latency = new Latency(options, (error, result) => {
|
|
333
|
-
testing.check(error, 'Could not compute latency', callback);
|
|
334
|
-
testing.assertEquals(result.totalRequests, 10, 'Invalid total requests', callback);
|
|
335
|
-
testing.assertEquals(result.totalErrors, 1, 'Invalid total errors', callback);
|
|
336
|
-
testing.assert(errorCode in result.errorCodes, 'Error code not found', callback);
|
|
337
|
-
testing.assertEquals(result.errorCodes[errorCode], 1, 'Should have one ' + errorCode, callback);
|
|
338
|
-
testing.success(callback);
|
|
339
|
-
});
|
|
340
|
-
let id;
|
|
341
|
-
for (let i = 0; i < 9; i++) {
|
|
342
|
-
id = latency.start();
|
|
343
|
-
latency.end(id);
|
|
344
|
-
}
|
|
345
|
-
id = latency.start();
|
|
346
|
-
latency.end(id, errorCode);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Check that percentiles are correct.
|
|
351
|
-
*/
|
|
352
|
-
function testLatencyPercentiles(callback) {
|
|
353
|
-
const options = {
|
|
354
|
-
maxRequests: 10
|
|
355
|
-
};
|
|
356
|
-
const latency = new Latency(options, error => {
|
|
357
|
-
testing.check(error, 'Error while testing latency percentiles', callback);
|
|
358
|
-
const percentiles = latency.getResults().percentiles;
|
|
359
|
-
|
|
360
|
-
Object.keys(percentiles).forEach(percentile => {
|
|
361
|
-
testing.assert(percentiles[percentile] !== false, 'Empty percentile for %s', percentile, callback);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
testing.success(percentiles, callback);
|
|
365
|
-
});
|
|
366
|
-
for (let ms = 1; ms <= 10; ms++) {
|
|
367
|
-
log.debug('Starting %s', ms);
|
|
368
|
-
(function() {
|
|
369
|
-
const id = latency.start();
|
|
370
|
-
setTimeout(() => {
|
|
371
|
-
log.debug('Ending %s', id);
|
|
372
|
-
latency.end(id);
|
|
373
|
-
}, ms);
|
|
374
|
-
})();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Run package tests.
|
|
380
|
-
*/
|
|
381
|
-
function test(callback) {
|
|
382
|
-
const tests = [
|
|
383
|
-
testLatencyIds,
|
|
384
|
-
testLatencyRequests,
|
|
385
|
-
testLatencyPercentiles,
|
|
386
|
-
];
|
|
387
|
-
testing.run(tests, callback);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
module.exports = {
|
|
391
|
-
Latency,
|
|
392
|
-
test,
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// run tests if invoked directly
|
|
396
|
-
if (__filename == process.argv[1]) {
|
|
397
|
-
test(testing.show);
|
|
398
|
-
}
|
|
399
|
-
|