Skip to content

perf(transport): stop re-deriving per request what does not change per request - #112

Open
burruplambert wants to merge 5 commits into
sardanioss:mainfrom
burruplambert:header-maps
Open

perf(transport): stop re-deriving per request what does not change per request#112
burruplambert wants to merge 5 commits into
sardanioss:mainfrom
burruplambert:header-maps

Conversation

@burruplambert

@burruplambert burruplambert commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Five commits, each removing work a header path re-derived per request or per response although the result never changed.

buildHeadersMap runs on every response and spent its time three ways: the result map grew from zero, every canonical cased key paid an allocating ToLower, and every entry copied its values into its own slice. The map is now sized to the source, lowercasing goes through lowerHeaderName, which hands back an already lowercase name unchanged and memoises canonical cased ones in a bounded cache that stores copies rather than the peer's strings, and the value copies are carved out of one shared backing array with full slice expressions, so each entry stays as isolated as a separate copy at the cost of one allocation. Fourteen headers with a repeated Set-Cookie: 2500ns and 33 allocations per call to 1460ns and 5.

applyPresetHeaders wrote the preset's fixed headers with Header.Set, so every request re-canonicalised the same keys and allocated a one element slice for each. That request invariant block now lives in presetWireHeaders, derived once per preset and held on the Transport keyed by preset identity, which is sound because a preset is never mutated once a Transport holds it: fingerprint.Get returns a fresh value per call and only construction writes the header fields, so SetPreset installs a new pointer and the next request rebuilds. The XHR sniff, the priority table and the HTTP/1.1 cleanup still run per request. Tests assert that replaying a cached entry writes the same map as deriving afresh, across every built in preset, all three protocols, and both the client hint strip and the TLS only switch. A Chrome preset with five caller headers: 8391ns and 52 allocations per call to 6424ns and 16.

CompleteHeaderOrder lowercased every name it placed although the preset's HPACK table and any caller shuffled order built from it are lowercase already, and built the named prefix and the sorted remainder as two allocations plus a copy. It now builds both in one buffer and sorts the remainder in place. Output is unchanged: same names, same order, same deduplication, same exclusion of the ordering keys, all pinned by the existing suite. A Chrome preset with a caller shuffled order: 5180ns and 18 allocations per call to 4271ns and 4.

mergeCallerHeaders folded the caller's headers over the preset's with Header.Set and Header.Add, which canonicalise the key on every call, and programmatic callers overwhelmingly pass the same lowercase names on every request, the worst case for that conversion. The merge now assigns each entry directly under a memoised canonical key: canonicalHeaderName opens with the same quick scan CanonicalHeaderKey does, hands an already canonical name back as is, hands an invalid name back unchanged exactly as CanonicalHeaderKey would, and remembers any other conversion in a second bounded cache with the same ownership rules as the lowercase one. The value slices come out of one shared backing array, one allocation instead of one per header. The fold semantics are unchanged, and the helper is checked against CanonicalHeaderKey for every byte value in every position class the scan distinguishes. Five lowercase caller headers: 1532ns and 8 allocations per call to 962ns and 1.

The XHR coercion and the priority blocks wrote constant names (Sec-Fetch-Mode, Sec-Fetch-Dest, Sec-Fetch-Site, Accept, Priority) through the same canonicalising accessors. The canonical spelling of a literal is known at the call site, so each write and delete is now one direct map operation. The lowercase spelled Del and Get calls that sat next to canonical ones are dropped rather than converted: those accessors canonicalise their argument, so each pair performed the identical operation twice, and a raw lowercase map entry was never reachable through them; the lowercase priority mirror keeps the direct map access it always used. Behaviour is verified against the previous implementation across every built in preset, all three protocols, and request shapes covering pinned Sec-Fetch-* values and a raw lowercase priority entry. Re-measured on one run against the 1.7.2 shape with the coercion included, the preset block went from 11.7us and 52 allocations per call to 7.8us and 16.

…rcasing

buildHeadersMap runs on every response and spent its time three ways: the
result map grew from zero because make had no size hint, every canonical
cased key paid an allocating ToLower, and every entry copied its values
into its own freshly allocated slice.

The map is now sized to the source header. Lowercasing goes through
lowerHeaderName, which returns an already lowercase name as is and
memoises the conversion of a canonical cased one in a bounded cache, so
neither shape the read paths produce allocates in the steady state. The
per entry copies are carved out of one shared backing array with full
slice expressions, which keeps each entry as isolated as a separate copy
while costing one allocation instead of one per header.

Response header names arrive lowercase over HTTP/2 and HTTP/3 (RFC 9113
8.2.1) and canonical cased over HTTP/1.1, so both paths are covered. The
cache is bounded because the names come from the peer, and it stores
copies rather than the strings it was handed, which would otherwise pin
whole read buffers for the life of the process.

