mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-09 22:08:15 +00:00
3dc962b301
gzip.NewWriterLevel allocates fresh deflate window and hash table
buffers (~1.18 MiB) on every request. This commit pools them in a closure-local
sync.Pool so each middleware instance reuses its writers.
The level is validated once at setup (NewWriterLevel against
io.Discard); pooled writers are reset to io.Discard on Put so the
pool doesn't pin response writers between requests.
Only call site is RenderIndex (lib/http.go), which serves the
challenge page, so this directly cuts the per-challenge allocation
footprint.
I benchmarked the change using the following benchmark,
put in the commit message instead of in a file since it's pretty much useless
outside of this particular change.
```
package internal
import (
"io"
"net/http"
"net/http/httptest"
"testing"
)
func BenchmarkGzipMiddleware(b *testing.B) {
payload := make([]byte, 4096)
for i := range payload {
payload[i] = byte(i)
}
inner := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(payload)
})
h := GzipMiddleware(1, inner)
b.ReportAllocs()
b.RunParallel(func(pb *testing.PB) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
for pb.Next() {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
io.Copy(io.Discard, rec.Body)
}
})
}
```
The results are pretty nice:
Benchmarks (Linux arm64, count=10, benchstat, vs origin/main):
GzipMiddleware-8 sec/op 158.8µs ± 4% -> 5.2µs ± 3% -96.72% (p=0.000)
GzipMiddleware-8 B/op 1180.6 KiB -> 1.9 KiB -99.84% (p=0.000)
GzipMiddleware-8 allocs/op 32 -> 13 -59.38% (p=0.000)
Signed-off-by: jvoisin <julien.voisin@dustri.org>
55 lines
1.2 KiB
Go
55 lines
1.2 KiB
Go
package internal
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
func GzipMiddleware(level int, next http.Handler) http.Handler {
|
|
// Validate the level once at setup; gzip.NewWriterLevel only fails for
|
|
// invalid levels and we'd rather panic now than mid-request.
|
|
if _, err := gzip.NewWriterLevel(io.Discard, level); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Per-middleware pool of *gzip.Writer. Each entry carries ~40 KiB of
|
|
// deflate buffers; reusing them avoids that allocation on every request.
|
|
pool := sync.Pool{
|
|
New: func() any {
|
|
gz, _ := gzip.NewWriterLevel(io.Discard, level)
|
|
return gz
|
|
},
|
|
}
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Encoding", "gzip")
|
|
gz := pool.Get().(*gzip.Writer)
|
|
gz.Reset(w)
|
|
defer func() {
|
|
gz.Close()
|
|
gz.Reset(io.Discard)
|
|
pool.Put(gz)
|
|
}()
|
|
|
|
grw := gzipResponseWriter{ResponseWriter: w, sink: gz}
|
|
next.ServeHTTP(grw, r)
|
|
})
|
|
}
|
|
|
|
type gzipResponseWriter struct {
|
|
http.ResponseWriter
|
|
sink *gzip.Writer
|
|
}
|
|
|
|
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
|
return w.sink.Write(b)
|
|
}
|