loadtest 6.0.0 → 6.2.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/README.md +108 -64
- package/bin/loadtest.js +3 -2
- package/lib/httpClient.js +21 -10
- package/lib/latency.js +18 -65
- package/lib/loadtest.js +44 -29
- package/lib/options.js +52 -100
- package/lib/result.js +68 -0
- package/lib/testserver.js +28 -23
- package/package.json +2 -2
- package/sample/request-generator.js +2 -2
- package/sample/request-generator.ts +2 -2
- package/test/body-generator.js +3 -2
- package/test/integration.js +40 -12
- package/test/latency.js +1 -1
- package/test/loadtest.js +43 -12
- package/test/request-generator.js +3 -2
- package/test/testserver.js +1 -0
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ but the resulting figure is much more robust.
|
|
|
84
84
|
`loadtest` is also quite extensible.
|
|
85
85
|
Using the provided API it is very easy to integrate loadtest with your package, and run programmatic load tests.
|
|
86
86
|
loadtest makes it very easy to run load tests as part of systems tests, before deploying a new version of your software.
|
|
87
|
-
The
|
|
87
|
+
The result includes mean response times and percentiles,
|
|
88
88
|
so that you can abort deployment e.g. if 99% of the requests don't finish in 10 ms or less.
|
|
89
89
|
|
|
90
90
|
### Usage Don'ts
|
|
@@ -111,6 +111,7 @@ The following parameters are compatible with Apache ab.
|
|
|
111
111
|
#### `-n requests`
|
|
112
112
|
|
|
113
113
|
Number of requests to send out.
|
|
114
|
+
Default is no limit; will keep on sending if not specified.
|
|
114
115
|
|
|
115
116
|
Note: the total number of requests sent can be bigger than the parameter if there is a concurrency parameter;
|
|
116
117
|
loadtest will report just the first `n`.
|
|
@@ -119,6 +120,7 @@ loadtest will report just the first `n`.
|
|
|
119
120
|
|
|
120
121
|
loadtest will create a certain number of clients; this parameter controls how many.
|
|
121
122
|
Requests from them will arrive concurrently to the server.
|
|
123
|
+
Default value is 1.
|
|
122
124
|
|
|
123
125
|
Note: requests are not sent in parallel (from different processes),
|
|
124
126
|
but concurrently (a second request may be sent before the first has been answered).
|
|
@@ -126,6 +128,7 @@ but concurrently (a second request may be sent before the first has been answere
|
|
|
126
128
|
#### `-t timelimit`
|
|
127
129
|
|
|
128
130
|
Max number of seconds to wait until requests no longer go out.
|
|
131
|
+
Default is no limit; will keep on sending if not specified.
|
|
129
132
|
|
|
130
133
|
Note: this is different than Apache `ab`, which stops _receiving_ requests after the given seconds.
|
|
131
134
|
|
|
@@ -175,15 +178,17 @@ Send the string as the PATCH body. E.g.: `-A '{"key": "a9acf03f"}'`
|
|
|
175
178
|
|
|
176
179
|
#### `-m method`
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
Set method that will be sent to the test URL.
|
|
182
|
+
Accepts: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`,
|
|
183
|
+
and lowercase versions. Default is `GET`.
|
|
184
|
+
Example: `-m POST`.
|
|
180
185
|
|
|
181
|
-
#### `--data
|
|
186
|
+
#### `--data body`
|
|
182
187
|
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
Add some data to send in the body. It does not support method GET.
|
|
189
|
+
Requires setting the method with `-m` and the type with `-T`.
|
|
190
|
+
Example: `--data '{"username": "test", "password": "test"}' -T 'application/x-www-form-urlencoded' -m POST`
|
|
185
191
|
|
|
186
|
-
It required `-m` and `-T 'application/x-www-form-urlencoded'`
|
|
187
192
|
|
|
188
193
|
#### `-p POST-file`
|
|
189
194
|
|
|
@@ -256,6 +261,7 @@ The following parameters are _not_ compatible with Apache ab.
|
|
|
256
261
|
|
|
257
262
|
Controls the number of requests per second that are sent.
|
|
258
263
|
Can be fractional, e.g. `--rps 0.5` sends one request every two seconds.
|
|
264
|
+
Not used by default: each request is sent as soon as the previous one is responded.
|
|
259
265
|
|
|
260
266
|
Note: Concurrency doesn't affect the final number of requests per second,
|
|
261
267
|
since rps will be shared by all the clients. E.g.:
|
|
@@ -309,17 +315,15 @@ Open connections using keep-alive.
|
|
|
309
315
|
|
|
310
316
|
Note: instead of using the default agent, this option is now an alias for `-k`.
|
|
311
317
|
|
|
312
|
-
#### `--quiet`
|
|
318
|
+
#### `--quiet`
|
|
313
319
|
|
|
314
320
|
Do not show any messages.
|
|
315
321
|
|
|
316
|
-
Note: deprecated in version 6+, shows a warning.
|
|
317
|
-
|
|
318
322
|
#### `--debug` (deprecated)
|
|
319
323
|
|
|
320
324
|
Show debug messages.
|
|
321
325
|
|
|
322
|
-
Note: deprecated in version 6
|
|
326
|
+
Note: deprecated in version 6+.
|
|
323
327
|
|
|
324
328
|
#### `--insecure`
|
|
325
329
|
|
|
@@ -383,7 +387,7 @@ with concurrency 10 (only relevant results are shown):
|
|
|
383
387
|
99% 14 ms
|
|
384
388
|
100% 35997 ms (longest request)
|
|
385
389
|
|
|
386
|
-
|
|
390
|
+
The result was quite erratic, with some requests taking up to 36 seconds;
|
|
387
391
|
this suggests that Node.js is queueing some requests for a long time, and answering them irregularly.
|
|
388
392
|
Now we will try a fixed rate of 1000 rps:
|
|
389
393
|
|
|
@@ -438,10 +442,10 @@ We now know that our server can accept 500 rps without problems.
|
|
|
438
442
|
Not bad for a single-process naïve Node.js server...
|
|
439
443
|
We may refine our results further to find at which point from 500 to 1000 rps our server breaks down.
|
|
440
444
|
|
|
441
|
-
But instead let us research how to improve the
|
|
445
|
+
But instead let us research how to improve the result.
|
|
442
446
|
One obvious candidate is to add keep-alive to the requests so we don't have to create
|
|
443
447
|
a new connection for every request.
|
|
444
|
-
The
|
|
448
|
+
The result (with the same test server) is impressive:
|
|
445
449
|
|
|
446
450
|
$ loadtest http://localhost:7357/ -t 20 -c 10 -k
|
|
447
451
|
...
|
|
@@ -477,7 +481,28 @@ thus allowing you to load test your application in your own tests.
|
|
|
477
481
|
|
|
478
482
|
### Invoke Load Test
|
|
479
483
|
|
|
480
|
-
To run a load test, just
|
|
484
|
+
To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
import {loadTest} from 'loadtest'
|
|
488
|
+
|
|
489
|
+
const options = {
|
|
490
|
+
url: 'http://localhost:8000',
|
|
491
|
+
maxRequests: 1000,
|
|
492
|
+
}
|
|
493
|
+
const result = await loadTest(options)
|
|
494
|
+
result.show()
|
|
495
|
+
console.log('Tests run successfully')
|
|
496
|
+
})
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
The call returns a `Result` object that contains all info about the load test, also described below.
|
|
500
|
+
Call `result.show()` to display the results in the standard format on the console.
|
|
501
|
+
|
|
502
|
+
As a legacy from before promises existed,
|
|
503
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
504
|
+
the callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
505
|
+
or when the max number of seconds has elapsed.
|
|
481
506
|
|
|
482
507
|
```javascript
|
|
483
508
|
import {loadTest} from 'loadtest'
|
|
@@ -490,16 +515,51 @@ loadTest(options, function(error, result) {
|
|
|
490
515
|
if (error) {
|
|
491
516
|
return console.error('Got an error: %s', error)
|
|
492
517
|
}
|
|
518
|
+
result.show()
|
|
493
519
|
console.log('Tests run successfully')
|
|
494
520
|
})
|
|
495
521
|
```
|
|
496
522
|
|
|
497
|
-
The callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
498
|
-
or when the max number of seconds has elapsed.
|
|
499
523
|
|
|
500
524
|
Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
|
|
501
525
|
and will not call the callback.
|
|
502
526
|
|
|
527
|
+
### Result
|
|
528
|
+
|
|
529
|
+
The latency result returned at the end of the load test contains a full set of data, including:
|
|
530
|
+
mean latency, number of errors and percentiles.
|
|
531
|
+
An example follows:
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
{
|
|
535
|
+
url: 'http://localhost:80/',
|
|
536
|
+
maxRequests: 1000,
|
|
537
|
+
maxSeconds: 0,
|
|
538
|
+
concurrency: 10,
|
|
539
|
+
agent: 'none',
|
|
540
|
+
requestsPerSecond: undefined,
|
|
541
|
+
totalRequests: 1000,
|
|
542
|
+
percentiles: {
|
|
543
|
+
'50': 7,
|
|
544
|
+
'90': 10,
|
|
545
|
+
'95': 11,
|
|
546
|
+
'99': 15
|
|
547
|
+
},
|
|
548
|
+
rps: 2824,
|
|
549
|
+
totalTimeSeconds: 0.354108,
|
|
550
|
+
meanLatencyMs: 7.72,
|
|
551
|
+
maxLatencyMs: 20,
|
|
552
|
+
totalErrors: 3,
|
|
553
|
+
errorCodes: {
|
|
554
|
+
'0': 1,
|
|
555
|
+
'500': 2
|
|
556
|
+
},
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The `result` object also has a `result.show()` function
|
|
561
|
+
that displays the results on the console in the standard format.
|
|
562
|
+
|
|
503
563
|
### Options
|
|
504
564
|
|
|
505
565
|
All options but `url` are, as their name implies, optional.
|
|
@@ -587,6 +647,9 @@ function(params, options, client, callback) {
|
|
|
587
647
|
}
|
|
588
648
|
```
|
|
589
649
|
|
|
650
|
+
See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
|
|
651
|
+
(or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
|
|
652
|
+
|
|
590
653
|
#### `agentKeepAlive`
|
|
591
654
|
|
|
592
655
|
Use an agent with 'Connection: Keep-alive'.
|
|
@@ -659,11 +722,11 @@ loadTest(options, function(error) {
|
|
|
659
722
|
|
|
660
723
|
#### `statusCallback`
|
|
661
724
|
|
|
662
|
-
If present, this function executes after every request operation completes. Provides immediate access to test
|
|
725
|
+
If present, this function executes after every request operation completes. Provides immediate access to the test result while the
|
|
663
726
|
test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
|
|
664
|
-
statistical analysis of
|
|
727
|
+
statistical analysis of the result.
|
|
665
728
|
|
|
666
|
-
The
|
|
729
|
+
The result and error passed to the callback are in the same format as the result passed to the final callback.
|
|
667
730
|
|
|
668
731
|
In addition, the following three properties are added to the `result` object:
|
|
669
732
|
|
|
@@ -673,6 +736,19 @@ In addition, the following three properties are added to the `result` object:
|
|
|
673
736
|
|
|
674
737
|
You will need to check if `error` is populated in order to determine which object to check for these properties.
|
|
675
738
|
|
|
739
|
+
The second parameter contains info about the current request:
|
|
740
|
+
|
|
741
|
+
```javascript
|
|
742
|
+
{
|
|
743
|
+
host: 'localhost',
|
|
744
|
+
path: '/',
|
|
745
|
+
method: 'GET',
|
|
746
|
+
statusCode: 200,
|
|
747
|
+
body: '<html><body>hi</body></html>',
|
|
748
|
+
headers: [...]
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
676
752
|
Example:
|
|
677
753
|
|
|
678
754
|
```javascript
|
|
@@ -699,7 +775,6 @@ loadTest(options, function(error) {
|
|
|
699
775
|
console.log('Tests run successfully')
|
|
700
776
|
})
|
|
701
777
|
```
|
|
702
|
-
|
|
703
778
|
|
|
704
779
|
In some situations request data needs to be available in the statusCallBack.
|
|
705
780
|
This data can be assigned to `request.labels` in the requestGenerator:
|
|
@@ -753,57 +828,26 @@ function contentInspector(result) {
|
|
|
753
828
|
}
|
|
754
829
|
},
|
|
755
830
|
```
|
|
756
|
-
|
|
757
|
-
### Results
|
|
758
|
-
|
|
759
|
-
The latency results passed to your callback at the end of the load test contains a full set of data, including:
|
|
760
|
-
mean latency, number of errors and percentiles.
|
|
761
|
-
An example follows:
|
|
762
|
-
|
|
763
|
-
```javascript
|
|
764
|
-
{
|
|
765
|
-
totalRequests: 1000,
|
|
766
|
-
percentiles: {
|
|
767
|
-
'50': 7,
|
|
768
|
-
'90': 10,
|
|
769
|
-
'95': 11,
|
|
770
|
-
'99': 15
|
|
771
|
-
},
|
|
772
|
-
rps: 2824,
|
|
773
|
-
totalTimeSeconds: 0.354108,
|
|
774
|
-
meanLatencyMs: 7.72,
|
|
775
|
-
maxLatencyMs: 20,
|
|
776
|
-
totalErrors: 3,
|
|
777
|
-
errorCodes: {
|
|
778
|
-
'0': 1,
|
|
779
|
-
'500': 2
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
The second parameter contains info about the current request:
|
|
785
|
-
|
|
786
|
-
```javascript
|
|
787
|
-
{
|
|
788
|
-
host: 'localhost',
|
|
789
|
-
path: '/',
|
|
790
|
-
method: 'GET',
|
|
791
|
-
statusCode: 200,
|
|
792
|
-
body: '<html><body>hi</body></html>',
|
|
793
|
-
headers: [...]
|
|
794
|
-
}
|
|
795
|
-
```
|
|
796
831
|
|
|
797
832
|
### Start Test Server
|
|
798
833
|
|
|
799
|
-
To start the test server use the exported function `startServer()` with a set of options
|
|
834
|
+
To start the test server use the exported function `startServer()` with a set of options:
|
|
800
835
|
|
|
801
836
|
```javascript
|
|
802
837
|
import {startServer} from 'loadtest'
|
|
803
|
-
const server = startServer({port: 8000})
|
|
838
|
+
const server = await startServer({port: 8000})
|
|
839
|
+
// do your thing
|
|
840
|
+
await server.close()
|
|
804
841
|
```
|
|
805
842
|
|
|
806
|
-
This function returns
|
|
843
|
+
This function returns when the server is up and running,
|
|
844
|
+
with an HTTP server which can be `close()`d when it is no longer useful.
|
|
845
|
+
As a legacy from before promises existed,
|
|
846
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
847
|
+
|
|
848
|
+
```
|
|
849
|
+
const server = startServer({port: 8000}, error => console.error(error))
|
|
850
|
+
```
|
|
807
851
|
|
|
808
852
|
The following options are available.
|
|
809
853
|
|
package/bin/loadtest.js
CHANGED
|
@@ -31,8 +31,8 @@ const options = stdio.getopt({
|
|
|
31
31
|
insecure: {description: 'Allow self-signed certificates over https'},
|
|
32
32
|
key: {args: 1, description: 'The client key to use'},
|
|
33
33
|
cert: {args: 1, description: 'The client certificate to use'},
|
|
34
|
+
quiet: {description: 'Do not log any messages'},
|
|
34
35
|
agent: {description: 'Use a keep-alive http agent (deprecated)'},
|
|
35
|
-
quiet: {description: 'Do not log any messages (deprecated)'},
|
|
36
36
|
debug: {description: 'Show debug messages (deprecated)'}
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -52,7 +52,8 @@ async function processAndRun(options) {
|
|
|
52
52
|
}
|
|
53
53
|
options.url = options.args[0];
|
|
54
54
|
try {
|
|
55
|
-
loadTest(options)
|
|
55
|
+
const result = await loadTest(options)
|
|
56
|
+
result.show()
|
|
56
57
|
} catch(error) {
|
|
57
58
|
console.error(error.message)
|
|
58
59
|
help()
|
package/lib/httpClient.js
CHANGED
|
@@ -28,6 +28,7 @@ class HttpClient {
|
|
|
28
28
|
constructor(operation, params) {
|
|
29
29
|
this.operation = operation
|
|
30
30
|
this.params = params
|
|
31
|
+
this.stopped = false
|
|
31
32
|
this.init();
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -45,11 +46,15 @@ class HttpClient {
|
|
|
45
46
|
this.options.key = this.params.key;
|
|
46
47
|
}
|
|
47
48
|
this.options.agent = false;
|
|
49
|
+
if (this.params.requestsPerSecond) {
|
|
50
|
+
// rps for each client is total / concurrency (# of clients)
|
|
51
|
+
this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
|
|
52
|
+
}
|
|
48
53
|
if (this.params.agentKeepAlive) {
|
|
49
|
-
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive;
|
|
54
|
+
const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
|
|
50
55
|
let maxSockets = 10;
|
|
51
|
-
if (this.
|
|
52
|
-
maxSockets += Math.floor(this.
|
|
56
|
+
if (this.options.requestsPerSecond) {
|
|
57
|
+
maxSockets += Math.floor(this.options.requestsPerSecond);
|
|
53
58
|
}
|
|
54
59
|
this.options.agent = new KeepAlive({
|
|
55
60
|
maxSockets: maxSockets,
|
|
@@ -71,7 +76,7 @@ class HttpClient {
|
|
|
71
76
|
} else if (typeof this.params.body == 'function') {
|
|
72
77
|
this.generateMessage = this.params.body;
|
|
73
78
|
} else {
|
|
74
|
-
|
|
79
|
+
throw new Error(`Unrecognized body: ${typeof this.params.body}`);
|
|
75
80
|
}
|
|
76
81
|
this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
|
|
77
82
|
}
|
|
@@ -81,7 +86,7 @@ class HttpClient {
|
|
|
81
86
|
} else if (typeof this.params.cookies == 'string') {
|
|
82
87
|
this.options.headers.Cookie = this.params.cookies;
|
|
83
88
|
} else {
|
|
84
|
-
|
|
89
|
+
throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
addUserAgent(this.options.headers);
|
|
@@ -94,10 +99,15 @@ class HttpClient {
|
|
|
94
99
|
* Start the HTTP client.
|
|
95
100
|
*/
|
|
96
101
|
start() {
|
|
97
|
-
if (
|
|
102
|
+
if (this.stopped) {
|
|
103
|
+
// solves testing issue: with requestsPerSecond clients are started at random,
|
|
104
|
+
// so sometimes they are stopped before they have even started
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (!this.options.requestsPerSecond) {
|
|
98
108
|
return this.makeRequest();
|
|
99
109
|
}
|
|
100
|
-
const interval = 1000 / this.
|
|
110
|
+
const interval = 1000 / this.options.requestsPerSecond;
|
|
101
111
|
this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
|
|
102
112
|
}
|
|
103
113
|
|
|
@@ -105,6 +115,7 @@ class HttpClient {
|
|
|
105
115
|
* Stop the HTTP client.
|
|
106
116
|
*/
|
|
107
117
|
stop() {
|
|
118
|
+
this.stopped = true
|
|
108
119
|
if (this.requestTimer) {
|
|
109
120
|
this.requestTimer.stop();
|
|
110
121
|
}
|
|
@@ -133,7 +144,6 @@ class HttpClient {
|
|
|
133
144
|
// adding proxy configuration
|
|
134
145
|
if (this.params.proxy) {
|
|
135
146
|
const proxy = this.params.proxy;
|
|
136
|
-
//console.log('using proxy server %j', proxy);
|
|
137
147
|
const agent = new HttpsProxyAgent(proxy);
|
|
138
148
|
this.options.agent = agent;
|
|
139
149
|
}
|
|
@@ -154,7 +164,8 @@ class HttpClient {
|
|
|
154
164
|
delete this.options.headers['Content-Length'];
|
|
155
165
|
}
|
|
156
166
|
if (typeof this.params.requestGenerator == 'function') {
|
|
157
|
-
|
|
167
|
+
const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
|
|
168
|
+
request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
|
|
158
169
|
} else {
|
|
159
170
|
request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
|
|
160
171
|
}
|
|
@@ -205,7 +216,7 @@ class HttpClient {
|
|
|
205
216
|
result.instanceIndex = this.operation.instanceIndex;
|
|
206
217
|
}
|
|
207
218
|
let callback;
|
|
208
|
-
if (!this.
|
|
219
|
+
if (!this.options.requestsPerSecond) {
|
|
209
220
|
callback = this.makeRequest.bind(this);
|
|
210
221
|
}
|
|
211
222
|
this.operation.callback(error, result, callback);
|
package/lib/latency.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import * as crypto from 'crypto'
|
|
2
|
+
import {Result} from './result.js'
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Latency measurements. Options can be:
|
|
6
7
|
* - maxRequests: max number of requests to measure before stopping.
|
|
7
8
|
* - maxSeconds: max seconds, alternative to max requests.
|
|
8
|
-
* An optional callback(error,
|
|
9
|
-
* or the
|
|
9
|
+
* An optional callback(error, result) will be called with an error,
|
|
10
|
+
* or the result after max is reached.
|
|
10
11
|
*/
|
|
11
12
|
export class Latency {
|
|
12
13
|
constructor(options, callback) {
|
|
@@ -106,7 +107,7 @@ export class Latency {
|
|
|
106
107
|
showPartial() {
|
|
107
108
|
const elapsedSeconds = this.getElapsed(this.lastShown) / 1000;
|
|
108
109
|
const meanTime = this.partialTime / this.partialRequests || 0.0;
|
|
109
|
-
const
|
|
110
|
+
const result = {
|
|
110
111
|
meanLatencyMs: Math.round(meanTime * 10) / 10,
|
|
111
112
|
rps: Math.round(this.partialRequests / elapsedSeconds)
|
|
112
113
|
};
|
|
@@ -114,10 +115,12 @@ export class Latency {
|
|
|
114
115
|
if (this.options.maxRequests) {
|
|
115
116
|
percent = ' (' + Math.round(100 * this.totalRequests / this.options.maxRequests) + '%)';
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
if (!this.options.quiet) {
|
|
119
|
+
console.info('Requests: %s%s, requests per second: %s, mean latency: %s ms', this.totalRequests, percent, result.rps, result.meanLatencyMs);
|
|
120
|
+
if (this.totalErrors) {
|
|
121
|
+
percent = Math.round(100 * 10 * this.totalErrors / this.totalRequests) / 10;
|
|
122
|
+
console.info('Errors: %s, accumulated errors: %s, %s% of total requests', this.partialErrors, this.totalErrors, percent);
|
|
123
|
+
}
|
|
121
124
|
}
|
|
122
125
|
this.partialTime = 0;
|
|
123
126
|
this.partialRequests = 0;
|
|
@@ -163,27 +166,16 @@ export class Latency {
|
|
|
163
166
|
finish() {
|
|
164
167
|
this.running = false;
|
|
165
168
|
if (this.callback) {
|
|
166
|
-
return this.callback(null, this.
|
|
169
|
+
return this.callback(null, this.getResult());
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
/**
|
|
171
|
-
* Get final
|
|
174
|
+
* Get final result.
|
|
172
175
|
*/
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
totalRequests: this.totalRequests,
|
|
178
|
-
totalErrors: this.totalErrors,
|
|
179
|
-
totalTimeSeconds: elapsedSeconds,
|
|
180
|
-
rps: Math.round(this.totalRequests / elapsedSeconds),
|
|
181
|
-
meanLatencyMs: Math.round(meanTime * 10) / 10,
|
|
182
|
-
maxLatencyMs: this.maxLatencyMs,
|
|
183
|
-
minLatencyMs: this.minLatencyMs,
|
|
184
|
-
percentiles: this.computePercentiles(),
|
|
185
|
-
errorCodes: this.errorCodes
|
|
186
|
-
};
|
|
176
|
+
getResult() {
|
|
177
|
+
const result = new Result(this.options, this)
|
|
178
|
+
return result
|
|
187
179
|
}
|
|
188
180
|
|
|
189
181
|
/**
|
|
@@ -215,57 +207,18 @@ export class Latency {
|
|
|
215
207
|
}
|
|
216
208
|
|
|
217
209
|
/**
|
|
218
|
-
* Show final
|
|
210
|
+
* Show final result.
|
|
219
211
|
*/
|
|
220
212
|
show() {
|
|
221
213
|
if (this.totalsShown) {
|
|
222
214
|
return;
|
|
223
215
|
}
|
|
224
216
|
this.totalsShown = true;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
console.info('Target URL: %s', this.options.url);
|
|
228
|
-
if (this.options.maxRequests) {
|
|
229
|
-
console.info('Max requests: %s', this.options.maxRequests);
|
|
230
|
-
} else if (this.options.maxSeconds) {
|
|
231
|
-
console.info('Max time (s): %s', this.options.maxSeconds);
|
|
232
|
-
}
|
|
233
|
-
console.info('Concurrency level: %s', this.options.concurrency);
|
|
234
|
-
let agent = 'none';
|
|
235
|
-
if (this.options.agentKeepAlive) {
|
|
236
|
-
agent = 'keepalive';
|
|
237
|
-
}
|
|
238
|
-
console.info('Agent: %s', agent);
|
|
239
|
-
if (this.options.requestsPerSecond) {
|
|
240
|
-
console.info('Requests per second: %s', this.options.requestsPerSecond * this.options.concurrency);
|
|
241
|
-
}
|
|
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');
|
|
250
|
-
|
|
251
|
-
Object.keys(results.percentiles).forEach(percentile => {
|
|
252
|
-
console.info(' %s% %s ms', percentile, results.percentiles[percentile]);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
256
|
-
if (results.totalErrors) {
|
|
257
|
-
console.info('');
|
|
258
|
-
console.info(' 100% %s ms (longest request)', this.maxLatencyMs);
|
|
259
|
-
console.info('');
|
|
260
|
-
Object.keys(results.errorCodes).forEach(errorCode => {
|
|
261
|
-
const padding = ' '.repeat(errorCode.length < 4 ? 4 - errorCode.length : 1);
|
|
262
|
-
console.info(' %s%s: %s errors', padding, errorCode, results.errorCodes[errorCode]);
|
|
263
|
-
});
|
|
264
|
-
}
|
|
217
|
+
const result = this.getResult();
|
|
218
|
+
result.show()
|
|
265
219
|
}
|
|
266
220
|
}
|
|
267
221
|
|
|
268
|
-
|
|
269
222
|
/**
|
|
270
223
|
* Create a unique, random token.
|
|
271
224
|
*/
|
package/lib/loadtest.js
CHANGED
|
@@ -14,33 +14,49 @@ https.globalAgent.maxSockets = 1000;
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Run a load test.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
17
|
+
* Parameters:
|
|
18
|
+
* - `options`: an object which may have:
|
|
19
|
+
* - url [string]: URL to access (mandatory).
|
|
20
|
+
* - concurrency [number]: how many concurrent clients to use.
|
|
21
|
+
* - maxRequests [number]: how many requests to send
|
|
22
|
+
* - maxSeconds [number]: how long to run the tests.
|
|
23
|
+
* - cookies [array]: a string or an array of strings, each with name:value.
|
|
24
|
+
* - headers [map]: a map with headers: {key1: value1, key2: value2}.
|
|
25
|
+
* - method [string]: the method to use: POST, PUT. Default: GET, what else.
|
|
26
|
+
* - body [string]: the contents to send along a POST or PUT request.
|
|
27
|
+
* - contentType [string]: the MIME type to use for the body, default text/plain.
|
|
28
|
+
* - requestsPerSecond [number]: how many requests per second to send.
|
|
29
|
+
* - agentKeepAlive: if true, then use connection keep-alive.
|
|
30
|
+
* - indexParam [string]: string to replace with a unique index.
|
|
31
|
+
* - insecure: allow https using self-signed certs.
|
|
32
|
+
* - secureProtocol [string]: TLS/SSL secure protocol method to use.
|
|
33
|
+
* - proxy [string]: use a proxy for requests e.g. http://localhost:8080.
|
|
34
|
+
* - quiet: do not log any messages.
|
|
35
|
+
* - debug: show debug messages (deprecated).
|
|
36
|
+
* - `callback`: optional `function(result, error)` called if/when the test finishes;
|
|
37
|
+
* if not present a promise is returned.
|
|
34
38
|
*/
|
|
35
39
|
export function loadTest(options, callback) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
if (!callback) {
|
|
41
|
+
return loadTestAsync(options)
|
|
42
|
+
}
|
|
43
|
+
loadTestAsync(options).then(result => callback(null, result)).catch(error => callback(error))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadTestAsync(options) {
|
|
47
|
+
const processed = await processOptions(options)
|
|
48
|
+
return await runOperation(processed)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runOperation(options) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const operation = new Operation(options, (error, result) => {
|
|
54
|
+
if (error) {
|
|
55
|
+
return reject(error)
|
|
56
|
+
}
|
|
57
|
+
return resolve(result)
|
|
58
|
+
});
|
|
42
59
|
operation.start();
|
|
43
|
-
return operation;
|
|
44
60
|
})
|
|
45
61
|
}
|
|
46
62
|
|
|
@@ -93,7 +109,7 @@ class Operation {
|
|
|
93
109
|
next();
|
|
94
110
|
}
|
|
95
111
|
if (this.options.statusCallback) {
|
|
96
|
-
this.options.statusCallback(error, result, this.latency.
|
|
112
|
+
this.options.statusCallback(error, result, this.latency.getResult());
|
|
97
113
|
}
|
|
98
114
|
}
|
|
99
115
|
|
|
@@ -143,20 +159,19 @@ class Operation {
|
|
|
143
159
|
* Stop clients.
|
|
144
160
|
*/
|
|
145
161
|
stop() {
|
|
162
|
+
this.running = false;
|
|
163
|
+
this.latency.running = false;
|
|
146
164
|
if (this.showTimer) {
|
|
147
165
|
this.showTimer.stop();
|
|
148
166
|
}
|
|
149
167
|
if (this.stopTimeout) {
|
|
150
168
|
clearTimeout(this.stopTimeout);
|
|
151
169
|
}
|
|
152
|
-
this.running = false;
|
|
153
|
-
this.latency.running = false;
|
|
154
|
-
|
|
155
170
|
Object.keys(this.clients).forEach(index => {
|
|
156
171
|
this.clients[index].stop();
|
|
157
172
|
});
|
|
158
173
|
if (this.finalCallback) {
|
|
159
|
-
const result = this.latency.
|
|
174
|
+
const result = this.latency.getResult();
|
|
160
175
|
result.instanceIndex = this.instanceIndex;
|
|
161
176
|
this.finalCallback(null, result);
|
|
162
177
|
} else {
|