Optimize author log viewer performance with tail behavior

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
This commit is contained in:
Stu Leak 2025-12-26 14:21:32 -05:00
parent 49e01f5817
commit d781ce2d58
2 changed files with 54 additions and 2 deletions

View File

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

View File

@ -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