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/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 +633 -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/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
|
|
22
|
+
export class HttpClient {
|
|
23
|
+
constructor(loadTest) {
|
|
31
24
|
this.loadTest = loadTest
|
|
32
|
-
this.
|
|
33
|
-
this.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
141
|
-
return
|
|
115
|
+
if (!this.running) {
|
|
116
|
+
return
|
|
142
117
|
}
|
|
143
|
-
if (this.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const id = this.
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
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
|
-
|
|
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
|
|
139
|
-
const elapsedNs =
|
|
137
|
+
const stopTimeNs = this.getTimeNs()
|
|
138
|
+
const elapsedNs = stopTimeNs - startTimeNs
|
|
140
139
|
return Number(elapsedNs / 1000000n)
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
161
|
-
this.
|
|
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
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return
|
|
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
|
|
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.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
115
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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 ||
|
|
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
|
+
|