Skip to content

Commit 4ffc868

Browse files
Merge pull request #23 from AlexsanderHamir/refactors
Tracker Supports HTML
2 parents 1979dbc + 0c887b3 commit 4ffc868

File tree

5 files changed

+244
-33
lines changed

5 files changed

+244
-33
lines changed

cli/api.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,10 @@ func runSetup(_ *cobra.Command, _ []string) error {
238238
}
239239

240240
var validFormats = map[string]bool{
241-
"summary": true,
242-
"detailed": true,
241+
"summary": true,
242+
"detailed": true,
243+
"summary-html": true,
244+
"detailed-html": true,
243245
}
244246

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

262-
switch outputFormat {
263-
case "summary":
264-
printSummary(report)
265-
case "detailed":
266-
printDetailedReport(report)
267-
}
264+
chooseOutputFormat(report)
268265

269266
return nil
270267
}
@@ -285,11 +282,7 @@ func runTrackManual(_ *cobra.Command, _ []string) error {
285282
return nil
286283
}
287284

288-
switch outputFormat {
289-
case "summary":
290-
printSummary(report)
291-
case "detailed":
292-
printDetailedReport(report)
293-
}
285+
chooseOutputFormat(report)
286+
294287
return nil
295288
}

cli/helpers.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ package cli
22

33
import (
44
"fmt"
5+
"html/template"
56
"log/slog"
67
"math"
8+
"os"
79
"sort"
810

911
"github.com/AlexsanderHamir/prof/engine/benchmark"
1012
"github.com/AlexsanderHamir/prof/engine/tracker"
1113
"github.com/AlexsanderHamir/prof/internal/args"
1214
"github.com/AlexsanderHamir/prof/internal/config"
1315
"github.com/AlexsanderHamir/prof/internal/shared"
16+
"github.com/microcosm-cc/bluemonday"
1417
)
1518

