Skip to content

Commit 659d6b9

Browse files
committed
http: improve performance with known-length calls to end()
This boosts RPS performance for the common API case where you call `res.end(data)` with the entire response by up to 9%. Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent 15940ef commit 659d6b9

3 files changed

Lines changed: 80 additions & 17 deletions

File tree

benchmark/http/end-string.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Responses sent as a single res.end(string) with a known Content-Length -
2+
// the shape a JSON or HTML endpoint produces.
3+
'use strict';
4+
5+
const common = require('../common.js');
6+
7+
const bench = common.createBenchmark(main, {
8+
len: [4, 64, 1024, 16384, 102400],
9+
c: [50],
10+
duration: 5,
11+
});
12+
13+
function main({ len, c, duration }) {
14+
const http = require('http');
15+
const body = 'a'.repeat(len);
16+
const headers = {
17+
'Content-Type': 'text/plain',
18+
'Content-Length': `${len}`,
19+
};
20+
21+
const server = http.createServer((req, res) => {
22+
res.writeHead(200, headers);
23+
res.end(body);
24+
});
25+
26+
server.listen(0, () => {
27+
bench.http({
28+
connections: c,
29+
duration,
30+
port: server.address().port,
31+
}, () => {
32+
server.close();
33+
});
34+
});
35+
}

lib/_http_outgoing.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,25 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
10331033
}
10341034

10351035

1036+
// If this last write can be delivered immediately as the final chunk, this
1037+
// prepares to do so, and then returns true. If not, it returns false and
1038+
// a separate _send call and tick will be required to finish up.
1039+
function maybePrepareFinalChunk(msg, chunk, encoding) {
1040+
if (typeof chunk !== 'string' && !isUint8Array(chunk))
1041+
return false;
1042+
1043+
if (msg.destroyed || msg.strictContentLength)
1044+
return false;
1045+
1046+
if (!msg._header) {
1047+
msg._contentLength = typeof chunk === 'string' ?
1048+
Buffer.byteLength(chunk, encoding) : chunk.byteLength;
1049+
msg._implicitHeader();
1050+
}
1051+
1052+
return !!msg._header && msg._hasBody && !msg.chunkedEncoding;
1053+
}
1054+
10361055
function connectionCorkNT(conn) {
10371056
conn.uncork();
10381057
}
@@ -1142,6 +1161,8 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11421161
encoding = null;
11431162
}
11441163

1164+
let finishCallback = null;
1165+
11451166
if (chunk) {
11461167
if (this.finished) {
11471168
onError(this,
@@ -1154,7 +1175,18 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11541175
this[kSocket].cork();
11551176
}
11561177

1157-
write_(this, chunk, encoding, null, true);
1178+
if (maybePrepareFinalChunk(this, chunk, encoding)) {
1179+
// If just one final write is required, with nothing to follow, we
1180+
// attach finish to the write to avoid a separate send() & tick step
1181+
// later on - this is purely a performance optimization.
1182+
if (typeof callback === 'function') {
1183+
queueEndCallback(this, callback);
1184+
callback = undefined;
1185+
}
1186+
finishCallback = onFinish.bind(undefined, this);
1187+
}
1188+
1189+
write_(this, chunk, encoding, finishCallback, true);
11581190
} else if (this.finished) {
11591191
if (typeof callback === 'function') {
11601192
queueEndCallback(this, callback);
@@ -1176,14 +1208,17 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11761208
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength);
11771209
}
11781210

1179-
const finish = onFinish.bind(undefined, this);
1211+
if (finishCallback === null) {
1212+
// If we didn't early finish, send the last data and schedule 'finish' now:
1213+
finishCallback = onFinish.bind(undefined, this);
11801214

1181-
if (this._hasBody && this.chunkedEncoding) {
1182-
this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish);
1183-
} else if (!this._headerSent || this.writableLength || chunk) {
1184-
this._send('', 'latin1', finish);
1185-
} else {
1186-
process.nextTick(finish);
1215+
if (this._hasBody && this.chunkedEncoding) {
1216+
this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finishCallback);
1217+
} else if (!this._headerSent || this.writableLength || chunk) {
1218+
this._send('', 'latin1', finishCallback);
1219+
} else {
1220+
process.nextTick(finishCallback);
1221+
}
11871222
}
11881223

11891224
if (this[kSocket]) {

test/parallel/test-http-server-response-standalone.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,11 @@ const res = new ServerResponse({
1515
httpVersionMinor: 1
1616
});
1717

18-
let firstChunk = true;
19-
2018
const ws = new Writable({
2119
write: common.mustCall((chunk, encoding, callback) => {
22-
if (firstChunk) {
23-
assert(chunk.toString().endsWith('hello world'));
24-
firstChunk = false;
25-
} else {
26-
assert.strictEqual(chunk.length, 0);
27-
}
20+
assert(chunk.toString().endsWith('hello world'));
2821
setImmediate(callback);
29-
}, 2)
22+
}, 1)
3023
});
3124

3225
res.assignSocket(ws);

0 commit comments

Comments
 (0)