@marcel91200 / caddy Mirror · read-only / commits / 323e3fe4
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 08:30 UTC

encode: reduce allocations in AcceptedEncodings (#7847)

AcceptedEncodings runs for every request when response compression is enabled, so its allocations contribute directly to per-request garbage and GC pressure. While the gains are modest, this cuts allocations by two-thirds and bytes nearly in half on a hot path that executes on every compressed request. Fewer allocations means less work for the garbage collector, which at high request rates translates into lower GC frequency and steadier tail latency, not just faster execution of this one function in isolation. For each token in the Accept-Encoding header it called strings.Split(accepted, ";"), allocating a throwaway slice per encoding, and grew the prefs slice from empty. This commit replaces the per-token Split with strings.Cut (zero allocation) and presize prefs from the comma count. As requested per the policy, this commit adds a benchmark to exercise the change. With header "gzip, deflate, br;q=0.9, zstd;q=0.8": AcceptedEncodings - sec/op 1158.5n -> 771.8n (-33.38%) - B/op 408 -> 216 (-47.06%) - allocs/op 9 -> 3 (-66.67%) Co-authored-by: @dunglas

Julien Voisin committed Jul 31, 2026 at 16:23 UTC 323e3fe4b70debcfc5867d41f849713bd99065ec
2 files changed +24 -6
modules/caddyhttp/encode/accepted_encodings_bench_test.go new
+17
@@ -0,0 +1,17 @@
1 +package encode
2 +
3 +import (
4 + "net/http"
5 + "net/http/httptest"
6 + "testing"
7 +)
8 +
9 +func BenchmarkAcceptedEncodings(b *testing.B) {
10 + r := httptest.NewRequest(http.MethodGet, "/", nil)
11 + r.Header.Set("Accept-Encoding", "gzip, deflate, br;q=0.9, zstd;q=0.8")
12 + prefer := []string{"zstd", "br", "gzip"}
13 + b.ReportAllocs()
14 + for b.Loop() {
15 + _ = AcceptedEncodings(r, prefer)
16 + }
17 +}
modules/caddyhttp/encode/encode.go
+7 -6
@@ -504,17 +504,18 @@ func AcceptedEncodings(r *http.Request, preferredOrder []string) []string {
504 }
505 websocketKey := r.Header.Get("Sec-Websocket-Key")
506
507 - prefs := []encodingPreference{}
507 + prefs := make([]encodingPreference, 0, strings.Count(acceptEncHeader, ",")+1)
508
509 for accepted := range strings.SplitSeq(acceptEncHeader, ",") {
510 - parts := strings.Split(accepted, ";")
511 - encName := strings.ToLower(strings.TrimSpace(parts[0]))
510 + encName, params, found := strings.Cut(accepted, ";")
511 + encName = strings.ToLower(strings.TrimSpace(encName))
512
513 // determine q-factor
514 qFactor := 1.0
515 - if len(parts) > 1 {
516 - qFactorStr := strings.ToLower(strings.TrimSpace(parts[1]))
517 - if strings.HasPrefix(qFactorStr, "q=") {
515 + if found {
516 + qFactorStr, _, _ := strings.Cut(params, ";")
517 + qFactorStr = strings.TrimSpace(qFactorStr)
518 + if len(qFactorStr) >= 2 && strings.EqualFold(qFactorStr[:2], "q=") {
519 if qFactorFloat, err := strconv.ParseFloat(qFactorStr[2:], 32); err == nil {
520 if qFactorFloat >= 0 && qFactorFloat <= 1 {
521 qFactor = qFactorFloat