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.
This commit is contained in:
Stu Leak 2025-12-13 13:07:51 -05:00
parent dfb7796f10
commit 9f6e41b927
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) {