Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions internal/api/middleware/request_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
22 changes: 21 additions & 1 deletion internal/api/middleware/response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"bytes"
"net/http"
"strings"
"sync"

"github.com/gin-gonic/gin"
"github.com/traylinx/switchAILocal/internal/interfaces"
Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
}
Loading