From 4e1877565b00e786fe0dd815c04d0f94d8ca48cb Mon Sep 17 00:00:00 2001 From: Burrup Lambert Date: Wed, 1 Jul 2026 15:20:56 +0100 Subject: [PATCH 1/3] perf: use klauspost/compress for gzip and flate decompression The stdlib compress/gzip and compress/flate use a slower Huffman decoder. klauspost/compress is a drop-in replacement with a faster implementation, measured at ~5% total CPU reduction in production profiling of a high-throughput HTTP client workload. --- transport.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/transport.go b/transport.go index ba675a29..f121fcbc 100644 --- a/transport.go +++ b/transport.go @@ -12,8 +12,8 @@ package http import ( "bufio" "bytes" - "compress/flate" - "compress/gzip" + "github.com/klauspost/compress/flate" + "github.com/klauspost/compress/gzip" "compress/zlib" "container/list" "context" From f9d572be60f6d4f912e5dcd2dc7d2c386686de4e Mon Sep 17 00:00:00 2001 From: Burrup Lambert Date: Wed, 1 Jul 2026 15:21:31 +0100 Subject: [PATCH 2/3] perf: eliminate per-comparison strings.ToLower in header sorting and encoding headerSorter.Less called strings.ToLower on every comparison during sort, causing O(n log n) allocations per request. Pre-compute lowercase keys once before sorting and swap them in parallel with the key-value pairs. In http2 encodeHeaders and encodeTrailers, replace strings.ToLower with lowerHeader which uses the existing common header map for zero-alloc lookups on standard headers. Combined savings: ~2% total CPU in production profiling. --- header.go | 27 +++++++++++++++++++-------- http2/transport.go | 4 ++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/header.go b/header.go index b6481131..da0843eb 100644 --- a/header.go +++ b/header.go @@ -175,21 +175,23 @@ type HeaderKeyValues struct { // It's used as a pointer, so it can fit in a sort.Interface // interface value without allocation. type headerSorter struct { - kvs []HeaderKeyValues - order map[string]int + kvs []HeaderKeyValues + order map[string]int + lowerKeys []string } -func (s *headerSorter) Len() int { return len(s.kvs) } -func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] } +func (s *headerSorter) Len() int { return len(s.kvs) } +func (s *headerSorter) Swap(i, j int) { + s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] + s.lowerKeys[i], s.lowerKeys[j] = s.lowerKeys[j], s.lowerKeys[i] +} func (s *headerSorter) Less(i, j int) bool { // If the order isn't defined, sort lexicographically. if s.order == nil { return s.kvs[i].Key < s.kvs[j].Key } - //idxi, iok := s.order[s.kvs[i].Key] - //idxj, jok := s.order[s.kvs[j].Key] - idxi, iok := s.order[strings.ToLower(s.kvs[i].Key)] - idxj, jok := s.order[strings.ToLower(s.kvs[j].Key)] + idxi, iok := s.order[s.lowerKeys[i]] + idxj, jok := s.order[s.lowerKeys[j]] if !iok && !jok { return s.kvs[i].Key < s.kvs[j].Key } else if !iok && jok { @@ -242,6 +244,15 @@ func (h Header) SortedKeyValuesBy(order map[string]int, exclude map[string]bool) } hs.kvs = kvs hs.order = order + + if cap(hs.lowerKeys) < len(kvs) { + hs.lowerKeys = make([]string, len(kvs)) + } + hs.lowerKeys = hs.lowerKeys[:len(kvs)] + for i, kv := range kvs { + hs.lowerKeys[i] = strings.ToLower(kv.Key) + } + sort.Sort(hs) return kvs, hs diff --git a/http2/transport.go b/http2/transport.go index 594516af..17264353 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -1914,7 +1914,7 @@ func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool, trail return } - name = strings.ToLower(name) + name = lowerHeader(name) cc.writeHeader(name, value) if traceHeaders { traceWroteHeaderField(trace, name, value) @@ -1964,7 +1964,7 @@ func (cc *ClientConn) encodeTrailers(req *http.Request) ([]byte, error) { for k, vv := range req.Trailer { // Transfer-Encoding, etc.. have already been filtered at the // start of RoundTrip - lowKey := strings.ToLower(k) + lowKey := lowerHeader(k) for _, v := range vv { cc.writeHeader(lowKey, v) } From bf3807eb0282a8e3494b056e0073007fb6ba1c04 Mon Sep 17 00:00:00 2001 From: Burrup Lambert Date: Tue, 28 Jul 2026 15:03:46 +0100 Subject: [PATCH 3/3] fix: reset pooled headerSorter state in SortedKeyValues SortedKeyValues sorts without populating lowerKeys, but Swap swapped lowerKeys unconditionally, panicking with index out of range for any header map without a Header-Order key (reachable from Header.Write, WriteSubset, and the http2 enumerateHeaders fallback). Guard the lowerKeys swap and reset order/lowerKeys on sorters reused from the pool, so a sorter previously used by SortedKeyValuesBy cannot leak a stale order (or stale lowered keys) into an orderless sort. Adds regression tests for both the panic and the pool-reuse case. --- header.go | 11 ++++++++++- header_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/header.go b/header.go index da0843eb..73234dcc 100644 --- a/header.go +++ b/header.go @@ -183,7 +183,11 @@ type headerSorter struct { func (s *headerSorter) Len() int { return len(s.kvs) } func (s *headerSorter) Swap(i, j int) { s.kvs[i], s.kvs[j] = s.kvs[j], s.kvs[i] - s.lowerKeys[i], s.lowerKeys[j] = s.lowerKeys[j], s.lowerKeys[i] + // lowerKeys is only populated by SortedKeyValuesBy; SortedKeyValues + // sorts without it. + if s.lowerKeys != nil { + s.lowerKeys[i], s.lowerKeys[j] = s.lowerKeys[j], s.lowerKeys[i] + } } func (s *headerSorter) Less(i, j int) bool { // If the order isn't defined, sort lexicographically. @@ -225,6 +229,11 @@ func (h Header) SortedKeyValues(exclude map[string]bool) (kvs []HeaderKeyValues, mutex.RUnlock() } hs.kvs = kvs + // Reset state a pooled sorter may carry from a previous + // SortedKeyValuesBy call: a stale order would make Less sort by the + // wrong order (and index lowerKeys, which this path leaves empty). + hs.order = nil + hs.lowerKeys = nil sort.Sort(hs) return kvs, hs } diff --git a/header_test.go b/header_test.go index 3d9361bd..c409a0fe 100644 --- a/header_test.go +++ b/header_test.go @@ -6,6 +6,7 @@ package http import ( "bytes" + "io" "reflect" "runtime" "testing" @@ -359,3 +360,51 @@ func TestHTTP1HeaderOrder(t *testing.T) { t.Fatalf("got:\n%swant:\n%s", buf.String(), expected) } } + +func TestHeaderWriteWithoutOrder(t *testing.T) { + // Sorting without a HeaderOrderKey goes through SortedKeyValues, + // which does not populate headerSorter.lowerKeys. Swap must not + // touch lowerKeys in that case. + h := Header{ + "Zebra": {"1"}, + "Apple": {"2"}, + "Mango": {"3"}, + "Banana": {"4"}, + "Orange": {"5"}, + "Kiwi": {"6"}, + "Grape": {"7"}, + "Lemon": {"8"}, + "Peach": {"9"}, + "Cherry": {"10"}, + "Plum": {"11"}, + "Apricot": {"12"}, + } + if err := h.Write(io.Discard); err != nil { + t.Fatal(err) + } +} + +func TestHeaderSorterPoolReuse(t *testing.T) { + // A sorter used by SortedKeyValuesBy carries order and lowerKeys. + // When it is pulled from the pool again by SortedKeyValues (no + // order), the stale state must not be used. + ordered := Header{ + "Zebra": {"1"}, + "Apple": {"2"}, + } + // Deliberately the reverse of lexicographic order. + _, hs := ordered.SortedKeyValuesBy(map[string]int{"zebra": 0, "apple": 1}, nil) + headerSorterPool.Put(hs) + + plain := Header{ + "Apple": {"1"}, + "Banana": {"2"}, + "Zebra": {"3"}, + } + kvs, _ := plain.SortedKeyValues(nil) + for i := 1; i < len(kvs); i++ { + if kvs[i-1].Key > kvs[i].Key { + t.Fatalf("keys not sorted lexicographically: %q before %q", kvs[i-1].Key, kvs[i].Key) + } + } +}