loadtest 7.1.1 → 8.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/README.md +60 -42
- package/bin/loadtest.js +3 -2
- package/bin/tcp-performance.js +21 -0
- package/bin/testserver.js +28 -26
- package/doc/api.md +103 -52
- package/doc/tcp-sockets.md +569 -0
- package/lib/baseClient.js +3 -7
- package/lib/cluster.js +1 -1
- package/lib/httpClient.js +18 -43
- package/lib/latency.js +27 -27
- package/lib/loadtest.js +26 -69
- package/lib/options.js +5 -1
- package/lib/parser.js +167 -0
- package/lib/pool.js +106 -0
- package/lib/result.js +22 -14
- package/lib/tcpClient.js +198 -0
- package/lib/testserver.js +24 -7
- package/lib/websocket.js +5 -11
- package/package.json +3 -2
- package/test/all.js +5 -0
- package/test/body-generator.js +3 -3
- package/test/httpClient.js +3 -3
- package/test/integration.js +47 -12
- package/test/latency.js +29 -25
- package/test/loadtest.js +7 -7
- package/test/request-generator.js +3 -3
- package/test/result.js +1 -1
- package/test/tcpClient.js +23 -0
- package/test/websocket.js +3 -3
package/lib/pool.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {HttpClient} from './httpClient.js'
|
|
2
|
+
import {TcpClient} from './tcpClient.js'
|
|
3
|
+
import {WebsocketClient} from './websocket.js'
|
|
4
|
+
import {HighResolutionTimer} from './hrtimer.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used to keep track of individual load test runs.
|
|
8
|
+
*/
|
|
9
|
+
let instanceIndex = 0;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A pool of clients.
|
|
14
|
+
*/
|
|
15
|
+
export class Pool {
|
|
16
|
+
constructor(loadTest) {
|
|
17
|
+
this.loadTest = loadTest
|
|
18
|
+
this.options = loadTest.options
|
|
19
|
+
this.clients = [];
|
|
20
|
+
this.freeClients = []
|
|
21
|
+
this.requestTimer = null
|
|
22
|
+
this.instanceIndex = instanceIndex++
|
|
23
|
+
this.requestIndex = 0
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start a number of measuring clients.
|
|
28
|
+
*/
|
|
29
|
+
start() {
|
|
30
|
+
if (this.options.requestsPerSecond) {
|
|
31
|
+
const interval = 1000 / this.options.requestsPerSecond;
|
|
32
|
+
this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
for (let index = 0; index < this.options.concurrency; index++) {
|
|
36
|
+
const client = this.addClient();
|
|
37
|
+
client.start();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
addClient() {
|
|
42
|
+
const client = this.createClient();
|
|
43
|
+
this.clients.push(client)
|
|
44
|
+
this.freeClients.push(client)
|
|
45
|
+
return client
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
createClient() {
|
|
49
|
+
// TODO: || this.options.url.startsWith('wss:'))
|
|
50
|
+
if (this.options.url.startsWith('ws:')) {
|
|
51
|
+
return new WebsocketClient(this.loadTest)
|
|
52
|
+
}
|
|
53
|
+
if (this.options.tcp) {
|
|
54
|
+
return new TcpClient(this.loadTest)
|
|
55
|
+
}
|
|
56
|
+
return new HttpClient(this.loadTest);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
makeRequest() {
|
|
60
|
+
if (!this.loadTest.running) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
if (!this.freeClients.length) {
|
|
64
|
+
this.addClient()
|
|
65
|
+
}
|
|
66
|
+
const client = this.freeClients.shift()
|
|
67
|
+
client.makeRequest()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Call after each request has finished.
|
|
72
|
+
*/
|
|
73
|
+
finishRequest(client, result, error) {
|
|
74
|
+
if (this.options.statusCallback) {
|
|
75
|
+
result.requestIndex = this.requestIndex++
|
|
76
|
+
result.instanceIndex = this.loadTest.instanceIndex
|
|
77
|
+
this.options.statusCallback(error, result);
|
|
78
|
+
}
|
|
79
|
+
if (this.loadTest.checkStop()) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (!this.loadTest.latency.shouldSend()) {
|
|
83
|
+
if (this.requestTimer) {
|
|
84
|
+
this.requestTimer.stop()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!this.options.requestsPerSecond) {
|
|
88
|
+
client.makeRequest()
|
|
89
|
+
} else {
|
|
90
|
+
this.freeClients.push(client)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Stop clients.
|
|
96
|
+
*/
|
|
97
|
+
stop() {
|
|
98
|
+
if (this.requestTimer) {
|
|
99
|
+
this.requestTimer.stop()
|
|
100
|
+
}
|
|
101
|
+
for (const client of this.clients) {
|
|
102
|
+
client.stop();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
package/lib/result.js
CHANGED
|
@@ -11,9 +11,10 @@ export class Result {
|
|
|
11
11
|
this.maxSeconds = 0
|
|
12
12
|
this.concurrency = 0
|
|
13
13
|
this.agent = null
|
|
14
|
-
this.requestsPerSecond =
|
|
14
|
+
this.requestsPerSecond = null
|
|
15
|
+
this.clients = 0
|
|
15
16
|
this.startTimeMs = Number.MAX_SAFE_INTEGER
|
|
16
|
-
this.
|
|
17
|
+
this.stopTimeMs = 0
|
|
17
18
|
this.elapsedSeconds = 0
|
|
18
19
|
this.totalRequests = 0
|
|
19
20
|
this.totalErrors = 0
|
|
@@ -25,18 +26,24 @@ export class Result {
|
|
|
25
26
|
this.histogramMs = {}
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
compute(
|
|
29
|
-
|
|
29
|
+
compute(latency) {
|
|
30
|
+
const options = latency.options
|
|
30
31
|
this.url = options.url
|
|
31
32
|
this.cores = options.cores
|
|
32
33
|
this.maxRequests = parseInt(options.maxRequests)
|
|
33
34
|
this.maxSeconds = parseInt(options.maxSeconds)
|
|
34
35
|
this.concurrency = parseInt(options.concurrency)
|
|
35
|
-
this.
|
|
36
|
+
this.clients = latency.clients
|
|
37
|
+
if (options.tcp) {
|
|
38
|
+
this.agent = 'tcp'
|
|
39
|
+
} else if (options.agentKeepAlive) {
|
|
40
|
+
this.agent = 'keepalive'
|
|
41
|
+
} else {
|
|
42
|
+
this.agent = 'none'
|
|
43
|
+
}
|
|
36
44
|
this.requestsPerSecond = parseInt(options.requestsPerSecond)
|
|
37
|
-
// result
|
|
38
45
|
this.startTimeMs = Number(latency.startTimeNs / 1000000n)
|
|
39
|
-
this.
|
|
46
|
+
this.stopTimeMs = Number(latency.stopTimeNs / 1000000n)
|
|
40
47
|
this.totalRequests = latency.totalRequests
|
|
41
48
|
this.totalErrors = latency.totalErrors
|
|
42
49
|
this.accumulatedMs = latency.totalTime
|
|
@@ -48,7 +55,7 @@ export class Result {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
computeDerived() {
|
|
51
|
-
this.elapsedSeconds = (this.
|
|
58
|
+
this.elapsedSeconds = (this.stopTimeMs - this.startTimeMs) / 1000
|
|
52
59
|
this.totalTimeSeconds = this.elapsedSeconds // backwards compatibility
|
|
53
60
|
const meanTime = this.accumulatedMs / this.totalRequests
|
|
54
61
|
this.meanLatencyMs = Math.round(meanTime * 10) / 10
|
|
@@ -90,9 +97,10 @@ export class Result {
|
|
|
90
97
|
this.concurrency = this.concurrency || result.concurrency
|
|
91
98
|
this.agent = this.agent || result.agent
|
|
92
99
|
this.requestsPerSecond += result.requestsPerSecond || 0
|
|
100
|
+
this.clients += result.clients
|
|
93
101
|
// result
|
|
94
102
|
this.startTimeMs = Math.min(this.startTimeMs, result.startTimeMs)
|
|
95
|
-
this.
|
|
103
|
+
this.stopTimeMs = Math.max(this.stopTimeMs, result.stopTimeMs)
|
|
96
104
|
this.totalRequests += result.totalRequests
|
|
97
105
|
this.totalErrors += result.totalErrors
|
|
98
106
|
this.accumulatedMs += result.accumulatedMs
|
|
@@ -125,14 +133,14 @@ export class Result {
|
|
|
125
133
|
} else if (this.maxSeconds) {
|
|
126
134
|
console.info('Max time (s): %s', this.maxSeconds);
|
|
127
135
|
}
|
|
128
|
-
|
|
136
|
+
if (this.requestsPerSecond) {
|
|
137
|
+
console.info('Target rps: %s', this.requestsPerSecond);
|
|
138
|
+
}
|
|
139
|
+
console.info('Concurrent clients: %s', this.clients)
|
|
129
140
|
if (this.cores) {
|
|
130
141
|
console.info('Running on cores: %s', this.cores);
|
|
131
142
|
}
|
|
132
143
|
console.info('Agent: %s', this.agent);
|
|
133
|
-
if (this.requestsPerSecond) {
|
|
134
|
-
console.info('Target rps: %s', this.requestsPerSecond);
|
|
135
|
-
}
|
|
136
144
|
console.info('');
|
|
137
145
|
console.info('Completed requests: %s', this.totalRequests);
|
|
138
146
|
console.info('Total errors: %s', this.totalErrors);
|
|
@@ -140,7 +148,7 @@ export class Result {
|
|
|
140
148
|
console.info('Mean latency: %s ms', this.meanLatencyMs);
|
|
141
149
|
console.info('Effective rps: %s', this.effectiveRps);
|
|
142
150
|
console.info('');
|
|
143
|
-
console.info('Percentage of
|
|
151
|
+
console.info('Percentage of requests served within a certain time');
|
|
144
152
|
|
|
145
153
|
Object.keys(this.percentiles).forEach(percentile => {
|
|
146
154
|
console.info(' %s% %s ms', percentile, this.percentiles[percentile]);
|
package/lib/tcpClient.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as urlLib from 'url'
|
|
2
|
+
import * as net from 'net'
|
|
3
|
+
import * as querystring from 'querystring'
|
|
4
|
+
import {addUserAgent} from './headers.js'
|
|
5
|
+
import {Parser} from './parser.js'
|
|
6
|
+
|
|
7
|
+
const forbiddenOptions = [
|
|
8
|
+
'indexParam', 'statusCallback', 'requestGenerator'
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A client for an HTTP connection, using TCP sockets. Constructor:
|
|
14
|
+
* - `loadTest`: an object with the following attributes:
|
|
15
|
+
* - `latency`: a variable to measure latency.
|
|
16
|
+
* - `options`: same options as exports.loadTest.
|
|
17
|
+
* - `running`: if the loadTest is running or not.
|
|
18
|
+
*/
|
|
19
|
+
export class TcpClient {
|
|
20
|
+
constructor(loadTest) {
|
|
21
|
+
this.loadTest = loadTest
|
|
22
|
+
this.latency = loadTest.latency
|
|
23
|
+
this.options = loadTest.options
|
|
24
|
+
this.running = true
|
|
25
|
+
this.connection = null
|
|
26
|
+
this.currentId = null
|
|
27
|
+
this.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Init params and message to send.
|
|
32
|
+
*/
|
|
33
|
+
init() {
|
|
34
|
+
this.params = urlLib.parse(this.options.url);
|
|
35
|
+
this.params.headers = this.options.headers || {}
|
|
36
|
+
if (this.options.cert && this.options.key) {
|
|
37
|
+
this.params.cert = this.options.cert;
|
|
38
|
+
this.params.key = this.options.key;
|
|
39
|
+
}
|
|
40
|
+
this.params.agent = false;
|
|
41
|
+
this.params.method = this.options.method || 'GET';
|
|
42
|
+
if (this.options.cookies) {
|
|
43
|
+
if (Array.isArray(this.options.cookies)) {
|
|
44
|
+
this.params.headers.Cookie = this.options.cookies.join('; ');
|
|
45
|
+
} else if (typeof this.options.cookies == 'string') {
|
|
46
|
+
this.params.headers.Cookie = this.options.cookies;
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error(`Invalid cookies ${JSON.stringify(this.options.cookies)}, please use an array or a string`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
addUserAgent(this.params.headers);
|
|
52
|
+
this.params.headers.Connection = 'keep-alive'
|
|
53
|
+
this.params.request = this.createRequest()
|
|
54
|
+
for (const option of forbiddenOptions) {
|
|
55
|
+
if (this.options[option]) {
|
|
56
|
+
throw new Error(`${option} not supported with TCP sockets`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
createRequest() {
|
|
62
|
+
const body = this.generateBody()
|
|
63
|
+
if (body?.length) {
|
|
64
|
+
this.params.headers['Content-Type'] = this.options.contentType || 'text/plain';
|
|
65
|
+
this.params.headers['Content-Length'] = Buffer.byteLength(body)
|
|
66
|
+
}
|
|
67
|
+
const lines = [`${this.params.method} ${this.params.path} HTTP/1.1`]
|
|
68
|
+
for (const header in this.params.headers) {
|
|
69
|
+
const value = this.params.headers[header]
|
|
70
|
+
const line = `${header}: ${value}`
|
|
71
|
+
lines.push(line)
|
|
72
|
+
}
|
|
73
|
+
lines.push(`\r\n`)
|
|
74
|
+
const text = lines.join('\r\n')
|
|
75
|
+
return Buffer.from(text)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
generateBody() {
|
|
79
|
+
if (!this.options.body) {
|
|
80
|
+
return ''
|
|
81
|
+
}
|
|
82
|
+
if (typeof this.options.body == 'string') {
|
|
83
|
+
return this.options.body
|
|
84
|
+
} else if (typeof this.options.body == 'object') {
|
|
85
|
+
if (this.options.contentType === 'application/x-www-form-urlencoded') {
|
|
86
|
+
return querystring.stringify(this.options.body);
|
|
87
|
+
}
|
|
88
|
+
return JSON.stringify(this.options.body)
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Unrecognized body: ${typeof this.options.body}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start the HTTP client.
|
|
96
|
+
*/
|
|
97
|
+
start() {
|
|
98
|
+
if (!this.options.requestsPerSecond) {
|
|
99
|
+
return this.makeRequest();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stop the HTTP client.
|
|
105
|
+
*/
|
|
106
|
+
stop() {
|
|
107
|
+
this.running = false
|
|
108
|
+
if (this.connection) {
|
|
109
|
+
this.connection.end()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Make a single request to the server.
|
|
115
|
+
*/
|
|
116
|
+
makeRequest() {
|
|
117
|
+
if (!this.running) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
this.connect()
|
|
121
|
+
this.parser = new Parser(this.params.method)
|
|
122
|
+
const id = this.latency.begin();
|
|
123
|
+
this.currentId = id
|
|
124
|
+
this.connection.write(this.params.request)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
connect() {
|
|
128
|
+
if (this.connection && !this.connection.destroyed) {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
this.connection = net.connect(this.params.port, this.params.hostname)
|
|
132
|
+
this.connection.on('data', data => {
|
|
133
|
+
this.parser.addPacket(data)
|
|
134
|
+
if (this.parser.finished) {
|
|
135
|
+
this.finishRequest(null);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this.connection.on('error', error => {
|
|
139
|
+
this.finishRequest(`Connection ${this.currentId} failed: ${error}`);
|
|
140
|
+
this.connection = null
|
|
141
|
+
});
|
|
142
|
+
this.connection.on('end', () => {
|
|
143
|
+
this.connection = null
|
|
144
|
+
if (this.parser) {
|
|
145
|
+
// connection waiting for a response; remake
|
|
146
|
+
this.makeRequest()
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
finishRequest(error) {
|
|
152
|
+
// reset parser for next request
|
|
153
|
+
const parser = this.parser
|
|
154
|
+
this.parser = null
|
|
155
|
+
if (!this.running) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
let errorCode = null;
|
|
159
|
+
if (parser.statusCode >= 400) {
|
|
160
|
+
errorCode = parser.statusCode
|
|
161
|
+
}
|
|
162
|
+
if (error) {
|
|
163
|
+
// console.error(error)
|
|
164
|
+
errorCode = '-1';
|
|
165
|
+
}
|
|
166
|
+
const elapsed = this.latency.end(this.currentId, errorCode);
|
|
167
|
+
if (elapsed < 0) {
|
|
168
|
+
// not found or not running
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (this.options.maxRequests && this.latency.totalRequests >= this.options.maxRequests) {
|
|
172
|
+
this.loadTest.stop()
|
|
173
|
+
return;
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
this.makeRequest()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
createResult(connection, body) {
|
|
180
|
+
if (!this.options.statusCallback && !this.options.contentInspector) {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
const client = connection.connection || connection.client
|
|
184
|
+
const result = {
|
|
185
|
+
host: client._host,
|
|
186
|
+
path: connection.req.path,
|
|
187
|
+
method: connection.req.method,
|
|
188
|
+
statusCode: connection.statusCode,
|
|
189
|
+
body,
|
|
190
|
+
headers: connection.headers,
|
|
191
|
+
};
|
|
192
|
+
if (connection.req.labels) {
|
|
193
|
+
result.labels = connection.req.labels
|
|
194
|
+
}
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
package/lib/testserver.js
CHANGED
|
@@ -3,6 +3,7 @@ import {server as WebSocketServer} from 'websocket'
|
|
|
3
3
|
import * as util from 'util'
|
|
4
4
|
import * as net from 'net'
|
|
5
5
|
import {Latency} from './latency.js'
|
|
6
|
+
import {readFileSync} from 'fs'
|
|
6
7
|
|
|
7
8
|
const PORT = 7357;
|
|
8
9
|
const LOG_HEADERS_INTERVAL_MS = 5000;
|
|
@@ -17,6 +18,8 @@ const LOG_HEADERS_INTERVAL_MS = 5000;
|
|
|
17
18
|
* - percent: give an error (default 500) on some % of requests.
|
|
18
19
|
* - error: set an HTTP error code, default is 500.
|
|
19
20
|
* - logger: function to log all incoming requests.
|
|
21
|
+
* - body: to return in requests.
|
|
22
|
+
* - file: to read and return as body.
|
|
20
23
|
* - `callback`: optional callback, called after the server has started.
|
|
21
24
|
* If not present will return a promise.
|
|
22
25
|
*/
|
|
@@ -44,7 +47,12 @@ class TestServer {
|
|
|
44
47
|
this.wsServer = null;
|
|
45
48
|
this.latency = new Latency({});
|
|
46
49
|
this.totalRequests = 0
|
|
50
|
+
this.partialRequests = 0
|
|
47
51
|
this.debuggedTime = Date.now();
|
|
52
|
+
this.body = options.body || 'OK'
|
|
53
|
+
if (options.file) {
|
|
54
|
+
this.body = readFileSync(options.file)
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
/**
|
|
@@ -52,7 +60,7 @@ class TestServer {
|
|
|
52
60
|
* The callback parameter will be called after the server has started.
|
|
53
61
|
*/
|
|
54
62
|
start(callback) {
|
|
55
|
-
if (this.options.
|
|
63
|
+
if (this.options.tcp) {
|
|
56
64
|
// just for internal debugging
|
|
57
65
|
this.server = net.createServer(() => this.socketListen());
|
|
58
66
|
} else {
|
|
@@ -72,7 +80,7 @@ class TestServer {
|
|
|
72
80
|
});
|
|
73
81
|
this.server.listen(this.port, () => {
|
|
74
82
|
if (!this.options.quiet) console.info(`Listening on http://localhost:${this.port}/`)
|
|
75
|
-
callback(null, this
|
|
83
|
+
callback(null, this)
|
|
76
84
|
});
|
|
77
85
|
this.wsServer.on('request', request => {
|
|
78
86
|
// explicity omitting origin check here.
|
|
@@ -88,7 +96,7 @@ class TestServer {
|
|
|
88
96
|
if (!this.options.quiet) console.info('Peer %s disconnected', connection.remoteAddress);
|
|
89
97
|
});
|
|
90
98
|
});
|
|
91
|
-
return this
|
|
99
|
+
return this
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
/**
|
|
@@ -102,7 +110,7 @@ class TestServer {
|
|
|
102
110
|
* Listen to an incoming request.
|
|
103
111
|
*/
|
|
104
112
|
listen(request, response) {
|
|
105
|
-
const id = this.latency.
|
|
113
|
+
const id = this.latency.begin();
|
|
106
114
|
const bodyBuffers = []
|
|
107
115
|
request.on('data', data => {
|
|
108
116
|
bodyBuffers.push(data)
|
|
@@ -114,6 +122,7 @@ class TestServer {
|
|
|
114
122
|
})
|
|
115
123
|
request.on('end', () => {
|
|
116
124
|
request.body = Buffer.concat(bodyBuffers).toString();
|
|
125
|
+
this.partialRequests += 1
|
|
117
126
|
this.totalRequests += 1
|
|
118
127
|
const elapsedMs = Date.now() - this.debuggedTime
|
|
119
128
|
if (elapsedMs > LOG_HEADERS_INTERVAL_MS) {
|
|
@@ -154,7 +163,7 @@ class TestServer {
|
|
|
154
163
|
const headers = util.inspect(request.headers)
|
|
155
164
|
const now = Date.now()
|
|
156
165
|
const elapsedMs = now - this.debuggedTime
|
|
157
|
-
const rps = (this.
|
|
166
|
+
const rps = (this.partialRequests / elapsedMs) * 1000
|
|
158
167
|
if (rps > 1) {
|
|
159
168
|
console.info(`Requests per second: ${rps.toFixed(0)}`)
|
|
160
169
|
}
|
|
@@ -163,7 +172,7 @@ class TestServer {
|
|
|
163
172
|
console.info(`Body: ${request.body}`);
|
|
164
173
|
}
|
|
165
174
|
this.debuggedTime = now;
|
|
166
|
-
this.
|
|
175
|
+
this.partialRequests = 0
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
/**
|
|
@@ -175,7 +184,7 @@ class TestServer {
|
|
|
175
184
|
response.writeHead(code);
|
|
176
185
|
response.end('ERROR');
|
|
177
186
|
} else {
|
|
178
|
-
response.end(
|
|
187
|
+
response.end(this.body);
|
|
179
188
|
}
|
|
180
189
|
this.latency.end(id);
|
|
181
190
|
if (this.options.logger) {
|
|
@@ -197,5 +206,13 @@ class TestServer {
|
|
|
197
206
|
}
|
|
198
207
|
return (Math.random() < percent / 100);
|
|
199
208
|
}
|
|
209
|
+
|
|
210
|
+
close(callback) {
|
|
211
|
+
if (callback) {
|
|
212
|
+
this.server.close(callback)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
return this.server.close()
|
|
216
|
+
}
|
|
200
217
|
}
|
|
201
218
|
|
package/lib/websocket.js
CHANGED
|
@@ -4,19 +4,13 @@ import {BaseClient} from './baseClient.js'
|
|
|
4
4
|
let latency;
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Create a client for a websocket.
|
|
9
|
-
*/
|
|
10
|
-
export function create(loadTest, options) {
|
|
11
|
-
return new WebsocketClient(loadTest, options);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
7
|
/**
|
|
15
8
|
* A client that connects to a websocket.
|
|
16
9
|
*/
|
|
17
|
-
class WebsocketClient extends BaseClient {
|
|
18
|
-
constructor(loadTest
|
|
19
|
-
super(loadTest
|
|
10
|
+
export class WebsocketClient extends BaseClient {
|
|
11
|
+
constructor(loadTest) {
|
|
12
|
+
super(loadTest);
|
|
13
|
+
this.latency = loadTest.latency
|
|
20
14
|
this.connection = null;
|
|
21
15
|
this.lastCall = null;
|
|
22
16
|
this.client = null;
|
|
@@ -55,7 +49,7 @@ class WebsocketClient extends BaseClient {
|
|
|
55
49
|
* Make a single request to the server.
|
|
56
50
|
*/
|
|
57
51
|
makeRequest() {
|
|
58
|
-
const id = this.
|
|
52
|
+
const id = this.latency.begin();
|
|
59
53
|
const requestFinished = this.getRequestFinisher(id);
|
|
60
54
|
|
|
61
55
|
if (this.connection.connected) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"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.",
|
|
6
6
|
"homepage": "https://github.com/alexfernandez/loadtest",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"eslint": "^8.47.0",
|
|
27
|
-
"microprofiler": "^2.0.0"
|
|
27
|
+
"microprofiler": "^2.0.0",
|
|
28
|
+
"why-is-node-running": "^2.2.2"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"testing",
|
package/test/all.js
CHANGED
|
@@ -15,6 +15,8 @@ import {test as testLoadtest} from './loadtest.js'
|
|
|
15
15
|
import {test as testWebsocket} from './websocket.js'
|
|
16
16
|
import {test as integrationTest} from './integration.js'
|
|
17
17
|
import {test as testResult} from './result.js'
|
|
18
|
+
import {test as testTcpClient} from './tcpClient.js'
|
|
19
|
+
//import log from 'why-is-node-running'
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -25,10 +27,13 @@ function test() {
|
|
|
25
27
|
testHrtimer, testHeaders, testLatency, testHttpClient,
|
|
26
28
|
testServer, integrationTest, testLoadtest, testWebsocket,
|
|
27
29
|
testRequestGenerator, testBodyGenerator, testResult,
|
|
30
|
+
testTcpClient,
|
|
28
31
|
];
|
|
29
32
|
testing.run(tests, 4200);
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
//setTimeout(log, 4000)
|
|
36
|
+
|
|
32
37
|
test()
|
|
33
38
|
|
|
34
39
|
|
package/test/body-generator.js
CHANGED
|
@@ -2,16 +2,16 @@ import testing from 'testing'
|
|
|
2
2
|
import {loadTest} from '../lib/loadtest.js'
|
|
3
3
|
import {startServer} from '../lib/testserver.js'
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const port = 10453;
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
function testBodyGenerator(callback) {
|
|
9
|
-
const server = startServer({port
|
|
9
|
+
const server = startServer({port, quiet: true}, error => {
|
|
10
10
|
if (error) {
|
|
11
11
|
return callback('Could not start test server');
|
|
12
12
|
}
|
|
13
13
|
const options = {
|
|
14
|
-
url:
|
|
14
|
+
url: `http://localhost:${port}`,
|
|
15
15
|
requestsPerSecond: 1000,
|
|
16
16
|
maxRequests: 100,
|
|
17
17
|
concurrency: 10,
|
package/test/httpClient.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import testing from 'testing'
|
|
2
|
-
import {
|
|
2
|
+
import {HttpClient} from '../lib/httpClient.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
function testHttpClient(callback) {
|
|
6
6
|
const options = {
|
|
7
|
-
url: 'http://localhost:
|
|
7
|
+
url: 'http://localhost:7358/',
|
|
8
8
|
maxSeconds: 0.1,
|
|
9
9
|
concurrency: 1,
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
new HttpClient({options});
|
|
12
12
|
testing.success(callback);
|
|
13
13
|
}
|
|
14
14
|
|