mirror of
https://github.com/caddyserver/caddy.git
synced 2026-08-04 14:58:47 +00:00
fileserver: reduce allocations in calculateEtag
calculateEtag runs for every static file served (and on conditional If-None-Match revalidations). It used a non-presized strings.Builder plus two strconv.FormatInt calls, each allocating an intermediate string and triggering builder growth, which is about six allocations per call. This commit builds the quoted etag into a fixed 32-byte stack array with strconv.AppendInt instead, so only the final string is heap-allocated. The output format is byte-for-byte identical. Two base-36 int64 values plus the two quote bytes are at most 30 bytes (the worst case is MinInt64, which is `1y2p0ij32e8e8`, so 13 digits plus a minus sign, so two times 14, plus the quotes, for a total of 30 characters), so the buffer never spills. And even if it did, which it doesn't append would simply grow it onto the heap without changing the result. Adds a benchmark, as required by the policy: CalculateEtag: - sec/op 347.6n -> 186.2n (-46.42%) - B/op 112 -> 56 (-50%) - allocs/op 6 -> 2 (-66.67%) It removes some CPU work, but more interestingly four of six per-file allocations on a hot path of a very common handler, lowering per-request garbage and GC pressure.
This commit is contained in:
parent
13a4c3f43c
commit
8c35fb2a38
2 changed files with 35 additions and 6 deletions
27
modules/caddyhttp/fileserver/calculate_etag_bench_test.go
Normal file
27
modules/caddyhttp/fileserver/calculate_etag_bench_test.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package fileserver
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type benchFileInfo struct {
|
||||
size int64
|
||||
mtime time.Time
|
||||
}
|
||||
|
||||
func (fi benchFileInfo) Name() string { return "file.txt" }
|
||||
func (fi benchFileInfo) Size() int64 { return fi.size }
|
||||
func (fi benchFileInfo) Mode() fs.FileMode { return 0o644 }
|
||||
func (fi benchFileInfo) ModTime() time.Time { return fi.mtime }
|
||||
func (fi benchFileInfo) IsDir() bool { return false }
|
||||
func (fi benchFileInfo) Sys() any { return nil }
|
||||
|
||||
func BenchmarkCalculateEtag(b *testing.B) {
|
||||
fi := benchFileInfo{size: 1234567, mtime: time.Unix(1700000000, 123456789)}
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
_ = calculateEtag(fi)
|
||||
}
|
||||
}
|
||||
|
|
@ -765,12 +765,14 @@ func calculateEtag(d os.FileInfo) string {
|
|||
if !usefulModTime(mtime) {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteRune('"')
|
||||
sb.WriteString(strconv.FormatInt(mtime.UnixNano(), 36))
|
||||
sb.WriteString(strconv.FormatInt(d.Size(), 36))
|
||||
sb.WriteRune('"')
|
||||
return sb.String()
|
||||
// A quoted etag of two base-36 int64 values is 30 bytes, thus fitting
|
||||
// comfortably in 32 bytes, on the stack to prevent allocations
|
||||
var buf [32]byte
|
||||
b := append(buf[:0], '"')
|
||||
b = strconv.AppendInt(b, mtime.UnixNano(), 36)
|
||||
b = strconv.AppendInt(b, d.Size(), 36)
|
||||
b = append(b, '"')
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Finds the first corresponding etag file for a given file in the file system and return its content
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue