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 }