Skip to content

Commit 15184e8

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 684cae7 commit 15184e8

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
@@ -73,6 +73,7 @@ const {
7373
const {
7474
IncomingMessage,
7575
kDetachAbortSignal,
76+
kHeadersCount,
7677
} = require('_http_incoming');
7778
const {
7879
ConnResetException,
@@ -116,6 +117,10 @@ const kServerResponseStatistics = Symbol('ServerResponseStatistics');
116117
const kUpgradeStream = Symbol('UpgradeStream');
117118

118119
const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
120+
const kConnectionState = Symbol('ConnectionState');
121+
const kResponseOptions = Symbol('ResponseOptions');
122+
123+
const statusLineCache = [];
119124

120125
const {
121126
hasObserver,
@@ -470,10 +475,18 @@ function writeHead(statusCode, reason, obj) {
470475
headers = obj;
471476
}
472477

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

476-
const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
488+
statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
489+
}
477490

478491
if (statusCode === 204 || statusCode === 304 ||
479492
(statusCode >= 100 && statusCode <= 199)) {
@@ -820,7 +833,11 @@ function connectionListenerInternal(server, socket) {
820833
outgoingData: 0,
821834
requestsCount: 0,
822835
keepAliveTimeoutSet: false,
836+
onPendingData: null,
837+
server,
838+
socket,
823839
};
840+
state.onPendingData = updateOutgoingData.bind(undefined, socket, state);
824841
state.onData = socketOnData.bind(undefined,
825842
server, socket, parser, state);
826843
state.onEnd = socketOnEnd.bind(undefined,
@@ -1257,8 +1274,61 @@ function emitCloseNT(self) {
12571274
}
12581275
}
12591276

1260-
function hasBodyHeaders(headers) {
1261-
return ('content-length' in headers) || ('transfer-encoding' in headers);
1277+
// Check for the presence of a request header by scanning rawHeaders instead
1278+
// of materializing the req.headers object. Only the first req[kHeadersCount]
1279+
// entries are considered, matching what the req.headers getter exposes.
1280+
function hasRequestHeader(req, length, lowerName) {
1281+
const rawHeaders = req.rawHeaders;
1282+
const count = req[kHeadersCount];
1283+
for (let i = 0; i < count; i += 2) {
1284+
const key = rawHeaders[i];
1285+
if (key.length === length &&
1286+
(key === lowerName || key.toLowerCase() === lowerName)) {
1287+
return true;
1288+
}
1289+
}
1290+
return false;
1291+
}
1292+
1293+
function hasBodyHeaders(req) {
1294+
const rawHeaders = req.rawHeaders;
1295+
const count = req[kHeadersCount];
1296+
for (let i = 0; i < count; i += 2) {
1297+
const length = rawHeaders[i].length;
1298+
if (length === 14 || length === 17) {
1299+
const key = rawHeaders[i].toLowerCase();
1300+
if (key === 'content-length' || key === 'transfer-encoding') {
1301+
return true;
1302+
}
1303+
}
1304+
}
1305+
return false;
1306+
}
1307+
1308+
// The options object passed to the ServerResponse constructor is identical
1309+
// for every request of a given server: cache it. Custom response classes get
1310+
// a fresh object since they may retain or mutate it.
1311+
function getResponseOptions(server, socket) {
1312+
const highWaterMark = socket.writableHighWaterMark;
1313+
const rejectNonStandardBodyWrites = server.rejectNonStandardBodyWrites;
1314+
if (server[kServerResponse] !== ServerResponse) {
1315+
return { highWaterMark, rejectNonStandardBodyWrites };
1316+
}
1317+
let options = server[kResponseOptions];
1318+
if (options === undefined ||
1319+
options.highWaterMark !== highWaterMark ||
1320+
options.rejectNonStandardBodyWrites !== rejectNonStandardBodyWrites) {
1321+
options = server[kResponseOptions] =
1322+
{ highWaterMark, rejectNonStandardBodyWrites };
1323+
}
1324+
return options;
1325+
}
1326+
1327+
// Shared 'finish' listener: everything resOnFinish needs is reachable from
1328+
// the response, avoiding a bound function per request.
1329+
function onResponseFinish() {
1330+
const state = this[kConnectionState];
1331+
resOnFinish(this.req, this, state.socket, state, state.server);
12621332
}
12631333

12641334
// The following callback is issued after the headers have been read on a
@@ -1291,15 +1361,11 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
12911361
}
12921362
}
12931363

1294-
const res = new server[kServerResponse](req,
1295-
{
1296-
highWaterMark: socket.writableHighWaterMark,
1297-
rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites,
1298-
});
1364+
const res = new server[kServerResponse](req, getResponseOptions(server, socket));
12991365
res._keepAliveTimeout = server.keepAliveTimeout;
13001366
res._maxRequestsPerSocket = server.maxRequestsPerSocket;
1301-
res._onPendingData = updateOutgoingData.bind(undefined,
1302-
socket, state);
1367+
res._onPendingData = state.onPendingData;
1368+
res[kConnectionState] = state;
13031369

13041370
res.shouldKeepAlive = keepAlive;
13051371
res[kUniqueHeaders] = server[kUniqueHeaders];
@@ -1314,7 +1380,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
13141380
}
13151381

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

13191385
if (shouldOptimize) {
13201386
// Fast processing where emitting 'data', 'end' and 'close' events is
@@ -1335,9 +1401,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
13351401

13361402
// When we're finished writing the response, check if this is the last
13371403
// response, if so destroy the socket.
1338-
res.on('finish',
1339-
resOnFinish.bind(undefined,
1340-
req, res, socket, state, server));
1404+
res.on('finish', onResponseFinish);
13411405

13421406
let handled = false;
13431407

@@ -1347,7 +1411,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
13471411
// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
13481412
// A server MUST respond with a 400 (Bad Request) status code to any
13491413
// HTTP/1.1 request message that lacks a Host header field
1350-
if (server.requireHostHeader && req.headers.host === undefined) {
1414+
if (server.requireHostHeader && !hasRequestHeader(req, 4, 'host')) {
13511415
res.writeHead(400, ['Connection', 'close']);
13521416
res.end();
13531417
return 0;
@@ -1370,7 +1434,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) {
13701434
server.emit('dropRequest', req, socket);
13711435
res.writeHead(503);
13721436
res.end();
1373-
} else if (req.headers.expect !== undefined) {
1437+
} else if (hasRequestHeader(req, 6, 'expect')) {
13741438
handled = true;
13751439

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

‎lib/internal/http.js‎

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

2627
function utcDate() {
2728
if (!utcCache) cache();
2829
return utcCache;
2930
}
3031

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

3745
function resetCache() {
3846
utcCache = undefined;
47+
utcHeaderCache = undefined;
3948
}
4049

4150
let traceEventId = 0;
@@ -273,6 +282,7 @@ module.exports = {
273282
checkShouldUseProxy,
274283
parseProxyConfigFromEnv,
275284
utcDate,
285+
utcDateHeader,
276286
traceBegin,
277287
traceEnd,
278288
getNextTraceEventId,

0 commit comments

Comments
 (0)