github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/bench/parse.go (about)

     1  // Copyright 2016 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 bench reads and writes Go benchmarks results files.
     6  //
     7  // This format is specified at:
     8  // https://github.com/golang/proposal/blob/master/design/14313-benchmark-format.md
     9  package bench
    10  
    11  import (
    12  	"bufio"
    13  	"io"
    14  	"regexp"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  	"unicode"
    19  	"unicode/utf8"
    20  )
    21  
    22  // Benchmark records the configuration and results of a single
    23  // benchmark run (a single line of a benchmark results file).
    24  type Benchmark struct {
    25  	// Name is the name of the benchmark, without the "Benchmark"
    26  	// prefix and without the trailing GOMAXPROCS number.
    27  	Name string
    28  
    29  	// Iterations is the number of times this benchmark executed.
    30  	Iterations int
    31  
    32  	// Config is the set of configuration pairs for this
    33  	// Benchmark. These can be specified in both configuration
    34  	// blocks and in individual benchmark lines. If the benchmark
    35  	// name is of the form "BenchmarkX-N", the N is stripped out
    36  	// and stored as "gomaxprocs" here.
    37  	Config map[string]*Config
    38  
    39  	// Result is the set of (unit, value) metrics for this
    40  	// benchmark run.
    41  	Result map[string]float64
    42  }
    43  
    44  // Config represents a single key/value configuration pair.
    45  type Config struct {
    46  	// Value is the parsed value of this configuration value.
    47  	Value interface{}
    48  
    49  	// RawValue is the value of this configuration value, exactly
    50  	// as written in the original benchmark file.
    51  	RawValue string
    52  
    53  	// InBlock indicates that this configuration value was
    54  	// specified in a configuration block line. Otherwise, it was
    55  	// specified in the benchmark line.
    56  	InBlock bool
    57  }
    58  
    59  var configRe = regexp.MustCompile(`^(\p{Ll}[^\p{Lu}\s\x85\xa0\x{1680}\x{2000}-\x{200a}\x{2028}\x{2029}\x{202f}\x{205f}\x{3000}]*):(?:[ \t]+(.*))?$`)
    60  
    61  // Parse parses a standard Go benchmark results file from r. It
    62  // returns a *Benchmark for each benchmark result line in the file.
    63  // There may be many result lines for the same benchmark name and
    64  // configuration, indicating that the benchmark was run multiple
    65  // times.
    66  //
    67  // In the returned Benchmarks, RawValue is set, but Value is always
    68  // nil. Use ParseValues to convert raw values to structured types.
    69  func Parse(r io.Reader) ([]*Benchmark, error) {
    70  	benchmarks := []*Benchmark{}
    71  	config := make(map[string]*Config)
    72  
    73  	scanner := bufio.NewScanner(r)
    74  	for scanner.Scan() {
    75  		line := scanner.Text()
    76  
    77  		if line == "testing: warning: no tests to run" {
    78  			continue
    79  		}
    80  
    81  		// Configuration lines.
    82  		m := configRe.FindStringSubmatch(line)
    83  		if m != nil {
    84  			config[m[1]] = &Config{RawValue: m[2], InBlock: true}
    85  			continue
    86  		}
    87  
    88  		// Benchmark lines.
    89  		if strings.HasPrefix(line, "Benchmark") {
    90  			b := parseBenchmark(line, config)
    91  			if b != nil {
    92  				benchmarks = append(benchmarks, b)
    93  			}
    94  		}
    95  	}
    96  	if err := scanner.Err(); err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	return benchmarks, nil
   101  }
   102  
   103  func parseBenchmark(line string, gconfig map[string]*Config) *Benchmark {
   104  	// TODO: Consider using scanner to avoid the slice allocation.
   105  	f := strings.Fields(line)
   106  	if len(f) < 4 {
   107  		return nil
   108  	}
   109  	if f[0] != "Benchmark" {
   110  		next, _ := utf8.DecodeRuneInString(f[0][len("Benchmark"):])
   111  		if !unicode.IsUpper(next) {
   112  			return nil
   113  		}
   114  	}
   115  
   116  	b := &Benchmark{
   117  		Config: make(map[string]*Config),
   118  		Result: make(map[string]float64),
   119  	}
   120  
   121  	// Copy global config.
   122  	for k, v := range gconfig {
   123  		b.Config[k] = v
   124  	}
   125  
   126  	// Parse name and configuration.
   127  	name := strings.TrimPrefix(f[0], "Benchmark")
   128  	if strings.Contains(name, "/") {
   129  		parts := strings.Split(name, "/")
   130  		b.Name = parts[0]
   131  		for _, part := range parts[1:] {
   132  			if i := strings.Index(part, ":"); i >= 0 {
   133  				k, v := part[:i], part[i+1:]
   134  				b.Config[k] = &Config{RawValue: v}
   135  			}
   136  		}
   137  	} else if i := strings.LastIndex(name, "-"); i >= 0 {
   138  		_, err := strconv.Atoi(name[i+1:])
   139  		if err == nil {
   140  			b.Name = name[:i]
   141  			b.Config["gomaxprocs"] = &Config{RawValue: name[i+1:]}
   142  		} else {
   143  			b.Name = name
   144  		}
   145  	} else {
   146  		b.Name = name
   147  	}
   148  	if b.Config["gomaxprocs"] == nil {
   149  		b.Config["gomaxprocs"] = &Config{RawValue: "1"}
   150  	}
   151  
   152  	// Parse iterations.
   153  	n, err := strconv.Atoi(f[1])
   154  	if err != nil || n <= 0 {
   155  		return nil
   156  	}
   157  	b.Iterations = n
   158  
   159  	// Parse results.
   160  	for i := 2; i+2 <= len(f); i += 2 {
   161  		val, err := strconv.ParseFloat(f[i], 64)
   162  		if err != nil {
   163  			continue
   164  		}
   165  		b.Result[f[i+1]] = val
   166  	}
   167  
   168  	return b
   169  }
   170  
   171  // ValueParser is a function that parses a string value into a
   172  // structured type or returns an error if the string cannot be parsed.
   173  type ValueParser func(string) (interface{}, error)
   174  
   175  // DefaultValueParsers is the default sequence of value parsers used
   176  // by ParseValues if no parsers are specified.
   177  var DefaultValueParsers = []ValueParser{
   178  	func(s string) (interface{}, error) { return strconv.Atoi(s) },
   179  	func(s string) (interface{}, error) { return strconv.ParseFloat(s, 64) },
   180  	func(s string) (interface{}, error) { return time.ParseDuration(s) },
   181  }
   182  
   183  // ParseValues parses the raw configuration values in benchmarks into
   184  // structured types using best-effort pattern-based parsing.
   185  //
   186  // If all of the raw values for a given configuration key can be
   187  // parsed by one of the valueParsers, ParseValues sets the parsed
   188  // values to the results of that ValueParser. If multiple ValueParsers
   189  // can parse all of the raw values, it uses the earliest such parser
   190  // in the valueParsers list.
   191  //
   192  // If valueParsers is nil, it uses DefaultValueParsers.
   193  func ParseValues(benchmarks []*Benchmark, valueParsers []ValueParser) {
   194  	if valueParsers == nil {
   195  		valueParsers = DefaultValueParsers
   196  	}
   197  
   198  	// Collect all configuration keys.
   199  	keys := map[string]bool{}
   200  	for _, b := range benchmarks {
   201  		for k := range b.Config {
   202  			keys[k] = true
   203  		}
   204  	}
   205  
   206  	// For each configuration key, try value parsers in priority order.
   207  	for key := range keys {
   208  		good := false
   209  	tryParsers:
   210  		for _, vp := range valueParsers {
   211  			// Clear all values. This way we can detect
   212  			// aliasing and not parse the same value
   213  			// multiple times.
   214  			for _, b := range benchmarks {
   215  				c, ok := b.Config[key]
   216  				if ok {
   217  					c.Value = nil
   218  				}
   219  			}
   220  
   221  			good = true
   222  		tryValues:
   223  			for _, b := range benchmarks {
   224  				c, ok := b.Config[key]
   225  				if !ok || c.Value != nil {
   226  					continue
   227  				}
   228  
   229  				res, err := vp(c.RawValue)
   230  				if err != nil {
   231  					// Parse error. Fail this parser.
   232  					good = false
   233  					break tryValues
   234  				}
   235  				c.Value = res
   236  			}
   237  
   238  			if good {
   239  				// This ValueParser converted all of
   240  				// the values.
   241  				break tryParsers
   242  			}
   243  		}
   244  		if !good {
   245  			// All of the value parsers failed. Fall back
   246  			// to strings.
   247  			for _, b := range benchmarks {
   248  				c, ok := b.Config[key]
   249  				if ok {
   250  					c.Value = nil
   251  				}
   252  			}
   253  			for _, b := range benchmarks {
   254  				c, ok := b.Config[key]
   255  				if ok && c.Value == nil {
   256  					c.Value = c.RawValue
   257  				}
   258  			}
   259  		}
   260  	}
   261  }