Răsfoiți Sursa

perf(response): pool on-the-fly compression writers

For every dynamic response above the 1 KiB compression threshold the
builder previously did one of:
    brotli.NewWriterV2(b.w, brotli.DefaultCompression)
    gzip.NewWriter(b.w)
    flate.NewWriter(b.w, -1)

Each constructor allocates the encoder's working set from scratch:
brotli's sliding window + hash tables (~3.5 MiB at quality 6), gzip's
CRC32 + deflate state (~800 KiB), flate's hash chains (~800 KiB).
On a busy server that's per-request churn the GC then has to clean up.

All three writer types expose Reset(dst io.Writer), which rebinds the
destination without touching the internal buffers. So put each behind a
sync.Pool and Get/Reset/Put around the existing Write+Close pair. The
constructor signatures didn't change; only Pool.Get + Reset are new.

Note that brotli.NewWriterV2 (kept from before this change) returns
*matchfinder.Writer, not *brotli.Writer, as V2 is the pure-Go encoder
built on top of github.com/andybalholm/brotli/matchfinder, where the
actual Writer type lives. Hence the matchfinder import.

On a local artificial benchmarks of a 130 KiB HTML-like payload consisting of
250 entry-list items, on a single-core it improves performances by around 10%,
and for multicore under GC pressure, ns/op is reduces by ~80% and B/op by ~99%.
jvoisin 1 lună în urmă
părinte
comite
ef24215bde
1 a modificat fișierele cu 37 adăugiri și 3 ștergeri
  1. 37 3
      internal/http/response/builder.go

+ 37 - 3
internal/http/response/builder.go

@@ -13,13 +13,38 @@ import (
 	"mime"
 	"net/http"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/andybalholm/brotli"
+	"github.com/andybalholm/brotli/matchfinder"
 )
 
 const compressionThreshold = 1024
 
+// Compression writers are pooled so each request reuses their internal
+// state (brotli sliding window + hash tables, flate dictionary, etc.)
+// instead of allocating it from scratch. Reset(dst) rebinds the
+// destination without re-allocating the buffers.
+var (
+	brotliWriterPool = sync.Pool{
+		New: func() any {
+			return brotli.NewWriterV2(io.Discard, brotli.DefaultCompression)
+		},
+	}
+	gzipWriterPool = sync.Pool{
+		New: func() any {
+			return gzip.NewWriter(io.Discard)
+		},
+	}
+	flateWriterPool = sync.Pool{
+		New: func() any {
+			w, _ := flate.NewWriter(io.Discard, flate.DefaultCompression)
+			return w
+		},
+	}
+)
+
 // Builder generates HTTP responses.
 type Builder struct {
 	w                 http.ResponseWriter
@@ -146,25 +171,34 @@ func (b *Builder) compress(data []byte) {
 			b.headers.Set("Content-Encoding", "br")
 			b.writeHeaders()
 
-			brotliWriter := brotli.NewWriterV2(b.w, brotli.DefaultCompression)
+			brotliWriter := brotliWriterPool.Get().(*matchfinder.Writer)
+			brotliWriter.Reset(b.w)
 			brotliWriter.Write(data)
 			brotliWriter.Close()
+			brotliWriter.Reset(io.Discard)
+			brotliWriterPool.Put(brotliWriter)
 			return
 		case "gzip":
 			b.headers.Set("Content-Encoding", "gzip")
 			b.writeHeaders()
 
-			gzipWriter := gzip.NewWriter(b.w)
+			gzipWriter := gzipWriterPool.Get().(*gzip.Writer)
+			gzipWriter.Reset(b.w)
 			gzipWriter.Write(data)
 			gzipWriter.Close()
+			gzipWriter.Reset(io.Discard)
+			gzipWriterPool.Put(gzipWriter)
 			return
 		case "deflate":
 			b.headers.Set("Content-Encoding", "deflate")
 			b.writeHeaders()
 
-			flateWriter, _ := flate.NewWriter(b.w, -1)
+			flateWriter := flateWriterPool.Get().(*flate.Writer)
+			flateWriter.Reset(b.w)
 			flateWriter.Write(data)
 			flateWriter.Close()
+			flateWriter.Reset(io.Discard)
+			flateWriterPool.Put(flateWriter)
 			return
 		}
 	}