Skip to content

Commit 47e8434

Browse files
committed
http2: avoid per-write closures in kWriteGeneric
Every _write()/_writev() on an Http2Stream allocated four closures and an anonymous nextTick callback to coordinate the write callback with the end-of-stream check. Since the stream machinery dispatches at most one write at a time, that coordination state can live on the stream's kState object instead, with shared named functions for the end check and completion logic. When trailers are pending the writable side cannot be shut down early anyway, so the end-of-stream check tick is now skipped entirely for those writes. Also pre-initialize the kState fields that used to be added dynamically (shutdownWritableCalled, fd) so hot-path stores no longer transition the object shape. h2load, 1 KiB response payload, -c 4 -m 100, mean of 6 alternating runs vs main: core API 61.0k -> 70.7k req/s (+15.9% cumulative), compat API 43.7k -> 50.4k req/s (+15.3% cumulative). Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent bedf22f commit 47e8434

1 file changed

Lines changed: 71 additions & 35 deletions

File tree

lib/internal/http2/core.js

Lines changed: 71 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,47 @@ function shutdownWritable(callback) {
20432043
return afterShutdown.call(req, 0);
20442044
}
20452045

2046+
// Completes one of the two halves of a dispatched write (the write callback
2047+
// itself and the end-of-stream check); the stream machinery callback runs
2048+
// once both have finished. The state lives on stream[kState] because only a
2049+
// single write may be in flight at any given time.
2050+
function finishWrite(stream) {
2051+
const state = stream[kState];
2052+
if (--state.writePending !== 0)
2053+
return;
2054+
const cb = state.writeCb;
2055+
state.writeCb = null;
2056+
const err = aggregateTwoErrors(state.endErr, state.writeErr);
2057+
state.writeErr = null;
2058+
state.endErr = null;
2059+
// writeGeneric does not destroy on error and
2060+
// we cannot enable autoDestroy,
2061+
// so make sure to destroy on error.
2062+
if (err) {
2063+
stream.destroy(err);
2064+
}
2065+
cb(err);
2066+
}
2067+
2068+
// Runs on the tick after a write was dispatched: if the write turned out to
2069+
// be the last chunk of an ending writable, shut the writable side down right
2070+
// away so the final DATA frame can include the END_STREAM flag.
2071+
function endCheckNT(stream) {
2072+
const state = stream[kState];
2073+
if (state.writeErr ||
2074+
!stream._writableState.ending ||
2075+
stream._writableState.buffered.length ||
2076+
(state.flags & STREAM_FLAGS_HAS_TRAILERS)) {
2077+
finishWrite(stream);
2078+
return;
2079+
}
2080+
debugStreamObj(stream, 'shutting down writable on last write');
2081+
shutdownWritable.call(stream, (err) => {
2082+
state.endErr = err;
2083+
finishWrite(stream);
2084+
});
2085+
}
2086+
20462087
function finishSendTrailers(stream, headersList) {
20472088
// The stream might be destroyed and in that case
20482089
// there is nothing to do.
@@ -2160,6 +2201,12 @@ class Http2Stream extends Duplex {
21602201
writeQueueSize: 0,
21612202
trailersReady: false,
21622203
endAfterHeaders: false,
2204+
writeCb: null,
2205+
writeErr: null,
2206+
endErr: null,
2207+
writePending: 0,
2208+
shutdownWritableCalled: false,
2209+
fd: -1,
21632210
};
21642211

21652212
// Fields used by the compat API to avoid megamorphisms.
@@ -2391,45 +2438,34 @@ class Http2Stream extends Duplex {
23912438
if (!this.headersSent)
23922439
this[kProceed]();
23932440

2394-
let req;
2441+
// The stream machinery dispatches at most one _write()/_writev() at a
2442+
// time, so the coordination state between the write callback and the
2443+
// end-of-stream check below can live on the stream state instead of
2444+
// being captured by per-write closures.
2445+
const state = this[kState];
2446+
state.writeCb = cb;
2447+
state.writeErr = null;
2448+
state.endErr = null;
2449+
2450+
if (state.flags & STREAM_FLAGS_HAS_TRAILERS) {
2451+
// Trailers are pending, so the writable side cannot be shut down
2452+
// early anyway; there is no point in scheduling the end check.
2453+
state.writePending = 1;
2454+
} else {
2455+
state.writePending = 2;
2456+
// Shutdown write stream right after last chunk is sent
2457+
// so final DATA frame can include END_STREAM flag
2458+
process.nextTick(endCheckNT, this);
2459+
}
23952460

2396-
let waitingForWriteCallback = true;
2397-
let waitingForEndCheck = true;
2398-
let writeCallbackErr;
2399-
let endCheckCallbackErr;
2400-
const done = () => {
2401-
if (waitingForEndCheck || waitingForWriteCallback) return;
2402-
const err = aggregateTwoErrors(endCheckCallbackErr, writeCallbackErr);
2403-
// writeGeneric does not destroy on error and
2404-
// we cannot enable autoDestroy,
2405-
// so make sure to destroy on error.
2406-
if (err) {
2407-
this.destroy(err);
2408-
}
2409-
cb(err);
2410-
};
2461+
// This is invoked both as a method on the write req and as a plain
2462+
// call, so the stream has to be captured here.
24112463
const writeCallback = (err) => {
2412-
waitingForWriteCallback = false;
2413-
writeCallbackErr = err;
2414-
done();
2415-
};
2416-
const endCheckCallback = (err) => {
2417-
waitingForEndCheck = false;
2418-
endCheckCallbackErr = err;
2419-
done();
2464+
state.writeErr = err;
2465+
finishWrite(this);
24202466
};
2421-
// Shutdown write stream right after last chunk is sent
2422-
// so final DATA frame can include END_STREAM flag
2423-
process.nextTick(() => {
2424-
if (writeCallbackErr ||
2425-
!this._writableState.ending ||
2426-
this._writableState.buffered.length ||
2427-
(this[kState].flags & STREAM_FLAGS_HAS_TRAILERS))
2428-
return endCheckCallback();
2429-
debugStreamObj(this, 'shutting down writable on last write');
2430-
shutdownWritable.call(this, endCheckCallback);
2431-
});
24322467

2468+
let req;
24332469
if (writev)
24342470
req = writevGeneric(this, data, writeCallback);
24352471
else

0 commit comments

Comments
 (0)