188 lines
5.5 KiB
Go
188 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()
|
|
}
|
|
}
|