mirror of
https://github.com/caddyserver/caddy.git
synced 2026-11-12 13:54:28 +00:00
caddyhttp: presize buffers in hot paths to reduce allocations (#7842)
Some checks are pending
Tests / test (./cmd/caddy/caddy, ~1.27.0, macos-latest, 0, 1.27, mac) (push) Waiting to run
Tests / test (./cmd/caddy/caddy, ~1.27.0, ubuntu-latest, 0, 1.27, linux) (push) Waiting to run
Tests / test (./cmd/caddy/caddy.exe, ~1.27.0, windows-latest, True, 1.27, windows) (push) Waiting to run
Tests / test (s390x on IBM Z) (push) Waiting to run
Tests / goreleaser-check (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, aix) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, linux) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, windows) (push) Waiting to run
Lint / lint (push) Waiting to run
Lint / lint-1 (push) Waiting to run
Lint / lint-2 (push) Waiting to run
Lint / govulncheck (push) Waiting to run
Lint / dependency-review (push) Waiting to run
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Some checks are pending
Tests / test (./cmd/caddy/caddy, ~1.27.0, macos-latest, 0, 1.27, mac) (push) Waiting to run
Tests / test (./cmd/caddy/caddy, ~1.27.0, ubuntu-latest, 0, 1.27, linux) (push) Waiting to run
Tests / test (./cmd/caddy/caddy.exe, ~1.27.0, windows-latest, True, 1.27, windows) (push) Waiting to run
Tests / test (s390x on IBM Z) (push) Waiting to run
Tests / goreleaser-check (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, aix) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, darwin) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, dragonfly) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, freebsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, illumos) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, linux) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, netbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, openbsd) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, solaris) (push) Waiting to run
Cross-Build / build (~1.26.0, 1.26, windows) (push) Waiting to run
Lint / lint (push) Waiting to run
Lint / lint-1 (push) Waiting to run
Lint / lint-2 (push) Waiting to run
Lint / govulncheck (push) Waiting to run
Lint / dependency-review (push) Waiting to run
OpenSSF Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
* 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>
This commit is contained in:
parent
7145e5eba8
commit
c8492563ee
9 changed files with 138 additions and 5 deletions
|
|
@ -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)
|
||||
|
|
|
|||
25
modules/caddyhttp/caddyhttp_bench_test.go
Normal file
25
modules/caddyhttp/caddyhttp_bench_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -259,11 +259,9 @@ func (ops *HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
|
|||
if expanded := repl.ReplaceKnown(fieldName, ""); expanded != fieldName {
|
||||
fieldName = http.CanonicalHeaderKey(expanded)
|
||||
}
|
||||
var newVals []string
|
||||
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], "")
|
||||
}
|
||||
if fieldName == "Set-Cookie" && len(newVals) > 1 {
|
||||
hdr.Del(fieldName)
|
||||
|
|
|
|||
38
modules/caddyhttp/headers/headers_bench_test.go
Normal file
38
modules/caddyhttp/headers/headers_bench_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -552,6 +552,7 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b
|
|||
// of the path, then switching to the escaped parts where
|
||||
// the pattern hints to us wherever % is present.
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(escapedPath))
|
||||
|
||||
// iterate the pattern and escaped path in lock-step;
|
||||
// increment iPattern every time we consume a char from the pattern,
|
||||
|
|
|
|||
|
|
@ -152,7 +152,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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -374,6 +374,7 @@ func escapePathPreservingSlashes(path string) string {
|
|||
// duplicate keys rather than replaces.
|
||||
func buildQueryString(qs string, repl *caddy.Replacer) string {
|
||||
var sb strings.Builder
|
||||
sb.Grow(len(qs))
|
||||
|
||||
// first component must be key, which is the same
|
||||
// as if we just wrote a value in previous iteration
|
||||
|
|
|
|||
30
modules/caddyhttp/rewrite/rewrite_bench_test.go
Normal file
30
modules/caddyhttp/rewrite/rewrite_bench_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright 2015 Matthew Holt and The Caddy Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package rewrite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func BenchmarkBuildQueryString(b *testing.B) {
|
||||
repl := caddy.NewReplacer()
|
||||
const qs = "foo=bar&baz=qux&search=hello+world&page=2&sort=desc&filter=active&id=12345"
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = buildQueryString(qs, repl)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue