diff --git a/cli/api.go b/cli/api.go index 806d109..0d1ac5c 100644 --- a/cli/api.go +++ b/cli/api.go @@ -242,6 +242,8 @@ var validFormats = map[string]bool{ "detailed": true, "summary-html": true, "detailed-html": true, + "summary-json": true, + "detailed-json": true, } // runTrack handles the track command execution diff --git a/engine/tracker/helpers.go b/engine/tracker/helpers.go index a681a34..a37f90e 100644 --- a/engine/tracker/helpers.go +++ b/engine/tracker/helpers.go @@ -52,20 +52,12 @@ func DetectChange(baseline, current *parser.LineObj) (*FunctionChangeResult, err ChangeType: changeType, FlatChangePercent: flatChange, CumChangePercent: cumChange, - FlatAbsolute: struct { - Before float64 - After float64 - Delta float64 - }{ + FlatAbsolute: AbsoluteChange{ Before: baseline.Flat, After: current.Flat, Delta: current.Flat - baseline.Flat, }, - CumAbsolute: struct { - Before float64 - After float64 - Delta float64 - }{ + CumAbsolute: AbsoluteChange{ Before: baseline.Cum, After: current.Cum, Delta: current.Cum - baseline.Cum, diff --git a/engine/tracker/profile_change_report.go b/engine/tracker/profile_change_report.go index a8e686d..d1df303 100644 --- a/engine/tracker/profile_change_report.go +++ b/engine/tracker/profile_change_report.go @@ -1,6 +1,7 @@ package tracker import ( + "encoding/json" "fmt" "html/template" "log/slog" @@ -323,6 +324,126 @@ func (r *ProfileChangeReport) generateDetailedHTMLReport(outputPath string) erro return t.Execute(file, data) } +// JSON data structures +type jsonSummaryData struct { + TotalFunctions int `json:"total_functions"` + Statistics jsonStatistics `json:"statistics"` + Regressions []*FunctionChangeResult `json:"regressions"` + Improvements []*FunctionChangeResult `json:"improvements"` +} + +type jsonStatistics struct { + Regressions int `json:"regressions"` + Improvements int `json:"improvements"` + Stable int `json:"stable"` +} + +type jsonDetailedData struct { + TotalFunctions int `json:"total_functions"` + Statistics jsonStatistics `json:"statistics"` + Changes []*FunctionChangeResult `json:"changes"` + SortOrder string `json:"sort_order"` +} + +func (r *ProfileChangeReport) generateJSONSummary(outputPath string) error { + var regressionList, improvementList []*FunctionChangeResult + var stable int + + for _, change := range r.FunctionChanges { + switch change.ChangeType { + case shared.REGRESSION: + regressionList = append(regressionList, change) + case shared.IMPROVEMENT: + improvementList = append(improvementList, change) + default: + stable++ + } + } + + // Sort regressions by percentage (biggest regression first) + sort.Slice(regressionList, func(i, j int) bool { + return regressionList[i].FlatChangePercent > regressionList[j].FlatChangePercent + }) + + // Sort improvements by absolute percentage (biggest improvement first) + sort.Slice(improvementList, func(i, j int) bool { + return math.Abs(improvementList[i].FlatChangePercent) > math.Abs(improvementList[j].FlatChangePercent) + }) + + data := jsonSummaryData{ + TotalFunctions: len(r.FunctionChanges), + Statistics: jsonStatistics{ + Regressions: len(regressionList), + Improvements: len(improvementList), + Stable: stable, + }, + Regressions: regressionList, + Improvements: improvementList, + } + + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} + +func (r *ProfileChangeReport) generateDetailedJSONReport(outputPath string) error { + changes := r.FunctionChanges + + // Count types + var regressions, improvements, stable int + for _, change := range changes { + switch change.ChangeType { + case shared.REGRESSION: + regressions++ + case shared.IMPROVEMENT: + improvements++ + default: + stable++ + } + } + + // Sort: regressions → improvements → stable, each by magnitude + typePriority := map[string]int{ + shared.REGRESSION: regressionPriority, + shared.IMPROVEMENT: improvementPriority, + shared.STABLE: stablePriority, + } + + sort.Slice(changes, func(i, j int) bool { + if typePriority[changes[i].ChangeType] != typePriority[changes[j].ChangeType] { + return typePriority[changes[i].ChangeType] < typePriority[changes[j].ChangeType] + } + return math.Abs(changes[i].FlatChangePercent) > math.Abs(changes[j].FlatChangePercent) + }) + + data := jsonDetailedData{ + TotalFunctions: len(changes), + Statistics: jsonStatistics{ + Regressions: regressions, + Improvements: improvements, + Stable: stable, + }, + Changes: changes, + SortOrder: "Regressions (worst → best), then Improvements (best → worst), then Stable", + } + + file, err := os.Create(outputPath) + if err != nil { + return err + } + defer file.Close() + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + return encoder.Encode(data) +} + func (r *ProfileChangeReport) ChooseOutputFormat(outputFormat string) { switch outputFormat { case "summary": @@ -337,5 +458,13 @@ func (r *ProfileChangeReport) ChooseOutputFormat(outputFormat string) { if err := r.generateDetailedHTMLReport("detailed.html"); err != nil { slog.Info("detailed-html failed", "err", err) } + case "summary-json": + if err := r.generateJSONSummary("summary.json"); err != nil { + slog.Info("summary-json failed", "err", err) + } + case "detailed-json": + if err := r.generateDetailedJSONReport("detailed.json"); err != nil { + slog.Info("detailed-json failed", "err", err) + } } } diff --git a/engine/tracker/types.go b/engine/tracker/types.go index d679af0..c003dbe 100644 --- a/engine/tracker/types.go +++ b/engine/tracker/types.go @@ -8,20 +8,18 @@ type ProfileChangeReport struct { FunctionChanges []*FunctionChangeResult } +type AbsoluteChange struct { + Before float64 `json:"before"` + After float64 `json:"after"` + Delta float64 `json:"delta"` +} + type FunctionChangeResult struct { - FunctionName string - ChangeType string // shared.REGRESSION, shred.IMPROVEMENT, shared.STABLE - FlatChangePercent float64 - CumChangePercent float64 - FlatAbsolute struct { - Before float64 - After float64 - Delta float64 - } - CumAbsolute struct { - Before float64 - After float64 - Delta float64 - } - Timestamp time.Time + FunctionName string `json:"function_name"` + ChangeType string `json:"change_type"` + FlatChangePercent float64 `json:"flat_change_percent"` + CumChangePercent float64 `json:"cum_change_percent"` + FlatAbsolute AbsoluteChange `json:"flat_absolute"` + CumAbsolute AbsoluteChange `json:"cum_absolute"` + Timestamp time.Time `json:"timestamp"` } diff --git a/go.mod b/go.mod index f6c38ae..85d28ff 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,15 @@ module github.com/AlexsanderHamir/prof go 1.24.3 -require github.com/spf13/cobra v1.9.1 +require ( + github.com/microcosm-cc/bluemonday v1.0.27 + github.com/spf13/cobra v1.9.1 +) require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/spf13/pflag v1.0.7 // indirect golang.org/x/net v0.26.0 // indirect )