Skip to content

Commit 4493f40

Browse files
committed
Perf for worktrees
1 parent 52413eb commit 4493f40

File tree

1 file changed

+65
-5
lines changed

1 file changed

+65
-5
lines changed

internal/plugins/worktree/agent.go

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package worktree
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"os"
@@ -30,12 +31,50 @@ type paneCache struct {
3031
ttl time.Duration
3132
}
3233

34+
type captureCoordinator struct {
35+
mu sync.Mutex
36+
inFlight bool
37+
cond *sync.Cond
38+
}
39+
40+
func newCaptureCoordinator() *captureCoordinator {
41+
cc := &captureCoordinator{}
42+
cc.cond = sync.NewCond(&cc.mu)
43+
return cc
44+
}
45+
46+
// runBatch executes fn if no batch is currently running. If a batch is in-flight,
47+
// it waits for completion and returns ran=false so callers can re-check cache.
48+
func (c *captureCoordinator) runBatch(fn func() (map[string]string, error)) (outputs map[string]string, err error, ran bool) {
49+
c.mu.Lock()
50+
if c.inFlight {
51+
for c.inFlight {
52+
c.cond.Wait()
53+
}
54+
c.mu.Unlock()
55+
return nil, nil, false
56+
}
57+
c.inFlight = true
58+
c.mu.Unlock()
59+
60+
outputs, err = fn()
61+
62+
c.mu.Lock()
63+
c.inFlight = false
64+
c.cond.Broadcast()
65+
c.mu.Unlock()
66+
67+
return outputs, err, true
68+
}
69+
3370
// Global cache instance for pane captures
3471
var globalPaneCache = &paneCache{
3572
entries: make(map[string]paneCacheEntry),
3673
ttl: 300 * time.Millisecond, // Cache valid for 300ms
3774
}
3875

76+
var globalCaptureCoordinator = newCaptureCoordinator()
77+
3978
// get returns cached output if valid, or empty string if expired/missing
4079
func (c *paneCache) get(session string) (string, bool) {
4180
c.mu.Lock()
@@ -84,6 +123,10 @@ const (
84123
// We only need recent output for status detection and display
85124
captureLineCount = 600
86125

126+
// Timeout for tmux capture commands to avoid blocking on hung sessions
127+
tmuxCaptureTimeout = 2 * time.Second
128+
tmuxBatchCaptureTimeout = 3 * time.Second
129+
87130
// Polling intervals - adaptive based on agent status
88131
// Conservative values to reduce CPU with multiple worktrees while maintaining responsiveness
89132
pollIntervalInitial = 500 * time.Millisecond // First poll after agent starts
@@ -570,14 +613,21 @@ func capturePane(sessionName string) (string, error) {
570613
return output, nil
571614
}
572615

573-
// Cache miss - batch capture all sidecar sessions
574-
outputs, err := batchCaptureAllSessions()
616+
// Cache miss - batch capture all sidecar sessions (singleflight)
617+
outputs, err, ran := globalCaptureCoordinator.runBatch(batchCaptureAllSessions)
618+
if !ran {
619+
// Another goroutine captured; re-check cache
620+
if output, ok := globalPaneCache.get(sessionName); ok {
621+
return output, nil
622+
}
623+
return capturePaneDirect(sessionName)
624+
}
575625
if err != nil {
576626
// Fall back to single capture on batch error
577627
return capturePaneDirect(sessionName)
578628
}
579629

580-
// Cache all results
630+
// Cache all results from batch
581631
globalPaneCache.setAll(outputs)
582632

583633
// Return requested session's output
@@ -592,8 +642,13 @@ func capturePane(sessionName string) (string, error) {
592642
// capturePaneDirect captures a single pane without caching.
593643
func capturePaneDirect(sessionName string) (string, error) {
594644
startLine := fmt.Sprintf("-%d", captureLineCount)
595-
cmd := exec.Command("tmux", "capture-pane", "-p", "-e", "-J", "-S", startLine, "-t", sessionName)
645+
ctx, cancel := context.WithTimeout(context.Background(), tmuxCaptureTimeout)
646+
defer cancel()
647+
cmd := exec.CommandContext(ctx, "tmux", "capture-pane", "-p", "-e", "-J", "-S", startLine, "-t", sessionName)
596648
output, err := cmd.Output()
649+
if ctx.Err() == context.DeadlineExceeded {
650+
return "", fmt.Errorf("capture-pane: timeout after %s", tmuxCaptureTimeout)
651+
}
597652
if err != nil {
598653
return "", fmt.Errorf("capture-pane: %w", err)
599654
}
@@ -612,8 +667,13 @@ for session in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep '^%s
612667
done
613668
`, tmuxSessionPrefix, captureLineCount)
614669

615-
cmd := exec.Command("bash", "-c", script)
670+
ctx, cancel := context.WithTimeout(context.Background(), tmuxBatchCaptureTimeout)
671+
defer cancel()
672+
cmd := exec.CommandContext(ctx, "bash", "-c", script)
616673
output, err := cmd.Output()
674+
if ctx.Err() == context.DeadlineExceeded {
675+
return nil, fmt.Errorf("batch capture: timeout after %s", tmuxBatchCaptureTimeout)
676+
}
617677
if err != nil {
618678
return nil, fmt.Errorf("batch capture: %w", err)
619679
}

0 commit comments

Comments
 (0)