Skip to content

Commit a28a490

Browse files
committed
http: coalesce chunked writes during auto-corking
Signed-off-by: GetThatCookie <NimmenKeks@gmx.de>
1 parent f00fb75 commit a28a490

4 files changed

Lines changed: 298 additions & 39 deletions

File tree

benchmark/http/cork.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
5+
const bench = common.createBenchmark(main, {
6+
type: ['string', 'buffer'],
7+
chunks: [4, 16],
8+
len: [64],
9+
c: [50],
10+
duration: [5]
11+
});
12+
13+
function main({ type, chunks, len, c, duration }) {
14+
const http = require('http');
15+
const chunk = type === 'string' ? 'a'.repeat(len) : Buffer.alloc(len, 'a');
16+
17+
const server = http.createServer((req, res) => {
18+
for (let n = 0; n < chunks; n++) {
19+
res.write(chunk);
20+
}
21+
res.end();
22+
});
23+
24+
server.listen(0, () => {
25+
bench.http({
26+
connections: c,
27+
duration,
28+
port: server.address().port
29+
}, () => server.close());
30+
});
31+
}

lib/_http_outgoing.js

Lines changed: 135 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const {
6767
ERR_STREAM_DESTROYED,
6868
ERR_STREAM_NULL_VALUES,
6969
ERR_STREAM_WRITE_AFTER_END,
70+
ERR_UNKNOWN_ENCODING,
7071
},
7172
hideStackFrames,
7273
} = require('internal/errors');
@@ -82,6 +83,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
8283
});
8384

8485
const kCorked = Symbol('corked');
86+
const kAutoCorked = Symbol('autoCorked');
8587
const kSocket = Symbol('kSocket');
8688
const kChunkedBuffer = Symbol('kChunkedBuffer');
8789
const kChunkedLength = Symbol('kChunkedLength');
@@ -147,6 +149,7 @@ function OutgoingMessage(options) {
147149
this.finished = false;
148150
this._headerSent = false;
149151
this[kCorked] = 0;
152+
this[kAutoCorked] = false;
150153
this[kChunkedBuffer] = [];
151154
this[kChunkedLength] = 0;
152155
this._closed = false;
@@ -225,10 +228,19 @@ ObjectDefineProperty(OutgoingMessage.prototype, 'writableObjectMode', {
225228
},
226229
});
227230

231+
function chunkedBufferLength(msg) {
232+
const len = msg[kChunkedLength];
233+
if (len === 0) {
234+
return 0;
235+
}
236+
237+
return len + len.toString(16).length + 4 + (!msg._headerSent && msg._header !== null ? msg._header.length : 0);
238+
}
239+
228240
ObjectDefineProperty(OutgoingMessage.prototype, 'writableLength', {
229241
__proto__: null,
230242
get() {
231-
return this.outputSize + this[kChunkedLength] + (this[kSocket] ? this[kSocket].writableLength : 0);
243+
return this.outputSize + chunkedBufferLength(this) + (this[kSocket] ? this[kSocket].writableLength : 0);
232244
},
233245
});
234246

@@ -297,44 +309,85 @@ OutgoingMessage.prototype.cork = function cork() {
297309
}
298310
};
299311

