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-
functiononStreamTimeout(kind){
304-
returnfunctiononStreamTimeout(){
305-
constobj=this[kind];
306-
obj.emit('timeout');
307-
};
303+
functiononStreamTimeoutRequest(){
304+
this[kRequest].emit('timeout');
305+
}
306+
307+
functiononStreamTimeoutResponse(){
308+
this[kResponse].emit('timeout');
308309
}
309310

310311
classHttp2ServerRequestextendsReadable{
@@ -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
letpseudoHeaders='';
771771
letcount=0;
772772

773-
constsingles=newSafeSet();
773+
letsingles;
774774
constsensitiveHeaders=arrayOrMap[kSensitiveHeaders]||emptyArray;
775-
constneverIndex=sensitiveHeaders.map((v)=>v.toLowerCase());
775+
constneverIndex=sensitiveHeaders.length===0 ?
776+
emptyArray : sensitiveHeaders.map((v)=>v.toLowerCase());
776777

777778
functionprocessHeader(key,value){
778779
key=key.toLowerCase();
780+
constisSingleValueField=kSingleValueFields.has(key);
779781
constisStrictSingleValueField=strictSingleValueFields&&
780-
kSingleValueFields.has(key);
782+
isSingleValueField;
781783
letisArray=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+
}elseif(singles.includes(key)){
799803
thrownewERR_HTTP2_HEADER_SINGLE_VALUE(key);
800-
singles.add(key);
804+
}else{
805+
singles.push(key);
806+
}
801807
}
802-
constflags=neverIndex.includes(key) ?
808+
constflags=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-
thrownewERR_INVALID_HTTP_TOKEN('Header name',key);
815-
}
816-
if(isIllegalConnectionSpecificHeader(key,value)){
817-
thrownewERR_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+
thrownewERR_INVALID_HTTP_TOKEN('Header name',key);
824+
}
825+
if(isIllegalConnectionSpecificHeader(key,value)){
826+
thrownewERR_HTTP2_INVALID_CONNECTION_HEADERS(key);
827+
}
818828
}
819829
if(isArray){
820830
for(letj=0;j<value.length;++j){

0 commit comments

Comments
 (0)