Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ func runSetup(_ *cobra.Command, _ []string) error {
}

var validFormats = map[string]bool{
"summary": true,
"detailed": true,
"summary": true,
"detailed": true,
"summary-html": true,
"detailed-html": true,
}

// runTrack handles the track command execution
Expand All @@ -259,12 +261,7 @@ func runTrackAuto(_ *cobra.Command, _ []string) error {
return nil
}

switch outputFormat {
case "summary":
printSummary(report)
case "detailed":
printDetailedReport(report)
}
chooseOutputFormat(report)

return nil
}
Expand All @@ -285,11 +282,7 @@ func runTrackManual(_ *cobra.Command, _ []string) error {
return nil
}

switch outputFormat {
case "summary":
printSummary(report)
case "detailed":
printDetailedReport(report)
}
chooseOutputFormat(report)

return nil
}
225 changes: 225 additions & 0 deletions cli/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package cli

import (
"fmt"
"html/template"
"log/slog"
"math"
"os"
"sort"

"github.com/AlexsanderHamir/prof/engine/benchmark"
"github.com/AlexsanderHamir/prof/engine/tracker"
"github.com/AlexsanderHamir/prof/internal/args"
"github.com/AlexsanderHamir/prof/internal/config"
"github.com/AlexsanderHamir/prof/internal/shared"
"github.com/microcosm-cc/bluemonday"
)

func printConfiguration(benchArgs *args.BenchArgs, functionFilterPerBench map[string]config.FunctionFilter) {
Expand Down Expand Up @@ -182,3 +185,225 @@ func printDetailedReport(report *tracker.ProfileChangeReport) {
fmt.Print(change.Report())
}
}

type htmlData struct {
TotalFunctions int
Regressions []*tracker.FunctionChangeResult
Improvements []*tracker.FunctionChangeResult
Stable int
}

func generateHTMLSummary(report *tracker.ProfileChangeReport, outputPath string) error {
var regressionList, improvementList []*tracker.FunctionChangeResult
var stable int

for _, change := range report.FunctionChanges {
switch change.ChangeType {
case shared.REGRESSION:
regressionList = append(regressionList, change)
case shared.IMPROVEMENT:
improvementList = append(improvementList, change)
default:
stable++
}
}

sort.Slice(regressionList, func(i, j int) bool {
return regressionList[i].FlatChangePercent > regressionList[j].FlatChangePercent
})
sort.Slice(improvementList, func(i, j int) bool {
return math.Abs(improvementList[i].FlatChangePercent) > math.Abs(improvementList[j].FlatChangePercent)
})

data := htmlData{
TotalFunctions: len(report.FunctionChanges),
Regressions: regressionList,
Improvements: improvementList,
Stable: stable,
}

tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Performance Tracking Summary</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 2rem;
font-size: 1.25rem; /* Increased again */
}
h2 {
color: #333;
font-size: 1.8rem; /* Bigger main heading */
}
h3 {
font-size: 1.5rem; /* Bigger subheading */
}
.regressions { color: red; }
.improvements { color: green; }
ul { padding-left: 1.5rem; }
</style>
</head>
<body>
<h2>Performance Tracking Summary</h2>
<p><strong>Total Functions Analyzed:</strong> {{.TotalFunctions}}</p>
<p><strong>Regressions:</strong> {{len .Regressions}}</p>
<p><strong>Improvements:</strong> {{len .Improvements}}</p>
<p><strong>Stable:</strong> {{.Stable}}</p>

{{if .Regressions}}
<h3 class="regressions">⚠️ Top Regressions (worst first):</h3>
<ul>
{{range .Regressions}}<li>{{summary .}}</li>{{end}}
</ul>
{{end}}

{{if .Improvements}}
<h3 class="improvements">✅ Top Improvements (best first):</h3>
<ul>
{{range .Improvements}}<li>{{summary .}}</li>{{end}}
</ul>
{{end}}
</body>
</html>
`

funcMap := template.FuncMap{
"summary": func(fc *tracker.FunctionChangeResult) string {
return fc.Summary()
},
}

t, err := template.New("report").Funcs(funcMap).Parse(tmpl)
if err != nil {
return err
}

file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()

return t.Execute(file, data)
}

type detailedHTMLData struct {
Total int
Regressions int
Improvements int
Stable int
Changes []*tracker.FunctionChangeResult
}

func generateDetailedHTMLReport(report *tracker.ProfileChangeReport, outputPath string) error {
changes := report.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 := detailedHTMLData{
Total: len(changes),
Regressions: regressions,
Improvements: improvements,
Stable: stable,
Changes: changes,
}

tmpl := `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Detailed Performance Report</title>
<style>
body { font-family: monospace, sans-serif; padding: 2rem; background: #f8f8f8; }
h1, h2 { color: #333; }
.stats { margin-bottom: 1rem; font-family: sans-serif; }
.report-block { margin-bottom: 3rem; white-space: pre-wrap;
background: #fff; padding: 1rem; border-left: 4px solid #ccc;
font-size: 1.25rem; }
</style>
</head>
<body>
<h1>📈 Detailed Performance Report</h1>

<div class="stats">
<p><strong>Total functions:</strong> {{.Total}}</p>
<p><strong>🔴 Regressions:</strong> {{.Regressions}} | <strong>🟢 Improvements:</strong> {{.Improvements}} | <strong>⚪ Stable:</strong> {{.Stable}}</p>
<p><em>Report Order: Regressions (worst → best), then Improvements (best → worst), then Stable</em></p>
</div>

{{range .Changes}}
<div class="report-block">
<pre>{{report .}}</pre>
</div>
{{end}}
</body>
</html>
`

sanitizer := bluemonday.StrictPolicy()
funcMap := template.FuncMap{
"report": func(fc *tracker.FunctionChangeResult) template.HTML {
safe := sanitizer.Sanitize(fc.Report())
return template.HTML(safe) //nolint:gosec // input is being sanatized
},
}

t, err := template.New("detailed").Funcs(funcMap).Parse(tmpl)
if err != nil {
return err
}

file, err := os.Create(outputPath)
if err != nil {
return err
}
defer file.Close()

return t.Execute(file, data)
}

func chooseOutputFormat(report *tracker.ProfileChangeReport) {
switch outputFormat {
case "summary":
printSummary(report)
case "detailed":
printDetailedReport(report)
case "summary-html":
if err := generateHTMLSummary(report, "summary.html"); err != nil {
slog.Info("summary-html failed", "err", err)
}
case "detailed-html":
if err := generateDetailedHTMLReport(report, "detailed.html"); err != nil {
slog.Info("detailed-html failed", "err", err)
}
}
}
19 changes: 0 additions & 19 deletions engine/tracker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,6 @@ type ProfileChangeReport struct {
FunctionChanges []*FunctionChangeResult
}

type ProfileChangeSummary struct {
TotalFunctions int
Regressions int
Improvements int
Stable int
NewFunctions int
DeletedFunctions int
WorstRegression *FunctionChangeResult // Function with biggest regression
BestImprovement *FunctionChangeResult // Function with biggest improvement
AverageFlatChange float64
}

type ChangeMetadata struct {
Environment string
Version string
TestType string // "benchmark", "load-test", "production"
Tags map[string]string
}

type FunctionChangeResult struct {
FunctionName string
ChangeType string // shared.REGRESSION, shred.IMPROVEMENT, shared.STABLE
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ go 1.24.3
require 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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading