@marcel91200 / caddy Mirror · read-only / commits / c8492563
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, 01:00 UTC

caddyhttp: presize buffers in hot paths to reduce allocations (#7842)

* 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) * caddyhttp: presize strings.Builder in query rewrite and path matcher Two builders whose output size is bounded by a known input length were growing from empty, causing repeated reallocation. Seed each with a capacity hint: - rewrite.buildQueryString: pre-Grow to len(qs) (runs per request when rewrite query manipulation is configured) - MatchPath.matchPatternWithEscapeSequence: pre-Grow to len(escapedPath) (reached only when a path pattern contains % escape sequences) Adds a benchmark for buildQueryString, measured with benchstat: BuildQueryString: - sec/op 965.2n -> 743.3n (-22.99%) - B/op 248->80 (-67.74%) - allocs/op 5->1 (-80.00%) The builder previously reallocated four times per call. The matcher path is cold and not separately benchmarked. * chore: add benchmark headers --------- Signed-off-by: Julien Voisin <jvoisin@users.noreply.github.com>

Julien Voisin committed Nov 4, 2026 at 21:26 UTC c8492563eefcb3c5afbc00f25e7b001ffc6c7d11
9 files changed +138 -5
modules/caddyhttp/caddyhttp.go
+1
@@ -286,6 +286,7 @@ func CleanPath(p string, collapseSlashes bool) string {
286 // and then remove the remaining temporary characters.
287 const tmpCh = 0xff
288 var sb strings.Builder
289 + sb.Grow(len(p) + 1)
290 for i, ch := range p {
291 if ch == '/' && i > 0 && p[i-1] == '/' {
292 sb.WriteByte(tmpCh)
modules/caddyhttp/caddyhttp_bench_test.go new
+25
@@ -0,0 +1,25 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyhttp
16 +
17 +import "testing"
18 +
19 +func BenchmarkCleanPathNoCollapse(b *testing.B) {
20 + const p = "/foo/bar/baz/qux/some/longer/path/segment/here/index.html"
21 + b.ReportAllocs()
22 + for b.Loop() {
23 + _ = CleanPath(p, false)
24 + }
25 +}
modules/caddyhttp/headers/headers.go
+2 -4
@@ -259,11 +259,9 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
259 if expanded := repl.ReplaceKnown(fieldName, ""); expanded != fieldName {
260 fieldName = http.CanonicalHeaderKey(expanded)
261 }
262 - var newVals []string
262 + newVals := make([]string, len(vals))
263 for i := range vals {
264 - // append to new slice so we don't overwrite
265 - // the original values in ops.Set
266 - newVals = append(newVals, repl.ReplaceKnown(vals[i], ""))
264 + newVals[i] = repl.ReplaceKnown(vals[i], "")
265 }
266 if fieldName == "Set-Cookie" && len(newVals) > 1 {
267 hdr.Del(fieldName)
modules/caddyhttp/headers/headers_bench_test.go new
+38
@@ -0,0 +1,38 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package headers
16 +
17 +import (
18 + "net/http"
19 + "testing"
20 +
21 + "github.com/caddyserver/caddy/v2"
22 +)
23 +
24 +func BenchmarkHeaderOpsApplyToSet(b *testing.B) {
25 + ops := &HeaderOps{
26 + Set: http.Header{
27 + "Content-Type": []string{"text/html; charset=utf-8"},
28 + "Cache-Control": []string{"public", "max-age=3600", "immutable"},
29 + "X-Custom-Header": []string{"value-one", "value-two"},
30 + },
31 + }
32 + repl := caddy.NewReplacer()
33 + b.ReportAllocs()
34 + for b.Loop() {
35 + hdr := make(http.Header)
36 + ops.ApplyTo(hdr, repl)
37 + }
38 +}
modules/caddyhttp/matchers.go
+1
@@ -552,6 +552,7 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
552 // of the path, then switching to the escaped parts where
553 // the pattern hints to us wherever % is present.
554 var sb strings.Builder
555 + sb.Grow(len(escapedPath))
556
557 // iterate the pattern and escaped path in lock-step;
558 // increment iPattern every time we consume a char from the pattern,
modules/caddyhttp/reverseproxy/selectionpolicies.go
+1 -1
@@ -152,7 +152,7 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
152 return pool[0]
153 }
154 var index, totalWeight int
155 - var weights []int
155 + weights := make([]int, 0, len(r.Weights))
156
157 for _, w := range r.Weights {
158 if w > 0 {
modules/caddyhttp/reverseproxy/selectionpolicies_bench_test.go new
+39
@@ -0,0 +1,39 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package reverseproxy
16 +
17 +import (
18 + "net/http"
19 + "testing"
20 +)
21 +
22 +func BenchmarkWeightedRoundRobinSelect(b *testing.B) {
23 + pool := UpstreamPool{
24 + {Host: new(Host), Dial: "0.0.0.1"},
25 + {Host: new(Host), Dial: "0.0.0.2"},
26 + {Host: new(Host), Dial: "0.0.0.3"},
27 + {Host: new(Host), Dial: "0.0.0.4"},
28 + {Host: new(Host), Dial: "0.0.0.5"},
29 + }
30 + wrrPolicy := &WeightedRoundRobinSelection{
31 + Weights: []int{5, 4, 3, 2, 1},
32 + totalWeight: 15,
33 + }
34 + req, _ := http.NewRequest("GET", "/", nil)
35 + b.ReportAllocs()
36 + for b.Loop() {
37 + _ = wrrPolicy.Select(pool, req, nil)
38 + }
39 +}
modules/caddyhttp/rewrite/rewrite.go
+1
@@ -374,6 +374,7 @@ func escapePathPreservingSlashes(path string) string {
374 // duplicate keys rather than replaces.
375 func buildQueryString(qs string, repl *caddy.Replacer) string {
376 var sb strings.Builder
377 + sb.Grow(len(qs))
378
379 // first component must be key, which is the same
380 // as if we just wrote a value in previous iteration
modules/caddyhttp/rewrite/rewrite_bench_test.go new
+30
@@ -0,0 +1,30 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package rewrite
16 +
17 +import (
18 + "testing"
19 +
20 + "github.com/caddyserver/caddy/v2"
21 +)
22 +
23 +func BenchmarkBuildQueryString(b *testing.B) {
24 + repl := caddy.NewReplacer()
25 + const qs = "foo=bar&baz=qux&search=hello+world&page=2&sort=desc&filter=active&id=12345"
26 + b.ReportAllocs()
27 + for b.Loop() {
28 + _ = buildQueryString(qs, repl)
29 + }
30 +}