Commit 1b4493ff authored by Russ Cox's avatar Russ Cox

benchstat: use template to generate html

The generated html changes in a few minor ways,
either unavoidable or not worth working around:

- Trailing <td></td> at end of row no longer removed.
- <td>~</td> instead of <td>~    </td>
- Literal + escaped to &#43;

Change-Id: I73a0d1f74ff4c3597dd96b1a4dac2d6a3d5cc462
Reviewed-on: https://go-review.googlesource.com/35938Reviewed-by: default avatarQuentin Smith <quentin@golang.org>
parent a7a7b5b2
...@@ -44,9 +44,6 @@ type Metrics struct { ...@@ -44,9 +44,6 @@ type Metrics struct {
// FormatMean formats m.Mean using scaler. // FormatMean formats m.Mean using scaler.
func (m *Metrics) FormatMean(scaler Scaler) string { func (m *Metrics) FormatMean(scaler Scaler) string {
if m == nil {
return ""
}
var s string var s string
if scaler != nil { if scaler != nil {
s = scaler(m.Mean) s = scaler(m.Mean)
...@@ -59,7 +56,7 @@ func (m *Metrics) FormatMean(scaler Scaler) string { ...@@ -59,7 +56,7 @@ func (m *Metrics) FormatMean(scaler Scaler) string {
// FormatDiff computes and formats the percent variation of max and min compared to mean. // FormatDiff computes and formats the percent variation of max and min compared to mean.
// If b.Mean or b.Max is zero, FormatDiff returns an empty string. // If b.Mean or b.Max is zero, FormatDiff returns an empty string.
func (m *Metrics) FormatDiff() string { func (m *Metrics) FormatDiff() string {
if m == nil || m.Mean == 0 || m.Max == 0 { if m.Mean == 0 || m.Max == 0 {
return "" return ""
} }
diff := 1 - m.Min/m.Mean diff := 1 - m.Min/m.Mean
...@@ -71,7 +68,7 @@ func (m *Metrics) FormatDiff() string { ...@@ -71,7 +68,7 @@ func (m *Metrics) FormatDiff() string {
// Format returns a textual formatting of "Mean ±Diff" using scaler. // Format returns a textual formatting of "Mean ±Diff" using scaler.
func (m *Metrics) Format(scaler Scaler) string { func (m *Metrics) Format(scaler Scaler) string {
if m == nil { if m.Unit == "" {
return "" return ""
} }
mean := m.FormatMean(scaler) mean := m.FormatMean(scaler)
......
...@@ -6,34 +6,27 @@ package main ...@@ -6,34 +6,27 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "html/template"
"html"
) )
var htmlTemplate = template.Must(template.New("").Parse(
`{{range $i, $table := .}}{{if gt $i 0}}
{{end}}<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>
<table class='benchstat'>
{{if .OldNewDelta}}<tr><th>name</th><th>old {{.Metric}}</th><th>new {{.Metric}}</th><th>delta</th>
{{else if eq (len .Configs) 1}}<tr><th>name</th><th>{{.Metric}}</th>
{{else}}<tr><th>name \ {{.Metric}}</th>{{range .Configs}}<th>{{.}}</th>{{end}}
{{end}}{{range $row := $table.Rows}}<tr><td>{{.Benchmark}}</td>{{range $m := .Metrics}}<td>{{$m.Format $row.Scaler}}</td>{{end}}{{if $table.OldNewDelta}}<td>{{.Delta}}</td><td>{{.Note}}</td>{{end}}
{{end -}}
</table>
{{end}}`))
// FormatHTML appends an HTML formatting of the tables to buf. // FormatHTML appends an HTML formatting of the tables to buf.
func FormatHTML(buf *bytes.Buffer, tables []*Table) { func FormatHTML(buf *bytes.Buffer, tables []*Table) {
var textTables [][]*textRow err := htmlTemplate.Execute(buf, tables)
for _, t := range tables { if err != nil {
textTables = append(textTables, toText(t)) // Only possible errors here are template not matching data structure.
} // Don't make caller check - it's our fault.
panic(err)
for i, table := range textTables {
if i > 0 {
fmt.Fprintf(buf, "\n")
}
fmt.Fprintf(buf, "<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>\n")
fmt.Fprintf(buf, "<table class='benchstat'>\n")
printRow := func(row *textRow, tag string) {
fmt.Fprintf(buf, "<tr>")
for _, cell := range row.cols {
fmt.Fprintf(buf, "<%s>%s</%s>", tag, html.EscapeString(cell), tag)
}
fmt.Fprintf(buf, "\n")
}
printRow(table[0], "th")
for _, row := range table[1:] {
printRow(row, "td")
}
fmt.Fprintf(buf, "</table>\n")
} }
} }
...@@ -22,6 +22,7 @@ func TestGolden(t *testing.T) { ...@@ -22,6 +22,7 @@ func TestGolden(t *testing.T) {
if t.Failed() { if t.Failed() {
t.Fatal("skipping other tests") t.Fatal("skipping other tests")
} }
check(t, "exampleoldhtml", "-html", "exampleold.txt")
check(t, "examplehtml", "-html", "exampleold.txt", "examplenew.txt") check(t, "examplehtml", "-html", "exampleold.txt", "examplenew.txt")
if t.Failed() { if t.Failed() {
t.Fatal("skipping other tests") t.Fatal("skipping other tests")
......
...@@ -12,16 +12,17 @@ import ( ...@@ -12,16 +12,17 @@ import (
// A Table is a table for display in the benchstat output. // A Table is a table for display in the benchstat output.
type Table struct { type Table struct {
Metric string Metric string
Configs []string OldNewDelta bool // is this an old-new-delta table?
Rows []*Row Configs []string
Rows []*Row
} }
// A Row is a table row for display in the benchstat output. // A Row is a table row for display in the benchstat output.
type Row struct { type Row struct {
Benchmark string // benchmark name Benchmark string // benchmark name
Scaler Scaler // formatter for stats means Scaler Scaler // formatter for stats means
Metrics []*Metrics // columns of statistics (nil slice entry means no data) Metrics []*Metrics // columns of statistics
Delta string // formatted percent change Delta string // formatted percent change
Note string // additional information Note string // additional information
Same bool // likely no change Same bool // likely no change
...@@ -40,31 +41,33 @@ func (c *Collection) Tables(deltaTest DeltaTest) []*Table { ...@@ -40,31 +41,33 @@ func (c *Collection) Tables(deltaTest DeltaTest) []*Table {
table := new(Table) table := new(Table)
table.Configs = c.Configs table.Configs = c.Configs
table.Metric = metricOf(key.Unit) table.Metric = metricOf(key.Unit)
table.OldNewDelta = len(c.Configs) == 2
for _, key.Benchmark = range c.Benchmarks { for _, key.Benchmark = range c.Benchmarks {
row := &Row{Benchmark: key.Benchmark} row := &Row{Benchmark: key.Benchmark}
for _, key.Config = range c.Configs { for _, key.Config = range c.Configs {
m := c.Metrics[key] m := c.Metrics[key]
row.Metrics = append(row.Metrics, m)
if m == nil { if m == nil {
row.Metrics = append(row.Metrics, new(Metrics))
continue continue
} }
row.Metrics = append(row.Metrics, m)
if row.Scaler == nil { if row.Scaler == nil {
row.Scaler = NewScaler(m.Mean, m.Unit) row.Scaler = NewScaler(m.Mean, m.Unit)
} }
} }
// If there are only two configs being compared, add stats. // If there are only two configs being compared, add stats.
// If one is missing, omit line entirely. if table.OldNewDelta {
// TODO: Control this better.
if len(c.Configs) == 2 {
k0 := key k0 := key
k0.Config = c.Configs[0] k0.Config = c.Configs[0]
k1 := key k1 := key
k1.Config = c.Configs[1] k1.Config = c.Configs[1]
old := c.Metrics[k0] old := c.Metrics[k0]
new := c.Metrics[k1] new := c.Metrics[k1]
if old == nil || new == nil { // If one is missing, omit row entirely.
// TODO: Control this better.
if old == new || new == nil {
continue continue
} }
pval, testerr := deltaTest(old, new) pval, testerr := deltaTest(old, new)
...@@ -90,7 +93,7 @@ func (c *Collection) Tables(deltaTest DeltaTest) []*Table { ...@@ -90,7 +93,7 @@ func (c *Collection) Tables(deltaTest DeltaTest) []*Table {
if len(table.Rows) > 0 { if len(table.Rows) > 0 {
if *flagGeomean { if *flagGeomean {
addGeomean(c, table, key.Unit, len(c.Configs) == 2) addGeomean(c, table, key.Unit, table.OldNewDelta)
} }
tables = append(tables, table) tables = append(tables, table)
} }
...@@ -128,7 +131,7 @@ func addGeomean(c *Collection, t *Table, unit string, delta bool) { ...@@ -128,7 +131,7 @@ func addGeomean(c *Collection, t *Table, unit string, delta bool) {
} }
} }
if len(means) == 0 { if len(means) == 0 {
row.Metrics = append(row.Metrics, nil) row.Metrics = append(row.Metrics, new(Metrics))
delta = false delta = false
} else { } else {
geomean := stats.GeoMean(means) geomean := stats.GeoMean(means)
......
<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style> <style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>
<table class='benchstat'> <table class='benchstat'>
<tr><th>name</th><th>old time/op</th><th>new time/op</th><th>delta</th> <tr><th>name</th><th>old time/op</th><th>new time/op</th><th>delta</th>
<tr><td>GobEncode</td><td>13.6ms ± 1%</td><td>11.8ms ± 1%</td><td>-13.31%</td><td>(p=0.016 n=4+5)</td> <tr><td>GobEncode</td><td>13.6ms ± 1%</td><td>11.8ms ± 1%</td><td>-13.31%</td><td>(p=0.016 n=4&#43;5)</td>
<tr><td>JSONEncode</td><td>32.1ms ± 1%</td><td>31.8ms ± 1%</td><td>~ </td><td>(p=0.286 n=4+5)</td> <tr><td>JSONEncode</td><td>32.1ms ± 1%</td><td>31.8ms ± 1%</td><td>~</td><td>(p=0.286 n=4&#43;5)</td>
</table> </table>
<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style> <style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>
<table class='benchstat'> <table class='benchstat'>
<tr><th>name</th><th>old speed</th><th>new speed</th><th>delta</th> <tr><th>name</th><th>old speed</th><th>new speed</th><th>delta</th>
<tr><td>GobEncode</td><td>56.4MB/s ± 1%</td><td>65.1MB/s ± 1%</td><td>+15.36%</td><td>(p=0.016 n=4+5)</td> <tr><td>GobEncode</td><td>56.4MB/s ± 1%</td><td>65.1MB/s ± 1%</td><td>&#43;15.36%</td><td>(p=0.016 n=4&#43;5)</td>
<tr><td>JSONEncode</td><td>60.4MB/s ± 1%</td><td>61.1MB/s ± 2%</td><td>~ </td><td>(p=0.286 n=4+5)</td> <tr><td>JSONEncode</td><td>60.4MB/s ± 1%</td><td>61.1MB/s ± 2%</td><td>~</td><td>(p=0.286 n=4&#43;5)</td>
</table> </table>
<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>
<table class='benchstat'>
<tr><th>name</th><th>time/op</th>
<tr><td>GobEncode</td><td>13.6ms ± 1%</td>
<tr><td>JSONEncode</td><td>32.1ms ± 1%</td>
</table>
<style>.benchstat tbody td:nth-child(1n+2) { text-align: right; padding: 0em 1em; }</style>
<table class='benchstat'>
<tr><th>name</th><th>speed</th>
<tr><td>GobEncode</td><td>56.4MB/s ± 1%</td>
<tr><td>JSONEncode</td><td>60.4MB/s ± 1%</td>
</table>
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment