Skip to content

Commit 790260a

Browse files
committed
http,net,stream: optimize write and parser paths
Signed-off-by: GetThatCookie <NimmenKeks@gmx.de>
1 parent f43086d commit 790260a

31 files changed

Lines changed: 2805 additions & 324 deletions

benchmark/common.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Benchmark {
2626

2727
// Parse job-specific configuration from the command line arguments
2828
const argv = process.argv.slice(2);
29-
const parsed_args = this._parseArgs(argv, configs, options);
29+
const parsed_args = this._parseArgs([...argv], configs, options);
3030

3131
this.originalOptions = options;
3232
this.options = parsed_args.cli;
@@ -38,8 +38,10 @@ class Benchmark {
3838
const groupNames = process.env.NODE_RUN_BENCHMARK_GROUPS?.split(',') ?? Object.keys(configs);
3939

4040
for (const groupName of groupNames) {
41-
const config = { ...configs[groupName][0], group: groupName };
42-
const parsed_args = this._parseArgs(argv, config, options);
41+
const groupConfig = Array.isArray(configs[groupName]) ?
42+
configs[groupName][0] : configs[groupName];
43+
const config = { ...groupConfig, group: groupName };
44+
const parsed_args = this._parseArgs([...argv], config, options);
4345

4446
this.options = parsed_args.cli;
4547
this.extra_options = parsed_args.extra;
@@ -221,6 +223,9 @@ class Benchmark {
221223
// function.
222224
const childEnv = { ...process.env };
223225
childEnv.NODE_RUN_BENCHMARK_FN = '';
226+
if (this.originalOptions.byGroups) {
227+
childEnv.NODE_RUN_BENCHMARK_GROUPS = config.group;
228+
}
224229

225230
// Create configuration arguments
226231
const childArgs = [];

benchmark/http/bench-parser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ function main({ len, n }) {
3131
function newParser(type) {
3232
const parser = new HTTPParser();
3333
parser.initialize(type, {});
34+
// Direct parsers bypass cleanParser(); use its production default.
35+
parser.maxHeaderPairs = 2000;
3436

3537
parser.headers = [];
3638

benchmark/http/cork.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const protocols = process.versions.openssl ? ['http', 'https'] : ['http'];
5+
6+
const configs = {
7+
sameTurn: [{
8+
type: ['bytes', 'buffer', 'uint8array'],
9+
len: [64, 1024],
10+
chunks: [1, 2, 4, 16],
11+
mode: ['auto', 'explicit'],
12+
transfer: ['chunked', 'length'],
13+
protocol: protocols,
14+
producer: ['sync'],
15+
callback: [0],
16+
c: [50],
17+
duration: 5,
18+
}],
19+
streaming: [{
20+
type: ['bytes', 'buffer', 'uint8array'],
21+
len: [64, 1024],
22+
chunks: [4],
23+
mode: ['auto'],
24+
transfer: ['chunked'],
25+
protocol: protocols,
26+
producer: ['nextTick', 'microtask', 'immediate'],
27+
callback: [0],
28+
c: [50],
29+
duration: 5,
30+
}],
31+
callbacks: [{
32+
type: ['bytes', 'buffer', 'uint8array'],
33+
len: [64],
34+
chunks: [4, 16],
35+
mode: ['auto', 'explicit'],
36+
transfer: ['chunked'],
37+
protocol: protocols,
38+
producer: ['sync'],
39+
callback: [1],
40+
c: [50],
41+
duration: 5,
42+
}],
43+
fixedBody: [{
44+
type: ['bytes', 'buffer', 'uint8array'],
45+
total: [64 * 1024],
46+
chunks: [1, 4, 16, 128],
47+
mode: ['auto', 'explicit'],
48+
transfer: ['chunked'],
49+
protocol: protocols,
50+
producer: ['sync'],
51+
callback: [0],
52+
c: [50],
53+
duration: 5,
54+
}],
55+
largeChunks: [{
56+
type: ['bytes', 'buffer', 'uint8array'],
57+
len: [4 * 1024, 8 * 1024, 16 * 1024, 64 * 1024],
58+
chunks: [1, 4],
59+
mode: ['auto'],
60+
transfer: ['chunked'],
61+
protocol: protocols,
62+
producer: ['sync'],
63+
callback: [0],
64+
c: [50],
65+
duration: 5,
66+
}],
67+
concurrency: [{
68+
type: ['bytes'],
69+
len: [64],
70+
chunks: [4],
71+
mode: ['auto'],
72+
transfer: ['chunked'],
73+
protocol: protocols,
74+
producer: ['sync'],
75+
callback: [0],
76+
c: [1, 50, 500],
77+
duration: 5,
78+
}],
79+
};
80+
81+
const bench = common.createBenchmark(main, configs, { byGroups: true });
82+
83+
function main({
84+
type,
85+
len,
86+
chunks,
87+
mode,
88+
transfer,
89+
protocol,
90+
producer,
91+
callback,
92+
c,
93+
duration,
94+
total,
95+
}) {
96+
const transport = require(protocol);
97+
len ??= total / chunks;
98+
const chunk = type === 'bytes' ? 'a'.repeat(len) :
99+
type === 'buffer' ? Buffer.alloc(len, 'a') :
100+
new Uint8Array(len).fill(0x61);
101+
const writeCallback = callback ? (err) => {
102+
if (err) throw err;
103+
} : undefined;
104+
105+
const schedule = producer === 'nextTick' ? process.nextTick :
106+
producer === 'microtask' ? queueMicrotask : setImmediate;
107+
108+
const onRequest = (req, res) => {
109+
if (transfer === 'length') {
110+
res.setHeader('Content-Length', len * chunks);
111+
}
112+
if (mode === 'explicit') {
113+
res.cork();
114+
}
115+
116+
if (producer === 'sync') {
117+
for (let i = 0; i < chunks; i++) {
118+
res.write(chunk, writeCallback);
119+
}
120+
res.end();
121+
return;
122+
}
123+
124+
let written = 0;
125+
function writeNext() {
126+
if (written++ === chunks) {
127+
res.end();
128+
return;
129+
}
130+
res.write(chunk, writeCallback);
131+
schedule(writeNext);
132+
}
133+
writeNext();
134+
};
135+
136+
let server;
137+
if (protocol === 'https') {
138+
const fixtures = require('../../test/common/fixtures');
139+
server = transport.createServer({
140+
key: fixtures.readKey('rsa_private.pem'),
141+
cert: fixtures.readKey('rsa_cert.crt'),
142+
}, onRequest);
143+
} else {
144+
server = transport.createServer(onRequest);
145+
}
146+
147+
server.listen(0, () => {
148+
bench.http({
149+
connections: c,
150+
duration,
151+
port: server.address().port,
152+
scheme: protocol,
153+
}, () => {
154+
server.close();
155+
});
156+
});
157+
}

lib/_http_client.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const {
5858
parseUniqueHeadersOption,
5959
OutgoingMessage,
6060
} = require('_http_outgoing');
61+
const { kDestroyMessageBuffer } = require('internal/streams/utils');
6162
const Agent = require('_http_agent');
6263
const { Buffer } = require('buffer');
6364
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
@@ -699,7 +700,11 @@ ClientRequest.prototype.destroy = function destroy(err) {
699700
}
700701

701702
this[kError] = err;
702-
this.socket?.destroy(err);
703+
try {
704+
this[kDestroyMessageBuffer](err);
705+
} finally {
706+
this.socket?.destroy(err);
707+
}
703708

704709
return this;
705710
};
@@ -710,7 +715,7 @@ function emitAbortNT(req) {
710715

711716
function ondrain() {
712717
const msg = this._httpMessage;
713-
if (msg && !msg.finished && msg[kNeedDrain]) {
718+
if (msg && !msg.finished && msg[kNeedDrain] && msg.writableLength === 0) {
714719
msg[kNeedDrain] = false;
715720
msg.emit('drain');
716721
}
@@ -726,6 +731,9 @@ function socketCloseListener() {
726731
const parser = socket.parser;
727732
const res = req.res;
728733

734+
req[kDestroyMessageBuffer](
735+
req[kError] ?? socket._writableState.errored,
736+
);
729737
req.destroyed = true;
730738
if (res) {
731739
// Socket closed before we emitted 'end' below.

0 commit comments

Comments
 (0)