// 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}) }