fetch_ml/tools/performance_regression_detector.go
Jeremie Fraeys ea15af1833 Fix multi-user authentication and clean up debug code
- Fix YAML tags in auth config struct (json -> yaml)
- Update CLI configs to use pre-hashed API keys
- Remove double hashing in WebSocket client
- Fix port mapping (9102 -> 9103) in CLI commands
- Update permission keys to use jobs:read, jobs:create, etc.
- Clean up all debug logging from CLI and server
- All user roles now authenticate correctly:
  * Admin: Can queue jobs and see all jobs
  * Researcher: Can queue jobs and see own jobs
  * Analyst: Can see status (read-only access)

Multi-user authentication is now fully functional.
2025-12-06 12:35:32 -05:00

183 lines
5.5 KiB
Go

// Package tools provides performance regression detection utilities.
package tools
import (
"encoding/json"
"fmt"
"os"
"time"
)
// PerformanceRegressionDetector detects performance regressions in benchmark results
type PerformanceRegressionDetector struct {
BaselineFile string
Threshold float64
}
// BenchmarkResult represents a single benchmark result
type BenchmarkResult struct {
Name string `json:"name"`
Value float64 `json:"value"`
Unit string `json:"unit"`
Timestamp time.Time `json:"timestamp"`
}
// RegressionReport contains regression analysis results
type RegressionReport struct {
Regressions []Regression `json:"regressions"`
Improvements []Improvement `json:"improvements"`
Summary string `json:"summary"`
}
// Regression represents a performance regression
type Regression struct {
Benchmark string `json:"benchmark"`
CurrentValue float64 `json:"current_value"`
BaselineValue float64 `json:"baseline_value"`
PercentChange float64 `json:"percent_change"`
Severity string `json:"severity"`
}
// Improvement represents a performance improvement
type Improvement struct {
Benchmark string `json:"benchmark"`
CurrentValue float64 `json:"current_value"`
BaselineValue float64 `json:"baseline_value"`
PercentChange float64 `json:"percent_change"`
}
// NewPerformanceRegressionDetector creates a new detector instance
func NewPerformanceRegressionDetector(baselineFile string, threshold float64) *PerformanceRegressionDetector {
return &PerformanceRegressionDetector{
BaselineFile: baselineFile,
Threshold: threshold,
}
}
// LoadBaseline loads baseline benchmark results from file
func (prd *PerformanceRegressionDetector) LoadBaseline() ([]BenchmarkResult, error) {
if _, err := os.Stat(prd.BaselineFile); os.IsNotExist(err) {
return nil, fmt.Errorf("baseline file not found: %s", prd.BaselineFile)
}
data, err := os.ReadFile(prd.BaselineFile)
if err != nil {
return nil, fmt.Errorf("failed to read baseline file: %w", err)
}
var results []BenchmarkResult
if err := json.Unmarshal(data, &results); err != nil {
return nil, fmt.Errorf("failed to parse baseline file: %w", err)
}
return results, nil
}
// AnalyzeResults analyzes current results against baseline
func (prd *PerformanceRegressionDetector) AnalyzeResults(current []BenchmarkResult) (*RegressionReport, error) {
baseline, err := prd.LoadBaseline()
if err != nil {
return nil, fmt.Errorf("failed to load baseline: %w", err)
}
report := &RegressionReport{
Regressions: []Regression{},
Improvements: []Improvement{},
}
baselineMap := make(map[string]BenchmarkResult)
for _, result := range baseline {
baselineMap[result.Name] = result
}
for _, currentResult := range current {
baselineResult, exists := baselineMap[currentResult.Name]
if !exists {
continue // Skip new benchmarks without baseline
}
percentChange := ((currentResult.Value - baselineResult.Value) / baselineResult.Value) * 100
if percentChange > prd.Threshold {
// Performance regression detected
severity := "minor"
if percentChange > prd.Threshold*2 {
severity = "major"
}
if percentChange > prd.Threshold*3 {
severity = "critical"
}
report.Regressions = append(report.Regressions, Regression{
Benchmark: currentResult.Name,
CurrentValue: currentResult.Value,
BaselineValue: baselineResult.Value,
PercentChange: percentChange,
Severity: severity,
})
} else if percentChange < -prd.Threshold {
// Performance improvement detected
report.Improvements = append(report.Improvements, Improvement{
Benchmark: currentResult.Name,
CurrentValue: currentResult.Value,
BaselineValue: baselineResult.Value,
PercentChange: percentChange,
})
}
}
// Generate summary
regressionCount := len(report.Regressions)
improvementCount := len(report.Improvements)
if regressionCount == 0 && improvementCount == 0 {
report.Summary = "No significant performance changes detected"
} else {
report.Summary = fmt.Sprintf("Detected %d regression(s) and %d improvement(s)",
regressionCount, improvementCount)
}
return report, nil
}
// SaveBaseline saves current results as new baseline
func (prd *PerformanceRegressionDetector) SaveBaseline(results []BenchmarkResult) error {
data, err := json.MarshalIndent(results, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal results: %w", err)
}
err = os.WriteFile(prd.BaselineFile, data, 0600)
if err != nil {
return fmt.Errorf("failed to write baseline file: %w", err)
}
return nil
}
// PrintReport prints a formatted regression report
func (prd *PerformanceRegressionDetector) PrintReport(report *RegressionReport) {
fmt.Printf("Performance Regression Analysis Report\n")
fmt.Printf("=====================================\n\n")
fmt.Printf("Summary: %s\n\n", report.Summary)
if len(report.Regressions) > 0 {
fmt.Printf("Regressions (%d):\n", len(report.Regressions))
for _, regression := range report.Regressions {
fmt.Printf(" [%s] %s: %.2f -> %.2f (%.1f%% worse)\n",
regression.Severity, regression.Benchmark,
regression.BaselineValue, regression.CurrentValue, regression.PercentChange)
}
fmt.Println()
}
if len(report.Improvements) > 0 {
fmt.Printf("Improvements (%d):\n", len(report.Improvements))
for _, improvement := range report.Improvements {
fmt.Printf(" [+] %s: %.2f -> %.2f (%.1f%% better)\n",
improvement.Benchmark,
improvement.BaselineValue, improvement.CurrentValue, improvement.PercentChange)
}
fmt.Println()
}
}