| // Copyright 2022 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 benchfmt provides a high-performance reader and writer for | |
| // the Go benchmark format. | |
| // | |
| // This implements the format documented at | |
| // https://golang.org/design/14313-benchmark-format. | |
| // | |
| // The reader and writer are structured as streaming operations to | |
| // allow incremental processing and avoid dictating a data model. This | |
| // allows consumers of these APIs to provide their own data model best | |
| // suited to its needs. The reader also performs in-place updates to | |
| // reduce allocation, enabling it to parse millions of benchmark | |
| // results per second on a typical laptop. | |
| // | |
| // This package is designed to be used with the higher-level packages | |
| // benchunit, benchmath, and benchproc. | |
| package benchfmt | |
| import "bytes" | |
| // A Result is a single benchmark result and all of its measurements. | |
| // | |
| // Results are designed to be mutated in place and reused to reduce | |
| // allocation. | |
| type Result struct { | |
| // Config is the set of key/value configuration pairs for this result, | |
| // including file and internal configuration. This does not include | |
| // sub-name configuration. | |
| // | |
| // This slice is mutable, as are the values in the slice. | |
| // Result internally maintains an index of the keys of this slice, | |
| // so callers must use SetConfig to add or delete keys, | |
| // but may modify values in place. There is one exception to this: | |
| // for convenience, new Results can be initialized directly, | |
| // e.g., using a struct literal. | |
| // | |
| // SetConfig appends new keys to this slice and updates existing ones | |
| // in place. To delete a key, it swaps the deleted key with | |
| // the final slice element. This way, the order of these keys is | |
| // deterministic. | |
| Config []Config | |
| // Name is the full name of this benchmark, including all | |
| // sub-benchmark configuration. | |
| Name Name | |
| // Iters is the number of iterations this benchmark's results | |
| // were averaged over. | |
| Iters int | |
| // Values is this benchmark's measurements and their units. | |
| Values []Value | |
| // configPos maps from Config.Key to index in Config. This | |
| // may be nil, which indicates the index needs to be | |
| // constructed. | |
| configPos map[string]int | |
| // fileName and line record where this Record was read from. | |
| fileName string | |
| line int | |
| } | |
| // A Config is a single key/value configuration pair. | |
| // This can be a file configuration, which was read directly from | |
| // a benchmark results file; or an "internal" configuration that was | |
| // supplied by tooling. | |
| type Config struct { | |
| Key string | |
| Value []byte | |
| File bool // Set if this is a file configuration key, otherwise internal | |
| } | |
| // Note: I tried many approaches to Config. Using two strings is nice | |
| // for the API, but forces a lot of allocation in extractors (since | |
| // they either need to convert strings to []byte or vice-versa). Using | |
| // a []byte for Value makes it slightly harder to use, but is good for | |
| // reusing space efficiently (Value is likely to have more distinct | |
| // values than Key) and lets all extractors work in terms of []byte | |
| // views. Making Key a []byte is basically all downside. | |
| // A Value is a single value/unit measurement from a benchmark result. | |
| // | |
| // Values should be tidied to use base units like "sec" and "B" when | |
| // constructed. Reader ensures this. | |
| type Value struct { | |
| Value float64 | |
| Unit string | |
| // OrigValue and OrigUnit, if non-zero, give the original, | |
| // untidied value and unit, typically as read from the | |
| // original input. OrigUnit may be "", indicating that the | |
| // value wasn't transformed. | |
| OrigValue float64 | |
| OrigUnit string | |
| } | |
| // Pos returns the file name and line number of a Result that was read | |
| // by a Reader. For Results that were not read from a file, it returns | |
| // "", 0. | |
| func (r *Result) Pos() (fileName string, line int) { | |
| return r.fileName, r.line | |
| } | |
| // Clone makes a copy of Result that shares no state with r. | |
| func (r *Result) Clone() *Result { | |
| r2 := &Result{ | |
| Config: make([]Config, len(r.Config)), | |
| Name: append([]byte(nil), r.Name...), | |
| Iters: r.Iters, | |
| Values: append([]Value(nil), r.Values...), | |
| fileName: r.fileName, | |
| line: r.line, | |
| } | |
| for i, cfg := range r.Config { | |
| r2.Config[i].Key = cfg.Key | |
| r2.Config[i].Value = append([]byte(nil), cfg.Value...) | |
| r2.Config[i].File = cfg.File | |
| } | |
| return r2 | |
| } | |
| // SetConfig sets configuration key to value, overriding or | |
| // adding the configuration as necessary, and marks it internal. | |
| // If value is "", SetConfig deletes key. | |
| func (r *Result) SetConfig(key, value string) { | |
| if value == "" { | |
| r.deleteConfig(key) | |
| } else { | |
| cfg := r.ensureConfig(key, false) | |
| cfg.Value = append(cfg.Value[:0], value...) | |
| } | |
| } | |
| // ensureConfig returns the Config for key, creating it if necessary. | |
| // | |
| // This sets Key and File of the returned Config, but it's up to the caller to | |
| // set Value. We take this approach because some callers have strings and others | |
| // have []byte, so leaving this to the caller avoids allocation in one of these | |
| // cases. | |
| func (r *Result) ensureConfig(key string, file bool) *Config { | |
| pos, ok := r.ConfigIndex(key) | |
| if ok { | |
| cfg := &r.Config[pos] | |
| cfg.File = file | |
| return cfg | |
| } | |
| // Add key. Reuse old space if possible. | |
| r.configPos[key] = len(r.Config) | |
| if len(r.Config) < cap(r.Config) { | |
| r.Config = r.Config[:len(r.Config)+1] | |
| cfg := &r.Config[len(r.Config)-1] | |
| cfg.Key = key | |
| cfg.File = file | |
| return cfg | |
| } | |
| r.Config = append(r.Config, Config{key, nil, file}) | |
| return &r.Config[len(r.Config)-1] | |
| } | |
| func (r *Result) deleteConfig(key string) { | |
| pos, ok := r.ConfigIndex(key) | |
| if !ok { | |
| return | |
| } | |
| // Delete key. | |
| cfg := &r.Config[pos] | |
| cfg2 := &r.Config[len(r.Config)-1] | |
| *cfg, *cfg2 = *cfg2, *cfg | |
| r.configPos[cfg.Key] = pos | |
| r.Config = r.Config[:len(r.Config)-1] | |
| delete(r.configPos, key) | |
| } | |
| // GetConfig returns the value of a configuration key, | |
| // or "" if not present. | |
| func (r *Result) GetConfig(key string) string { | |
| pos, ok := r.ConfigIndex(key) | |
| if !ok { | |
| return "" | |
| } | |
| return string(r.Config[pos].Value) | |
| } | |
| // ConfigIndex returns the index in r.Config of key. | |
| func (r *Result) ConfigIndex(key string) (pos int, ok bool) { | |
| if r.configPos == nil { | |
| // This is a fresh Result. Construct the index. | |
| r.configPos = make(map[string]int) | |
| for i, cfg := range r.Config { | |
| r.configPos[cfg.Key] = i | |
| } | |
| } | |
| pos, ok = r.configPos[key] | |
| return | |
| } | |
| // Value returns the measurement for the given unit. | |
| func (r *Result) Value(unit string) (float64, bool) { | |
| for _, v := range r.Values { | |
| if v.Unit == unit { | |
| return v.Value, true | |
| } | |
| } | |
| return 0, false | |
| } | |
| // A Name is a full benchmark name, including all sub-benchmark | |
| // configuration. | |
| type Name []byte | |
| // String returns the full benchmark name as a string. | |
| func (n Name) String() string { | |
| return string(n) | |
| } | |
| // Full returns the full benchmark name as a []byte. This is simply | |
| // the value of n, but helps with code readability. | |
| func (n Name) Full() []byte { | |
| return n | |
| } | |
| // Base returns the base part of a full benchmark name, without any | |
| // configuration keys or GOMAXPROCS. | |
| func (n Name) Base() []byte { | |
| slash := bytes.IndexByte(n.Full(), '/') | |
| if slash >= 0 { | |
| return n[:slash] | |
| } | |
| base, _ := n.splitGomaxprocs() | |
| return base | |
| } | |
| // Parts splits a benchmark name into the base name and sub-benchmark | |
| // configuration parts. Each sub-benchmark configuration part is one | |
| // of three forms: | |
| // | |
| // 1. "/<key>=<value>" indicates a key/value configuration pair. | |
| // | |
| // 2. "/<string>" indicates a positional configuration pair. | |
| // | |
| // 3. "-<gomaxprocs>" indicates the GOMAXPROCS of this benchmark. This | |
| // part can only appear last. | |
| // | |
| // Concatenating the base name and the configuration parts | |
| // reconstructs the full name. | |
| func (n Name) Parts() (baseName []byte, parts [][]byte) { | |
| // First pull off any GOMAXPROCS. | |
| buf, gomaxprocs := n.splitGomaxprocs() | |
| // Split the remaining parts. | |
| var nameParts [][]byte | |
| prev := 0 | |
| for i, c := range buf { | |
| if c == '/' { | |
| nameParts = append(nameParts, buf[prev:i]) | |
| prev = i | |
| } | |
| } | |
| nameParts = append(nameParts, buf[prev:]) | |
| if gomaxprocs != nil { | |
| nameParts = append(nameParts, gomaxprocs) | |
| } | |
| return nameParts[0], nameParts[1:] | |
| } | |
| func (n Name) splitGomaxprocs() (prefix, gomaxprocs []byte) { | |
| for i := len(n) - 1; i >= 0; i-- { | |
| if n[i] == '-' && i < len(n)-1 { | |
| return n[:i], n[i:] | |
| } | |
| if !('0' <= n[i] && n[i] <= '9') { | |
| // Not a digit. | |
| break | |
| } | |
| } | |
| return n, nil | |
| } |