Add benchmark history tracking and results browser

Extended the benchmark system to maintain a complete history of all
benchmark runs (up to last 10) with full results for each encoder/preset
combination tested.

Features:
- Stores complete benchmark run data including all test results
- History browser UI to view past benchmark runs
- Click any run to see detailed results for all encoders tested
- Compare performance across different presets and encoders
- Apply recommendations from past benchmarks
- Automatic history limit (keeps last 10 runs)

UI Changes:
- Renamed "Benchmark" button to "Run Benchmark"
- Added "View Results" button to main menu
- New benchmark history view showing all past runs
- Each run displays timestamp, recommended encoder, and test count
- Clicking a run shows full results with all encoder/preset combinations

Data Structure:
- benchmarkRun: stores single test run with all results
- benchmarkConfig: maintains array of benchmark runs
- Saves to ~/.config/VideoTools/benchmark.json

This allows users to review past benchmark results and make informed
decisions about which encoder settings to use by comparing FPS across
all available options on their hardware.

🤖 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-13 13:07:51 -05:00
parent 87c2d28e9f
commit 4d99f6ec78
3 changed files with 252 additions and 24 deletions

View File

@ -305,3 +305,114 @@ func BuildBenchmarkResultsView(
return container.NewPadded(body)
}
// BuildBenchmarkHistoryView creates the benchmark history browser UI
func BuildBenchmarkHistoryView(
history []BenchmarkHistoryRun,
onSelectRun func(int),
onClose func(),
titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject {
// Header
title := canvas.NewText("BENCHMARK HISTORY", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 24
closeBtn := widget.NewButton("← Back", onClose)
closeBtn.Importance = widget.LowImportance
header := container.NewBorder(
nil, nil,
closeBtn,
nil,
container.NewCenter(title),
)
if len(history) == 0 {
emptyMsg := widget.NewLabel("No benchmark history yet.\n\nRun your first benchmark to see results here.")
emptyMsg.Alignment = fyne.TextAlignCenter
emptyMsg.Wrapping = fyne.TextWrapWord
body := container.NewBorder(
header,
nil, nil, nil,
container.NewCenter(emptyMsg),
)
return container.NewPadded(body)
}
// Build list of benchmark runs
var runItems []fyne.CanvasObject
for i, run := range history {
idx := i // Capture for closure
runItems = append(runItems, buildHistoryRunItem(run, idx, onSelectRun, bgColor, textColor))
}
runsList := container.NewVBox(runItems...)
runsScroll := container.NewVScroll(runsList)
runsScroll.SetMinSize(fyne.NewSize(0, 400))
infoLabel := widget.NewLabel("Click on a benchmark run to view detailed results")
infoLabel.Alignment = fyne.TextAlignCenter
infoLabel.TextStyle = fyne.TextStyle{Italic: true}
body := container.NewBorder(
header,
container.NewVBox(widget.NewSeparator(), infoLabel),
nil, nil,
runsScroll,
)
return container.NewPadded(body)
}
// BenchmarkHistoryRun represents a benchmark run in the history view
type BenchmarkHistoryRun struct {
Timestamp string
ResultCount int
RecommendedEncoder string
RecommendedPreset string
RecommendedFPS float64
}
func buildHistoryRunItem(
run BenchmarkHistoryRun,
index int,
onSelect func(int),
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Timestamp label
timeLabel := widget.NewLabel(run.Timestamp)
timeLabel.TextStyle = fyne.TextStyle{Bold: true}
// Recommendation info
recLabel := widget.NewLabel(fmt.Sprintf("Recommended: %s (%s) - %.1f FPS",
run.RecommendedEncoder, run.RecommendedPreset, run.RecommendedFPS))
// Result count
countLabel := widget.NewLabel(fmt.Sprintf("%d encoders tested", run.ResultCount))
countLabel.TextStyle = fyne.TextStyle{Italic: true}
// Content
content := container.NewVBox(
timeLabel,
recLabel,
countLabel,
)
// Card background
card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4
item := container.NewPadded(
container.NewMax(card, content),
)
// Make it tappable
tappable := NewTappable(item, func() {
onSelect(index)
})
return tappable
}

View File

@ -23,20 +23,23 @@ type ModuleInfo struct {
}
// BuildMainMenu creates the main menu view with module tiles grouped by category
func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), onLogsClick func(), onBenchmarkClick func(), titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject {
func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), onLogsClick func(), onBenchmarkClick func(), onBenchmarkHistoryClick func(), titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject {
title := canvas.NewText("VIDEOTOOLS", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 28
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
benchmarkBtn := widget.NewButton("Benchmark", onBenchmarkClick)
benchmarkBtn := widget.NewButton("Run Benchmark", onBenchmarkClick)
benchmarkBtn.Importance = widget.LowImportance
viewResultsBtn := widget.NewButton("View Results", onBenchmarkHistoryClick)
viewResultsBtn.Importance = widget.LowImportance
logsBtn := widget.NewButton("Logs", onLogsClick)
logsBtn.Importance = widget.LowImportance
header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), benchmarkBtn, logsBtn, queueTile)
header := container.New(layout.NewHBoxLayout(), title, layout.NewSpacer(), benchmarkBtn, viewResultsBtn, logsBtn, queueTile)
categorized := map[string][]fyne.CanvasObject{}
for i := range modules {

156
main.go
View File

@ -509,12 +509,19 @@ func savePersistedConvertConfig(cfg convertConfig) error {
return os.WriteFile(path, data, 0o644)
}
// benchmarkConfig holds benchmark results and recommendations
// benchmarkRun represents a single benchmark test run
type benchmarkRun struct {
Timestamp time.Time `json:"timestamp"`
Results []benchmark.Result `json:"results"`
RecommendedEncoder string `json:"recommended_encoder"`
RecommendedPreset string `json:"recommended_preset"`
RecommendedHWAccel string `json:"recommended_hwaccel"`
RecommendedFPS float64 `json:"recommended_fps"`
}
// benchmarkConfig holds benchmark history
type benchmarkConfig struct {
RecommendedEncoder string `json:"recommended_encoder"`
RecommendedPreset string `json:"recommended_preset"`
RecommendedHWAccel string `json:"recommended_hwaccel"`
LastBenchmarkTime time.Time `json:"last_benchmark_time"`
History []benchmarkRun `json:"history"`
}
func benchmarkConfigPath() string {
@ -937,7 +944,7 @@ func (s *appState) showMainMenu() {
viewAppLogBtn,
)
dialog.ShowCustom("Logs", "Close", logOptions, s.window)
}, s.showBenchmark, titleColor, queueColor, textColor, queueCompleted, queueTotal)
}, s.showBenchmark, s.showBenchmarkHistory, titleColor, queueColor, textColor, queueCompleted, queueTotal)
// Update stats bar
s.updateStatsBar()
@ -1293,14 +1300,20 @@ func (s *appState) showBenchmark() {
// Get recommendation
encoder, preset, rec := suite.GetRecommendation()
// Save benchmark run to history
if err := s.saveBenchmarkRun(suite.Results, encoder, preset, rec.FPS); err != nil {
logging.Debug(logging.CatSystem, "failed to save benchmark run: %v", err)
}
if encoder != "" {
logging.Debug(logging.CatSystem, "benchmark recommendation: %s (preset: %s) - %.1f FPS", encoder, preset, rec.FPS)
// Show results dialog with option to apply
go func() {
topResults := suite.GetTopN(10)
allResults := suite.Results // Show all results, not just top 10
resultsView := ui.BuildBenchmarkResultsView(
topResults,
allResults,
rec,
func() {
// Apply recommended settings
@ -1351,7 +1364,7 @@ func (s *appState) detectHardwareEncoders() []string {
return available
}
func (s *appState) applyBenchmarkRecommendation(encoder, preset string) {
func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset string, fps float64) error {
// Map encoder to hardware acceleration setting
var hwAccel string
switch {
@ -1367,22 +1380,123 @@ func (s *appState) applyBenchmarkRecommendation(encoder, preset string) {
hwAccel = "none"
}
// Save benchmark recommendation
cfg := benchmarkConfig{
RecommendedEncoder: encoder,
RecommendedPreset: preset,
RecommendedHWAccel: hwAccel,
LastBenchmarkTime: time.Now(),
}
if err := saveBenchmarkConfig(cfg); err != nil {
logging.Debug(logging.CatSystem, "failed to save benchmark recommendation: %v", err)
// Load existing config
cfg, err := loadBenchmarkConfig()
if err != nil {
// Create new config if loading fails
cfg = benchmarkConfig{History: []benchmarkRun{}}
}
logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s hwaccel=%s", encoder, preset, hwAccel)
// Create new benchmark run
run := benchmarkRun{
Timestamp: time.Now(),
Results: results,
RecommendedEncoder: encoder,
RecommendedPreset: preset,
RecommendedHWAccel: hwAccel,
RecommendedFPS: fps,
}
// Add to history (keep last 10 runs)
cfg.History = append([]benchmarkRun{run}, cfg.History...)
if len(cfg.History) > 10 {
cfg.History = cfg.History[:10]
}
// Save config
if err := saveBenchmarkConfig(cfg); err != nil {
return err
}
logging.Debug(logging.CatSystem, "saved benchmark run: encoder=%s preset=%s fps=%.1f results=%d", encoder, preset, fps, len(results))
return nil
}
func (s *appState) applyBenchmarkRecommendation(encoder, preset string) {
logging.Debug(logging.CatSystem, "applied benchmark recommendation: encoder=%s preset=%s", encoder, preset)
dialog.ShowInformation("Benchmark Settings Applied",
fmt.Sprintf("Your system's optimal encoder settings have been saved:\n\nEncoder: %s\nPreset: %s\nHardware: %s\n\nThese are available for reference in the Convert module.",
encoder, preset, hwAccel), s.window)
fmt.Sprintf("Recommended encoder noted:\n\nEncoder: %s\nPreset: %s\n\nYou can reference these settings in the Convert module.",
encoder, preset), s.window)
}
func (s *appState) showBenchmarkHistory() {
s.stopPreview()
s.stopPlayer()
s.active = "benchmark-history"
// Load benchmark history
cfg, err := loadBenchmarkConfig()
if err != nil || len(cfg.History) == 0 {
// Show empty state
view := ui.BuildBenchmarkHistoryView(
[]ui.BenchmarkHistoryRun{},
nil,
s.showMainMenu,
utils.MustHex("#4CE870"),
utils.MustHex("#1E1E1E"),
utils.MustHex("#FFFFFF"),
)
s.setContent(view)
return
}
// Convert history to UI format
var historyRuns []ui.BenchmarkHistoryRun
for _, run := range cfg.History {
historyRuns = append(historyRuns, ui.BenchmarkHistoryRun{
Timestamp: run.Timestamp.Format("2006-01-02 15:04:05"),
ResultCount: len(run.Results),
RecommendedEncoder: run.RecommendedEncoder,
RecommendedPreset: run.RecommendedPreset,
RecommendedFPS: run.RecommendedFPS,
})
}
// Build history view
view := ui.BuildBenchmarkHistoryView(
historyRuns,
func(index int) {
// Show detailed results for this run
if index < 0 || index >= len(cfg.History) {
return
}
run := cfg.History[index]
// Create a fake recommendation result for the results view
rec := benchmark.Result{
Encoder: run.RecommendedEncoder,
Preset: run.RecommendedPreset,
FPS: run.RecommendedFPS,
Score: run.RecommendedFPS,
}
resultsView := ui.BuildBenchmarkResultsView(
run.Results,
rec,
func() {
// Apply this recommendation
s.applyBenchmarkRecommendation(run.RecommendedEncoder, run.RecommendedPreset)
s.showBenchmarkHistory()
},
func() {
// Back to history
s.showBenchmarkHistory()
},
utils.MustHex("#4CE870"),
utils.MustHex("#1E1E1E"),
utils.MustHex("#FFFFFF"),
)
s.setContent(resultsView)
},
s.showMainMenu,
utils.MustHex("#4CE870"),
utils.MustHex("#1E1E1E"),
utils.MustHex("#FFFFFF"),
)
s.setContent(view)
}
func (s *appState) showModule(id string) {