1619
func printConfiguration(benchArgs *args.BenchArgs, functionFilterPerBench map[string]config.FunctionFilter) {
@@ -182,3 +185,225 @@ func printDetailedReport(report *tracker.ProfileChangeReport) {
182185
fmt.Print(change.Report())
183186
}
184187
}
188+
189+
type htmlData struct {
190+
TotalFunctions int
191+
Regressions []*tracker.FunctionChangeResult
192+
Improvements []*tracker.FunctionChangeResult
193+
Stable int
194+
}
195+
196+
func generateHTMLSummary(report *tracker.ProfileChangeReport, outputPath string) error {
197+
var regressionList, improvementList []*tracker.FunctionChangeResult
198+
var stable int
199+
200+
for _, change := range report.FunctionChanges {
201+
switch change.ChangeType {
202+
case shared.REGRESSION:
203+
regressionList = append(regressionList, change)
204+
case shared.IMPROVEMENT:
205+
improvementList = append(improvementList, change)
206+
default:
207+
stable++
208+
}
209+
}
210+
211+
sort.Slice(regressionList, func(i, j int) bool {
212+
return regressionList[i].FlatChangePercent > regressionList[j].FlatChangePercent
213+
})
214+
sort.Slice(improvementList, func(i, j int) bool {
215+
return math.Abs(improvementList[i].FlatChangePercent) > math.Abs(improvementList[j].FlatChangePercent)
216+
})
217+
218+
data := htmlData{
219+
TotalFunctions: len(report.FunctionChanges),
220+
Regressions: regressionList,
221+
Improvements: improvementList,
222+
Stable: stable,
223+
}
224+
225+
tmpl := `
226+
<!DOCTYPE html>
227+
<html>
228+
<head>
229+
<meta charset="UTF-8">
230+
<title>Performance Tracking Summary</title>
231+
<style>
232+
body {
233+
font-family: Arial, sans-serif;
234+
margin: 2rem;
235+
font-size: 1.25rem; /* Increased again */
236+
}
237+
h2 {
238+
color: #333;
239+
font-size: 1.8rem; /* Bigger main heading */
240+
}
241+
h3 {
242+
font-size: 1.5rem; /* Bigger subheading */
243+
}
244+
.regressions { color: red; }
245+
.improvements { color: green; }
246+
ul { padding-left: 1.5rem; }
247+
</style>
248+
</head>
249+
<body>
250+
<h2>Performance Tracking Summary</h2>
251+
<p><strong>Total Functions Analyzed:</strong> {{.TotalFunctions}}</p>
252+
<p><strong>Regressions:</strong> {{len .Regressions}}</p>
253+
<p><strong>Improvements:</strong> {{len .Improvements}}</p>
254+
<p><strong>Stable:</strong> {{.Stable}}</p>
255+
256+
{{if .Regressions}}
257+
<h3 class="regressions">⚠️ Top Regressions (worst first):</h3>
258+
<ul>
259+
{{range .Regressions}}<li>{{summary .}}</li>{{end}}
260+
</ul>
261+
{{end}}
262+
263+
{{if .Improvements}}
264+
<h3 class="improvements">✅ Top Improvements (best first):</h3>
265+
<ul>
266+
{{range .Improvements}}<li>{{summary .}}</li>{{end}}
267+
</ul>
268+
{{end}}
269+
</body>
270+
</html>
271+
`
272+
273+
funcMap := template.FuncMap{
274+
"summary": func(fc *tracker.FunctionChangeResult) string {
275+
return fc.Summary()
276+
},
277+
}
278+
279+
t, err := template.New("report").Funcs(funcMap).Parse(tmpl)
280+
if err != nil {
281+
return err
282+
}
283+
284+
file, err := os.Create(outputPath)
285+
if err != nil {
286+
return err
287+
}
288+
defer file.Close()
289+
290+
return t.Execute(file, data)
291+
}
292+
293+
type detailedHTMLData struct {
294+
Total int
295+
Regressions int
296+
Improvements int
297+
Stable int
298+
Changes []*tracker.FunctionChangeResult
299+
}
300+
301+
func generateDetailedHTMLReport(report *tracker.ProfileChangeReport, outputPath string) error {
302+
changes := report.FunctionChanges
303+
304+
// Count types
305+
var regressions, improvements, stable int
306+
for _, change := range changes {
307+
switch change.ChangeType {
308+
case shared.REGRESSION:
309+
regressions++
310+
case shared.IMPROVEMENT:
311+
improvements++
312+
default:
313+
stable++
314+
}
315+
}
316+
317+
// Sort: regressions → improvements → stable, each by magnitude
318+
typePriority := map[string]int{
319+
shared.REGRESSION: regressionPriority,
320+
shared.IMPROVEMENT: improvementPriority,
321+
shared.STABLE: stablePriority,
322+
}
323+
324+
sort.Slice(changes, func(i, j int) bool {
325+
if typePriority[changes[i].ChangeType] != typePriority[changes[j].ChangeType] {
326+
return typePriority[changes[i].ChangeType] < typePriority[changes[j].ChangeType]
327+
}
328+
return math.Abs(changes[i].FlatChangePercent) > math.Abs(changes[j].FlatChangePercent)
329+
})
330+
331+
data := detailedHTMLData{
332+
Total: len(changes),
333+
Regressions: regressions,
334+
Improvements: improvements,
335+
Stable: stable,
336+
Changes: changes,
337+
}
338+
339+
tmpl := `
340+
<!DOCTYPE html>
341+
<html>
342+
<head>
343+
<meta charset="UTF-8">
344+
<title>Detailed Performance Report</title>
345+
<style>
346+
body { font-family: monospace, sans-serif; padding: 2rem; background: #f8f8f8; }
347+
h1, h2 { color: #333; }
348+
.stats { margin-bottom: 1rem; font-family: sans-serif; }
349+
.report-block { margin-bottom: 3rem; white-space: pre-wrap;
350+
background: #fff; padding: 1rem; border-left: 4px solid #ccc;
351+
font-size: 1.25rem; }
352+
</style>
353+
</head>
354+
<body>
355+
<h1>📈 Detailed Performance Report</h1>
356+
357+
<div class="stats">
358+
<p><strong>Total functions:</strong> {{.Total}}</p>
359+
<p><strong>🔴 Regressions:</strong> {{.Regressions}} | <strong>🟢 Improvements:</strong> {{.Improvements}} | <strong>⚪ Stable:</strong> {{.Stable}}</p>
360+
<p><em>Report Order: Regressions (worst → best), then Improvements (best → worst), then Stable</em></p>
361+
</div>
362+
363+
{{range .Changes}}
364+
<div class="report-block">
365+
<pre>{{report .}}</pre>
366+
</div>
367+
{{end}}
368+
</body>
369+
</html>
370+
`
371+
372+
sanitizer := bluemonday.StrictPolicy()
373+
funcMap := template.FuncMap{
374+
"report": func(fc *tracker.FunctionChangeResult) template.HTML {
375+
safe := sanitizer.Sanitize(fc.Report())
376+
return template.HTML(safe) //nolint:gosec // input is being sanatized
377+
},
378+
}
379+
380+
t, err := template.New("detailed").Funcs(funcMap).Parse(tmpl)
381+
if err != nil {
382+
return err
383+
}
384+
385+
file, err := os.Create(outputPath)
386+
if err != nil {
387+
return err
388+
}
389+
defer file.Close()
390+
391+
return t.Execute(file, data)
392+
}
393+
394+
func chooseOutputFormat(report *tracker.ProfileChangeReport) {
395+
switch outputFormat {
396+
case "summary":
397+
printSummary(report)
398+
case "detailed":
399+
printDetailedReport(report)
400+
case "summary-html":
401+
if err := generateHTMLSummary(report, "summary.html"); err != nil {
402+
slog.Info("summary-html failed", "err", err)
403+
}
404+
case "detailed-html":
405+
if err := generateDetailedHTMLReport(report, "detailed.html"); err != nil {
406+
slog.Info("detailed-html failed", "err", err)
407+
}
408+
}
409+
}

engine/tracker/types.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,6 @@ type ProfileChangeReport struct {
88
FunctionChanges []*FunctionChangeResult
99
}
1010

11-
type ProfileChangeSummary struct {
12-
TotalFunctions int
13-
Regressions int
14-
Improvements int
15-
Stable int
16-
NewFunctions int
17-
DeletedFunctions int
18-
WorstRegression *FunctionChangeResult // Function with biggest regression
19-
BestImprovement *FunctionChangeResult // Function with biggest improvement
20-
AverageFlatChange float64
21-
}
22-
23-
type ChangeMetadata struct {
24-
Environment string
25-
Version string
26-
TestType string // "benchmark", "load-test", "production"
27-
Tags map[string]string
28-
}
29-
3011
type FunctionChangeResult struct {
3112
FunctionName string
3213
ChangeType string // shared.REGRESSION, shred.IMPROVEMENT, shared.STABLE

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ go 1.24.3
55
require github.com/spf13/cobra v1.9.1
66

77
require (
8+
github.com/aymerick/douceur v0.2.0 // indirect
9+
github.com/gorilla/css v1.0.1 // indirect
810
github.com/inconshreveable/mousetrap v1.1.0 // indirect
11+
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
912
github.com/spf13/pflag v1.0.7 // indirect
13+
golang.org/x/net v0.26.0 // indirect
1014
)

go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
2+
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
13
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4+
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
5+
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
26
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
37
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
8+
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
9+
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
410
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
511
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
612
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
713
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
814
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
915
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
16+
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
17+
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
1018
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1119
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)