perf(transport): stop re-deriving per request what does not change per request - #112
Open
burruplambert wants to merge 5 commits into
Open
perf(transport): stop re-deriving per request what does not change per request#112burruplambert wants to merge 5 commits into
burruplambert wants to merge 5 commits into
Conversation
…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.
|
@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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.