From 3e419a5ab1869792329d1991cce5a5de7567e4a2 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Thu, 25 Jun 2026 00:25:22 +0200 Subject: [PATCH] caddyhttp: presize buffers in hot paths to reduce allocations Right-size three per-request allocations that previously grew from a nil/empty backing array, causing repeated reallocation: - CleanPath: pre-Grow the strings.Builder used when slash collapsing is disabled, instead of letting it reallocate as runes are appended - headers: presize the rewritten value slice in HeaderOps.ApplyTo's Set loop instead of appending from nil - reverseproxy: presize the weights slice in WeightedRoundRobinSelection .Select This commit also adds benchmarks exercising each path. Measured with benchstat with count=20 and GOMAXPROCS=1, with p=0.000: sec/op B/op allocs/op CleanPathNoCollapse 1013.4n -> 828.6n (-18.23%) 120->64 (-46.67%) 4->1 (-75.00%) HeaderOpsApplyToSet 1262.0n -> 955.8n (-24.27%) 255->152 (-40.39%) 9->6 (-33.33%) WeightedRoundRobinSelect 215.8n -> 200.3n ( -7.20%) 112->96 (-14.29%) 2->2 (N/A) --- modules/caddyhttp/caddyhttp.go | 1 + modules/caddyhttp/caddyhttp_bench_test.go | 11 ++++++++ modules/caddyhttp/headers/headers.go | 8 +++--- .../caddyhttp/headers/headers_bench_test.go | 24 ++++++++++++++++++ .../reverseproxy/selectionpolicies.go | 2 +- .../selectionpolicies_bench_test.go | 25 +++++++++++++++++++ 6 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 modules/caddyhttp/caddyhttp_bench_test.go create mode 100644 modules/caddyhttp/headers/headers_bench_test.go create mode 100644 modules/caddyhttp/reverseproxy/selectionpolicies_bench_test.go diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index aacafc92e..761c37a54 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -286,6 +286,7 @@ func CleanPath(p string, collapseSlashes bool) string { // and then remove the remaining temporary characters. const tmpCh = 0xff var sb strings.Builder + sb.Grow(len(p) + 1) for i, ch := range p { if ch == '/' && i > 0 && p[i-1] == '/' { sb.WriteByte(tmpCh) diff --git a/modules/caddyhttp/caddyhttp_bench_test.go b/modules/caddyhttp/caddyhttp_bench_test.go new file mode 100644 index 000000000..c8a71214a --- /dev/null +++ b/modules/caddyhttp/caddyhttp_bench_test.go @@ -0,0 +1,11 @@ +package caddyhttp + +import "testing" + +func BenchmarkCleanPathNoCollapse(b *testing.B) { + const p = "/foo/bar/baz/qux/some/longer/path/segment/here/index.html" + b.ReportAllocs() + for b.Loop() { + _ = CleanPath(p, false) + } +} diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 97eee07ba..da12c08ff 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -243,11 +243,11 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) { // set for fieldName, vals := range ops.Set { fieldName = repl.ReplaceKnown(fieldName, "") - var newVals []string + // use a new slice so we don't overwrite + // the original values in ops.Set + newVals := make([]string, len(vals)) for i := range vals { - // append to new slice so we don't overwrite - // the original values in ops.Set - newVals = append(newVals, repl.ReplaceKnown(vals[i], "")) + newVals[i] = repl.ReplaceKnown(vals[i], "") } hdr.Set(fieldName, strings.Join(newVals, ",")) } diff --git a/modules/caddyhttp/headers/headers_bench_test.go b/modules/caddyhttp/headers/headers_bench_test.go new file mode 100644 index 000000000..08f30c414 --- /dev/null +++ b/modules/caddyhttp/headers/headers_bench_test.go @@ -0,0 +1,24 @@ +package headers + +import ( + "net/http" + "testing" + + "github.com/caddyserver/caddy/v2" +) + +func BenchmarkHeaderOpsApplyToSet(b *testing.B) { + ops := &HeaderOps{ + Set: http.Header{ + "Content-Type": []string{"text/html; charset=utf-8"}, + "Cache-Control": []string{"public", "max-age=3600", "immutable"}, + "X-Custom-Header": []string{"value-one", "value-two"}, + }, + } + repl := caddy.NewReplacer() + b.ReportAllocs() + for b.Loop() { + hdr := make(http.Header) + ops.ApplyTo(hdr, repl) + } +} diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go index cc42b7a3b..3cf55b04e 100644 --- a/modules/caddyhttp/reverseproxy/selectionpolicies.go +++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go @@ -149,7 +149,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, return pool[0] } var index, totalWeight int - var weights []int + weights := make([]int, 0, len(r.Weights)) for _, w := range r.Weights { if w > 0 { diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_bench_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_bench_test.go new file mode 100644 index 000000000..9977d9c65 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/selectionpolicies_bench_test.go @@ -0,0 +1,25 @@ +package reverseproxy + +import ( + "net/http" + "testing" +) + +func BenchmarkWeightedRoundRobinSelect(b *testing.B) { + pool := UpstreamPool{ + {Host: new(Host), Dial: "0.0.0.1"}, + {Host: new(Host), Dial: "0.0.0.2"}, + {Host: new(Host), Dial: "0.0.0.3"}, + {Host: new(Host), Dial: "0.0.0.4"}, + {Host: new(Host), Dial: "0.0.0.5"}, + } + wrrPolicy := &WeightedRoundRobinSelection{ + Weights: []int{5, 4, 3, 2, 1}, + totalWeight: 15, + } + req, _ := http.NewRequest("GET", "/", nil) + b.ReportAllocs() + for b.Loop() { + _ = wrrPolicy.Select(pool, req, nil) + } +}