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