Skip to content

Commit dcc61cd

Browse files
mcollinaaduh95
authored andcommitted
http: reduce per-request allocations in the server
Remove allocations and repeated work performed for every request: - reuse a per-connection updateOutgoingData closure and a shared 'finish' listener instead of binding two functions per request, reaching resOnFinish through the connection state stored on the response - cache the ServerResponse options object per server (custom response classes keep receiving a fresh object) - check for Host, Expect, Content-Length and Transfer-Encoding by scanning rawHeaders instead of materializing req.headers, which was built (with per-name toLowerCase calls) for every request even when the application never reads it - cache the rendered status line per status code when the reason phrase is the default, skipping its character validation - cache the complete 'Date: ...' header line in the utcDate cache and the keep-alive header pair for the current server settings - compute the lenient-validation option chain once per message Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65802 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 35e9b53 commit dcc61cd

4 files changed

Lines changed: 128 additions & 33 deletions

File tree

‎lib/_http_incoming.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,7 @@ function onError(self, error, cb) {
536536
module.exports = {
537537
IncomingMessage,
538538
kDetachAbortSignal,
539+
kHeadersCount,
539540
readStart,
540541
readStop,
541542
};

‎lib/_http_outgoing.js‎

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const { getDefaultHighWaterMark } = require('internal/streams/state');
3838
const assert = require('internal/assert');
3939
const EE = require('events');
4040
const Stream = require('stream');
41-
const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
41+
const { kOutHeaders, utcDateHeader, kNeedDrain } = require('internal/http');
4242
const { Buffer } = require('buffer');
4343
const {
4444
_checkIsHttpToken: checkIsHttpToken,
@@ -86,6 +86,11 @@ const kChunkedLength = Symbol('kChunkedLength');
8686
const kUniqueHeaders = Symbol('kUniqueHeaders');
8787
const kBytesWritten = Symbol('kBytesWritten');
8888
const kErrored = Symbol('errored');
89+
const kLenientCache = Symbol('kLenientCache');
90+
91+
let keepAliveTimeoutCache = -1;
92+
let keepAliveMaxCache = -1;
93+
let keepAliveHeaderCache = '';
8994
const kWritableFinished = Symbol('kWritableFinished');
9095
const kEndCallbacks = Symbol('kEndCallbacks');
9196
const kFlushError = Symbol('kFlushError');
@@ -162,6 +167,7 @@ function OutgoingMessage(options) {
162167
this[kFlushError] = null;
163168
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
164169
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
170+
this[kLenientCache] = null;
165171
}
166172
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
167173
ObjectSetPrototypeOf(OutgoingMessage, Stream);
@@ -171,27 +177,34 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream);
171177
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
172178
// Falls back to global --insecure-http-parser flag.
173179
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
180+
// The underlying options cannot change during the lifetime of a message:
181+
// compute the lookup chain only once per message.
182+
this[kLenientCache] ??= isLenientHeaderValidation(this);
183+
return this[kLenientCache];
184+
};
185+
186+
function isLenientHeaderValidation(msg) {
174187
// New httpValidation option takes priority (ClientRequest case)
175-
if (this.httpValidation !== undefined) {
176-
return this.httpValidation !== 'strict';
188+
if (msg.httpValidation !== undefined) {
189+
return msg.httpValidation !== 'strict';
177190
}
178191
// ServerResponse: check server's httpValidation option
179-
const serverHttpValidation = this.req?.socket?.server?.httpValidation;
192+
const serverHttpValidation = msg.req?.socket?.server?.httpValidation;
180193
if (serverHttpValidation !== undefined) {
181194
return serverHttpValidation !== 'strict';
182195
}
183196
// Legacy insecureHTTPParser - ClientRequest has it directly
184-
if (typeof this.insecureHTTPParser === 'boolean') {
185-
return this.insecureHTTPParser;
197+
if (typeof msg.insecureHTTPParser === 'boolean') {
198+
return msg.insecureHTTPParser;
186199
}
187200
// ServerResponse can access via req.socket.server
188-
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
201+
const serverOption = msg.req?.socket?.server?.insecureHTTPParser;
189202
if (typeof serverOption === 'boolean') {
190203
return serverOption;
191204
}
192205
// Fall back to global option
193206
return isLenient();
194-
};
207+
}
195208

196209
ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
197210
__proto__: null,
@@ -501,7 +514,7 @@ function _storeHeader(firstLine, headers) {
501514

502515
// Date header
503516
if (this.sendDate && !state.date) {
504-
header += 'Date: ' + utcDate() + '\r\n';
517+
header += utcDateHeader();
505518
}
506519

507520
// Force the connection to close when the response is a 204 No Content or
@@ -534,14 +547,21 @@ function _storeHeader(firstLine, headers) {
534547
if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) {
535548
header += 'Connection: close\r\n';
536549
} else if (shouldSendKeepAlive) {
537-
header += 'Connection: keep-alive\r\n';
538550
if (this._keepAliveTimeout && this._defaultKeepAlive) {
539-
const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000);
540-
let max = '';
541-
if (~~this._maxRequestsPerSocket > 0) {
542-
max = `, max=${this._maxRequestsPerSocket}`;
551+
// The keep-alive header lines are identical for every response of a
552+
// given server: cache the last rendered value.
553+
const timeout = this._keepAliveTimeout;
554+
const max = ~~this._maxRequestsPerSocket;
555+
if (timeout !== keepAliveTimeoutCache || max !== keepAliveMaxCache) {
556+
keepAliveTimeoutCache = timeout;
557+
keepAliveMaxCache = max;
558+
keepAliveHeaderCache = 'Connection: keep-alive\r\n' +
559+
`Keep-Alive: timeout=${MathFloor(timeout / 1000)}` +
560+
(max > 0 ? `, max=${max}` : '') + '\r\n';
543561
}
544-
header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`;
562+
header += keepAliveHeaderCache;
563+
} else {
564+
header += 'Connection: keep-alive\r\n';
545565
}
546566
} else {
547567
this._last = true;

‎lib/_http_server.js‎

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const {
7171
const {
7272
IncomingMessage,
7373
kDetachAbortSignal,
74+
kHeadersCount,
7475
} = require('_http_incoming');
7576
const {
7677
ConnResetException,
@@ -113,6 +114,10 @@ const kServerResponse = Symbol('ServerResponse');
113114
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
114115

115116
const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
117+
const kConnectionState = Symbol('ConnectionState');
118+
const kResponseOptions = Symbol('ResponseOptions');
119+
120+
const statusLineCache = [];
116121

117122
const {
118123
hasObserver,
@@ -467,10 +472,18 @@ function writeHead(statusCode, reason, obj) {
467472
headers = obj;
468473
}
469474

470-
if (checkInvalidHeaderChar(this.statusMessage))
471-
throw new ERR_INVALID_CHAR('statusMessage');
475+
let statusLine;
476+
if (this.statusMessage === STATUS_CODES[statusCode]) {
477+
// Default reason phrases contain no invalid characters and are shared
478+
// across responses: cache the whole status line.
479+
statusLine = statusLineCache[statusCode] ??=
480+
`HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
481+
} else {
482+
if (checkInvalidHeaderChar(this.statusMessage))
483+
throw new ERR_INVALID_CHAR('statusMessage');
472484

473-
const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
485+
statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
486+
}
474487

475488
if (statusCode === 204 || statusCode === 304 ||
476489
(statusCode >= 100 && statusCode <= 199)) {
@@ -819,7 +832,11 @@ function connectionListenerInternal(server, socket) {
819832
outgoingData: 0,
820833
requestsCount: 0,
821834
keepAliveTimeoutSet: false,
835+
onPendingData: null,
836+
server,
837+
socket,
822838
};
839+
state.onPendingData = updateOutgoingData.bind(undefined, socket, state);
823840
state.onData = socketOnData.bind(undefined,
824841
server, socket, parser, state);
825842
state.onEnd = socketOnEnd.bind(undefined,
@@ -1157,8 +1174,61 @@ function emitCloseNT(self) {
11571174
}
11581175
}
11591176

1160-
function hasBodyHeaders(headers) {
1161-
return ('content-length' in headers) || ('transfer-encoding' in headers);
1177+
// Check for the presence of a request header by scanning rawHeaders instead
1178+
// of materializing the req.headers object. Only the first req[kHeadersCount]
1179+
// entries are considered, matching what the req.headers getter exposes.
1180+
function hasRequestHeader(req, length, lowerName) {
1181+
const rawHeaders = req.rawHeaders;
1182+
const count = req[kHeadersCount];
1183+
for (let i = 0; i < count; i += 2) {
1184+
const key = rawHeaders[i];
1185+
if (key.length === length &&
1186+
(key === lowerName || key.toLowerCase() === lowerName)) {
1187+
return true;
1188+
}
1189+
}
1190+
return false;
1191+
}
1192+
1193+
function hasBodyHeaders(req) {
1194+
const rawHeaders = req.rawHeaders;
1195+
const count = req[kHeadersCount];
1196+
for (let i = 0; i < count; i += 2) {
1197+
const length = rawHeaders[i].length;
1198+
if (length === 14 || length === 17) {
1199+
const key = rawHeaders[i].toLowerCase();
1200+
if (key === 'content-length' || key === 'transfer-encoding') {
1201+
return true;
1202+
}
1203+
}
1204+
}
1205+
return false;
1206+
}
1207+
1208+
// The options object passed to the ServerResponse constructor is identical
1209+
// for every request of a given server: cache it. Custom response classes get
1210+
// a fresh object since they may retain or mutate it.
1211+
function getResponseOptions(server, socket) {
1212+
const highWaterMark = socket.writableHighWaterMark;
1213+
const rejectNonStandardBodyWrites = server.rejectNonStandardBodyWrites;
1214+
if (server[kServerResponse] !== ServerResponse) {
1215+
return { highWaterMark, rejectNonStandardBodyWrites };
1216+
}
1217+
let options = server[kResponseOptions];
1218+
if (options === undefined ||
1219+
options.highWaterMark !== highWaterMark ||
1220+
options.rejectNonStandardBodyWrites !== rejectNonStandardBodyWrites) {
1221+
options = server[kResponseOptions] =
1222+
{ highWaterMark, rejectNonStandardBodyWrites };
1223+
}
1224+
return options;
1225+
}
1226+
1227+
// Shared 'finish' listener: everything resOnFinish needs is reachable from
1228+
// the response, avoiding a bound function per request.
1229+
function onResponseFinish() {
1230+
const state = this[kConnectionState];
1231+
resOnFinish(this.req, this, state.socket, state, state.server);
11621232
}
11631233

11641234
// The following callback is issued after the headers have been read on a
@@ -1190,15 +1260,11 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
11901260
}
11911261
}
11921262

1193-
const res = new server[kServerResponse](req,
1194-
{
1195-
highWaterMark: socket.writableHighWaterMark,
1196-
rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites,
1197-
});
1263+
const res = new server[kServerResponse](req, getResponseOptions(server, socket));
11981264
res._keepAliveTimeout = server.keepAliveTimeout;
11991265
res._maxRequestsPerSocket = server.maxRequestsPerSocket;
1200-
res._onPendingData = updateOutgoingData.bind(undefined,
1201-
socket, state);
1266+
res._onPendingData = state.onPendingData;
1267+
res[kConnectionState] = state;
12021268

12031269
res.shouldKeepAlive = keepAlive;
12041270
res[kUniqueHeaders] = server[kUniqueHeaders];
@@ -1213,7 +1279,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
12131279
}
12141280

12151281
// Check if we should optimize empty requests (those without Content-Length or Transfer-Encoding headers)
1216-
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req.headers);
1282+
const shouldOptimize = server[kOptimizeEmptyRequests] === true && !hasBodyHeaders(req);
12171283

12181284
if (shouldOptimize) {
12191285
// Fast processing where emitting 'data', 'end' and 'close' events is
@@ -1234,9 +1300,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
12341300

12351301
// When we're finished writing the response, check if this is the last
12361302
// response, if so destroy the socket.
1237-
res.on('finish',
1238-
resOnFinish.bind(undefined,
1239-
req, res, socket, state, server));
1303+
res.on('finish', onResponseFinish);
12401304

12411305
let handled = false;
12421306

@@ -1246,7 +1310,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
12461310
// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
12471311
// A server MUST respond with a 400 (Bad Request) status code to any
12481312
// HTTP/1.1 request message that lacks a Host header field
1249-
if (server.requireHostHeader && req.headers.host === undefined) {
1313+
if (server.requireHostHeader && !hasRequestHeader(req, 4, 'host')) {
12501314
res.writeHead(400, ['Connection', 'close']);
12511315
res.end();
12521316
return 0;
@@ -1269,7 +1333,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
12691333
server.emit('dropRequest', req, socket);
12701334
res.writeHead(503);
12711335
res.end();
1272-
} else if (req.headers.expect !== undefined) {
1336+
} else if (hasRequestHeader(req, 6, 'expect')) {
12731337
handled = true;
12741338

12751339
if (continueExpression.test(req.headers.expect)) {

‎lib/internal/http.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,20 @@ const { Buffer } = require('buffer');
2020
const { isIPv4 } = require('internal/net');
2121
const { ERR_PROXY_INVALID_CONFIG } = require('internal/errors').codes;
2222
let utcCache;
23+
let utcHeaderCache;
2324

2425
function utcDate() {
2526
if (!utcCache) cache();
2627
return utcCache;
2728
}
2829

30+
// The complete `Date: ...\r\n` header line, cached alongside utcDate() so
31+
// that the serializer does not re-concatenate it for every response.
32+
function utcDateHeader() {
33+
utcHeaderCache ||= 'Date: ' + utcDate() + '\r\n';
34+
return utcHeaderCache;
35+
}
36+
2937
function cache() {
3038
const d = new Date();
3139
utcCache = d.toUTCString();
@@ -34,6 +42,7 @@ function cache() {
3442

3543
function resetCache() {
3644
utcCache = undefined;
45+
utcHeaderCache = undefined;
3746
}
3847

3948
let traceEventId = 0;
@@ -275,6 +284,7 @@ module.exports = {
275284
checkShouldUseProxy,
276285
parseProxyConfigFromEnv,
277286
utcDate,
287+
utcDateHeader,
278288
traceBegin,
279289
traceEnd,
280290
getNextTraceEventId,

0 commit comments

Comments
 (0)