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:
parent
8cff33fcab
commit
da49a1dd7b
|
|
@ -212,7 +212,9 @@ func BuildQueueView(
|
||||||
onViewLog func(string),
|
onViewLog func(string),
|
||||||
onCopyCommand func(string),
|
onCopyCommand func(string),
|
||||||
titleColor, bgColor, textColor color.Color,
|
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
|
// Header
|
||||||
title := canvas.NewText("JOB QUEUE", titleColor)
|
title := canvas.NewText("JOB QUEUE", titleColor)
|
||||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||||
|
|
@ -264,7 +266,7 @@ func BuildQueueView(
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range jobs {
|
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,
|
scrollable,
|
||||||
)
|
)
|
||||||
|
|
||||||
return container.NewPadded(body), scrollable
|
return container.NewPadded(body), scrollable, activeProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildJobItem creates a single job item in the queue list
|
// buildJobItem creates a single job item in the queue list
|
||||||
|
|
@ -297,6 +299,7 @@ func buildJobItem(
|
||||||
onViewLog func(string),
|
onViewLog func(string),
|
||||||
onCopyCommand func(string),
|
onCopyCommand func(string),
|
||||||
bgColor, textColor color.Color,
|
bgColor, textColor color.Color,
|
||||||
|
activeProgress *[]*StripedProgress,
|
||||||
) fyne.CanvasObject {
|
) fyne.CanvasObject {
|
||||||
// Status color
|
// Status color
|
||||||
statusColor := GetStatusColor(job.Status)
|
statusColor := GetStatusColor(job.Status)
|
||||||
|
|
@ -325,6 +328,8 @@ func buildJobItem(
|
||||||
if job.Status == queue.JobStatusRunning {
|
if job.Status == queue.JobStatusRunning {
|
||||||
progress.SetActivity(job.Progress <= 0.01)
|
progress.SetActivity(job.Progress <= 0.01)
|
||||||
progress.StartAnimation()
|
progress.StartAnimation()
|
||||||
|
// Track active progress to stop animation on next refresh (prevents goroutine leaks)
|
||||||
|
*activeProgress = append(*activeProgress, progress)
|
||||||
} else {
|
} else {
|
||||||
progress.SetActivity(false)
|
progress.SetActivity(false)
|
||||||
progress.StopAnimation()
|
progress.StopAnimation()
|
||||||
|
|
|
||||||
23
main.go
23
main.go
|
|
@ -986,6 +986,7 @@ type appState struct {
|
||||||
|
|
||||||
queueAutoRefreshStop chan struct{}
|
queueAutoRefreshStop chan struct{}
|
||||||
queueAutoRefreshRunning bool
|
queueAutoRefreshRunning bool
|
||||||
|
queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks
|
||||||
|
|
||||||
// Main menu refresh throttling
|
// Main menu refresh throttling
|
||||||
mainMenuLastRefresh time.Time
|
mainMenuLastRefresh time.Time
|
||||||
|
|
@ -1781,7 +1782,16 @@ func (s *appState) refreshQueueView() {
|
||||||
}}, jobs...)
|
}}, 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,
|
jobs,
|
||||||
func() { // onBack
|
func() { // onBack
|
||||||
// Stop auto-refresh before navigating away for snappy response
|
// 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))
|
s.setContent(container.NewPadded(view))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1984,6 +1997,14 @@ func (s *appState) stopQueueAutoRefresh() {
|
||||||
}
|
}
|
||||||
s.queueAutoRefreshStop = nil
|
s.queueAutoRefreshStop = nil
|
||||||
s.queueAutoRefreshRunning = false
|
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
|
// addConvertToQueue adds a conversion job to the queue
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user