Skip to content

[v26.x backport] http2: reduce per-request allocations#64663

Open
mcollina wants to merge 3 commits into
nodejs:v26.x-stagingfrom
mcollina:backport-64265-v26.x-staging
Open

[v26.x backport] http2: reduce per-request allocations#64663
mcollina wants to merge 3 commits into
nodejs:v26.x-stagingfrom
mcollina:backport-64265-v26.x-staging

Conversation

@mcollina

@mcollina mcollina commented Jul 21, 2026

Copy link
Copy Markdown
Member

Backport the v26-compatible parts of #64265.

Included:

  • header construction and compat timeout-handler allocation reductions;
  • write-path closure reductions; and
  • response-option allocation reductions.

Excluded changes require the HTTP/2 stream-lifecycle work in #63249 or fail v26 regression tests: stream lifecycle/pending-stream changes, trailer-round-trip changes, native auto-empty trailers, and scheduled-callback reductions.

Tests:

  • make -j4
  • tools/test.py http2
  • test-h2-large-header-cause-client-to-hangup
  • test-h2leak-destroy-session-on-socket-ended
  • test-http2-many-writes-and-destroy

mcollina added 3 commits July 7, 2026 14:24
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: nodejs#64265
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Tim Perry <pimterry@gmail.com>
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>
PR-URL: nodejs#64265
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Tim Perry <pimterry@gmail.com>
respond() copied the user-provided options object on every call just
so it could normalize and locally flip options.endStream, and
prepareResponseHeadersObject() then looked the :status and date
fields up again on the dictionary-mode null-prototype headers copy
it had just built. Use a local variable for endStream and pick up
:status/date while copying the headers instead.

No measurable throughput change on its own; this removes an object
clone and several dictionary-mode property lookups per response.

Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: nodejs#64265
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Tim Perry <pimterry@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/http
  • @nodejs/http2
  • @nodejs/net

@nodejs-github-bot nodejs-github-bot added http2 Issues or PRs related to the http2 subsystem. needs-ci PRs that need a full CI run. v26.x Issues that can be reproduced on v26.x or PRs targeting the v26.x-staging branch. labels Jul 21, 2026
@aduh95 aduh95 changed the title http2: reduce per-request allocations [v26.x backport] http2: reduce per-request allocations Jul 21, 2026
@aduh95

aduh95 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The commit message does not match the ones that landed on main, the dates are also different, can you fix?

@mcollina
mcollina force-pushed the backport-64265-v26.x-staging branch from 38d5541 to 6bf6d34 Compare July 21, 2026 22:16
@mcollina

Copy link
Copy Markdown
Member Author

Updated the branch: the retained commits now preserve the messages and timestamps from main. I also reduced the backport to the v26-compatible hunks; the previously included lifecycle and trailer changes regress v26 HTTP/2 tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

http2 Issues or PRs related to the http2 subsystem. needs-ci PRs that need a full CI run. v26.x Issues that can be reproduced on v26.x or PRs targeting the v26.x-staging branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants