mirror of
https://github.com/caddyserver/caddy.git
synced 2026-06-23 01:20:32 +00:00
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
169 lines
4.5 KiB
Go
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
|
|
})
|
|
}
|