// Package report renders the bakeoff's comparison page from results.
package report
import (
_ "embed"
"fmt"
"html/template"
"io"
"sort"
"strings"
b "bakeoff/harness"
)
//go:embed report.html.tmpl
var pageSrc string
//go:embed style.tmpl
var styleSrc string
//go:embed writeup.html.tmpl
var writeupSrc string
var page = template.Must(template.New("page").Funcs(template.FuncMap{
"md": func(c Copy, key string) template.HTML { return markdown(c.T(key)) },
"t": func(c Copy, key string) string { return c.T(key) },
"opt": func(c Copy, key string) string { return c[key] },
// cp is a string for a page element; in a copy-review preview it
// carries data-copy so the reviewer can click it.
"cp": func(c Copy, key string, preview bool) template.HTML {
text := template.HTMLEscapeString(c.T(key))
if !preview {
return template.HTML(text)
}
return template.HTML(`` + text + ``)
},
"ms": ms,
"mb": mb,
"num": num,
"usd": usd,
"secs": secs,
"live": live,
"total": func(r b.AppResult) b.Tokens { return r.TokensBuild.Add(r.TokensFix).Add(r.TokensSecurity) },
"wake": func(r b.AppResult) float64 { return medianOf(r.Wake) },
"peak": func(r b.AppResult, sc string) float64 { return knee(r, sc).PeakRPS },
"loc": func(c b.CodeStats) int { return c.Server + c.Templates + c.JS },
"ran": func(x b.Behaviour) int { return x.Passed + x.Failed },
"float": func(n int64) float64 { return float64(n) },
"inc": func(i int) int { return i + 1 },
}).Parse(pageSrc + styleSrc))
var writeupPage = template.Must(template.Must(page.Clone()).New("writeup").Parse(writeupSrc))
// RenderWriteup writes the write-up page from its Markdown, with the
// report's styles; the title is the Markdown's first heading.
func RenderWriteup(src string, copy Copy, w io.Writer) error {
title := strings.TrimSpace(strings.TrimLeft(strings.SplitN(strings.TrimSpace(src), "\n", 2)[0], "# "))
return writeupPage.ExecuteTemplate(w, "writeup", struct {
Title string
Body template.HTML
Copy Copy
}{title, markdown(src), copy})
}
func live(r b.AppResult) bool { return strings.HasPrefix(r.Status, "LIVE") }
func ms(v float64) string {
if v >= 100 {
return fmt.Sprintf("%.0f ms", v)
}
return fmt.Sprintf("%.1f ms", v)
}
func mb(v float64) string { return fmt.Sprintf("%.1f MB", v/1e6) }
func usd(v float64) string { return fmt.Sprintf("$%.2f", v) }
func kb(v float64) string { return fmt.Sprintf("%.1f MB", v/1024) }
func secs(v float64) string {
if v >= 120 {
return fmt.Sprintf("%.1f min", v/60)
}
return fmt.Sprintf("%.1f s", v)
}
func num(v float64) string {
s := fmt.Sprintf("%.0f", v)
for i := len(s) - 3; i > 0 && s[i-1] != '-'; i -= 3 {
s = s[:i] + "," + s[i:]
}
return s
}
func medianOf(v []float64) float64 {
if len(v) == 0 {
return 0
}
c := append([]float64(nil), v...)
sort.Float64s(c)
return c[len(c)/2]
}
func knee(r b.AppResult, scenario string) b.KneeResult {
var sweep []b.LoadResult
for _, l := range r.Load {
if l.Scenario == scenario {
sweep = append(sweep, l)
}
}
return b.Knee(sweep)
}
// kneeChart shows, per app, where it stopped scaling; an app that never
// did shows that instead of a bar, and errors are named after the value.
func kneeChart(rs []b.AppResult, c Copy, scenario, id string) Chart {
ch := chart(rs, c, id, func(v float64) string { return c.T("unit.connections", "n", num(v)) },
func(r b.AppResult) (float64, bool) {
k := knee(r, scenario)
return float64(k.Concurrency), k.Found
})
for i, r := range rs {
k := knee(r, scenario)
switch {
case !live(r):
case k.ErrorsAt > 0 && k.ErrorsStatus == "503":
ch.Rows[i].Suffix = c.T("label.errors_from", "n", num(float64(k.ErrorsAt)))
case k.ErrorsAt > 0:
ch.Rows[i].Suffix = c.T("label.failed_from", "n", num(float64(k.ErrorsAt)))
case !k.Found && k.MaxTested > 0:
ch.Rows[i].Missing = c.T("label.still_scaling", "n", num(float64(k.MaxTested)))
}
}
return ch
}
// chart builds the chart whose title and note are chart..title and
// chart..note; get returns ok=false when the app has no measurement.
func chart(rs []b.AppResult, c Copy, id string, format func(float64) string, get func(b.AppResult) (float64, bool)) Chart {
ch := Chart{ID: id, Title: c.T("chart." + id + ".title"), Note: c.T("chart." + id + ".note"), Format: format}
for _, r := range rs {
row := Row{Language: r.Language}
if !live(r) {
row.Missing = c.T("label.blocked")
} else if v, ok := get(r); ok {
row.Value = v
} else {
row.Missing = c.T("label.not_measured")
}
ch.Rows = append(ch.Rows, row)
}
return ch
}
func charts(rs []b.AppResult, c Copy) []Chart {
pos := func(v float64) (float64, bool) { return v, v > 0 }
loc := Chart{ID: "loc", Title: c.T("chart.loc.title"), Note: c.T("chart.loc.note"),
Format: func(v float64) string { return num(v) },
Legend: []string{c.T("chart.loc.server"), c.T("chart.loc.templates"), c.T("chart.loc.js")},
LegendIDs: []string{"chart.loc.server", "chart.loc.templates", "chart.loc.js"}}
for _, r := range rs {
row := Row{Language: r.Language}
code := r.Code
if !live(r) {
row.Missing = c.T("label.blocked")
} else if code.Server+code.Templates+code.JS == 0 {
row.Missing = c.T("label.not_measured")
} else {
row.Parts = []float64{float64(code.Server), float64(code.Templates), float64(code.JS)}
row.Value = float64(code.Server + code.Templates + code.JS)
}
loc.Rows = append(loc.Rows, row)
}
all := func(r b.AppResult) b.Tokens { return r.TokensBuild.Add(r.TokensFix).Add(r.TokensSecurity) }
return []Chart{
chart(rs, c, "ttfb_empty", ms, func(r b.AppResult) (float64, bool) { return pos(r.TTFBEmpty.P50) }),
chart(rs, c, "ttfb_50", ms, func(r b.AppResult) (float64, bool) { return pos(r.TTFB50.P50) }),
chart(rs, c, "wake", ms, func(r b.AppResult) (float64, bool) { return pos(medianOf(r.Wake)) }),
chart(rs, c, "reads_peak", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "read").PeakRPS) }),
kneeChart(rs, c, "read", "reads_knee"),
chart(rs, c, "toggles_peak", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "toggle").PeakRPS) }),
kneeChart(rs, c, "toggle", "toggles_knee"),
chart(rs, c, "reads_after", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "read-after").PeakRPS) }),
kneeChart(rs, c, "read-after", "reads_after_knee"),
chart(rs, c, "memory_idle", kb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Memory.IdleKB)) }),
chart(rs, c, "memory_peak", kb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Memory.PeakKB)) }),
chart(rs, c, "binary", mb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Build.Bytes)) }),
chart(rs, c, "build_cold", secs, func(r b.AppResult) (float64, bool) { return pos(r.Build.ColdSeconds) }),
chart(rs, c, "build_edit", secs, func(r b.AppResult) (float64, bool) { return pos(r.Build.EditSeconds) }),
loc,
chart(rs, c, "cost", usd, func(r b.AppResult) (float64, bool) { return pos(all(r).CostUSD) }),
chart(rs, c, "tokens", num, func(r b.AppResult) (float64, bool) { return pos(float64(all(r).Output)) }),
chart(rs, c, "readability", func(v float64) string { return c.T("unit.out_of_ten", "n", fmt.Sprintf("%.0f", v)) },
func(r b.AppResult) (float64, bool) { return pos(r.Readability.Overall) }),
}
}
// Render writes the report page.
func Render(rs []b.AppResult, copy Copy, w io.Writer) error { return render(rs, copy, w, false) }
// RenderPreview writes the page for copy review: every string that sits
// in its own element carries data-copy="".
func RenderPreview(rs []b.AppResult, copy Copy, w io.Writer) error { return render(rs, copy, w, true) }
func render(rs []b.AppResult, copy Copy, w io.Writer, preview bool) error {
return page.Execute(w, struct {
Results []b.AppResult
Charts []Chart
Copy Copy
Preview bool
}{rs, charts(rs, copy), copy, preview})
}