loadtest 7.1.1 → 8.0.1

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
@@ -3,21 +3,14 @@ import * as http from 'http'
3
3
  import * as https from 'https'
4
4
  import * as querystring from 'querystring'
5
5
  import * as websocket from 'websocket'
6
- import {HighResolutionTimer} from './hrtimer.js'
7
6
  import {addUserAgent} from './headers.js'
8
7
  import * as agentkeepalive from 'agentkeepalive'
9
8
  import * as HttpsProxyAgent from 'https-proxy-agent'
10
9
 
11
-
10
+ http.globalAgent.maxSockets = 1000;
11
+ https.globalAgent.maxSockets = 1000;
12
12
  let uniqueIndex = 1
13
13
 
14
- /**
15
- * Create a new HTTP client.
16
- * Seem parameters below.
17
- */
18
- export function create(loadTest, options) {
19
- return new HttpClient(loadTest, options);
20
- }
21
14
 
22
15
  /**
23
16
  * A client for an HTTP connection. Constructor:
@@ -26,11 +19,12 @@ export function create(loadTest, options) {
26
19
  * - running: if the loadTest is running or not.
27
20
  * - `options`: same options as exports.loadTest.
28
21
  */
29
- class HttpClient {
30
- constructor(loadTest, options) {
22
+ export class HttpClient {
23
+ constructor(loadTest) {
31
24
  this.loadTest = loadTest
32
- this.options = options
33
- this.stopped = false
25
+ this.latency = loadTest.latency
26
+ this.options = loadTest.options
27
+ this.running = true
34
28
  this.init();
35
29
  }
36
30
 
@@ -45,18 +39,11 @@ class HttpClient {
45
39
  this.params.key = this.options.key;
46
40
  }
47
41
  this.params.agent = false;
48
- if (this.options.requestsPerSecond) {
49
- // rps for each client is total / concurrency (# of clients)
50
- this.params.requestsPerSecond = this.options.requestsPerSecond / this.options.concurrency
51
- }
52
42
  if (this.options.agentKeepAlive) {
53
43
  const KeepAlive = (this.params.protocol == 'https:') ? agentkeepalive.HttpsAgent : agentkeepalive.default;
54
44
  let maxSockets = 10;
55
- if (this.params.requestsPerSecond) {
56
- maxSockets += Math.floor(this.params.requestsPerSecond);
57
- }
58
45
  this.params.agent = new KeepAlive({
59
- maxSockets: maxSockets,
46
+ maxSockets,
60
47
  maxKeepAliveRequests: 0, // max requests per keepalive socket, default is 0, no limit
61
48
  maxKeepAliveTime: 30000 // keepalive for 30 seconds
62
49
  });
@@ -111,39 +98,27 @@ class HttpClient {
111
98
  * Start the HTTP client.
112
99
  */
113
100
  start() {
114
- if (this.stopped) {
115
- // solves testing issue: with requestsPerSecond clients are started at random,
116
- // so sometimes they are stopped before they have even started
117
- return
118
- }
119
- if (!this.params.requestsPerSecond) {
120
- return this.makeRequest();
121
- }
122
- const interval = 1000 / this.params.requestsPerSecond;
123
- this.requestTimer = new HighResolutionTimer(interval, () => this.makeRequest());
101
+ return this.makeRequest();
124
102
  }
125
103
 
126
104
  /**
127
105
  * Stop the HTTP client.
128
106
  */
129
107
  stop() {
130
- this.stopped = true
131
- if (this.requestTimer) {
132
- this.requestTimer.stop();
133
- }
108
+ this.running = false
134
109
  }
135
110
 
136
111
  /**
137
112
  * Make a single request to the server.
138
113
  */
139
114
  makeRequest() {
140
- if (!this.loadTest.running) {
141
- return;
115
+ if (!this.running) {
116
+ return
142
117
  }
143
- if (this.loadTest.options.maxRequests && this.loadTest.requests >= this.loadTest.options.maxRequests) return
144
- this.loadTest.requests += 1;
145
-
146
- const id = this.loadTest.latency.start();
118
+ if (!this.latency.shouldSend()) {
119
+ return
120
+ }
121
+ const id = this.latency.begin();
147
122
  const params = {...this.params, headers: {...this.params.headers}}
148
123
  this.customizeIndex(params)
149
124
  const request = this.getRequest(id, params)
@@ -226,7 +201,7 @@ class HttpClient {
226
201
  errorCode = '-1';
227
202
  }
228
203
  }
229
- const elapsed = this.loadTest.latency.end(id, errorCode);
204
+ const elapsed = this.latency.end(id, errorCode);
230
205
  if (elapsed < 0) {
231
206
  // not found or not running
232
207
  return;
@@ -234,7 +209,7 @@ class HttpClient {
234
209
  if (result) {
235
210
  result.requestElapsed = elapsed;
236
211
  }
237
- this.loadTest.finishRequest(error, result, () => this.makeRequest());
212
+ this.loadTest.pool.finishRequest(this, result, error);
238
213
  }
239
214
 
240
215
  connect(connection, id) {
package/lib/latency.js CHANGED
@@ -3,37 +3,36 @@ import {Result} from './result.js'
3
3
 
4
4
 
5
5
  /**
6
- * Latency measurements. Options can be:
6
+ * Latency measurements. Receives a load test instance, containing options which can be:
7
7
  * - maxRequests: max number of requests to measure before stopping.
8
8
  * - maxSeconds: max seconds, alternative to max requests.
9
- * An optional callback(error, result) will be called with an error,
10
- * or the result after max is reached.
11
9
  */
12
10
  export class Latency {
13
- constructor(options, callback) {
14
- this.options = options
15
- this.callback = callback
11
+ constructor(loadTest) {
12
+ this.loadTest = loadTest
13
+ this.options = loadTest.options
16
14
  this.requests = new Map();
17
15
  this.partialRequests = 0;
18
16
  this.partialTime = 0;
19
17
  this.partialErrors = 0;
18
+ this.begunRequests = 0;
19
+ this.totalRequests = 0
20
20
  this.lastShownNs = this.getTimeNs();
21
21
  this.startTimeNs = this.getTimeNs();
22
- this.totalRequests = 0;
23
22
  this.totalTime = 0;
24
23
  this.totalErrors = 0;
25
24
  this.maxLatencyMs = 0;
26
25
  this.minLatencyMs = 999999;
27
26
  this.histogramMs = {};
28
27
  this.errorCodes = {};
29
- this.running = true;
30
28
  this.totalsShown = false;
31
29
  }
32
30
 
33
31
  /**
34
32
  * Start the request with the given id.
35
33
  */
36
- start(requestId) {
34
+ begin(requestId) {
35
+ this.begunRequests += 1
37
36
  requestId = requestId || randomUUID();
38
37
  this.requests.set(requestId, this.getTimeNs())
39
38
  return requestId;
@@ -44,10 +43,11 @@ export class Latency {
44
43
  * Accepts an optional error code signaling an error.
45
44
  */
46
45
  end(requestId, errorCode) {
46
+ this.totalRequests += 1
47
47
  if (!this.requests.has(requestId)) {
48
48
  return -2;
49
49
  }
50
- if (!this.running) {
50
+ if (!this.loadTest.running) {
51
51
  return -1;
52
52
  }
53
53
  const elapsed = this.getElapsedMs(this.requests.get(requestId));
@@ -64,7 +64,6 @@ export class Latency {
64
64
  this.partialTime += time;
65
65
  this.partialRequests++;
66
66
  this.totalTime += time;
67
- this.totalRequests++;
68
67
  if (errorCode) {
69
68
  errorCode = String(errorCode);
70
69
  this.partialErrors++;
@@ -85,9 +84,6 @@ export class Latency {
85
84
  this.histogramMs[rounded] = 0;
86
85
  }
87
86
  this.histogramMs[rounded] += 1;
88
- if (this.isFinished()) {
89
- this.finish();
90
- }
91
87
  }
92
88
 
93
89
  /**
@@ -95,6 +91,9 @@ export class Latency {
95
91
  */
96
92
  showPartial() {
97
93
  const elapsedSeconds = this.getElapsedMs(this.lastShownNs) / 1000;
94
+ if (elapsedSeconds < 1) {
95
+ return
96
+ }
98
97
  const meanTime = this.partialTime / this.partialRequests || 0.0;
99
98
  const result = {
100
99
  meanLatencyMs: Math.round(meanTime * 10) / 10,
@@ -135,15 +134,19 @@ export class Latency {
135
134
  * @return {Number} the elapsed time in milliseconds
136
135
  */
137
136
  getElapsedMs(startTimeNs) {
138
- const endTimeNs = this.getTimeNs()
139
- const elapsedNs = endTimeNs - startTimeNs
137
+ const stopTimeNs = this.getTimeNs()
138
+ const elapsedNs = stopTimeNs - startTimeNs
140
139
  return Number(elapsedNs / 1000000n)
141
140
  }
142
141
 
143
- /**
144
- * Check out if the measures are finished.
145
- */
146
- isFinished() {
142
+ shouldSend() {
143
+ if (this.options.maxRequests && this.begunRequests >= this.options.maxRequests) {
144
+ return false;
145
+ }
146
+ return true
147
+ }
148
+
149
+ shouldStop() {
147
150
  if (this.options.maxRequests && this.totalRequests >= this.options.maxRequests) {
148
151
  return true;
149
152
  }
@@ -157,12 +160,8 @@ export class Latency {
157
160
  /**
158
161
  * We are finished.
159
162
  */
160
- finish() {
161
- this.running = false;
162
- this.endTimeNs = this.getTimeNs()
163
- if (this.callback) {
164
- return this.callback(null, this.getResult());
165
- }
163
+ stop() {
164
+ this.stopTimeNs = this.getTimeNs()
166
165
  }
167
166
 
168
167
  /**
@@ -170,7 +169,8 @@ export class Latency {
170
169
  */
171
170
  getResult() {
172
171
  const result = new Result()
173
- result.compute(this.options, this)
172
+ result.compute(this)
173
+ result.clients = this.loadTest.countClients()
174
174
  return result
175
175
  }
176
176
 
package/lib/loadtest.js CHANGED
@@ -1,16 +1,10 @@
1
- import * as http from 'http'
2
- import * as https from 'https'
3
- import * as httpClient from './httpClient.js'
4
- import * as websocket from './websocket.js'
1
+ import {Pool} from './pool.js'
5
2
  import {Latency} from './latency.js'
6
3
  import {HighResolutionTimer} from './hrtimer.js'
7
4
  import {processOptions} from './options.js'
8
5
 
9
6
  const SHOW_INTERVAL_MS = 5000;
10
7
 
11
- http.globalAgent.maxSockets = 1000;
12
- https.globalAgent.maxSockets = 1000;
13
-
14
8
 
15
9
  /**
16
10
  * Run a load test.
@@ -35,6 +29,7 @@ https.globalAgent.maxSockets = 1000;
35
29
  * - debug: show debug messages (deprecated).
36
30
  * - requestGenerator: use a custom function to generate requests.
37
31
  * - statusCallback: function called after every request.
32
+ * - tcp: use TCP sockets (experimental).
38
33
  * - `callback`: optional `function(result, error)` called if/when the test finishes;
39
34
  * if not present a promise is returned.
40
35
  */
@@ -52,20 +47,19 @@ async function loadTestAsync(options) {
52
47
 
53
48
  function runLoadTest(options) {
54
49
  return new Promise((resolve, reject) => {
55
- const operation = new LoadTest(options, (error, result) => {
56
- if (error) {
57
- return reject(error)
58
- }
59
- return resolve(result)
60
- });
61
- operation.start();
50
+ try {
51
+ const loadTest = new LoadTest(options, result => resolve(result))
52
+ loadTest.start();
53
+ } catch(error) {
54
+ return reject(error)
55
+ }
62
56
  })
63
57
  }
64
58
 
65
59
  /**
66
60
  * Used to keep track of individual load test runs.
67
61
  */
68
- let operationInstanceIndex = 0;
62
+ let instanceIndex = 0;
69
63
 
70
64
  /**
71
65
  * A load test operation.
@@ -74,74 +68,39 @@ class LoadTest {
74
68
  constructor(options, callback) {
75
69
  this.options = options;
76
70
  this.finalCallback = callback;
77
- this.running = true;
78
- this.latency = null;
79
- this.clients = {};
80
- this.requests = 0;
81
- this.completedRequests = 0;
71
+ this.latency = new Latency(this);
72
+ this.pool = new Pool(this)
73
+ this.instanceIndex = instanceIndex++
82
74
  this.showTimer = null;
83
75
  this.stopTimeout = null;
84
- this.instanceIndex = operationInstanceIndex++;
85
- this.requestIndex = 0
76
+ this.running = true;
86
77
  }
87
78
 
88
79
  /**
89
80
  * Start the operation.
90
81
  */
91
82
  start() {
92
- this.latency = new Latency(this.options);
93
- this.startClients();
94
83
  if (this.options.maxSeconds) {
95
84
  this.stopTimeout = setTimeout(() => this.stop(), this.options.maxSeconds * 1000).unref();
96
85
  }
97
86
  this.showTimer = new HighResolutionTimer(SHOW_INTERVAL_MS, () => this.latency.showPartial());
98
87
  this.showTimer.unref();
88
+ this.pool.start()
99
89
  }
100
90
 
101
- /**
102
- * Call after each request has finished.
103
- */
104
- finishRequest(error, result, next) {
105
- this.completedRequests += 1;
106
- if (this.options.maxRequests) {
107
- if (this.completedRequests == this.options.maxRequests) {
108
- this.stop();
109
- }
110
- }
111
- if (this.running && !this.options.requestsPerSecond) {
112
- next();
91
+ checkStop() {
92
+ if (!this.running) {
93
+ return true
113
94
  }
114
- if (this.options.statusCallback) {
115
- result.requestIndex = this.requestIndex++
116
- result.instanceIndex = this.instanceIndex
117
- this.options.statusCallback(error, result);
95
+ if (!this.latency.shouldStop()) {
96
+ return false
118
97
  }
98
+ this.stop()
99
+ return true
119
100
  }
120
101
 
121
- /**
122
- * Start a number of measuring clients.
123
- */
124
- startClients() {
125
- for (let index = 0; index < this.options.concurrency; index++) {
126
- const createClient = this.getClientCreator()
127
- const client = createClient(this, this.options);
128
- this.clients[index] = client;
129
- if (!this.options.requestsPerSecond) {
130
- client.start();
131
- } else {
132
- // start each client at a random moment in one second
133
- const offset = Math.floor(Math.random() * 1000);
134
- setTimeout(() => client.start(), offset);
135
- }
136
- }
137
- }
138
-
139
- getClientCreator() {
140
- // TODO: || this.options.url.startsWith('wss:'))
141
- if (this.options.url.startsWith('ws:')) {
142
- return websocket.create;
143
- }
144
- return httpClient.create;
102
+ countClients() {
103
+ return this.pool.clients.length
145
104
  }
146
105
 
147
106
  /**
@@ -149,20 +108,18 @@ class LoadTest {
149
108
  */
150
109
  stop() {
151
110
  this.running = false;
152
- this.latency.finish()
111
+ this.pool.stop()
112
+ this.latency.stop()
153
113
  if (this.showTimer) {
154
114
  this.showTimer.stop();
155
115
  }
156
116
  if (this.stopTimeout) {
157
117
  clearTimeout(this.stopTimeout);
158
118
  }
159
- Object.keys(this.clients).forEach(index => {
160
- this.clients[index].stop();
161
- });
162
119
  if (this.finalCallback) {
163
120
  const result = this.latency.getResult();
164
121
  result.instanceIndex = this.instanceIndex;
165
- this.finalCallback(null, result);
122
+ this.finalCallback(result);
166
123
  } else {
167
124
  this.latency.show();
168
125
  }
package/lib/options.js CHANGED
@@ -17,7 +17,7 @@ class Options {
17
17
  constructor(options) {
18
18
  this.url = this.getUrl(options)
19
19
  const configuration = loadConfig();
20
- this.concurrency = options.concurrency || configuration.concurrency || 1
20
+ this.concurrency = options.concurrency || configuration.concurrency || 10
21
21
  const rps = options.rps ? parseFloat(options.rps) : null
22
22
  this.requestsPerSecond = options.requestsPerSecond || rps || configuration.requestsPerSecond
23
23
  this.agentKeepAlive = options.keepalive || options.agent || options.agentKeepAlive || configuration.agentKeepAlive;
@@ -46,6 +46,10 @@ class Options {
46
46
  this.proxy = options.proxy || configuration.proxy
47
47
  this.quiet = options.quiet || configuration.quiet
48
48
  this.statusCallback = options.statusCallback
49
+ this.tcp = options.tcp || configuration.tcp
50
+ if (!this.maxRequests && !this.maxSeconds) {
51
+ this.maxSeconds = 10
52
+ }
49
53
  }
50
54
 
51
55
  getUrl(options) {
package/lib/parser.js ADDED
@@ -0,0 +1,167 @@
1
+ import microprofiler from 'microprofiler'
2
+
3
+ const bodySeparator = '\r\n\r\n'
4
+ const lineSeparator = '\r\n'
5
+ const packetInfos = new Map()
6
+ // max size to consider packets duplicated
7
+ const maxPacketSize = 1000
8
+
9
+
10
+ export class Parser {
11
+ constructor(method) {
12
+ this.method = method
13
+ this.pending = null
14
+ this.finished = false
15
+ this.headers = {}
16
+ this.partialBody = false
17
+ this.packetInfo = null
18
+ }
19
+
20
+ addPacket(data) {
21
+ if (this.pending) {
22
+ this.pending = Buffer.concat([this.pending, data])
23
+ } else {
24
+ this.pending = data
25
+ }
26
+ if (this.partialBody) {
27
+ this.parseBody(this.pending, this.packetInfo)
28
+ return
29
+ }
30
+ this.parsePacket()
31
+ }
32
+
33
+ parsePacket() {
34
+ const division = this.pending.indexOf(bodySeparator)
35
+ if (division == -1) {
36
+ // cannot parse yet
37
+ return
38
+ }
39
+ //const start1 = microprofiler.start()
40
+ this.packetInfo = new PacketInfo(this.pending.length, division)
41
+ const messageHeader = this.pending.subarray(0, division)
42
+ const messageBody = this.pending.subarray(division + 4)
43
+ this.parseFirstLine(messageHeader)
44
+ //microprofiler.measureFrom(start1, 'parse first line', 100000)
45
+ //const start2 = microprofiler.start()
46
+ const key = this.packetInfo.getKey()
47
+ const existing = packetInfos.get(key)
48
+ if (existing && this.packetInfo.isDuplicated(existing)) {
49
+ // no need to parse headers or body
50
+ this.finished = true
51
+ return
52
+ }
53
+ packetInfos.set(key, this.packetInfo)
54
+ this.parseHeaders(messageHeader)
55
+ //microprofiler.measureFrom(start2, 'parse headers', 100000)
56
+ const start3 = microprofiler.start()
57
+ this.parseBody(messageBody)
58
+ microprofiler.measureFrom(start3, 'parse body', 100000)
59
+ }
60
+
61
+ parseFirstLine(messageHeader) {
62
+ let firstReturn = messageHeader.indexOf(lineSeparator)
63
+ if (firstReturn == -1) {
64
+ // no headers
65
+ firstReturn = messageHeader.length
66
+ }
67
+ const firstLine = messageHeader.toString('utf8', 0, firstReturn)
68
+ this.packetInfo.parseFirstLine(firstLine)
69
+ }
70
+
71
+ parseHeaders(messageHeader) {
72
+ if (this.packetInfo.hasNoBody(this.method)) {
73
+ // do not parse headers
74
+ return
75
+ }
76
+ let firstReturn = messageHeader.indexOf(lineSeparator)
77
+ let position = firstReturn + 2
78
+ while (position < messageHeader.length) {
79
+ let nextReturn = messageHeader.indexOf(lineSeparator, position)
80
+ if (nextReturn == -1) {
81
+ nextReturn = messageHeader.length
82
+ }
83
+ const line = messageHeader.toString('utf8', position, nextReturn)
84
+ this.parseHeader(line)
85
+ position = nextReturn + 2
86
+ }
87
+ }
88
+
89
+ parseBody(messageBody) {
90
+ if (this.packetInfo.hasNoBody(this.method)) {
91
+ if (messageBody.length != 0) {
92
+ throw new Error(`Should not have a body`)
93
+ }
94
+ // do not parse body
95
+ this.finished = true
96
+ return
97
+ }
98
+ if (!this.packetInfo.contentLength) {
99
+ throw new Error(`Missing content length`)
100
+ }
101
+ if (this.packetInfo.contentLength != messageBody.length) {
102
+ this.partialBody = true
103
+ this.pending = messageBody
104
+ return
105
+ }
106
+ // we do not actually parse the body, have no use for it
107
+ this.finished = true
108
+ }
109
+
110
+ parseHeader(line) {
111
+ const colon = line.indexOf(': ')
112
+ if (colon == -1) {
113
+ throw new Error(`Invalid header ${line}`)
114
+ }
115
+ const key = line.substring(0, colon).toLowerCase()
116
+ if (key == 'content-length') {
117
+ const value = line.substring(colon + 2)
118
+ this.packetInfo.contentLength = parseInt(value)
119
+ }
120
+ // this.headers[key] = value
121
+ }
122
+ }
123
+
124
+ class PacketInfo {
125
+ constructor(length, division) {
126
+ this.length = length
127
+ this.division = division
128
+ this.contentLength = 0
129
+ this.statusCode = 0
130
+ }
131
+
132
+ getKey() {
133
+ return `${this.length}-${this.division}`
134
+ }
135
+
136
+ parseFirstLine(firstLine) {
137
+ const words = firstLine.split(' ')
138
+ if (words.length < 2) {
139
+ throw new Error(`Unexpected response line ${firstLine}`)
140
+ }
141
+ if (words[0] != 'HTTP/1.1') {
142
+ throw new Error(`Unexpected first word ${words[0]}`)
143
+ }
144
+ this.statusCode = parseInt(words[1])
145
+ if (!this.statusCode || this.statusCode < 100 || this.statusCode >= 600) {
146
+ throw new Error(`Unexpected status code ${this.statusCode}`)
147
+ }
148
+ }
149
+
150
+ hasNoBody(method) {
151
+ if (method == 'HEAD') {
152
+ return true
153
+ }
154
+ if (this.statusCode < 200 || this.statusCode == 204 || this.statusCode == 304) {
155
+ return true
156
+ }
157
+ return false
158
+ }
159
+
160
+ isDuplicated(info) {
161
+ if (this.length > maxPacketSize) {
162
+ return false
163
+ }
164
+ return this.length == info.length && this.division == info.division && this.statusCode == info.statusCode
165
+ }
166
+ }
167
+