http: cut per-message outgoing overhead

AN
20d ago
+145-33

Behavior-preserving performance work on lib/_http_outgoing.js, reducing
per-message and per-header overhead on the outgoing-message hot paths:

  • Cache the lenient-header-validation decision per message. It was
    re-derived on every setHeader/appendHeader call (and per addTrailers
    key) through a six-branch this.req?.socket?.server? walk, although the
    inputs are fixed once the message is constructed. The decision is now
    resolved lazily on first use and cached on the instance.
  • Pre-filter the known-field matcher on (length, first character).
    matchHeader lower-cased every outgoing header name of length 4–17 just
    to feed a switch with eight known fields. Ordinary headers
    (X-Request-Id, Cache-Control, …) now skip the per-header
    toLowerCase() allocation; the filter only rejects names that cannot
    possibly match (| 0x20 lower-cases ASCII letters and maps no other token
    character onto a letter, and every known-field length is enumerated).
  • Load the socket prototype accessor once per body write instead of
    invoking the getter three times in write_.
  • Hoist repeated property loads the engine cannot fold across calls:
    _header in _send, headers.length across processHeader calls,
    _keepAliveTimeout/_maxRequestsPerSocket/_contentLength in
    _storeHeader, value.length in processHeader.
  • Flush corked chunked buffers through a shared callback runner instead
    of allocating a fresh closure per flush.

The hoists mirror what Bun's fork of these files carries on top of the shared
lineage (bun/src/js/node/_http_outgoing.ts: write_ msgSocket local,
_send header local, _storeHeader/processHeader length locals,
runChunkCallbacks, flat one-shot setHeader validation); the matcher
pre-filter is profiling-driven on top of that set.

Benchmarks

The stock benchmark/http serving benchmarks route through a load generator
(test-double/autocannon); on my machine their run-to-run CV is 8–25%, which
cannot resolve effects of this size — across ~60 captured configs they show
consistently positive means and zero statistically significant
regressions
(headers, incoming_headers, chunked, end-vs-write-end,
client-request-body, create-clientrequest, check_*; Welch t-test on
benchmark/compare.js CSVs, 30–90 runs).

The claimed effect is from mechanism benchmarks that drive the shipped
classes through the public API (http.createServer + new ServerResponse(req) + setHeader/flushHeaders), one fresh process per
sample, 30 interleaved samples per binary, Welch t-test:

caseimprovementp
flushHeaders() with five headers, per message+8.25%6.2e-11
24-header response (_storeHeader dominated)+4.26%6.7e-7
tight setHeader loop (three headers × 2e6)+2.64%2.0e-3

Sign-stable in an independent 15-sample repeat (+6.8% / +4.7% / +0.5%).
Raw CSVs and the analysis scripts are available on request.

Correctness

  • No observable behavior change: header order/content, error types, and
    event ordering are preserved; the validation-leniency inputs are fixed at
    message construction, so caching the derived boolean is not observable
    through supported usage.
  • All test/parallel/test-http-*, test-http.js, and test-https-* pass.
  • The new test-http-outgoing-known-header-casing.js locks the matcher
    semantics: known fields keep being recognized in any casing
    (CoNnEcTiOn, CONTENT-LENGTH), and unknown names sharing a known
    field's length and first letter (Dote, Xonnection) are emitted
    verbatim without affecting framing. The test also passes on the
    unpatched binary.

AI assistance disclosure

This is an AI-assisted pull request. The survey of Bun's implementation, the
implementation, the benchmark methodology and analysis, the regression test,
and this description were produced by an AI agent (Grok, xAI) operating
under my direction; commits carry Assisted-by: Grok. I reviewed and take
responsibility for every change.