diff --git a/.jules/bolt.md b/.jules/bolt.md index 5049e2f8..0d0c52f2 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -219,3 +219,6 @@ go tool pprof mem.prof Remember: You're Bolt, making switchAILocal lightning fast. But speed without correctness is useless. Measure, optimize, verify. **If you can't find a clear performance win today, stop and do not create a PR.** +## $(date +%Y-%m-%d) - Pooling Response Writer Buffers +**Learning:** In the API gateway's hot path, `ResponseWriterWrapper` previously allocated a new `bytes.Buffer` for every HTTP response. This created unnecessary allocations and GC pressure, particularly for non-streaming requests. Using a `sync.Pool` to recycle these buffers provides measurable allocation savings. +**Action:** Always consider `sync.Pool` for buffers created on a per-request basis in middleware, ensuring to cap the maximum buffer size returned to the pool to prevent memory bloat from occasional massive payloads. diff --git a/internal/api/middleware/request_logging.go b/internal/api/middleware/request_logging.go index 7593f759..bd086b67 100644 --- a/internal/api/middleware/request_logging.go +++ b/internal/api/middleware/request_logging.go @@ -51,6 +51,7 @@ func RequestLoggingMiddleware(logger logging.RequestLogger) gin.HandlerFunc { // Create response writer wrapper wrapper := NewResponseWriterWrapper(c.Writer, logger, requestInfo) + defer wrapper.Release() if !logger.IsEnabled() { wrapper.logOnErrorOnly = true } diff --git a/internal/api/middleware/response_writer.go b/internal/api/middleware/response_writer.go index 52ef09b2..f4b6f9da 100644 --- a/internal/api/middleware/response_writer.go +++ b/internal/api/middleware/response_writer.go @@ -11,6 +11,7 @@ import ( "bytes" "net/http" "strings" + "sync" "github.com/gin-gonic/gin" "github.com/traylinx/switchAILocal/internal/interfaces" @@ -28,6 +29,14 @@ type RequestInfo struct { // ResponseWriterWrapper wraps the standard gin.ResponseWriter to intercept and log response data. // It is designed to handle both standard and streaming responses, ensuring that logging operations do not block the client response. + +// bufferPool recycles bytes.Buffer to reduce allocations. +var bufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + type ResponseWriterWrapper struct { gin.ResponseWriter body *bytes.Buffer // body is a buffer to store the response body for non-streaming responses. @@ -55,7 +64,7 @@ type ResponseWriterWrapper struct { func NewResponseWriterWrapper(w gin.ResponseWriter, logger logging.RequestLogger, requestInfo *RequestInfo) *ResponseWriterWrapper { return &ResponseWriterWrapper{ ResponseWriter: w, - body: &bytes.Buffer{}, + body: bufferPool.Get().(*bytes.Buffer), logger: logger, requestInfo: requestInfo, headers: make(map[string][]string), @@ -384,3 +393,14 @@ func (w *ResponseWriterWrapper) logRequest(statusCode int, headers map[string][] w.requestInfo.RequestID, ) } + +// Release returns the internal buffer to the pool. +func (w *ResponseWriterWrapper) Release() { + if w.body != nil { + if w.body.Cap() <= 128*1024 { + w.body.Reset() + bufferPool.Put(w.body) + } + w.body = nil + } +}