Skip to content
Merged
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
48 changes: 47 additions & 1 deletion daemon/cc_info_timer.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type CCInfoCache struct {
FetchedAt time.Time
}


// CCInfoTimerService manages lazy-fetching of CC info data
type CCInfoTimerService struct {
config *model.ShellTimeConfig
Expand All @@ -35,6 +36,10 @@ type CCInfoTimerService struct {
ticker *time.Ticker
stopChan chan struct{}
wg sync.WaitGroup

// Git info (single active working directory, fetched by timer)
activeWorkingDir string
gitInfo GitInfo
}

// NewCCInfoTimerService creates a new CC info timer service
Expand Down Expand Up @@ -117,9 +122,11 @@ func (s *CCInfoTimerService) stopTimer() {
s.ticker.Stop()
s.timerRunning = false

// Clear active ranges when stopping
// Clear active ranges and git info when stopping
s.mu.Lock()
s.activeRanges = make(map[CCInfoTimeRange]bool)
s.activeWorkingDir = ""
s.gitInfo = GitInfo{}
s.mu.Unlock()

slog.Info("CC info timer stopped due to inactivity")
Expand All @@ -131,6 +138,7 @@ func (s *CCInfoTimerService) timerLoop() {

// Fetch immediately on start
s.fetchActiveRanges(context.Background())
s.fetchGitInfo()

for {
select {
Expand All @@ -143,6 +151,7 @@ func (s *CCInfoTimerService) timerLoop() {
return
}
s.fetchActiveRanges(context.Background())
s.fetchGitInfo()

case <-s.stopChan:
return
Expand Down Expand Up @@ -262,3 +271,40 @@ func (s *CCInfoTimerService) fetchCCInfo(ctx context.Context, timeRange CCInfoTi
TotalSessionSeconds: analytics.TotalSessionSeconds,
}, nil
}

// GetCachedGitInfo marks the working directory as active and returns cached git info.
// Git info is fetched by the background timer, so first call may return empty.
func (s *CCInfoTimerService) GetCachedGitInfo(workingDir string) GitInfo {
if workingDir == "" {
return GitInfo{}
}

s.mu.Lock()
s.activeWorkingDir = workingDir
info := s.gitInfo
s.mu.Unlock()

return info
}

// fetchGitInfo fetches git info for the active working directory
func (s *CCInfoTimerService) fetchGitInfo() {
s.mu.RLock()
workingDir := s.activeWorkingDir
s.mu.RUnlock()

if workingDir == "" {
return
}

info := GetGitInfo(workingDir)

s.mu.Lock()
s.gitInfo = info
s.mu.Unlock()

slog.Debug("Git info updated",
slog.String("workingDir", workingDir),
slog.String("branch", info.Branch),
slog.Bool("dirty", info.Dirty))
}
Comment on lines +277 to +310
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There's a potential 'thundering herd' issue here. If multiple concurrent requests for the same workingDir occur when the cache is stale, all of them will execute GetGitInfo(workingDir) simultaneously. Since this is a slow operation, this could lead to all those concurrent requests timing out, which is the problem this PR is trying to solve.

Consider using a mechanism like golang.org/x/sync/singleflight to ensure only one goroutine fetches the data for a given key, while others wait for the result. This would make the caching more robust under concurrent load.

4 changes: 2 additions & 2 deletions daemon/socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ func (p *SocketHandler) handleCCInfo(conn net.Conn, msg SocketMessage) {
cache := p.ccInfoTimer.GetCachedCost(timeRange)
p.ccInfoTimer.NotifyActivity()

// Get git info (fast, no caching needed since it changes frequently)
gitInfo := GetGitInfo(workingDir)
// Get git info (cached to avoid slow worktree.Status() on large repos)
gitInfo := p.ccInfoTimer.GetCachedGitInfo(workingDir)

response := CCInfoResponse{
TotalCostUSD: cache.TotalCostUSD,
Expand Down
Loading