carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1// Package report renders the bakeoff's comparison page from results.
2package report
3
4import (
5 _ "embed"
6 "fmt"
7 "html/template"
8 "io"
9 "sort"
10 "strings"
11
12 b "bakeoff/harness"
13)
14
15//go:embed report.html.tmpl
16var pageSrc string
17
18//go:embed style.tmpl
19var styleSrc string
20
21//go:embed writeup.html.tmpl
22var writeupSrc string
23
24var page = template.Must(template.New("page").Funcs(template.FuncMap{
25 "md": func(c Copy, key string) template.HTML { return markdown(c.T(key)) },
26 "t": func(c Copy, key string) string { return c.T(key) },
27 "opt": func(c Copy, key string) string { return c[key] },
28 // cp is a string for a page element; in a copy-review preview it
29 // carries data-copy so the reviewer can click it.
30 "cp": func(c Copy, key string, preview bool) template.HTML {
31 text := template.HTMLEscapeString(c.T(key))
32 if !preview {
33 return template.HTML(text)
34 }
35 return template.HTML(`<span data-copy="` + key + `">` + text + `</span>`)
36 },
37 "ms": ms,
38 "mb": mb,
39 "num": num,
40 "usd": usd,
41 "secs": secs,
42 "live": live,
43 "total": func(r b.AppResult) b.Tokens { return r.TokensBuild.Add(r.TokensFix).Add(r.TokensSecurity) },
44 "wake": func(r b.AppResult) float64 { return medianOf(r.Wake) },
45 "peak": func(r b.AppResult, sc string) float64 { return knee(r, sc).PeakRPS },
46 "loc": func(c b.CodeStats) int { return c.Server + c.Templates + c.JS },
47 "ran": func(x b.Behaviour) int { return x.Passed + x.Failed },
48 "float": func(n int64) float64 { return float64(n) },
49 "inc": func(i int) int { return i + 1 },
50}).Parse(pageSrc + styleSrc))
51
52var writeupPage = template.Must(template.Must(page.Clone()).New("writeup").Parse(writeupSrc))
53
54// RenderWriteup writes the write-up page from its Markdown, with the
55// report's styles; the title is the Markdown's first heading.
56func RenderWriteup(src string, copy Copy, w io.Writer) error {
57 title := strings.TrimSpace(strings.TrimLeft(strings.SplitN(strings.TrimSpace(src), "\n", 2)[0], "# "))
58 return writeupPage.ExecuteTemplate(w, "writeup", struct {
59 Title string
60 Body template.HTML
61 Copy Copy
62 }{title, markdown(src), copy})
63}
64
65func live(r b.AppResult) bool { return strings.HasPrefix(r.Status, "LIVE") }
66
67func ms(v float64) string {
68 if v >= 100 {
69 return fmt.Sprintf("%.0f ms", v)
70 }
71 return fmt.Sprintf("%.1f ms", v)
72}
73func mb(v float64) string { return fmt.Sprintf("%.1f MB", v/1e6) }
74func usd(v float64) string { return fmt.Sprintf("$%.2f", v) }
75func kb(v float64) string { return fmt.Sprintf("%.1f MB", v/1024) }
76func secs(v float64) string {
77 if v >= 120 {
78 return fmt.Sprintf("%.1f min", v/60)
79 }
80 return fmt.Sprintf("%.1f s", v)
81}
82func num(v float64) string {
83 s := fmt.Sprintf("%.0f", v)
84 for i := len(s) - 3; i > 0 && s[i-1] != '-'; i -= 3 {
85 s = s[:i] + "," + s[i:]
86 }
87 return s
88}
89
90func medianOf(v []float64) float64 {
91 if len(v) == 0 {
92 return 0
93 }
94 c := append([]float64(nil), v...)
95 sort.Float64s(c)
96 return c[len(c)/2]
97}
98
99func knee(r b.AppResult, scenario string) b.KneeResult {
100 var sweep []b.LoadResult
101 for _, l := range r.Load {
102 if l.Scenario == scenario {
103 sweep = append(sweep, l)
104 }
105 }
106 return b.Knee(sweep)
107}
108
109// kneeChart shows, per app, where it stopped scaling; an app that never
110// did shows that instead of a bar, and errors are named after the value.
111func kneeChart(rs []b.AppResult, c Copy, scenario, id string) Chart {
112 ch := chart(rs, c, id, func(v float64) string { return c.T("unit.connections", "n", num(v)) },
113 func(r b.AppResult) (float64, bool) {
114 k := knee(r, scenario)
115 return float64(k.Concurrency), k.Found
116 })
117 for i, r := range rs {
118 k := knee(r, scenario)
119 switch {
120 case !live(r):
121 case k.ErrorsAt > 0 && k.ErrorsStatus == "503":
122 ch.Rows[i].Suffix = c.T("label.errors_from", "n", num(float64(k.ErrorsAt)))
123 case k.ErrorsAt > 0:
124 ch.Rows[i].Suffix = c.T("label.failed_from", "n", num(float64(k.ErrorsAt)))
125 case !k.Found && k.MaxTested > 0:
126 ch.Rows[i].Missing = c.T("label.still_scaling", "n", num(float64(k.MaxTested)))
127 }
128 }
129 return ch
130}
131
132// chart builds the chart whose title and note are chart.<id>.title and
133// chart.<id>.note; get returns ok=false when the app has no measurement.
134func chart(rs []b.AppResult, c Copy, id string, format func(float64) string, get func(b.AppResult) (float64, bool)) Chart {
135 ch := Chart{ID: id, Title: c.T("chart." + id + ".title"), Note: c.T("chart." + id + ".note"), Format: format}
136 for _, r := range rs {
137 row := Row{Language: r.Language}
138 if !live(r) {
139 row.Missing = c.T("label.blocked")
140 } else if v, ok := get(r); ok {
141 row.Value = v
142 } else {
143 row.Missing = c.T("label.not_measured")
144 }
145 ch.Rows = append(ch.Rows, row)
146 }
147 return ch
148}
149
150func charts(rs []b.AppResult, c Copy) []Chart {
151 pos := func(v float64) (float64, bool) { return v, v > 0 }
152 loc := Chart{ID: "loc", Title: c.T("chart.loc.title"), Note: c.T("chart.loc.note"),
153 Format: func(v float64) string { return num(v) },
154 Legend: []string{c.T("chart.loc.server"), c.T("chart.loc.templates"), c.T("chart.loc.js")},
155 LegendIDs: []string{"chart.loc.server", "chart.loc.templates", "chart.loc.js"}}
156 for _, r := range rs {
157 row := Row{Language: r.Language}
158 code := r.Code
159 if !live(r) {
160 row.Missing = c.T("label.blocked")
161 } else if code.Server+code.Templates+code.JS == 0 {
162 row.Missing = c.T("label.not_measured")
163 } else {
164 row.Parts = []float64{float64(code.Server), float64(code.Templates), float64(code.JS)}
165 row.Value = float64(code.Server + code.Templates + code.JS)
166 }
167 loc.Rows = append(loc.Rows, row)
168 }
169 all := func(r b.AppResult) b.Tokens { return r.TokensBuild.Add(r.TokensFix).Add(r.TokensSecurity) }
170 return []Chart{
171 chart(rs, c, "ttfb_empty", ms, func(r b.AppResult) (float64, bool) { return pos(r.TTFBEmpty.P50) }),
172 chart(rs, c, "ttfb_50", ms, func(r b.AppResult) (float64, bool) { return pos(r.TTFB50.P50) }),
173 chart(rs, c, "wake", ms, func(r b.AppResult) (float64, bool) { return pos(medianOf(r.Wake)) }),
174 chart(rs, c, "reads_peak", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "read").PeakRPS) }),
175 kneeChart(rs, c, "read", "reads_knee"),
176 chart(rs, c, "toggles_peak", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "toggle").PeakRPS) }),
177 kneeChart(rs, c, "toggle", "toggles_knee"),
178 chart(rs, c, "reads_after", num, func(r b.AppResult) (float64, bool) { return pos(knee(r, "read-after").PeakRPS) }),
179 kneeChart(rs, c, "read-after", "reads_after_knee"),
180 chart(rs, c, "memory_idle", kb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Memory.IdleKB)) }),
181 chart(rs, c, "memory_peak", kb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Memory.PeakKB)) }),
182 chart(rs, c, "binary", mb, func(r b.AppResult) (float64, bool) { return pos(float64(r.Build.Bytes)) }),
183 chart(rs, c, "build_cold", secs, func(r b.AppResult) (float64, bool) { return pos(r.Build.ColdSeconds) }),
184 chart(rs, c, "build_edit", secs, func(r b.AppResult) (float64, bool) { return pos(r.Build.EditSeconds) }),
185 loc,
186 chart(rs, c, "cost", usd, func(r b.AppResult) (float64, bool) { return pos(all(r).CostUSD) }),
187 chart(rs, c, "tokens", num, func(r b.AppResult) (float64, bool) { return pos(float64(all(r).Output)) }),
188 chart(rs, c, "readability", func(v float64) string { return c.T("unit.out_of_ten", "n", fmt.Sprintf("%.0f", v)) },
189 func(r b.AppResult) (float64, bool) { return pos(r.Readability.Overall) }),
190 }
191}
192
193// Render writes the report page.
194func Render(rs []b.AppResult, copy Copy, w io.Writer) error { return render(rs, copy, w, false) }
195
196// RenderPreview writes the page for copy review: every string that sits
197// in its own element carries data-copy="<id>".
198func RenderPreview(rs []b.AppResult, copy Copy, w io.Writer) error { return render(rs, copy, w, true) }
199
200func render(rs []b.AppResult, copy Copy, w io.Writer, preview bool) error {
201 return page.Execute(w, struct {
202 Results []b.AppResult
203 Charts []Chart
204 Copy Copy
205 Preview bool
206 }{rs, charts(rs, copy), copy, preview})
207}
208