mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-06-09 22:08:15 +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.
|
- 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 increase the performances of requests processing
|
||||||
- Marginally improve the performances of PoW validation
|
- Marginally improve the performances of PoW validation
|
||||||
|
- Significantly improve the performances of the gzip middleware
|
||||||
|
|
||||||
## v1.25.0: Necron
|
## v1.25.0: Necron
|
||||||
|
|
||||||
|
|||||||
+24
-5
@@ -2,11 +2,28 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GzipMiddleware(level int, next http.Handler) http.Handler {
|
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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
@@ -14,11 +31,13 @@ func GzipMiddleware(level int, next http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
gz, err := gzip.NewWriterLevel(w, level)
|
gz := pool.Get().(*gzip.Writer)
|
||||||
if err != nil {
|
gz.Reset(w)
|
||||||
panic(err)
|
defer func() {
|
||||||
}
|
gz.Close()
|
||||||
defer gz.Close()
|
gz.Reset(io.Discard)
|
||||||
|
pool.Put(gz)
|
||||||
|
}()
|
||||||
|
|
||||||
grw := gzipResponseWriter{ResponseWriter: w, sink: gz}
|
grw := gzipResponseWriter{ResponseWriter: w, sink: gz}
|
||||||
next.ServeHTTP(grw, r)
|
next.ServeHTTP(grw, r)
|
||||||
|
|||||||
Reference in New Issue
Block a user