blob: 6332950ec3c7b4206931d27bb258769782fe87f7 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package benchstat
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"golang.org/x/perf/internal/stats"
"golang.org/x/perf/storage/benchfmt"
)
// A Collection is a collection of benchmark results.
type Collection struct {
// Configs, Groups, and Units give the set of configs,
// groups, and units from the keys in Stats in an order
// meant to match the order the benchmarks were read in.
Configs, Groups, Units []string
// Benchmarks gives the set of benchmarks from the keys in
// Stats by group in an order meant to match the order
// benchmarks were read in.
Benchmarks map[string][]string
// Metrics holds the accumulated metrics for each key.
Metrics map[Key]*Metrics
// DeltaTest is the test to use to decide if a change is significant.
// If nil, it defaults to UTest.
DeltaTest DeltaTest
// Alpha is the p-value cutoff to report a change as significant.
// If zero, it defaults to 0.05.
Alpha float64
// AddGeoMean specifies whether to add a line to the table
// showing the geometric mean of all the benchmark results.
AddGeoMean bool
// SplitBy specifies the labels to split results by.
// By default, results will only be split by full name.
SplitBy []string
// Order specifies the row display order for this table.
// If Order is nil, the table rows are printed in order of
// first appearance in the input.
Order Order
}
// A Key identifies one metric (e.g., "ns/op", "B/op") from one
// benchmark (function name sans "Benchmark" prefix) and optional
// group in one configuration (input file name).
type Key struct {
Config, Group, Benchmark, Unit string
}
// A Metrics holds the measurements of a single metric
// (for example, ns/op or MB/s)
// for all runs of a particular benchmark.
type Metrics struct {
Unit string // unit being measured
Values []float64 // measured values
RValues []float64 // Values with outliers removed
Min float64 // min of RValues
Mean float64 // mean of RValues
Max float64 // max of RValues
}
// FormatMean formats m.Mean using scaler.
func (m *Metrics) FormatMean(scaler Scaler) string {
var s string
if scaler != nil {
s = scaler(m.Mean)
} else {
s = fmt.Sprint(m.Mean)
}
return s
}
// 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.
func (m *Metrics) FormatDiff() string {
if m.Mean == 0 || m.Max == 0 {
return ""
}
diff := 1 - m.Min/m.Mean
if d := m.Max/m.Mean - 1; d > diff {
diff = d
}
return fmt.Sprintf("%.0f%%", diff*100.0)
}
// Format returns a textual formatting of "Mean ±Diff" using scaler.
func (m *Metrics) Format(scaler Scaler) string {
if m.Unit == "" {
return ""
}
mean := m.FormatMean(scaler)
diff := m.FormatDiff()
if diff == "" {
return mean + " "
}
return fmt.Sprintf("%s ±%3s", mean, diff)
}
// computeStats updates the derived statistics in m from the raw
// samples in m.Values.
func (m *Metrics) computeStats() {
// Discard outliers.
values := stats.Sample{Xs: m.Values}
q1, q3 := values.Percentile(0.25), values.Percentile(0.75)
lo, hi := q1-1.5*(q3-q1), q3+1.5*(q3-q1)
for _, value := range m.Values {
if lo <= value && value <= hi {
m.RValues = append(m.RValues, value)
}
}
// Compute statistics of remaining data.
m.Min, m.Max = stats.Bounds(m.RValues)
m.Mean = stats.Mean(m.RValues)
}
// addMetrics returns the metrics with the given key from c,
// creating a new one if needed.
func (c *Collection) addMetrics(key Key) *Metrics {
if c.Metrics == nil {
c.Metrics = make(map[Key]*Metrics)
}
if stat, ok := c.Metrics[key]; ok {
return stat
}
addString := func(strings *[]string, add string) {
for _, s := range *strings {
if s == add {
return
}
}
*strings = append(*strings, add)
}
addString(&c.Configs, key.Config)
addString(&c.Groups, key.Group)
if c.Benchmarks == nil {
c.Benchmarks = make(map[string][]string)
}
benchmarks := c.Benchmarks[key.Group]
addString(&benchmarks, key.Benchmark)
c.Benchmarks[key.Group] = benchmarks
addString(&c.Units, key.Unit)
m := &Metrics{Unit: key.Unit}
c.Metrics[key] = m
return m
}
// AddConfig adds the benchmark results in the formatted data
// to the named configuration.
// If the input is large, AddFile should be preferred,
// since it avoids the need to read a copy of the entire raw input
// into memory.
func (c *Collection) AddConfig(config string, data []byte) {
err := c.AddFile(config, bytes.NewReader(data))
if err != nil {
// bytes.Reader never returns errors
panic(err)
}
}
// AddFile adds the benchmark results in the formatted data
// (read from the reader r) to the named configuration.
func (c *Collection) AddFile(config string, r io.Reader) error {
c.Configs = append(c.Configs, config)
key := Key{Config: config}
br := benchfmt.NewReader(r)
for br.Next() {
c.addResult(key, br.Result())
}
return br.Err()
}
// AddResults adds the benchmark results to the named configuration.
func (c *Collection) AddResults(config string, results []*benchfmt.Result) {
c.Configs = append(c.Configs, config)
key := Key{Config: config}
for _, r := range results {
c.addResult(key, r)
}
}
func (c *Collection) addResult(key Key, r *benchfmt.Result) {
f := strings.Fields(r.Content)
if len(f) < 4 {
return
}
name := f[0]
if !strings.HasPrefix(name, "Benchmark") {
return
}
name = strings.TrimPrefix(name, "Benchmark")
n, _ := strconv.Atoi(f[1])
if n == 0 {
return
}
key.Group = c.makeGroup(r)
key.Benchmark = name
for i := 2; i+2 <= len(f); i += 2 {
val, err := strconv.ParseFloat(f[i], 64)
if err != nil {
continue
}
key.Unit = f[i+1]
m := c.addMetrics(key)
m.Values = append(m.Values, val)
}
}
func (c *Collection) makeGroup(r *benchfmt.Result) string {
var out string
for _, s := range c.SplitBy {
v := r.NameLabels[s]
if v == "" {
v = r.Labels[s]
}
if v != "" {
if out != "" {
out = out + " "
}
out += fmt.Sprintf("%s:%s", s, v)
}
}
return out
}