300-
OutgoingMessage.prototype.uncork = function uncork() {
301-
this[kCorked]--;
302-
if (this[kSocket]) {
303-
this[kSocket].uncork();
312+
function callChunkedCallbacks(callbacks, error) {
313+
for (let n = 0; n < callbacks.length; n++) {
314+
callbacks[n](error);
304315
}
316+
}
305317

306-
if (this[kCorked] || this[kChunkedBuffer].length === 0) {
318+
function destroyChunkedBuffer(msg, error) {
319+
const buf = msg[kChunkedBuffer];
320+
if (buf.length === 0) {
307321
return;
308322
}
309323

310-
const len = this[kChunkedLength];
311-
const buf = this[kChunkedBuffer];
324+
const callbacks = [];
325+
for (let n = 2; n < buf.length; n += 3) {
326+
if (buf[n] !== nop) {
327+
callbacks.push(buf[n]);
328+
}
329+
}
330+
331+
buf.length = 0;
332+
msg[kChunkedLength] = 0;
333+
if (callbacks.length !== 0) {
334+
process.nextTick(callChunkedCallbacks, callbacks, error || new ERR_STREAM_DESTROYED('write'));
335+
}
336+
}
337+
338+
function flushChunkedBuffer(msg) {
339+
if (msg.destroyed || msg[kSocket]?.destroyed) {
340+
destroyChunkedBuffer(msg, msg[kErrored] || msg[kSocket]?._writableState?.errored);
341+
return false;
342+
}
312343

313-
assert(this.chunkedEncoding);
344+
const buf = msg[kChunkedBuffer];
345+
const len = msg[kChunkedLength];
314346

315-
let callbacks;
316-
this._send(len.toString(16), 'latin1', null);
317-
this._send(crlf_buf, null, null);
347+
assert(msg.chunkedEncoding);
348+
349+
const callbacks = [];
350+
msg._send(len.toString(16), 'latin1', null);
351+
msg._send(crlf_buf, null, null);
318352
for (let n = 0; n < buf.length; n += 3) {
319-
this._send(buf[n + 0], buf[n + 1], null);
320-
if (buf[n + 2]) {
321-
callbacks ??= [];
353+
msg._send(buf[n], buf[n + 1], null);
354+
if (buf[n + 2] !== nop) {
322355
callbacks.push(buf[n + 2]);
323356
}
324357
}
325-
this._send(crlf_buf, null, callbacks.length ? (err) => {
326-
for (const callback of callbacks) {
327-
callback(err);
328-
}
329-
} : null);
358+
msg._send(crlf_buf, null, callbacks.length === 0 ? null : (error) => callChunkedCallbacks(callbacks, error));
330359

331-
this[kChunkedBuffer].length = 0;
332-
this[kChunkedLength] = 0;
360+
buf.length = 0;
361+
msg[kChunkedLength] = 0;
362+
return true;
363+
}
364+
365+
function emitDrainIfNeeded(msg) {
366+
if (msg[kNeedDrain] && msg.writableLength === 0) {
367+
msg[kNeedDrain] = false;
368+
msg.emit('drain');
369+
}
370+
}
333371

334-
// If we had a pending drain and flushed all data, emit the drain event.
335-
if (this[kNeedDrain] && this.writableLength === 0) {
336-
this[kNeedDrain] = false;
337-
this.emit('drain');
372+
OutgoingMessage.prototype.uncork = function uncork() {
373+
if (this[kCorked] === 0) {
374+
return;
375+
}
376+
this[kCorked]--;
377+
378+
const hasBufferedChunks = this[kCorked] === 0 && this[kChunkedBuffer].length !== 0;
379+
let flushed = false;
380+
try {
381+
if (hasBufferedChunks) {
382+
flushed = flushChunkedBuffer(this);
383+
}
384+
} finally {
385+
this[kSocket]?.uncork();
386+
}
387+
388+
if (flushed) {
389+
// If we had a pending drain and flushed all data, emit the drain event.
390+
emitDrainIfNeeded(this);
338391
}
339392
};
340393

@@ -1006,21 +1059,39 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
10061059

10071060
if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
10081061
msg.socket.cork();
1009-
process.nextTick(connectionCorkNT, msg.socket);
1062+
msg[kAutoCorked] = true;
1063+
process.nextTick(connectionCorkNT, msg, msg.socket);
10101064
}
10111065

10121066
let ret;
1013-
if (msg.chunkedEncoding && chunk.length !== 0) {
1014-
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
1015-
if (msg[kCorked] && msg._headerSent) {
1067+
if (msg.chunkedEncoding) {
1068+
const buf = msg[kChunkedBuffer];
1069+
const buffering = (msg[kAutoCorked] || msg[kCorked]) && (chunk.length !== 0 || buf.length !== 0);
1070+
if (buffering) {
1071+
if (encoding && (encoding === 'buffer' ? typeof chunk === 'string' : !Buffer.isEncoding(encoding))) {
1072+
throw new ERR_UNKNOWN_ENCODING(encoding);
1073+
}
1074+
1075+
if (chunk.length !== 0) {
1076+
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
1077+
if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
1078+
chunk = Stream._uint8ArrayToBuffer(chunk);
1079+
}
1080+
msg[kChunkedLength] += len;
1081+
}
10161082
msg[kChunkedBuffer].push(chunk, encoding, callback);
1017-
msg[kChunkedLength] += len;
1018-
ret = msg[kChunkedLength] < msg[kHighWaterMark];
1019-
} else {
1083+
ret = msg.writableLength < msg.writableHighWaterMark;
1084+
if (msg[kAutoCorked] && msg[kCorked] === 0 && chunkedBufferLength(msg) >= msg.writableHighWaterMark) {
1085+
flushChunkedBuffer(msg);
1086+
}
1087+
} else if (chunk.length !== 0) {
1088+
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
10201089
msg._send(len.toString(16), 'latin1', null);
10211090
msg._send(crlf_buf, null, null);
10221091
msg._send(chunk, encoding, null, len);
10231092
ret = msg._send(crlf_buf, null, callback);
1093+
} else {
1094+
ret = msg._send(chunk, encoding, callback, len);
10241095
}
10251096
} else {
10261097
ret = msg._send(chunk, encoding, callback, len);
@@ -1031,8 +1102,26 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
10311102
}
10321103

10331104

1034-
function connectionCorkNT(conn) {
1035-
conn.uncork();
1105+
function connectionCorkNT(msg, conn) {
1106+
if (!msg[kAutoCorked]) {
1107+
return;
1108+
}
1109+
1110+
msg[kAutoCorked] = false;
1111+
let flushed = false;
1112+
try {
1113+
if (msg.destroyed || conn.destroyed) {
1114+
destroyChunkedBuffer(msg, msg[kErrored] || conn._writableState?.errored);
1115+
} else if (msg[kCorked] === 0 && msg[kChunkedBuffer].length !== 0) {
1116+
flushed = flushChunkedBuffer(msg);
1117+
}
1118+
} finally {
1119+
conn.uncork();
1120+
}
1121+
1122+
if (flushed) {
1123+
emitDrainIfNeeded(msg);
1124+
}
10361125
}
10371126

10381127
OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
@@ -1138,6 +1227,11 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11381227
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength);
11391228
}
11401229

1230+
// Flush message-level corked data before the terminating chunk. Keep the
1231+
// socket corked so all HTTP framing can be written as a single batch.
1232+
const hasBufferedChunks = this[kChunkedBuffer].length !== 0;
1233+
const flushed = hasBufferedChunks && flushChunkedBuffer(this);
1234+
11411235
const finish = onFinish.bind(undefined, this);
11421236

11431237
if (this._hasBody && this.chunkedEncoding) {
@@ -1148,6 +1242,7 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11481242
process.nextTick(finish);
11491243
}
11501244

1245+
this[kAutoCorked] = false;
11511246
if (this[kSocket]) {
11521247
// Fully uncork connection on end().
11531248
this[kSocket]._writableState.corked = 1;
@@ -1156,8 +1251,13 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11561251
this[kCorked] = 1;
11571252
this.uncork();
11581253

1254+
// A synchronous drain listener must not write after the terminating chunk.
11591255
this.finished = true;
11601256

1257+
if (flushed) {
1258+
emitDrainIfNeeded(this);
1259+
}
1260+
11611261
// There is the first message on the outgoing queue, and we've sent
11621262
// everything to the socket.
11631263
debug('outgoing message end.');

test/parallel/test-http-1.0.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,8 @@ function test(handler, request_generator, response_validator) {
148148
'Connection: close\r\n' +
149149
'Transfer-Encoding: chunked\r\n' +
150150
'\r\n' +
151-
'7\r\n' +
152-
'Hello, \r\n' +
153-
'6\r\n' +
154-
'world!\r\n' +
151+
'd\r\n' +
152+
'Hello, world!\r\n' +
155153
'0\r\n' +
156154
'\r\n';
157155

0 commit comments

Comments
 (0)