Compare commits

...

3 Commits

Author SHA1 Message Date
b41e41e5ad fix(windows): Hide command prompt windows during benchmarking
Issue:
- Jake reported command prompts popping up during benchmark runs on Windows
- FFmpeg processes were showing console windows during tests
- Disruptive user experience, not discreet

Root Cause:
- exec.CommandContext on Windows shows command prompt by default
- Benchmark suite runs multiple FFmpeg processes (test video generation + encoder tests)
- No platform-specific window hiding applied

Solution:
- Apply utils.ApplyNoWindow() to all FFmpeg benchmark commands
- Uses SysProcAttr{HideWindow: true} on Windows
- No-op on Linux/macOS (cross-platform safe)

Implementation:
- Import internal/utils in benchmark package
- Call ApplyNoWindow() on test video generation command
- Call ApplyNoWindow() on each encoder benchmark test command
- Ensures all benchmark processes run hidden on Windows

Files Changed:
- internal/benchmark/benchmark.go: Added ApplyNoWindow() calls

Platform-Specific Code:
- internal/utils/proc_windows.go: HideWindow implementation (existing)
- internal/utils/proc_other.go: No-op implementation (existing)

Impact:
- Clean, discreet benchmarking on Windows
- No console windows popping up during tests
- Same behavior on all platforms

Reported-by: Jake (Windows command prompt popups)
Tested-on: Linux (build successful, no-op behavior verified)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 19:25:55 -05:00
da49a1dd7b 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>
2025-12-28 19:24:17 -05:00
8cff33fcab fix(ui): Enable text wrapping for batch settings toggle button
Fixed Issue:
- "Hide Batch Settings" button text was overflowing beyond button boundary
- Text was truncated and hard to read in narrow layouts

Solution:
- Created wrapped label overlay on button using container.NewStack
- Label has TextWrapWord enabled for automatic line breaking
- Maintains button click functionality while improving readability
- Text now wraps to multiple lines when space is constrained

Files Changed:
- main.go: Batch settings toggle button (lines 6858-6879)

Reported-by: User (screenshot showing text overflow)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 19:08:39 -05:00
3 changed files with 48 additions and 8 deletions

View File

@ -7,6 +7,8 @@ import (
"os/exec"
"path/filepath"
"time"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// Result stores the outcome of a single encoder benchmark test
@ -60,6 +62,7 @@ func (s *Suite) GenerateTestVideo(ctx context.Context, duration int) (string, er
}
cmd := exec.CommandContext(ctx, s.FFmpegPath, args...)
utils.ApplyNoWindow(cmd) // Hide command window on Windows during benchmark test video generation
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to generate test video: %w", err)
}
@ -131,6 +134,7 @@ func (s *Suite) TestEncoder(ctx context.Context, encoder, preset string) Result
// Measure encoding time
start := time.Now()
cmd := exec.CommandContext(ctx, s.FFmpegPath, args...)
utils.ApplyNoWindow(cmd) // Hide command window on Windows during benchmark encoding test
if err := cmd.Run(); err != nil {
result.Error = fmt.Sprintf("encoding failed: %v", err)

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()

41
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
@ -6855,21 +6876,31 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
settingsContent.Hide()
settingsVisible := false
toggleSettingsLabel := widget.NewLabel("Show Batch Settings")
toggleSettingsLabel.Wrapping = fyne.TextWrapWord
toggleSettingsLabel.Alignment = fyne.TextAlignCenter
var toggleSettingsBtn *widget.Button
toggleSettingsBtn = widget.NewButton("Show Batch Settings", func() {
toggleSettingsBtn = widget.NewButton("", func() {
if settingsVisible {
settingsContent.Hide()
toggleSettingsBtn.SetText("Show Batch Settings")
toggleSettingsLabel.SetText("Show Batch Settings")
} else {
settingsContent.Show()
toggleSettingsBtn.SetText("Hide Batch Settings")
toggleSettingsLabel.SetText("Hide Batch Settings")
}
settingsVisible = !settingsVisible
})
toggleSettingsBtn.Importance = widget.LowImportance
settingsBox := container.NewVBox(
// Replace button text with wrapped label
toggleSettingsBtnWithLabel := container.NewStack(
toggleSettingsBtn,
container.NewPadded(toggleSettingsLabel),
)
settingsBox := container.NewVBox(
toggleSettingsBtnWithLabel,
settingsContent,
widget.NewSeparator(),
)