Benchmarked on a response of fourteen headers with a repeated
Set-Cookie: 2500ns and 33 allocations per call before, 1460ns and 5 after.
applyPresetHeaders wrote the preset's headers with Header.Set, so every
request paid CanonicalMIMEHeaderKey on the same fixed set of keys, and
allocated a one element value slice for each of them. The preset's keys
and values do not vary per request, and neither does whether the preset
sends Sec-Fetch-* at all, yet all of it was recomputed per request.

That request invariant part now lives in presetWireHeaders, derived once
and held on the Transport in an atomic pointer, keyed by preset identity.
Writing it is a direct map assignment under the already canonical key,
which is what Set would have produced, and the single element value
slices come out of one backing array sized to the pair count, so the
whole preset block costs one allocation. Presets are immutable once a
Transport holds one: fingerprint.Get returns a fresh preset per call and
only preset construction writes the header fields, so pointer identity is
enough to tell a swap from a reuse. SetPreset installs a new pointer and
the next request rebuilds.

Only the fixed block moved. The XHR sniff, the priority table and the
HTTP/1.1 cleanup below it all depend on the request and still run per
request. applyPresetHeaders now takes the derived set instead of the
preset, so the preset it applies and the set it replays cannot disagree.

The tests assert what the cache has to guarantee: replaying an entry
writes the same header map as deriving the preset afresh, across every
built in preset, all three protocols, and both the client hint strip and
the TLS only switch; cached keys are canonical; and a preset swap is
picked up rather than served from the old entry.

Benchmarked on a Chrome preset with five caller headers: 8391ns and 52
allocations per call before, 6424ns and 16 after.
… names

CompleteHeaderOrder runs on every request and lowercased every name it
placed, though the lists it walks are already lowercase: the preset's
HPACK position table is lowercase by definition, and a caller that
shuffles the order per request builds it from that table. Each ToLower
was a fresh copy of a string that already had the wanted form. The
function also allocated the named prefix and the sorted remainder
separately, then copied the remainder onto the prefix to return it.

Names now go through lowerHeaderName, which returns an already lowercase
name unchanged. The prefix and the remainder are built end to end in one
buffer, so sorting the remainder in place leaves the finished list
contiguous with no second allocation and no copy, and the remainder sorts
with slices.Sort. Output is unchanged: same names, same order, same
deduplication, same exclusion of the ordering keys.

No new test. The existing suite already pins every property this touches:
the named prefix keeps its order while the remainder is sorted, which is
the boundary the single buffer has to get right, and the rest covers
lowercasing, deduplication across sources, ordering key exclusion, and
stability across repeated calls.

Benchmarked on a Chrome preset with a caller supplied shuffled order:
5180ns and 18 allocations per call before, 4271ns and 4 after.
@vercel

vercel Bot commented Sep 4, 2026

Copy link
Copy Markdown

@burruplambert is attempting to deploy a commit to the sardanioss' projects Team on Vercel.

A member of the Team first needs to authorize it.

mergeCallerHeaders folded Request.Headers over the preset's headers with
Header.Set and Header.Add, which canonicalise the key on every call.
Programmatic callers overwhelmingly pass the same lowercase names on
every request, the worst case for that conversion: every key walks the
name twice and allocates the canonical form again.

The merge now assigns each entry directly under a memoised canonical
key. canonicalHeaderName opens with the same quick scan
CanonicalHeaderKey does, so an already-canonical name returns as-is
without touching the cache, an invalid name comes back unchanged
exactly as CanonicalHeaderKey leaves it, and any other name is
converted once and remembered in a cache bounded at 1,024 names with
the same ownership rules as the response-side lowercase cache. The
value slices are carved out of one shared backing array with full
slice expressions, one allocation instead of one per header, the
buildHeadersMap pattern.

The fold semantics are unchanged: the first value replaces whatever
the preset pipeline produced, the rest append, an empty value slice
leaves the entry alone, and ExactHeaders still short-circuits the
merge entirely. canonicalHeaderName is checked against
CanonicalHeaderKey for every byte value in every position class the
quick scan distinguishes.

Merging five lowercase caller headers: 1532ns and 8 allocations to
962ns and 1.
…keys

The XHR coercion and the priority blocks in applyPresetHeaders wrote
constant names (Sec-Fetch-Mode, Sec-Fetch-Dest, Sec-Fetch-Site,
Accept, Priority) through Header.Set, Get and Del, which canonicalise
their argument on every call. The names are literals, so the canonical
spelling is known at the call site; each write and delete is now one
direct map operation.

The lowercase-spelled calls that sat next to canonical ones,
Del("sec-fetch-user"), Del("upgrade-insecure-requests"),
Get("priority") and Del("priority"), are dropped rather than
converted. Those accessors canonicalise their argument, so each pair
performed the identical operation twice, and a raw lowercase map entry
was never reachable through them. The one place that maintains such an
entry, the lowercase priority mirror for callers that bypassed Set
when constructing the request, keeps the direct map access it always
used.

Behaviour is unchanged, verified against the previous implementation
across every built-in preset, all three protocols, both switches, and
request shapes covering pinned Sec-Fetch-* values, a raw lowercase
priority entry, API and form content types.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant