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
7 changes: 6 additions & 1 deletion .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Create a PR with:
⚑ Pre-allocate slices with known capacity in hot paths
⚑ Cache compiled regex patterns in pattern matcher
⚑ Reuse HTTP client connections with proper pooling
⚑ Use strings.Builder instead of string concatenation
⚑ Use bytes.Buffer instead of string concatenation
⚑ Add buffer pooling for streaming responses
⚑ Replace map lookups in loops with single lookup
⚑ Add early returns to skip unnecessary processing
Expand Down Expand Up @@ -219,3 +219,8 @@ 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) - Optimize bytes.Buffer for request/response logging
**Learning:** In switchAILocal, `bytes.Buffer` is used for accumulating upstream API request and response logs. The codebase repeatedly instantiates `&bytes.Buffer{}` for these logs in high-traffic paths like `recordAPIRequest` and during log aggregation (`updateAggregatedRequest`, `updateAggregatedResponse`). The previous attempt to use `sync.Pool` with `strings.Builder` was an anti-pattern in Go because `strings.Builder.Reset()` explicitly discards the underlying byte buffer (`b.buf = nil`), trading away stack allocation for `sync.Pool` overhead with zero real memory savings. `bytes.Buffer.Reset()`, on the other hand, retains its capacity for reuse.
**Action:** Introduced a `sync.Pool` for `*bytes.Buffer` in `internal/runtime/executor/logging_helpers.go`. Acquired buffers via `byteBufferPool.Get()` and returned them via a custom `putByteBuffer(b)` function that enforces a maximum capacity check (e.g., `b.Cap() <= 128 * 1024`) to prevent permanent memory bloat from occasional massive payloads. Replaced local variable declarations with this pooled buffer pattern, properly reducing memory allocations in the hot logging paths.
47 changes: 35 additions & 12 deletions internal/runtime/executor/logging_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,29 @@ type upstreamRequestLog struct {
AuthValue string
}

var byteBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}

func getByteBuffer() *bytes.Buffer {
b := byteBufferPool.Get().(*bytes.Buffer)
b.Reset()
return b
}

func putByteBuffer(b *bytes.Buffer) {
if b.Cap() > 128*1024 {
return
}
byteBufferPool.Put(b)
}

type upstreamAttempt struct {
index int
request string
response *strings.Builder
response *bytes.Buffer
responseIntroWritten bool
statusWritten bool
headersWritten bool
Expand Down Expand Up @@ -94,7 +113,8 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ
attempts := getAttempts(ginCtx)
index := len(attempts) + 1

builder := &strings.Builder{}
builder := getByteBuffer()
defer putByteBuffer(builder)
builder.WriteString(fmt.Sprintf("=== API REQUEST %d ===\n", index))
builder.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano)))
if info.URL != "" {
Expand All @@ -121,7 +141,7 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ
attempt := &upstreamAttempt{
index: index,
request: builder.String(),
response: &strings.Builder{},
response: &bytes.Buffer{},
}
attempts = append(attempts, attempt)
ginCtx.Set(apiAttemptsKey, attempts)
Expand All @@ -145,7 +165,8 @@ func (s *LoggingStore) RecordRequest(info upstreamRequestLog) {
defer s.mu.Unlock()

index := len(s.attempts) + 1
builder := &strings.Builder{}
builder := getByteBuffer()
defer putByteBuffer(builder)
builder.WriteString(fmt.Sprintf("=== API REQUEST %d ===\n", index))
builder.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().Format(time.RFC3339Nano)))
if info.URL != "" {
Expand All @@ -170,7 +191,7 @@ func (s *LoggingStore) RecordRequest(info upstreamRequestLog) {
attempt := &upstreamAttempt{
index: index,
request: builder.String(),
response: &strings.Builder{},
response: &bytes.Buffer{},
}
s.attempts = append(s.attempts, attempt)
}
Expand Down Expand Up @@ -212,7 +233,7 @@ func (s *LoggingStore) AppendResponseChunk(chunk []byte) {
s.attempts = append(s.attempts, &upstreamAttempt{
index: 1,
request: "=== API REQUEST 1 ===\n<missing>\n\n",
response: &strings.Builder{},
response: &bytes.Buffer{},
})
}
attempt := s.attempts[len(s.attempts)-1]
Expand Down Expand Up @@ -411,7 +432,7 @@ func ensureAttempt(ginCtx *gin.Context) ([]*upstreamAttempt, *upstreamAttempt) {
attempt := &upstreamAttempt{
index: 1,
request: "=== API REQUEST 1 ===\n<missing>\n\n",
response: &strings.Builder{},
response: &bytes.Buffer{},
}
attempts = []*upstreamAttempt{attempt}
ginCtx.Set(apiAttemptsKey, attempts)
Expand All @@ -434,18 +455,20 @@ func updateAggregatedRequest(ginCtx *gin.Context, attempts []*upstreamAttempt) {
if ginCtx == nil {
return
}
var builder strings.Builder
builder := getByteBuffer()
defer putByteBuffer(builder)
for _, attempt := range attempts {
builder.WriteString(attempt.request)
}
ginCtx.Set(apiRequestKey, []byte(builder.String()))
ginCtx.Set(apiRequestKey, bytes.Clone(builder.Bytes()))
}

func updateAggregatedResponse(ginCtx *gin.Context, attempts []*upstreamAttempt) {
if ginCtx == nil {
return
}
var builder strings.Builder
builder := getByteBuffer()
defer putByteBuffer(builder)
for idx, attempt := range attempts {
if attempt == nil || attempt.response == nil {
continue
Expand All @@ -462,10 +485,10 @@ func updateAggregatedResponse(ginCtx *gin.Context, attempts []*upstreamAttempt)
builder.WriteString("\n")
}
}
ginCtx.Set(apiResponseKey, []byte(builder.String()))
ginCtx.Set(apiResponseKey, bytes.Clone(builder.Bytes()))
}

func writeHeaders(builder *strings.Builder, headers http.Header) {
func writeHeaders(builder *bytes.Buffer, headers http.Header) {
if builder == nil {
return
}
Expand Down
Loading