loadtest 6.4.0 → 7.1.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/lib/httpClient.js CHANGED
@@ -15,94 +15,94 @@ let uniqueIndex = 1
15
15
  * Create a new HTTP client.
16
16
  * Seem parameters below.
17
17
  */
18
- export function create(operation, params) {
19
- return new HttpClient(operation, params);
18
+ export function create(loadTest, options) {
19
+ return new HttpClient(loadTest, options);
20
20
  }
21
21
 
22
22
  /**
23
- * A client for an HTTP connection.
24
- * Operation is an object which has these attributes:
25
- * - latency: a variable to measure latency.
26
- * - running: if the operation is running or not.
27
- * Params is an object with the same options as exports.loadTest.
23
+ * A client for an HTTP connection. Constructor:
24
+ * - `loadTest`: an object with the following attributes:
25
+ * - latency: a variable to measure latency.
26
+ * - running: if the loadTest is running or not.
27
+ * - `options`: same options as exports.loadTest.
28
28
  */
29
29
  class HttpClient {
30
- constructor(operation, params) {
31
- this.operation = operation
32
- this.params = params
30
+ constructor(loadTest, options) {
31
+ this.loadTest = loadTest
32
+ this.options = options
33
33
  this.stopped = false
34
34
  this.init();
35
35
  }
36
36
 
37
37
  /**
38
- * Init options and message to send.
38
+ * Init and message to send.
39
39
  */
40
40
  init() {
41
- this.options = urlLib.parse(this.params.url);
42
- this.options.headers = this.params.headers || {}
43
- if (this.params.cert && this.params.key) {
44
- this.options.cert = this.params.cert;
45
- this.options.key = this.params.key;
41
+ this.params = urlLib.parse(this.options.url);
42
+ this.params.headers = this.options.headers || {}
43
+ if (this.options.cert && this.options.key) {
44
+ this.params.cert = this.options.cert;
45
+ this.params.key = this.options.key;
46
46
  }
47
- this.options.agent = false;
48
- if (this.params.requestsPerSecond) {
47
+ this.params.agent = false;
48
+ if (this.options.requestsPerSecond) {
49
49
  // rps for each client is total / concurrency (# of clients)
50
- this.options.requestsPerSecond = this.params.requestsPerSecond / this.params.concurrency
50
+ this.params.requestsPerSecond = this.options.requestsPerSecond / this.options.concurrency
51
51
  }
52
- if (this.params.agentKeepAlive) {
53
- const KeepAlive = (this.options.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
52
+ if (this.options.agentKeepAlive) {
53
+ const KeepAlive = (this.params.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
54
54
  let maxSockets = 10;
55
- if (this.options.requestsPerSecond) {
56
- maxSockets += Math.floor(this.options.requestsPerSecond);
55
+ if (this.params.requestsPerSecond) {
56
+ maxSockets += Math.floor(this.params.requestsPerSecond);
57
57
  }
58
- this.options.agent = new KeepAlive({
58
+ this.params.agent = new KeepAlive({
59
59
  maxSockets: maxSockets,
60
60
  maxKeepAliveRequests: 0, // max requests per keepalive socket, default is 0, no limit
61
61
  maxKeepAliveTime: 30000 // keepalive for 30 seconds
62
62
  });
63
63
  }
64
- if (this.params.method) {
65
- this.options.method = this.params.method;
64
+ if (this.options.method) {
65
+ this.params.method = this.options.method;
66
66
  }
67
- if (this.params.body) {
68
- if (typeof this.params.body == 'string') {
69
- this.generateMessage = () => this.params.body;
70
- } else if (typeof this.params.body == 'object') {
71
- if (this.params.contentType === 'application/x-www-form-urlencoded') {
72
- this.params.body = querystring.stringify(this.params.body);
67
+ if (this.options.body) {
68
+ if (typeof this.options.body == 'string') {
69
+ this.generateMessage = () => this.options.body;
70
+ } else if (typeof this.options.body == 'object') {
71
+ if (this.options.contentType === 'application/x-www-form-urlencoded') {
72
+ this.options.body = querystring.stringify(this.options.body);
73
73
  }
74
- this.generateMessage = () => this.params.body;
75
- } else if (typeof this.params.body == 'function') {
76
- this.generateMessage = this.params.body;
74
+ this.generateMessage = () => this.options.body;
75
+ } else if (typeof this.options.body == 'function') {
76
+ this.generateMessage = this.options.body;
77
77
  } else {
78
- throw new Error(`Unrecognized body: ${typeof this.params.body}`);
78
+ throw new Error(`Unrecognized body: ${typeof this.options.body}`);
79
79
  }
80
- this.options.headers['Content-Type'] = this.params.contentType || 'text/plain';
80
+ this.params.headers['Content-Type'] = this.options.contentType || 'text/plain';
81
81
  }
82
- if (this.params.cookies) {
83
- if (Array.isArray(this.params.cookies)) {
84
- this.options.headers.Cookie = this.params.cookies.join('; ');
85
- } else if (typeof this.params.cookies == 'string') {
86
- this.options.headers.Cookie = this.params.cookies;
82
+ if (this.options.cookies) {
83
+ if (Array.isArray(this.options.cookies)) {
84
+ this.params.headers.Cookie = this.options.cookies.join('; ');
85
+ } else if (typeof this.options.cookies == 'string') {
86
+ this.params.headers.Cookie = this.options.cookies;
87
87
  } else {
88
- throw new Error(`Invalid cookies ${JSON.stringify(this.params.cookies)}, please use an array or a string`);
88
+ throw new Error(`Invalid cookies ${JSON.stringify(this.options.cookies)}, please use an array or a string`);
89
89
  }
90
90
  }
91
- addUserAgent(this.options.headers);
92
- if (this.params.secureProtocol) {
93
- this.options.secureProtocol = this.params.secureProtocol;
91
+ addUserAgent(this.params.headers);
92
+ if (this.options.secureProtocol) {
93
+ this.params.secureProtocol = this.options.secureProtocol;
94
94
  }
95
95
  // adding proxy configuration
96
- if (this.params.proxy) {
97
- const proxy = this.params.proxy;
96
+ if (this.options.proxy) {
97
+ const proxy = this.options.proxy;
98
98
  const agent = new HttpsProxyAgent(proxy);
99
- this.options.agent = agent;
99
+ this.params.agent = agent;
100
100
  }
101
- if (this.params.indexParam) {
102
- this.options.indexParamFinder = new RegExp(this.params.indexParam, 'g');
101
+ if (this.options.indexParam) {
102
+ this.params.indexParamFinder = new RegExp(this.options.indexParam, 'g');
103
103
  }
104
104
  // Disable certificate checking
105
- if (this.params.insecure === true) {
105
+ if (this.options.insecure === true) {
106
106
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
107
107
  }
108
108
  }
@@ -116,10 +116,10 @@ class HttpClient {
116
116
  // so sometimes they are stopped before they have even started
117
117
  return
118
118
  }
119
- if (!this.options.requestsPerSecond) {
119
+ if (!this.params.requestsPerSecond) {
120
120
  return this.makeRequest();
121
121
  }
122
- const interval = 1000 / this.options.requestsPerSecond;
122
+ const interval = 1000 / this.params.requestsPerSecond;
123
123
  this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
124
124
  }
125
125
 
@@ -137,159 +137,147 @@ class HttpClient {
137
137
  * Make a single request to the server.
138
138
  */
139
139
  makeRequest() {
140
- if (!this.operation.running) {
140
+ if (!this.loadTest.running) {
141
141
  return;
142
142
  }
143
- if (this.operation.options.maxRequests && this.operation.requests >= this.operation.options.maxRequests) return
144
- this.operation.requests += 1;
143
+ if (this.loadTest.options.maxRequests && this.loadTest.requests >= this.loadTest.options.maxRequests) return
144
+ this.loadTest.requests += 1;
145
145
 
146
- const id = this.operation.latency.start();
147
- const options = {...this.options, headers: {...this.options.headers}}
148
- const requestFinished = this.getRequestFinisher(id);
149
- this.customizeIndex(options)
150
- const request = this.getRequest(id, options, requestFinished)
151
- if (this.params.timeout) {
152
- const timeout = parseInt(this.params.timeout);
146
+ const id = this.loadTest.latency.start();
147
+ const params = {...this.params, headers: {...this.params.headers}}
148
+ this.customizeIndex(params)
149
+ const request = this.getRequest(id, params)
150
+ if (this.options.timeout) {
151
+ const timeout = parseInt(this.options.timeout);
153
152
  if (!timeout) {
154
- console.error('Invalid timeout %s', this.params.timeout);
153
+ console.error(`Invalid timeout ${this.options.timeout}`);
155
154
  }
156
155
  request.setTimeout(timeout, () => {
157
- requestFinished('Connection timed out');
156
+ this.finishRequest(id, 'Connection timed out');
158
157
  });
159
158
  }
160
- const message = this.getMessage(id, options)
159
+ const message = this.getMessage(id, params)
161
160
  if (message) {
162
161
  request.write(message);
163
- options.headers['Content-Length'] = Buffer.byteLength(message);
162
+ params.headers['Content-Length'] = Buffer.byteLength(message);
164
163
  }
165
164
  request.on('error', error => {
166
- requestFinished('Connection error: ' + error.message);
165
+ this.finishRequest(id, `Connection error: ${error.message}`);
167
166
  });
168
167
  request.end();
169
168
  }
170
169
 
171
- customizeIndex(options) {
172
- if (!this.options.indexParamFinder) {
170
+ customizeIndex(params) {
171
+ if (!this.params.indexParamFinder) {
173
172
  return
174
173
  }
175
- options.customIndex = this.getCustomIndex()
176
- options.path = this.options.path.replace(this.options.indexParamFinder, options.customIndex);
174
+ params.customIndex = this.getCustomIndex()
175
+ params.path = this.params.path.replace(this.params.indexParamFinder, params.customIndex);
177
176
  }
178
177
 
179
178
  getCustomIndex() {
180
- if (this.options.indexParamCallback instanceof Function) {
181
- return this.options.indexParamCallback();
179
+ if (this.params.indexParamCallback instanceof Function) {
180
+ return this.params.indexParamCallback();
182
181
  }
183
182
  const customIndex = uniqueIndex
184
183
  uniqueIndex += 1
185
184
  return customIndex
186
185
  }
187
186
 
188
- getLib() {
189
- if (this.options.protocol == 'https:') {
190
- return https;
191
- }
192
- if (this.options.protocol == 'ws:') {
193
- return websocket;
194
- }
195
- return http;
196
- }
197
-
198
- getMessage(id, options) {
187
+ getMessage(id, params) {
199
188
  if (!this.generateMessage) {
200
189
  return
201
190
  }
202
191
  const candidate = this.generateMessage(id);
203
192
  const message = typeof candidate === 'object' ? JSON.stringify(candidate) : candidate
204
- if (this.options.indexParamFinder) {
205
- return message.replace(this.options.indexParamFinder, options.customIndex);
193
+ if (this.params.indexParamFinder) {
194
+ return message.replace(this.params.indexParamFinder, params.customIndex);
206
195
  }
207
196
  return message
208
197
  }
209
198
 
210
- getRequest(id, options, requestFinished) {
199
+ getRequest(id, params) {
211
200
  const lib = this.getLib()
212
- if (typeof this.params.requestGenerator == 'function') {
213
- const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
214
- return this.params.requestGenerator(this.params, options, lib.request, connect);
201
+ if (typeof this.options.requestGenerator == 'function') {
202
+ return this.options.requestGenerator(this.options, params, lib.request, connection => this.connect(connection, id))
215
203
  }
216
- return lib.request(options, this.getConnect(id, requestFinished, this.params.contentInspector));
204
+ return lib.request(params, connection => this.connect(connection, id))
217
205
  }
218
206
 
219
- /**
220
- * Get a function that finishes one request and goes for the next.
221
- */
222
- getRequestFinisher(id) {
223
- return (error, result) => {
224
- let errorCode = null;
225
- if (error) {
226
- if (result) {
227
- errorCode = result.statusCode;
228
- if (result.customErrorCode !== undefined) {
229
- errorCode = errorCode + ":" + result.customErrorCode
230
- }
231
- } else {
232
- errorCode = '-1';
207
+ getLib() {
208
+ if (this.params.protocol == 'https:') {
209
+ return https;
210
+ }
211
+ if (this.params.protocol == 'ws:') {
212
+ return websocket;
213
+ }
214
+ return http;
215
+ }
216
+
217
+ finishRequest(id, error, result) {
218
+ let errorCode = null;
219
+ if (error) {
220
+ if (result) {
221
+ errorCode = result.statusCode;
222
+ if (result.customErrorCode !== undefined) {
223
+ errorCode = errorCode + ":" + result.customErrorCode
233
224
  }
225
+ } else {
226
+ errorCode = '-1';
234
227
  }
228
+ }
229
+ const elapsed = this.loadTest.latency.end(id, errorCode);
230
+ if (elapsed < 0) {
231
+ // not found or not running
232
+ return;
233
+ }
234
+ if (result) {
235
+ result.requestElapsed = elapsed;
236
+ }
237
+ this.loadTest.finishRequest(error, result, () => this.makeRequest());
238
+ }
235
239
 
236
- const elapsed = this.operation.latency.end(id, errorCode);
237
- if (elapsed < 0) {
238
- // not found or not running
239
- return;
240
+ connect(connection, id) {
241
+ const bodyBuffers = []
242
+ connection.on('data', chunk => {
243
+ bodyBuffers.push(chunk)
244
+ });
245
+ connection.on('error', error => {
246
+ this.finishRequest(id, `Connection ${id} failed: ${error}`);
247
+ });
248
+ connection.on('end', () => {
249
+ const body = Buffer.concat(bodyBuffers).toString()
250
+ const result = this.createResult(connection, body)
251
+ if (this.options.contentInspector) {
252
+ this.options.contentInspector(result)
240
253
  }
241
- const index = this.operation.latency.getRequestIndex(id);
242
- if (result) {
243
- result.requestElapsed = elapsed;
244
- result.requestIndex = index;
245
- result.instanceIndex = this.operation.instanceIndex;
254
+ if (connection.statusCode >= 400) {
255
+ return this.finishRequest(id, `Status code ${connection.statusCode}`, result);
246
256
  }
247
- let callback;
248
- if (!this.options.requestsPerSecond) {
249
- callback = this.makeRequest.bind(this);
257
+ if (result?.customError) {
258
+ return this.finishRequest(id, `Custom error: ${result.customError}`, result);
250
259
  }
251
- this.operation.callback(error, result, callback);
252
- };
260
+ this.finishRequest(id, null, result);
261
+ });
253
262
  }
254
263
 
255
- /**
256
- * Get a function to connect the player.
257
- */
258
- getConnect(id, callback, contentInspector) {
259
- let body = '';
260
- return connection => {
261
- connection.setEncoding('utf8');
262
- connection.on('data', chunk => {
263
- body += chunk;
264
- });
265
- connection.on('error', error => {
266
- callback('Connection ' + id + ' failed: ' + error, '1');
267
- });
268
- connection.on('end', () => {
269
- const client = connection.connection || connection.client
270
- const result = {
271
- host: client._host,
272
- path: connection.req.path,
273
- method: connection.req.method,
274
- statusCode: connection.statusCode,
275
- body: body,
276
- headers: connection.headers,
277
- };
278
- if (connection.req.labels) {
279
- result.labels = connection.req.labels
280
- }
281
- if (contentInspector) {
282
- contentInspector(result)
283
- }
284
- if (connection.statusCode >= 400) {
285
- return callback('Status code ' + connection.statusCode, result);
286
- }
287
- if (result.customError) {
288
- return callback('Custom error: ' + result.customError, result);
289
- }
290
- callback(null, result);
291
- });
264
+ createResult(connection, body) {
265
+ if (!this.options.statusCallback && !this.options.contentInspector) {
266
+ return null
267
+ }
268
+ const client = connection.connection || connection.client
269
+ const result = {
270
+ host: client._host,
271
+ path: connection.req.path,
272
+ method: connection.req.method,
273
+ statusCode: connection.statusCode,
274
+ body,
275
+ headers: connection.headers,
292
276
  };
277
+ if (connection.req.labels) {
278
+ result.labels = connection.req.labels
279
+ }
280
+ return result
293
281
  }
294
282
  }
295
283
 
package/lib/latency.js CHANGED
@@ -1,4 +1,4 @@
1
- import * as crypto from 'crypto'
1
+ import {randomUUID} from 'crypto'
2
2
  import {Result} from './result.js'
3
3
 
4
4
 
@@ -13,7 +13,7 @@ export class Latency {
13
13
  constructor(options, callback) {
14
14
  this.options = options
15
15
  this.callback = callback
16
- this.requests = {};
16
+ this.requests = new Map();
17
17
  this.partialRequests = 0;
18
18
  this.partialTime = 0;
19
19
  this.partialErrors = 0;
@@ -28,25 +28,14 @@ export class Latency {
28
28
  this.errorCodes = {};
29
29
  this.running = true;
30
30
  this.totalsShown = false;
31
- this.requestIndex = 0;
32
- this.requestIdToIndex = {};
33
- }
34
-
35
- /**
36
- * Return the index of the request. This is useful for determining the order
37
- * in which requests returned values.
38
- */
39
- getRequestIndex(requestId) {
40
- return this.requestIdToIndex[requestId];
41
31
  }
42
32
 
43
33
  /**
44
34
  * Start the request with the given id.
45
35
  */
46
36
  start(requestId) {
47
- requestId = requestId || createId();
48
- this.requests[requestId] = this.getTimeNs();
49
- this.requestIdToIndex[requestId] = this.requestIndex++;
37
+ requestId = requestId || randomUUID();
38
+ this.requests.set(requestId, this.getTimeNs())
50
39
  return requestId;
51
40
  }
52
41
 
@@ -55,15 +44,15 @@ export class Latency {
55
44
  * Accepts an optional error code signaling an error.
56
45
  */
57
46
  end(requestId, errorCode) {
58
- if (!(requestId in this.requests)) {
47
+ if (!this.requests.has(requestId)) {
59
48
  return -2;
60
49
  }
61
50
  if (!this.running) {
62
51
  return -1;
63
52
  }
64
- const elapsed = this.getElapsedMs(this.requests[requestId]);
53
+ const elapsed = this.getElapsedMs(this.requests.get(requestId));
65
54
  this.add(elapsed, errorCode);
66
- delete this.requests[requestId];
55
+ this.requests.delete(requestId);
67
56
  return elapsed;
68
57
  }
69
58
 
@@ -77,7 +66,7 @@ export class Latency {
77
66
  this.totalTime += time;
78
67
  this.totalRequests++;
79
68
  if (errorCode) {
80
- errorCode = '' + errorCode;
69
+ errorCode = String(errorCode);
81
70
  this.partialErrors++;
82
71
  this.totalErrors++;
83
72
  if (!(errorCode in this.errorCodes)) {
@@ -111,15 +100,12 @@ export class Latency {
111
100
  meanLatencyMs: Math.round(meanTime * 10) / 10,
112
101
  rps: Math.round(this.partialRequests / elapsedSeconds)
113
102
  };
114
- let percent = '';
115
- if (this.options.maxRequests) {
116
- percent = ' (' + Math.round(100 * this.totalRequests / this.options.maxRequests) + '%)';
117
- }
103
+ const requestPercent = this.getRequestPercent()
118
104
  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);
105
+ console.info(`Requests: ${this.totalRequests}${requestPercent}, requests per second: ${result.rps}, mean latency: ${result.meanLatencyMs} ms`)
120
106
  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);
107
+ const errorPercent = Math.round(100 * 10 * this.totalErrors / this.totalRequests) / 10;
108
+ console.info(`Errors: ${this.partialErrors}, accumulated errors: ${this.totalErrors}, ${errorPercent}% of total requests`)
123
109
  }
124
110
  }
125
111
  this.partialTime = 0;
@@ -128,6 +114,13 @@ export class Latency {
128
114
  this.lastShownNs = this.getTimeNs();
129
115
  }
130
116
 
117
+ getRequestPercent() {
118
+ if (!this.options.maxRequests) {
119
+ return ''
120
+ }
121
+ return ` (${Math.round(100 * this.totalRequests / this.options.maxRequests)}%)`
122
+ }
123
+
131
124
  /**
132
125
  * Returns the current high-resolution real time in nanoseconds as a big int.
133
126
  * @return {*}
@@ -194,12 +187,3 @@ export class Latency {
194
187
  }
195
188
  }
196
189
 
197
- /**
198
- * Create a unique, random token.
199
- */
200
- function createId() {
201
- const value = '' + Date.now() + Math.random();
202
- const hash = crypto.createHash('sha256');
203
- return hash.update(value).digest('hex').toLowerCase();
204
- }
205
-
package/lib/loadtest.js CHANGED
@@ -16,24 +16,25 @@ https.globalAgent.maxSockets = 1000;
16
16
  * Run a load test.
17
17
  * Parameters:
18
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
- * - data [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.
19
+ * - url: URL to access (mandatory).
20
+ * - concurrency: how many concurrent clients to use.
21
+ * - maxRequests: how many requests to send
22
+ * - maxSeconds: how long to run the tests.
23
+ * - cookies: a string or an array of strings, each with name:value.
24
+ * - headers: a map with headers: {key1: value1, key2: value2}.
25
+ * - method: the method to use: POST, PUT. Default: GET, what else.
26
+ * - data: the contents to send along a POST or PUT request.
27
+ * - contentType: the MIME type to use for the body, default text/plain.
28
+ * - requestsPerSecond: how many requests per second to send.
29
29
  * - agentKeepAlive: if true, then use connection keep-alive.
30
- * - indexParam [string]: string to replace with a unique index.
30
+ * - indexParam: string to replace with a unique index.
31
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.
32
+ * - secureProtocol: TLS/SSL secure protocol method to use.
33
+ * - proxy: use a proxy for requests e.g. http://localhost:8080.
34
34
  * - quiet: do not log any messages.
35
35
  * - debug: show debug messages (deprecated).
36
- * - requestGenerator [function]: use a custom function to generate requests.
36
+ * - requestGenerator: use a custom function to generate requests.
37
+ * - statusCallback: function called after every request.
37
38
  * - `callback`: optional `function(result, error)` called if/when the test finishes;
38
39
  * if not present a promise is returned.
39
40
  */
@@ -46,12 +47,12 @@ export function loadTest(options, callback) {
46
47
 
47
48
  async function loadTestAsync(options) {
48
49
  const processed = await processOptions(options)
49
- return await runOperation(processed)
50
+ return await runLoadTest(processed)
50
51
  }
51
52
 
52
- function runOperation(options) {
53
+ function runLoadTest(options) {
53
54
  return new Promise((resolve, reject) => {
54
- const operation = new Operation(options, (error, result) => {
55
+ const operation = new LoadTest(options, (error, result) => {
55
56
  if (error) {
56
57
  return reject(error)
57
58
  }
@@ -62,14 +63,14 @@ function runOperation(options) {
62
63
  }
63
64
 
64
65
  /**
65
- * Used to keep track of individual load test Operation runs.
66
+ * Used to keep track of individual load test runs.
66
67
  */
67
68
  let operationInstanceIndex = 0;
68
69
 
69
70
  /**
70
71
  * A load test operation.
71
72
  */
72
- class Operation {
73
+ class LoadTest {
73
74
  constructor(options, callback) {
74
75
  this.options = options;
75
76
  this.finalCallback = callback;
@@ -81,6 +82,7 @@ class Operation {
81
82
  this.showTimer = null;
82
83
  this.stopTimeout = null;
83
84
  this.instanceIndex = operationInstanceIndex++;
85
+ this.requestIndex = 0
84
86
  }
85
87
 
86
88
  /**
@@ -97,20 +99,22 @@ class Operation {
97
99
  }
98
100
 
99
101
  /**
100
- * Call after each operation has finished.
102
+ * Call after each request has finished.
101
103
  */
102
- callback(error, result, next) {
104
+ finishRequest(error, result, next) {
103
105
  this.completedRequests += 1;
104
106
  if (this.options.maxRequests) {
105
107
  if (this.completedRequests == this.options.maxRequests) {
106
108
  this.stop();
107
109
  }
108
110
  }
109
- if (this.running && next) {
111
+ if (this.running && !this.requestsPerSecond) {
110
112
  next();
111
113
  }
112
114
  if (this.options.statusCallback) {
113
- this.options.statusCallback(error, result, this.latency.getResult());
115
+ result.requestIndex = this.requestIndex++
116
+ result.instanceIndex = this.instanceIndex
117
+ this.options.statusCallback(error, result);
114
118
  }
115
119
  }
116
120
 
package/lib/options.js CHANGED
@@ -45,6 +45,7 @@ class Options {
45
45
  this.recover = options.recover || configuration.recover
46
46
  this.proxy = options.proxy || configuration.proxy
47
47
  this.quiet = options.quiet || configuration.quiet
48
+ this.statusCallback = options.statusCallback
48
49
  }
49
50
 
50
51
  getUrl(options) {
package/lib/testserver.js CHANGED
@@ -103,9 +103,9 @@ class TestServer {
103
103
  */
104
104
  listen(request, response) {
105
105
  const id = this.latency.start();
106
- request.body = '';
106
+ const bodyBuffers = []
107
107
  request.on('data', data => {
108
- request.body += data.toString();
108
+ bodyBuffers.push(data)
109
109
  });
110
110
  request.on('error', () => {
111
111
  // ignore request
@@ -113,6 +113,7 @@ class TestServer {
113
113
  this.latency.end(id, -1);
114
114
  })
115
115
  request.on('end', () => {
116
+ request.body = Buffer.concat(bodyBuffers).toString();
116
117
  this.totalRequests += 1
117
118
  const elapsedMs = Date.now() - this.debuggedTime
118
119
  if (elapsedMs > LOG_HEADERS_INTERVAL_MS) {