mirror of
https://github.com/caddyserver/caddy.git
synced 2026-06-23 09:30:35 +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
224 lines
6.9 KiB
Go
224 lines
6.9 KiB
Go
// Package encode_test provides the standard encode benchmark and conformance suite
|
||
// for Caddy's gzip and zstd encoder modules.
|
||
//
|
||
// Run encoder-level benchmarks (direct NewEncoder calls):
|
||
//
|
||
// go test -bench=BenchmarkStandardEncodingPayloads -benchmem ./modules/caddyhttp/encode/
|
||
//
|
||
// Run middleware-level benchmarks (Encode.ServeHTTP, writer pools, responseWriter):
|
||
//
|
||
// go test -bench=BenchmarkEncodeHandlerCorpus -benchmem ./modules/caddyhttp/encode/
|
||
//
|
||
// Benchmark subtest names:
|
||
//
|
||
// payload-{html|json|js|css}/encoder-{gzip|zstd}/compress-level-{N|fastest|...}
|
||
//
|
||
// Each subtest uses 4 parallel workers (benchmarkParallelism in encode_bench_test.go).
|
||
// Go may append -{GOMAXPROCS} to the printed benchmark name; ignore it when comparing runs.
|
||
//
|
||
// Grid: 4 payloads × 6 compress levels (gzip 1/5/9, zstd fastest/default/best) = 24 subtests
|
||
// per benchmark function (48 total with encoder + handler).
|
||
//
|
||
// Run conformance tests (Reset/Flush/Close, Vary, ETag, 304/HEAD/range/WebSocket, minimum_length):
|
||
//
|
||
// go test -run='TestStandardEncoderContract|TestEncodeCorpusResponse|TestEncodeResponseSemantics' ./modules/caddyhttp/encode/
|
||
//
|
||
// Conformance also covers Cache-Control no-transform, content-type matcher rejection,
|
||
// and encoder Reset-after-Close reuse (pool pattern).
|
||
package encode_test
|
||
|
||
import (
|
||
"bytes"
|
||
stdgzip "compress/gzip"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"testing"
|
||
|
||
"github.com/klauspost/compress/zstd"
|
||
|
||
"github.com/caddyserver/caddy/v2"
|
||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/encode"
|
||
caddygzip "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/gzip"
|
||
caddyzstd "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
|
||
)
|
||
|
||
// benchmarkCorpus is a fixed payload used by both benchmarks and conformance tests.
|
||
type benchmarkCorpus struct {
|
||
name string
|
||
data []byte
|
||
contentType string
|
||
}
|
||
|
||
var (
|
||
benchmarkGzipLevels = []int{1, 5, 9}
|
||
benchmarkZstdLevels = []string{"fastest", "default", "best"}
|
||
)
|
||
|
||
type encoderCase struct {
|
||
name string // conformance subtest label, e.g. gzip-level-5
|
||
encoder string // gzip or zstd
|
||
level string // gzip numeric level or zstd level name
|
||
encoding encode.Encoding
|
||
decompress func([]byte) ([]byte, error)
|
||
contentType string
|
||
}
|
||
|
||
func benchmarkCorpora(tb testing.TB) []benchmarkCorpus {
|
||
tb.Helper()
|
||
|
||
return []benchmarkCorpus{
|
||
{name: "html", data: readBenchmarkPayload(tb, "testdata/caddy_home.html"), contentType: "text/html; charset=utf-8"},
|
||
{name: "json", data: readBenchmarkPayload(tb, "testdata/caddy_config_http_servers.json"), contentType: "application/json"},
|
||
{name: "js", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.js"), contentType: "application/javascript"},
|
||
{name: "css", data: readBenchmarkPayload(tb, "testdata/caddy_asciinema_player.css"), contentType: "text/css"},
|
||
}
|
||
}
|
||
|
||
func readBenchmarkPayload(tb testing.TB, filename string) []byte {
|
||
tb.Helper()
|
||
|
||
data, err := os.ReadFile(filename)
|
||
if err != nil {
|
||
tb.Fatalf("reading benchmark payload %s: %v", filename, err)
|
||
}
|
||
return data
|
||
}
|
||
|
||
// conformanceLargeBody returns a payload large enough to exceed default minimum_length.
|
||
func conformanceLargeBody() []byte {
|
||
data, err := os.ReadFile("testdata/caddy_home.html")
|
||
if err != nil {
|
||
panic("conformanceLargeBody: " + err.Error())
|
||
}
|
||
return data
|
||
}
|
||
|
||
func standardEncoderCases(t testing.TB) []encoderCase {
|
||
t.Helper()
|
||
return provisionEncoderCases(t, []int{5}, []string{"default"})
|
||
}
|
||
|
||
func benchmarkEncoderCases(t testing.TB) []encoderCase {
|
||
t.Helper()
|
||
return provisionEncoderCases(t, benchmarkGzipLevels, benchmarkZstdLevels)
|
||
}
|
||
|
||
func provisionEncoderCases(t testing.TB, gzipLevels []int, zstdLevels []string) []encoderCase {
|
||
t.Helper()
|
||
|
||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||
t.Cleanup(cancel)
|
||
|
||
var cases []encoderCase
|
||
for _, level := range gzipLevels {
|
||
gzipEncoding := &caddygzip.Gzip{Level: level}
|
||
if err := gzipEncoding.Provision(ctx); err != nil {
|
||
t.Fatalf("gzip level %d Provision() error = %v", level, err)
|
||
}
|
||
cases = append(cases, encoderCase{
|
||
name: fmt.Sprintf("gzip-level-%d", level),
|
||
encoder: "gzip",
|
||
level: fmt.Sprintf("%d", level),
|
||
encoding: gzipEncoding,
|
||
decompress: decompressGzip,
|
||
contentType: "text/plain",
|
||
})
|
||
}
|
||
for _, level := range zstdLevels {
|
||
zstdEncoding := &caddyzstd.Zstd{Level: level}
|
||
if err := zstdEncoding.Provision(ctx); err != nil {
|
||
t.Fatalf("zstd level %q Provision() error = %v", level, err)
|
||
}
|
||
cases = append(cases, encoderCase{
|
||
name: "zstd-level-" + level,
|
||
encoder: "zstd",
|
||
level: level,
|
||
encoding: zstdEncoding,
|
||
decompress: decompressZstd,
|
||
contentType: "text/plain",
|
||
})
|
||
}
|
||
return cases
|
||
}
|
||
|
||
func newEncodeHandler(tb testing.TB, encCase encoderCase, minLength int) *encode.Encode {
|
||
tb.Helper()
|
||
|
||
encodingName := encCase.encoding.AcceptEncoding()
|
||
enc := &encode.Encode{
|
||
EncodingsRaw: caddy.ModuleMap{
|
||
encodingName: caddyconfig.JSON(encCase.encoding, nil),
|
||
},
|
||
Prefer: []string{encodingName},
|
||
MinLength: minLength,
|
||
}
|
||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||
tb.Cleanup(cancel)
|
||
if err := enc.Provision(ctx); err != nil {
|
||
tb.Fatalf("Provision() error = %v", err)
|
||
}
|
||
if err := enc.Validate(); err != nil {
|
||
tb.Fatalf("Validate() error = %v", err)
|
||
}
|
||
return enc
|
||
}
|
||
|
||
func assertDecompresses(t *testing.T, encCase encoderCase, compressed, original []byte) {
|
||
t.Helper()
|
||
|
||
decompressed, err := encCase.decompress(compressed)
|
||
if err != nil {
|
||
t.Fatalf("decompress %s: %v", encCase.name, err)
|
||
}
|
||
if !bytes.Equal(decompressed, original) {
|
||
t.Fatalf("decompressed len = %d, want len = %d", len(decompressed), len(original))
|
||
}
|
||
}
|
||
|
||
// encodeAndVerifyRoundTrip exercises Write → Flush → Write → Close and verifies
|
||
// the compressed stream round-trips to original.
|
||
func encodeAndVerifyRoundTrip(t *testing.T, encCase encoderCase, encoder encode.Encoder, original []byte) {
|
||
t.Helper()
|
||
|
||
var compressed bytes.Buffer
|
||
encoder.Reset(&compressed)
|
||
|
||
if _, err := encoder.Write(original[:len(original)/2]); err != nil {
|
||
t.Fatalf("Write() error = %v", err)
|
||
}
|
||
if err := encoder.Flush(); err != nil {
|
||
t.Fatalf("Flush() error = %v", err)
|
||
}
|
||
if compressed.Len() == 0 {
|
||
t.Fatal("Flush() wrote no compressed bytes")
|
||
}
|
||
if _, err := encoder.Write(original[len(original)/2:]); err != nil {
|
||
t.Fatalf("Write() error = %v", err)
|
||
}
|
||
if err := encoder.Close(); err != nil {
|
||
t.Fatalf("Close() error = %v", err)
|
||
}
|
||
|
||
assertDecompresses(t, encCase, compressed.Bytes(), original)
|
||
}
|
||
|
||
func decompressGzip(compressed []byte) ([]byte, error) {
|
||
reader, err := stdgzip.NewReader(bytes.NewReader(compressed))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer reader.Close()
|
||
return io.ReadAll(reader)
|
||
}
|
||
|
||
func decompressZstd(compressed []byte) ([]byte, error) {
|
||
decoder, err := zstd.NewReader(nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer decoder.Close()
|
||
return decoder.DecodeAll(compressed, nil)
|
||
}
|