Skip to content

Commit 3ed3715

Browse files
mcollinaaduh95
authored andcommitted
http2: reduce per-request allocations
Cut several sources of per-stream/per-request overhead on the hot path: - Track 'priority'/'frameError' stream listeners by overriding the EventEmitter methods on Http2Stream instead of subscribing to 'newListener'/'removeListener', which made every listener add and remove on every stream emit an extra tracking event. - Replace the per-call SafeSet and sensitive-header mapping in buildNgHeaderString with a lazily allocated array and an empty-array fast path, and skip the HTTP token regex and connection-specific header checks for well-known single-value header names. - Replace per-call closures with shared named handlers in onStreamClose, afterShutdown and Http2Stream._destroy. - Skip the pendingStreams Set add/delete for streams that are created with their native handle already available (all server streams). - Hoist the per-request onStreamTimeout closure factories in the compat layer to module-level handlers, and avoid a once() wrapper allocation per server stream. h2load, 1 KiB response payload, -c 4 -m 100, mean of 6 alternating runs: core API 60.2k -> 69.3k req/s (+15%), compat API 43.6k -> 46.2k req/s (+5.9%). Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64265 Backport-PR-URL: #64663 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
1 parent bce92de commit 3ed3715

2 files changed

Lines changed: 29 additions & 18 deletions

File tree

lib/internal/http2/compat.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,12 @@ function onStreamCloseRequest() {
300300
req.emit('close');
301301
}
302302

303-
function onStreamTimeout(kind) {
304-
return function onStreamTimeout() {
305-
const obj = this[kind];
306-
obj.emit('timeout');
307-
};
303+
function onStreamTimeoutRequest() {
304+
this[kRequest].emit('timeout');
305+
}
306+
307+
function onStreamTimeoutResponse() {
308+
this[kResponse].emit('timeout');
308309
}
309310

310311
class Http2ServerRequest extends Readable {
@@ -332,7 +333,7 @@ class Http2ServerRequest extends Readable {
332333
stream.on('error', onStreamError);
333334
stream.on('aborted', onStreamAbortedRequest);
334335
stream.on('close', onStreamCloseRequest);
335-
stream.on('timeout', onStreamTimeout(kRequest));
336+
stream.on('timeout', onStreamTimeoutRequest);
336337
this.on('pause', onRequestPause);
337338
this.on('resume', onRequestResume);
338339
}
@@ -486,7 +487,7 @@ class Http2ServerResponse extends Stream {
486487
stream.on('aborted', onStreamAbortedResponse);
487488
stream.on('close', onStreamCloseResponse);
488489
stream.on('wantTrailers', onStreamTrailersReady);
489-
stream.on('timeout', onStreamTimeout(kResponse));
490+
stream.on('timeout', onStreamTimeoutResponse);
490491
}
491492

492493
// User land modules such as finalhandler just check truthiness of this

lib/internal/http2/util.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -770,14 +770,16 @@ function buildNgHeaderString(arrayOrMap,
770770
let pseudoHeaders = '';
771771
let count = 0;
772772

773-
const singles = new SafeSet();
773+
let singles;
774774
const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray;
775-
const neverIndex = sensitiveHeaders.map((v) => v.toLowerCase());
775+
const neverIndex = sensitiveHeaders.length === 0 ?
776+
emptyArray : sensitiveHeaders.map((v) => v.toLowerCase());
776777

777778
function processHeader(key, value) {
778779
key = key.toLowerCase();
780+
const isSingleValueField = kSingleValueFields.has(key);
779781
const isStrictSingleValueField = strictSingleValueFields &&
780-
kSingleValueFields.has(key);
782+
isSingleValueField;
781783
let isArray = ArrayIsArray(value);
782784
if (isArray) {
783785
switch (value.length) {
@@ -795,11 +797,15 @@ function buildNgHeaderString(arrayOrMap,
795797
value = String(value);
796798
}
797799
if (isStrictSingleValueField) {
798-
if (singles.has(key))
800+
if (singles === undefined) {
801+
singles = [key];
802+
} else if (singles.includes(key)) {
799803
throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key);
800-
singles.add(key);
804+
} else {
805+
singles.push(key);
806+
}
801807
}
802-
const flags = neverIndex.includes(key) ?
808+
const flags = neverIndex.length !== 0 && neverIndex.includes(key) ?
803809
kNeverIndexFlag :
804810
kNoHeaderFlags;
805811
if (key[0] === ':') {
@@ -810,11 +816,15 @@ function buildNgHeaderString(arrayOrMap,
810816
count++;
811817
return;
812818
}
813-
if (!checkIsHttpToken(key)) {
814-
throw new ERR_INVALID_HTTP_TOKEN('Header name', key);
815-
}
816-
if (isIllegalConnectionSpecificHeader(key, value)) {
817-
throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
819+
// Well-known single-value fields are all valid HTTP tokens and none of
820+
// them is a connection-specific header, so both checks can be skipped.
821+
if (!isSingleValueField) {
822+
if (!checkIsHttpToken(key)) {
823+
throw new ERR_INVALID_HTTP_TOKEN('Header name', key);
824+
}
825+
if (isIllegalConnectionSpecificHeader(key, value)) {
826+
throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
827+
}
818828
}
819829
if (isArray) {
820830
for (let j = 0; j < value.length; ++j) {

0 commit comments

Comments
 (0)