Skip to content

Commit bfd97c5

Browse files
committed
http2: reduce scheduled callbacks per request
Two per-request scheduling eliminations: - The end-of-stream check that lets the final DATA frame carry the END_STREAM flag was scheduled with process.nextTick() on every write. When the write is dispatched from inside end() - the common case of end(chunk) - the check can instead run synchronously once end() returns and the writable state has settled. An end() override marks the stream while the base method runs, and [kWriteGeneric] hands the check back to it instead of scheduling a tick. Writes not tied to end() keep the nextTick behavior. - Every stream destruction scheduled a setImmediate() to ask the session to clean itself up, but Http2Session[kMaybeDestroy] is a no-op unless the session is closed and has no remaining streams. Gate the setImmediate() on that condition: session.close() runs its own check, and the native side notifies again through ongracefulclosecomplete once pending data is flushed. The wire format is unchanged (verified byte-identical h2load traffic), and the END_STREAM merge is preserved. h2load -c 4 -m 100, 1 KiB payload, alternating runs vs the previous commit: consistently around +1% (within run-to-run noise on any single set, positive across 42 paired samples). Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 2739f63 commit bfd97c5

1 file changed

Lines changed: 37 additions & 2 deletions

File tree

lib/internal/http2/core.js

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2208,6 +2208,8 @@ class Http2Stream extends Duplex {
22082208
writeErr: null,
22092209
endErr: null,
22102210
writePending: 0,
2211+
endInProgress: false,
2212+
endCheckOwed: false,
22112213
shutdownWritableCalled: false,
22122214
fd: -1,
22132215
};
@@ -2454,6 +2456,12 @@ class Http2Stream extends Duplex {
24542456
// Trailers are pending, so the writable side cannot be shut down
24552457
// early anyway; there is no point in scheduling the end check.
24562458
state.writePending = 1;
2459+
} else if (state.endInProgress) {
2460+
// This write was dispatched from inside end(), which makes it the
2461+
// final chunk; end() runs the end-of-stream check synchronously once
2462+
// the stream machinery has settled, avoiding a nextTick per write.
2463+
state.writePending = 2;
2464+
state.endCheckOwed = true;
24572465
} else {
24582466
state.writePending = 2;
24592467
// Shutdown write stream right after last chunk is sent
@@ -2494,6 +2502,25 @@ class Http2Stream extends Duplex {
24942502
this[kWriteGeneric](true, data, '', cb);
24952503
}
24962504

2505+
end(chunk, encoding, cb) {
2506+
const state = this[kState];
2507+
// Any write dispatched while end() runs is the final chunk. Marking
2508+
// that lets [kWriteGeneric] hand its end-of-stream check back to us to
2509+
// run synchronously below (once the writable state has settled)
2510+
// instead of scheduling a nextTick for it.
2511+
state.endInProgress = true;
2512+
try {
2513+
super.end(chunk, encoding, cb);
2514+
} finally {
2515+
state.endInProgress = false;
2516+
if (state.endCheckOwed) {
2517+
state.endCheckOwed = false;
2518+
endCheckNT(this);
2519+
}
2520+
}
2521+
return this;
2522+
}
2523+
24972524
_final(cb) {
24982525
if (this.pending) {
24992526
this.once('ready', () => this._final(cb));
@@ -2656,8 +2683,16 @@ class Http2Stream extends Duplex {
26562683
// gives the session the opportunity to clean itself up. The session
26572684
// will destroy if it has been closed and there are no other open or
26582685
// pending streams. Delay with setImmediate so we don't do it on the
2659-
// nghttp2 stack.
2660-
setImmediate(sessionMaybeDestroy, session);
2686+
// nghttp2 stack. When the session is not closed (or other streams are
2687+
// still around), [kMaybeDestroy] would be a no-op, so skip scheduling
2688+
// it altogether: session.close() runs its own check, and the native
2689+
// side notifies again through ongracefulclosecomplete once all pending
2690+
// data has been flushed.
2691+
if (session.closed &&
2692+
sessionState.streams.size === 0 &&
2693+
sessionState.pendingStreams.size === 0) {
2694+
setImmediate(sessionMaybeDestroy, session);
2695+
}
26612696
if (err) {
26622697
if (session[kType] === NGHTTP2_SESSION_CLIENT) {
26632698
if (onClientStreamErrorChannel.hasSubscribers) {

0 commit comments

Comments
 (0)