mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-09 13:58:14 +00:00
perf(internal/gzip): pool *gzip.Writer per middleware instance (#1654)
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>
This commit is contained in:
@@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Fix a race in the bbolt store where the asynchronous cleanup scheduled by an expired read could delete a value that had just been refreshed; the delete now only fires when the key still carries the same expired generation it observed.
|
||||
- Marginally increase the performances of requests processing
|
||||
- Marginally improve the performances of PoW validation
|
||||
- Significantly improve the performances of the gzip middleware
|
||||
|
||||
## v1.25.0: Necron
|
||||
|
||||
|
||||
+24
-5
@@ -2,11 +2,28 @@ 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)
|
||||
@@ -14,11 +31,13 @@ func GzipMiddleware(level int, next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz, err := gzip.NewWriterLevel(w, level)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer gz.Close()
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user