caddy/modules/caddyhttp/encode/encode_bench_test.go
Sam Ottenhoff 997d3f6b0a
encode: add standard benchmark and conformance harness (#7804)
This is a shared encode_test harness with HTML/JSON/JS/CSS payloads taken from caddyserver.com

Benchmarks:
- BenchmarkStandardEncodingPayloads: raw encoder NewEncoder/Write/Close path
- BenchmarkEncodeHandlerCorpus: full Encode.ServeHTTP middleware path
- Grid: 4 payloads × gzip levels 1/5/9 × zstd fastest/default/best
- Each subtest runs with 4 parallel workers; compare runs on MB/s and allocs/op

Conformance tests:
- Encoder contract: Reset, Flush, Close, and pool-style Reset-after-Close reuse
- Corpus HTTP encoding: Content-Encoding, Vary, ETag suffix, header stripping
- Response semantics: minimum_length, 304, HEAD, range, WebSocket bypass,
  If-None-Match rewrite, Cache-Control no-transform, content-type matcher rejection
2026-06-11 17:55:18 -06:00

169 lines
4.5 KiB
Go

package encode_test
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
)
const (
benchmarkParallelism = 4
handlerBenchWarmupIterations = 5
)
// BenchmarkStandardEncodingPayloads measures raw encoder throughput (NewEncoder → Write → Close)
// across the standard HTML/JSON/JS/CSS payloads and gzip/zstd compression levels.
// Each subtest runs with 4 parallel workers (SetParallelism).
func BenchmarkStandardEncodingPayloads(b *testing.B) {
forEachBenchmarkCase(b, func(b *testing.B, corpus benchmarkCorpus, encCase encoderCase) {
benchmarkEncode(b, corpus.data, encCase.encoding)
})
}
// BenchmarkEncodeHandlerCorpus measures the full encode middleware path (ServeHTTP,
// responseWriter, writer pools) using the same payload and level grid.
func BenchmarkEncodeHandlerCorpus(b *testing.B) {
forEachBenchmarkCase(b, func(b *testing.B, corpus benchmarkCorpus, encCase encoderCase) {
enc := newEncodeHandler(b, encCase, 1)
benchmarkEncodeHandler(b, enc, encCase, corpus)
})
}
func forEachBenchmarkCase(b *testing.B, fn func(b *testing.B, corpus benchmarkCorpus, encCase encoderCase)) {
for _, corpus := range benchmarkCorpora(b) {
for _, encCase := range benchmarkEncoderCases(b) {
b.Run(benchmarkSubtestName(corpus.name, encCase), func(b *testing.B) {
fn(b, corpus, encCase)
})
}
}
}
func benchmarkSubtestName(corpus string, encCase encoderCase) string {
return fmt.Sprintf("payload-%s/encoder-%s/compress-level-%s",
corpus, encCase.encoder, encCase.level)
}
func benchmarkEncode(b *testing.B, payload []byte, encoding encode.Encoding) {
b.Helper()
b.ReportAllocs()
b.SetBytes(int64(len(payload)))
b.SetParallelism(benchmarkParallelism)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
encoder := encoding.NewEncoder()
var dst bytes.Buffer
for pb.Next() {
dst.Reset()
encoder.Reset(&dst)
if _, err := encoder.Write(payload); err != nil {
b.Fatalf("Write() error = %v", err)
}
if err := encoder.Close(); err != nil {
b.Fatalf("Close() error = %v", err)
}
}
})
}
func benchmarkEncodeHandler(b *testing.B, enc *encode.Encode, encCase encoderCase, corpus benchmarkCorpus) {
b.Helper()
b.ReportAllocs()
b.SetBytes(int64(len(corpus.data)))
b.SetParallelism(benchmarkParallelism)
next := corpusHandler(corpus)
w := newBenchmarkResponseWriter()
r := newHandlerBenchRequest(encCase)
warmupEncodeHandler(enc, w, r, next)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
w := newBenchmarkResponseWriter()
r := newHandlerBenchRequest(encCase)
for pb.Next() {
w.reset()
if err := enc.ServeHTTP(w, r, next); err != nil {
b.Fatalf("ServeHTTP() error = %v", err)
}
}
})
}
func newHandlerBenchRequest(encCase encoderCase) *http.Request {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("Accept-Encoding", encCase.encoding.AcceptEncoding())
return r
}
func warmupEncodeHandler(enc *encode.Encode, w *benchmarkResponseWriter, r *http.Request, next caddyhttp.Handler) {
for range handlerBenchWarmupIterations {
w.reset()
if err := enc.ServeHTTP(w, r, next); err != nil {
panic("warmup ServeHTTP: " + err.Error())
}
}
}
// benchmarkResponseWriter is a resettable http.ResponseWriter for handler benchmarks.
// httptest.ResponseRecorder cannot be safely reused because it keeps unexported state.
type benchmarkResponseWriter struct {
header http.Header
code int
body bytes.Buffer
wroteHeader bool
}
func newBenchmarkResponseWriter() *benchmarkResponseWriter {
return &benchmarkResponseWriter{
header: make(http.Header),
}
}
func (w *benchmarkResponseWriter) reset() {
w.code = 0
w.wroteHeader = false
w.body.Reset()
for k := range w.header {
delete(w.header, k)
}
}
func (w *benchmarkResponseWriter) Header() http.Header {
return w.header
}
func (w *benchmarkResponseWriter) Write(p []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
return w.body.Write(p)
}
func (w *benchmarkResponseWriter) WriteHeader(statusCode int) {
if w.wroteHeader {
return
}
w.code = statusCode
w.wroteHeader = true
}
func (w *benchmarkResponseWriter) Flush() {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
}
func corpusHandler(corpus benchmarkCorpus) caddyhttp.Handler {
return caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Content-Type", corpus.contentType)
_, err := w.Write(corpus.data)
return err
})
}