github.com/jgbaldwinbrown/perf@v0.1.1/benchfmt/result.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package benchfmt provides a high-performance reader and writer for
     6  // the Go benchmark format.
     7  //
     8  // This implements the format documented at
     9  // https://golang.org/design/14313-benchmark-format.
    10  //
    11  // The reader and writer are structured as streaming operations to
    12  // allow incremental processing and avoid dictating a data model. This
    13  // allows consumers of these APIs to provide their own data model best
    14  // suited to its needs. The reader also performs in-place updates to
    15  // reduce allocation, enabling it to parse millions of benchmark
    16  // results per second on a typical laptop.
    17  //
    18  // This package is designed to be used with the higher-level packages
    19  // benchunit, benchmath, and benchproc.
    20  package benchfmt
    21  
    22  import "bytes"
    23  
    24  // A Result is a single benchmark result and all of its measurements.
    25  //
    26  // Results are designed to be mutated in place and reused to reduce
    27  // allocation.
    28  type Result struct {
    29  	// Config is the set of key/value configuration pairs for this result,
    30  	// including file and internal configuration. This does not include
    31  	// sub-name configuration.
    32  	//
    33  	// This slice is mutable, as are the values in the slice.
    34  	// Result internally maintains an index of the keys of this slice,
    35  	// so callers must use SetConfig to add or delete keys,
    36  	// but may modify values in place. There is one exception to this:
    37  	// for convenience, new Results can be initialized directly,
    38  	// e.g., using a struct literal.
    39  	//
    40  	// SetConfig appends new keys to this slice and updates existing ones
    41  	// in place. To delete a key, it swaps the deleted key with
    42  	// the final slice element. This way, the order of these keys is
    43  	// deterministic.
    44  	Config []Config
    45  
    46  	// Name is the full name of this benchmark, including all
    47  	// sub-benchmark configuration.
    48  	Name Name
    49  
    50  	// Iters is the number of iterations this benchmark's results
    51  	// were averaged over.
    52  	Iters int
    53  
    54  	// Values is this benchmark's measurements and their units.
    55  	Values []Value
    56  
    57  	// configPos maps from Config.Key to index in Config. This
    58  	// may be nil, which indicates the index needs to be
    59  	// constructed.
    60  	configPos map[string]int
    61  
    62  	// fileName and line record where this Record was read from.
    63  	fileName string
    64  	line     int
    65  }
    66  
    67  // A Config is a single key/value configuration pair.
    68  // This can be a file configuration, which was read directly from
    69  // a benchmark results file; or an "internal" configuration that was
    70  // supplied by tooling.
    71  type Config struct {
    72  	Key   string
    73  	Value []byte
    74  	File  bool // Set if this is a file configuration key, otherwise internal
    75  }
    76  
    77  // Note: I tried many approaches to Config. Using two strings is nice
    78  // for the API, but forces a lot of allocation in extractors (since
    79  // they either need to convert strings to []byte or vice-versa). Using
    80  // a []byte for Value makes it slightly harder to use, but is good for
    81  // reusing space efficiently (Value is likely to have more distinct
    82  // values than Key) and lets all extractors work in terms of []byte
    83  // views. Making Key a []byte is basically all downside.
    84  
    85  // A Value is a single value/unit measurement from a benchmark result.
    86  //
    87  // Values should be tidied to use base units like "sec" and "B" when
    88  // constructed. Reader ensures this.
    89  type Value struct {
    90  	Value float64
    91  	Unit  string
    92  
    93  	// OrigValue and OrigUnit, if non-zero, give the original,
    94  	// untidied value and unit, typically as read from the
    95  	// original input. OrigUnit may be "", indicating that the
    96  	// value wasn't transformed.
    97  	OrigValue float64
    98  	OrigUnit  string
    99  }
   100  
   101  // Pos returns the file name and line number of a Result that was read
   102  // by a Reader. For Results that were not read from a file, it returns
   103  // "", 0.
   104  func (r *Result) Pos() (fileName string, line int) {
   105  	return r.fileName, r.line
   106  }
   107  
   108  // Clone makes a copy of Result that shares no state with r.
   109  func (r *Result) Clone() *Result {
   110  	r2 := &Result{
   111  		Config:   make([]Config, len(r.Config)),
   112  		Name:     append([]byte(nil), r.Name...),
   113  		Iters:    r.Iters,
   114  		Values:   append([]Value(nil), r.Values...),
   115  		fileName: r.fileName,
   116  		line:     r.line,
   117  	}
   118  	for i, cfg := range r.Config {
   119  		r2.Config[i].Key = cfg.Key
   120  		r2.Config[i].Value = append([]byte(nil), cfg.Value...)
   121  		r2.Config[i].File = cfg.File
   122  	}
   123  	return r2
   124  }
   125  
   126  // SetConfig sets configuration key to value, overriding or
   127  // adding the configuration as necessary, and marks it internal.
   128  // If value is "", SetConfig deletes key.
   129  func (r *Result) SetConfig(key, value string) {
   130  	if value == "" {
   131  		r.deleteConfig(key)
   132  	} else {
   133  		cfg := r.ensureConfig(key, false)
   134  		cfg.Value = append(cfg.Value[:0], value...)
   135  	}
   136  }
   137  
   138  // ensureConfig returns the Config for key, creating it if necessary.
   139  //
   140  // This sets Key and File of the returned Config, but it's up to the caller to
   141  // set Value. We take this approach because some callers have strings and others
   142  // have []byte, so leaving this to the caller avoids allocation in one of these
   143  // cases.
   144  func (r *Result) ensureConfig(key string, file bool) *Config {
   145  	pos, ok := r.ConfigIndex(key)
   146  	if ok {
   147  		cfg := &r.Config[pos]
   148  		cfg.File = file
   149  		return cfg
   150  	}
   151  	// Add key. Reuse old space if possible.
   152  	r.configPos[key] = len(r.Config)
   153  	if len(r.Config) < cap(r.Config) {
   154  		r.Config = r.Config[:len(r.Config)+1]
   155  		cfg := &r.Config[len(r.Config)-1]
   156  		cfg.Key = key
   157  		cfg.File = file
   158  		return cfg
   159  	}
   160  	r.Config = append(r.Config, Config{key, nil, file})
   161  	return &r.Config[len(r.Config)-1]
   162  }
   163  
   164  func (r *Result) deleteConfig(key string) {
   165  	pos, ok := r.ConfigIndex(key)
   166  	if !ok {
   167  		return
   168  	}
   169  	// Delete key.
   170  	cfg := &r.Config[pos]
   171  	cfg2 := &r.Config[len(r.Config)-1]
   172  	*cfg, *cfg2 = *cfg2, *cfg
   173  	r.configPos[cfg.Key] = pos
   174  	r.Config = r.Config[:len(r.Config)-1]
   175  	delete(r.configPos, key)
   176  }
   177  
   178  // GetConfig returns the value of a configuration key,
   179  // or "" if not present.
   180  func (r *Result) GetConfig(key string) string {
   181  	pos, ok := r.ConfigIndex(key)
   182  	if !ok {
   183  		return ""
   184  	}
   185  	return string(r.Config[pos].Value)
   186  }
   187  
   188  // ConfigIndex returns the index in r.Config of key.
   189  func (r *Result) ConfigIndex(key string) (pos int, ok bool) {
   190  	if r.configPos == nil {
   191  		// This is a fresh Result. Construct the index.
   192  		r.configPos = make(map[string]int)
   193  		for i, cfg := range r.Config {
   194  			r.configPos[cfg.Key] = i
   195  		}
   196  	}
   197  
   198  	pos, ok = r.configPos[key]
   199  	return
   200  }
   201  
   202  // Value returns the measurement for the given unit.
   203  func (r *Result) Value(unit string) (float64, bool) {
   204  	for _, v := range r.Values {
   205  		if v.Unit == unit {
   206  			return v.Value, true
   207  		}
   208  	}
   209  	return 0, false
   210  }
   211  
   212  // A Name is a full benchmark name, including all sub-benchmark
   213  // configuration.
   214  type Name []byte
   215  
   216  // String returns the full benchmark name as a string.
   217  func (n Name) String() string {
   218  	return string(n)
   219  }
   220  
   221  // Full returns the full benchmark name as a []byte. This is simply
   222  // the value of n, but helps with code readability.
   223  func (n Name) Full() []byte {
   224  	return n
   225  }
   226  
   227  // Base returns the base part of a full benchmark name, without any
   228  // configuration keys or GOMAXPROCS.
   229  func (n Name) Base() []byte {
   230  	slash := bytes.IndexByte(n.Full(), '/')
   231  	if slash >= 0 {
   232  		return n[:slash]
   233  	}
   234  	base, _ := n.splitGomaxprocs()
   235  	return base
   236  }
   237  
   238  // Parts splits a benchmark name into the base name and sub-benchmark
   239  // configuration parts. Each sub-benchmark configuration part is one
   240  // of three forms:
   241  //
   242  // 1. "/<key>=<value>" indicates a key/value configuration pair.
   243  //
   244  // 2. "/<string>" indicates a positional configuration pair.
   245  //
   246  // 3. "-<gomaxprocs>" indicates the GOMAXPROCS of this benchmark. This
   247  // part can only appear last.
   248  //
   249  // Concatenating the base name and the configuration parts
   250  // reconstructs the full name.
   251  func (n Name) Parts() (baseName []byte, parts [][]byte) {
   252  	// First pull off any GOMAXPROCS.
   253  	buf, gomaxprocs := n.splitGomaxprocs()
   254  	// Split the remaining parts.
   255  	var nameParts [][]byte
   256  	prev := 0
   257  	for i, c := range buf {
   258  		if c == '/' {
   259  			nameParts = append(nameParts, buf[prev:i])
   260  			prev = i
   261  		}
   262  	}
   263  	nameParts = append(nameParts, buf[prev:])
   264  	if gomaxprocs != nil {
   265  		nameParts = append(nameParts, gomaxprocs)
   266  	}
   267  	return nameParts[0], nameParts[1:]
   268  }
   269  
   270  func (n Name) splitGomaxprocs() (prefix, gomaxprocs []byte) {
   271  	for i := len(n) - 1; i >= 0; i-- {
   272  		if n[i] == '-' && i < len(n)-1 {
   273  			return n[:i], n[i:]
   274  		}
   275  		if !('0' <= n[i] && n[i] <= '9') {
   276  			// Not a digit.
   277  			break
   278  		}
   279  	}
   280  	return n, nil
   281  }