github.com/jgbaldwinbrown/perf@v0.1.1/storage/benchfmt/benchfmt.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 benchfmt provides readers and writers for the Go benchmark format. 6 // 7 // The format is documented at https://golang.org/design/14313-benchmark-format 8 // 9 // This package only parses file configuration lines, not benchmark 10 // result lines. Parsing the result lines is left to the caller. 11 // 12 // Deprecated: See the golang.org/x/perf/benchfmt package, which 13 // implements readers and writers for the full Go benchmark format. 14 // It is also higher performance. 15 package benchfmt 16 17 import ( 18 "bufio" 19 "bytes" 20 "fmt" 21 "io" 22 "sort" 23 "strconv" 24 "strings" 25 "unicode" 26 ) 27 28 // Reader reads benchmark results from an io.Reader. 29 // Use Next to advance through the results. 30 // 31 // br := benchfmt.NewReader(r) 32 // for br.Next() { 33 // res := br.Result() 34 // ... 35 // } 36 // err = br.Err() // get any error encountered during iteration 37 // ... 38 type Reader struct { 39 s *bufio.Scanner 40 labels Labels 41 // permLabels are permanent labels read from the start of the 42 // file or provided by AddLabels. They cannot be overridden. 43 permLabels Labels 44 lineNum int 45 // cached from last call to newResult, to save on allocations 46 lastName string 47 lastNameLabels Labels 48 // cached from the last call to Next 49 result *Result 50 err error 51 } 52 53 // NewReader creates a BenchmarkReader that reads from r. 54 func NewReader(r io.Reader) *Reader { 55 return &Reader{ 56 s: bufio.NewScanner(r), 57 labels: make(Labels), 58 } 59 } 60 61 // AddLabels adds additional labels as if they had been read from the header of a file. 62 // It must be called before the first call to r.Next. 63 func (r *Reader) AddLabels(labels Labels) { 64 r.permLabels = labels.Copy() 65 for k, v := range labels { 66 r.labels[k] = v 67 } 68 } 69 70 // Result represents a single line from a benchmark file. 71 // All information about that line is self-contained in the Result. 72 // A Result is immutable once created. 73 type Result struct { 74 // Labels is the set of persistent labels that apply to the result. 75 // Labels must not be modified. 76 Labels Labels 77 // NameLabels is the set of ephemeral labels that were parsed 78 // from the benchmark name/line. 79 // NameLabels must not be modified. 80 NameLabels Labels 81 // LineNum is the line number on which the result was found 82 LineNum int 83 // Content is the verbatim input line of the benchmark file, beginning with the string "Benchmark". 84 Content string 85 } 86 87 // SameLabels reports whether r and b have the same labels. 88 func (r *Result) SameLabels(b *Result) bool { 89 return r.Labels.Equal(b.Labels) && r.NameLabels.Equal(b.NameLabels) 90 } 91 92 // Labels is a set of key-value strings. 93 type Labels map[string]string 94 95 // String returns the labels formatted as a comma-separated 96 // list enclosed in braces. 97 func (l Labels) String() string { 98 var out bytes.Buffer 99 out.WriteString("{") 100 for k, v := range l { 101 fmt.Fprintf(&out, "%q: %q, ", k, v) 102 } 103 if out.Len() > 1 { 104 // Remove extra ", " 105 out.Truncate(out.Len() - 2) 106 } 107 out.WriteString("}") 108 return out.String() 109 } 110 111 // Keys returns a sorted list of the keys in l. 112 func (l Labels) Keys() []string { 113 var out []string 114 for k := range l { 115 out = append(out, k) 116 } 117 sort.Strings(out) 118 return out 119 } 120 121 // Equal reports whether l and b have the same keys and values. 122 func (l Labels) Equal(b Labels) bool { 123 if len(l) != len(b) { 124 return false 125 } 126 for k := range l { 127 if l[k] != b[k] { 128 return false 129 } 130 } 131 return true 132 } 133 134 // A Printer prints a sequence of benchmark results. 135 type Printer struct { 136 w io.Writer 137 labels Labels 138 } 139 140 // NewPrinter constructs a BenchmarkPrinter writing to w. 141 func NewPrinter(w io.Writer) *Printer { 142 return &Printer{w: w} 143 } 144 145 // Print writes the lines necessary to recreate r. 146 func (p *Printer) Print(r *Result) error { 147 var keys []string 148 // Print removed keys first. 149 for k := range p.labels { 150 if r.Labels[k] == "" { 151 keys = append(keys, k) 152 } 153 } 154 sort.Strings(keys) 155 for _, k := range keys { 156 if _, err := fmt.Fprintf(p.w, "%s:\n", k); err != nil { 157 return err 158 } 159 } 160 // Then print new or changed keys. 161 keys = keys[:0] 162 for k, v := range r.Labels { 163 if v != "" && p.labels[k] != v { 164 keys = append(keys, k) 165 } 166 } 167 sort.Strings(keys) 168 for _, k := range keys { 169 if _, err := fmt.Fprintf(p.w, "%s: %s\n", k, r.Labels[k]); err != nil { 170 return err 171 } 172 } 173 // Finally print the actual line itself. 174 if _, err := fmt.Fprintf(p.w, "%s\n", r.Content); err != nil { 175 return err 176 } 177 p.labels = r.Labels 178 return nil 179 } 180 181 // parseNameLabels extracts extra labels from a benchmark name and sets them in labels. 182 func parseNameLabels(name string, labels Labels) { 183 dash := strings.LastIndex(name, "-") 184 if dash >= 0 { 185 // Accept -N as an alias for /gomaxprocs=N 186 _, err := strconv.Atoi(name[dash+1:]) 187 if err == nil { 188 labels["gomaxprocs"] = name[dash+1:] 189 name = name[:dash] 190 } 191 } 192 parts := strings.Split(name, "/") 193 labels["name"] = parts[0] 194 for i, sub := range parts[1:] { 195 equals := strings.Index(sub, "=") 196 var key string 197 if equals >= 0 { 198 key, sub = sub[:equals], sub[equals+1:] 199 } else { 200 key = fmt.Sprintf("sub%d", i+1) 201 } 202 labels[key] = sub 203 } 204 } 205 206 // newResult parses a line and returns a Result object for the line. 207 func (r *Reader) newResult(labels Labels, lineNum int, name, content string) *Result { 208 res := &Result{ 209 Labels: labels, 210 LineNum: lineNum, 211 Content: content, 212 } 213 if r.lastName != name { 214 r.lastName = name 215 r.lastNameLabels = make(Labels) 216 parseNameLabels(name, r.lastNameLabels) 217 } 218 res.NameLabels = r.lastNameLabels 219 return res 220 } 221 222 // Copy returns a new copy of the labels map, to protect against 223 // future modifications to labels. 224 func (l Labels) Copy() Labels { 225 new := make(Labels) 226 for k, v := range l { 227 new[k] = v 228 } 229 return new 230 } 231 232 // TODO(quentin): How to represent and efficiently group multiple lines? 233 234 // Next returns the next benchmark result from the file. If there are 235 // no further results, it returns nil, io.EOF. 236 func (r *Reader) Next() bool { 237 if r.err != nil { 238 return false 239 } 240 copied := false 241 havePerm := r.permLabels != nil 242 for r.s.Scan() { 243 r.lineNum++ 244 line := r.s.Text() 245 if key, value, ok := parseKeyValueLine(line); ok { 246 if _, ok := r.permLabels[key]; ok { 247 continue 248 } 249 if !copied { 250 copied = true 251 r.labels = r.labels.Copy() 252 } 253 // TODO(quentin): Spec says empty value is valid, but 254 // we need a way to cancel previous labels, so we'll 255 // treat an empty value as a removal. 256 if value == "" { 257 delete(r.labels, key) 258 } else { 259 r.labels[key] = value 260 } 261 continue 262 } 263 // Blank line delimits the header. If we find anything else, the file must not have a header. 264 if !havePerm { 265 if line == "" { 266 r.permLabels = r.labels.Copy() 267 } else { 268 r.permLabels = Labels{} 269 } 270 } 271 if fullName, ok := parseBenchmarkLine(line); ok { 272 r.result = r.newResult(r.labels, r.lineNum, fullName, line) 273 return true 274 } 275 } 276 if err := r.s.Err(); err != nil { 277 r.err = err 278 return false 279 } 280 r.err = io.EOF 281 return false 282 } 283 284 // Result returns the most recent result generated by a call to Next. 285 func (r *Reader) Result() *Result { 286 return r.result 287 } 288 289 // Err returns the error state of the reader. 290 func (r *Reader) Err() error { 291 if r.err == io.EOF { 292 return nil 293 } 294 return r.err 295 } 296 297 // parseKeyValueLine attempts to parse line as a key: value pair. ok 298 // indicates whether the line could be parsed. 299 func parseKeyValueLine(line string) (key, val string, ok bool) { 300 for i, c := range line { 301 if i == 0 && !unicode.IsLower(c) { 302 return 303 } 304 if unicode.IsSpace(c) || unicode.IsUpper(c) { 305 return 306 } 307 if i > 0 && c == ':' { 308 key = line[:i] 309 val = line[i+1:] 310 break 311 } 312 } 313 if key == "" { 314 return 315 } 316 if val == "" { 317 ok = true 318 return 319 } 320 for len(val) > 0 && (val[0] == ' ' || val[0] == '\t') { 321 val = val[1:] 322 ok = true 323 } 324 return 325 } 326 327 // parseBenchmarkLine attempts to parse line as a benchmark result. If 328 // successful, fullName is the name of the benchmark with the 329 // "Benchmark" prefix stripped, and ok is true. 330 func parseBenchmarkLine(line string) (fullName string, ok bool) { 331 space := strings.IndexFunc(line, unicode.IsSpace) 332 if space < 0 { 333 return 334 } 335 name := line[:space] 336 if !strings.HasPrefix(name, "Benchmark") { 337 return 338 } 339 return name[len("Benchmark"):], true 340 }