From d781ce2d583ee2e1e155cc3f80440d5b3e697170 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Fri, 26 Dec 2025 14:21:32 -0500 Subject: [PATCH] Optimize author log viewer performance with tail behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Author log was causing severe UI lag and memory issues - Unbounded string growth (entire log kept in RAM) - Full widget rebuild on every line append - O(n²) string concatenation performance Solutions implemented: 1. Tail behavior - Keep only last 100 lines in UI - Uses circular buffer (authorLogLines slice) - Prevents unbounded memory growth - Rebuilds text from buffer on each append 2. Copy Log button - Copies full log from file (accurate) - Falls back to in-memory log if file unavailable 3. View Full Log button - Opens full log in dedicated log viewer - No UI lag from large logs 4. Track log file path - authorLogFilePath stored when log created - Used by copy and view buttons Performance improvements: - Memory: O(1) instead of O(n) - fixed 100 line buffer - CPU: One SetText() per line instead of concatenation - UI responsiveness: Dramatically improved with tail behavior UI changes: - Label shows "Authoring Log (last 100 lines)" - Copy/View buttons for accessing full log --- author_module.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++-- main.go | 2 ++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/author_module.go b/author_module.go index c6b3aa5..e026872 100644 --- a/author_module.go +++ b/author_module.go @@ -750,6 +750,42 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject { logScroll.SetMinSize(fyne.NewSize(0, 200)) state.authorLogScroll = logScroll + // Log control buttons + copyLogBtn := widget.NewButton("Copy Log", func() { + if state.authorLogFilePath != "" { + // Copy from file for accuracy + if data, err := os.ReadFile(state.authorLogFilePath); err == nil { + state.window.Clipboard().SetContent(string(data)) + dialog.ShowInformation("Copied", "Full authoring log copied to clipboard", state.window) + return + } + } + // Fallback to in-memory log + state.window.Clipboard().SetContent(state.authorLogText) + dialog.ShowInformation("Copied", "Authoring log copied to clipboard", state.window) + }) + copyLogBtn.Importance = widget.LowImportance + + viewFullLogBtn := widget.NewButton("View Full Log", func() { + if state.authorLogFilePath == "" || state.authorLogFilePath == "-" { + dialog.ShowInformation("No Log File", "No log file available to view", state.window) + return + } + if _, err := os.Stat(state.authorLogFilePath); err != nil { + dialog.ShowError(fmt.Errorf("log file not found: %w", err), state.window) + return + } + state.openLogViewer("Authoring Log", state.authorLogFilePath, false) + }) + viewFullLogBtn.Importance = widget.LowImportance + + logControls := container.NewHBox( + widget.NewLabel("Authoring Log (last 100 lines):"), + layout.NewSpacer(), + copyLogBtn, + viewFullLogBtn, + ) + controls := container.NewVBox( widget.NewLabel("Generate DVD/ISO:"), widget.NewSeparator(), @@ -759,7 +795,7 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject { statusLabel, progressBar, widget.NewSeparator(), - widget.NewLabel("Authoring Log:"), + logControls, logScroll, widget.NewSeparator(), generateBtn, @@ -1162,6 +1198,8 @@ func concatDVDMpg(inputs []string, output string) error { func (s *appState) resetAuthorLog() { s.authorLogText = "" + s.authorLogLines = nil + s.authorLogFilePath = "" if s.authorLogEntry != nil { s.authorLogEntry.SetText("") } @@ -1174,7 +1212,17 @@ func (s *appState) appendAuthorLog(line string) { if strings.TrimSpace(line) == "" { return } - s.authorLogText += line + "\n" + + // Keep only last 100 lines for UI display (tail behavior) + const maxLines = 100 + s.authorLogLines = append(s.authorLogLines, line) + if len(s.authorLogLines) > maxLines { + s.authorLogLines = s.authorLogLines[len(s.authorLogLines)-maxLines:] + } + + // Rebuild text from buffer + s.authorLogText = strings.Join(s.authorLogLines, "\n") + if s.authorLogEntry != nil { s.authorLogEntry.SetText(s.authorLogText) } @@ -1726,6 +1774,7 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres logging.Debug(logging.CatSystem, "author log open failed: %v", logErr) } else { job.LogPath = logPath + s.authorLogFilePath = logPath // Store for UI access defer logFile.Close() } @@ -1828,6 +1877,7 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres logging.Debug(logging.CatSystem, "author log open failed: %v", logErr) } else { job.LogPath = logPath + s.authorLogFilePath = logPath // Store for UI access defer logFile.Close() } diff --git a/main.go b/main.go index 0820abe..2475d32 100644 --- a/main.go +++ b/main.go @@ -962,6 +962,8 @@ type appState struct { authorChaptersRefresh func() // Refresh hook for chapter list UI authorDiscSize string // "DVD5" or "DVD9" authorLogText string + authorLogLines []string // Circular buffer for last N lines + authorLogFilePath string // Path to log file for full viewing authorLogEntry *widget.Entry authorLogScroll *container.Scroll authorProgress float64