mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
perf(chatd): fix six scale bottlenecks identified by benchmarking (#22957)
## Summary Scale-tested the `chatd` package with mock-based benchmarks to identify performance bottlenecks. This PR fixes 6 of the 8 identified issues, ranked by severity. ## Changes ### 1. Parallel tool execution (HIGH) — `chatloop.go` `executeTools` ran tool calls sequentially. Now dispatches all calls concurrently via goroutines with `sync.WaitGroup`. Results are pre-allocated by index (no mutex needed). `onResult` callbacks fire as each tool completes. ### 2. Pubsub-backed subagent await (HIGH) — `subagent.go` `awaitSubagentCompletion` polled the DB every 200ms. Now subscribes to the child chat's `ChatStreamNotifyChannel` via pubsub for near-instant notifications. Fallback poll reduced to 5s. Falls back to 200ms only when `pubsub == nil` (single-instance / in-memory). ### 3. Per-chat stream locking (MEDIUM) — `chatd.go` Replaced single global `streamMu` + `map[uuid.UUID]*chatStreamState` with `sync.Map` where each `chatStreamState` has its own `sync.Mutex`. Zero cross-chat contention. ### 4. Batch chat acquisition (MEDIUM) — `chatd.go` `processOnce` acquired 1 chat per tick. Now loops up to `maxChatsPerAcquire = 10` per tick, avoiding idle time when many chats are pending. ### 5. Reduced heartbeat frequency (LOW-MEDIUM) — `chatd.go` `chatHeartbeatInterval` changed from 30s to 60s. Safe given the 5-minute `DefaultInFlightChatStaleAfter`. ### 6. O(depth) descendant check (LOW) — `subagent.go` Replaced top-down BFS (`O(total_descendants)` queries) with bottom-up parent-chain walk (`O(depth)` queries). Includes cycle protection. ## Not addressed (intentionally) - Message serialization overhead - Buffer eviction (`buffer[1:]` pattern)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"charm.land/fantasy"
|
||||
@@ -575,9 +576,10 @@ func processStepStream(
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// executeTools runs each tool call sequentially after the stream
|
||||
// completes. Results are published via onResult as each tool
|
||||
// finishes.
|
||||
// executeTools runs all tool calls concurrently after the stream
|
||||
// completes. Results are published via onResult in the original
|
||||
// tool-call order after all tools finish, preserving deterministic
|
||||
// event ordering for SSE subscribers.
|
||||
func executeTools(
|
||||
ctx context.Context,
|
||||
allTools []fantasy.AgentTool,
|
||||
@@ -593,11 +595,32 @@ func executeTools(
|
||||
toolMap[t.Info().Name] = t
|
||||
}
|
||||
|
||||
results := make([]fantasy.ToolResultContent, 0, len(toolCalls))
|
||||
for _, tc := range toolCalls {
|
||||
tr := executeSingleTool(ctx, toolMap, tc)
|
||||
results = append(results, tr)
|
||||
if onResult != nil {
|
||||
results := make([]fantasy.ToolResultContent, len(toolCalls))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(toolCalls))
|
||||
for i, tc := range toolCalls {
|
||||
go func(i int, tc fantasy.ToolCallContent) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
results[i] = fantasy.ToolResultContent{
|
||||
ToolCallID: tc.ToolCallID,
|
||||
ToolName: tc.ToolName,
|
||||
Result: fantasy.ToolResultOutputContentError{
|
||||
Error: xerrors.Errorf("tool panicked: %v", r),
|
||||
},
|
||||
}
|
||||
}
|
||||
}()
|
||||
results[i] = executeSingleTool(ctx, toolMap, tc)
|
||||
}(i, tc)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Publish results in the original tool-call order so SSE
|
||||
// subscribers see a deterministic event sequence.
|
||||
if onResult != nil {
|
||||
for _, tr := range results {
|
||||
onResult(tr)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user