perf: do header clone, augment and sort once in http2 encodeHeaders - #20
Open
burruplambert wants to merge 2 commits into
Open
perf: do header clone, augment and sort once in http2 encodeHeaders#20burruplambert wants to merge 2 commits into
burruplambert wants to merge 2 commits into
Conversation
headerSorter instances are pooled and shared between SortedKeyValuesBy and SortedKeyValues, but SortedKeyValues never cleared hs.order. A sorter previously used for an ordered sort would keep its order map and apply it to a later orderless sort, producing the wrong header order whenever the new header's lowercased keys collide with entries in the stale order map. Reset hs.order before sorting in SortedKeyValues and add a regression test.
burruplambert
force-pushed
the
perf/h2-encode-headers-single-pass
branch
from
August 2, 2026 13:18
4ea3b00 to
385022a
Compare
encodeHeaders enumerates the request headers twice - once to count the header list size against peerMaxHeaderListSize, once to write them. The enumerate closure cloned req.Header, added content-length and accept-encoding, built the Header-Order: index map and ran the full sort on every call, so all of that work ran twice per request. Hoist the clone/augment/sort out of the closure so it runs once and both passes iterate the precomputed result. Also pass nil instead of an empty exclude map to SortedKeyValues/SortedKeyValuesBy - the map was allocated per request and only ever read. The emitted header sequence is unchanged, including the existing behavior that the magic Header-Order:/PHeader-Order: entries are counted by the size pass and skipped by the write pass. Mirrored in h2_bundle.go. Adds BenchmarkClientConnEncodeHeaders, which measures header encoding directly; the existing BenchmarkClientRequestHeaders cannot run here because a round trip without a configured pseudo-header order fails (pre-existing issue).
burruplambert
force-pushed
the
perf/h2-encode-headers-single-pass
branch
from
August 2, 2026 13:21
385022a to
985792c
Compare
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.
Depends on #18
This branch carries #18's commit as well as its own. The ordering test added here needs it: without that reset, a sorter returned to the pool by an earlier ordered write still has its order map set, and a later orderless sort silently uses it. That is observable in the test suite - run
TestHeaderOrderand then encode a request with no ordering keys, andaccept-encodingsorts to the front because it happens to appear in the earlier test'sHeader-Order:list.Merge #18 first and this rebases to a single commit, or merge this and #18 becomes redundant.
Summary
(*ClientConn).encodeHeadersenumerates the request headers twice - once to count the header list size againstpeerMaxHeaderListSize, once to write them. TheenumerateHeadersclosure clonedreq.Header, added content-length and accept-encoding, built theHeader-Order:index map and ran the full sort on every call, so all of that work ran twice per request.This hoists the clone, augment and sort out of the closure so they run once, and both passes iterate the precomputed result. It also passes
nilinstead of an empty exclude map toSortedKeyValues/SortedKeyValuesBy- the map was allocated per request and only ever read.The clone stays: it is what keeps the added headers off the caller's
req.Header, which is why it was introduced in 31b4c55. It just no longer happens twice.Tests
TestClientConnEncodeHeadersOrderpins the exact emitted header sequence across the combinations of the two ordering keys being present or absent:Transport.PseudoHeaderOrder(no pseudo-headers are emitted at all on this path; pre-existing behavior, see the note below)PHeader-Order:onlyHeader-Order:only, with some keys absent from the listIt passes unchanged against the base, which is the point - it pins existing behavior rather than describing the new code.
Measurements
BenchmarkClientConnEncodeHeadersmeasures header encoding directly. Against the base:In production profiling of a header-heavy client,
encodeHeadersfell from 10.7% to 7.1% of total CPU.Note on the existing benchmark
BenchmarkClientRequestHeaderscannot be used to measure this, because it fails on master: a round trip with noPHeader-Order:header and noTransport.PseudoHeaderOrderemits no pseudo-headers at all and the server rejects the stream with PROTOCOL_ERROR.TestTransportRequestPathPseudopanics on master in the same area (cc.tis nil). Both reproduce identically without this change and are not touched here, since fixing them changes wire behavior and belongs in its own PR.