fix(queue): Prevent massive goroutine leak from StripedProgress animations

Critical Fix:
- Goroutine dump showed hundreds of leaked animation goroutines
- Each queue refresh created NEW progress bars without stopping old ones
- Animation goroutines continued running forever, consuming resources

Root Cause:
- BuildQueueView() creates new StripedProgress widgets on every refresh
- StartAnimation() spawned goroutines for running jobs
- Old widgets were discarded but goroutines never stopped
- Fyne's Destroy() method not reliably called when rebuilding view

Solution:
- Track all active StripedProgress widgets in appState.queueActiveProgress
- Stop ALL animations before rebuilding queue view
- Stop ALL animations when leaving queue view (stopQueueAutoRefresh)
- BuildQueueView now returns list of active progress bars
- Prevents hundreds of leaked goroutines from accumulating

Implementation:
- Added queueActiveProgress []*ui.StripedProgress to appState
- Modified BuildQueueView signature to return progress list
- Stop old animations in refreshQueueView() before calling BuildQueueView
- Stop all animations in stopQueueAutoRefresh() when navigating away
- Track running job progress bars and append to activeProgress slice

Files Changed:
- main.go: appState field, refreshQueueView(), stopQueueAutoRefresh()
- internal/ui/queueview.go: BuildQueueView(), buildJobItem()

Impact:
- Eliminates goroutine leak that caused resource exhaustion
- Clean shutdown of animation goroutines on refresh and navigation
- Should dramatically reduce memory usage and CPU overhead

Reported-by: User (goroutine dump showing 900+ leaked goroutines)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-28 19:24:17 -05:00
parent 8cff33fcab
commit da49a1dd7b
2 changed files with 30 additions and 4 deletions

View File

@ -212,7 +212,9 @@ func BuildQueueView(
onViewLog func(string),
onCopyCommand func(string),
titleColor, bgColor, textColor color.Color,
) (fyne.CanvasObject, *container.Scroll) {
) (fyne.CanvasObject, *container.Scroll, []*StripedProgress) {
// Track active progress animations to prevent goroutine leaks
var activeProgress []*StripedProgress
// Header
title := canvas.NewText("JOB QUEUE", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
@ -264,7 +266,7 @@ func BuildQueueView(
}
for _, job := range jobs {
jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor))
jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor, &activeProgress))
}
}
@ -280,7 +282,7 @@ func BuildQueueView(
scrollable,
)
return container.NewPadded(body), scrollable
return container.NewPadded(body), scrollable, activeProgress
}
// buildJobItem creates a single job item in the queue list
@ -297,6 +299,7 @@ func buildJobItem(
onViewLog func(string),
onCopyCommand func(string),
bgColor, textColor color.Color,
activeProgress *[]*StripedProgress,
) fyne.CanvasObject {
// Status color
statusColor := GetStatusColor(job.Status)
@ -325,6 +328,8 @@ func buildJobItem(
if job.Status == queue.JobStatusRunning {
progress.SetActivity(job.Progress <= 0.01)
progress.StartAnimation()
// Track active progress to stop animation on next refresh (prevents goroutine leaks)
*activeProgress = append(*activeProgress, progress)
} else {
progress.SetActivity(false)
progress.StopAnimation()

23
main.go
View File

@ -986,6 +986,7 @@ type appState struct {
queueAutoRefreshStop chan struct{}
queueAutoRefreshRunning bool
queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks
// Main menu refresh throttling
mainMenuLastRefresh time.Time
@ -1781,7 +1782,16 @@ func (s *appState) refreshQueueView() {
}}, jobs...)
}
view, scroll := ui.BuildQueueView(
// CRITICAL: Stop all active progress animations before rebuilding to prevent goroutine leaks
// Each refresh creates new StripedProgress widgets, and old animation goroutines must be stopped
for _, progress := range s.queueActiveProgress {
if progress != nil {
progress.StopAnimation()
}
}
s.queueActiveProgress = nil
view, scroll, activeProgress := ui.BuildQueueView(
jobs,
func() { // onBack
// Stop auto-refresh before navigating away for snappy response
@ -1938,6 +1948,9 @@ func (s *appState) refreshQueueView() {
}()
}
// Store active progress bars to stop them on next refresh
s.queueActiveProgress = activeProgress
s.setContent(container.NewPadded(view))
}
@ -1984,6 +1997,14 @@ func (s *appState) stopQueueAutoRefresh() {
}
s.queueAutoRefreshStop = nil
s.queueAutoRefreshRunning = false
// Stop all active progress animations to prevent goroutine leaks when leaving queue view
for _, progress := range s.queueActiveProgress {
if progress != nil {
progress.StopAnimation()
}
}
s.queueActiveProgress = nil
}
// addConvertToQueue adds a conversion job to the queue