| // Copyright 2016 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 readers and writers for the Go benchmark format. | |
| // | |
| // The format is documented at https://golang.org/design/14313-benchmark-format | |
| // | |
| // This package only parses file configuration lines, not benchmark | |
| // result lines. Parsing the result lines is left to the caller. | |
| // | |
| // Deprecated: See the golang.org/x/perf/benchfmt package, which | |
| // implements readers and writers for the full Go benchmark format. | |
| // It is also higher performance. | |
| package benchfmt | |
| import ( | |
| "bufio" | |
| "bytes" | |
| "fmt" | |
| "io" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "unicode" | |
| ) | |
| // Reader reads benchmark results from an io.Reader. | |
| // Use Next to advance through the results. | |
| // | |
| // br := benchfmt.NewReader(r) | |
| // for br.Next() { | |
| // res := br.Result() | |
| // ... | |
| // } | |
| // err = br.Err() // get any error encountered during iteration | |
| // ... | |
| type Reader struct { | |
| s *bufio.Scanner | |
| labels Labels | |
| // permLabels are permanent labels read from the start of the | |
| // file or provided by AddLabels. They cannot be overridden. | |
| permLabels Labels | |
| lineNum int | |
| // cached from last call to newResult, to save on allocations | |
| lastName string | |
| lastNameLabels Labels | |
| // cached from the last call to Next | |
| result *Result | |
| err error | |
| } | |
| // NewReader creates a BenchmarkReader that reads from r. | |
| func NewReader(r io.Reader) *Reader { | |
| return &Reader{ | |
| s: bufio.NewScanner(r), | |
| labels: make(Labels), | |
| } | |
| } | |
| // AddLabels adds additional labels as if they had been read from the header of a file. | |
| // It must be called before the first call to r.Next. | |
| func (r *Reader) AddLabels(labels Labels) { | |
| r.permLabels = labels.Copy() | |
| for k, v := range labels { | |
| r.labels[k] = v | |
| } | |
| } | |
| // Result represents a single line from a benchmark file. | |
| // All information about that line is self-contained in the Result. | |
| // A Result is immutable once created. | |
| type Result struct { | |
| // Labels is the set of persistent labels that apply to the result. | |
| // Labels must not be modified. | |
| Labels Labels | |
| // NameLabels is the set of ephemeral labels that were parsed | |
| // from the benchmark name/line. | |
| // NameLabels must not be modified. | |
| NameLabels Labels | |
| // LineNum is the line number on which the result was found | |
| LineNum int | |
| // Content is the verbatim input line of the benchmark file, beginning with the string "Benchmark". | |
| Content string | |
| } | |
| // SameLabels reports whether r and b have the same labels. | |
| func (r *Result) SameLabels(b *Result) bool { | |
| return r.Labels.Equal(b.Labels) && r.NameLabels.Equal(b.NameLabels) | |
| } | |
| // Labels is a set of key-value strings. | |
| type Labels map[string]string | |
| // String returns the labels formatted as a comma-separated | |
| // list enclosed in braces. | |
| func (l Labels) String() string { | |
| var out bytes.Buffer | |
| out.WriteString("{") | |
| for k, v := range l { | |
| fmt.Fprintf(&out, "%q: %q, ", k, v) | |
| } | |
| if out.Len() > 1 { | |
| // Remove extra ", " | |
| out.Truncate(out.Len() - 2) | |
| } | |
| out.WriteString("}") | |
| return out.String() | |
| } | |
| // Keys returns a sorted list of the keys in l. | |
| func (l Labels) Keys() []string { | |
| var out []string | |
| for k := range l { | |
| out = append(out, k) | |
| } | |
| sort.Strings(out) | |
| return out | |
| } | |
| // Equal reports whether l and b have the same keys and values. | |
| func (l Labels) Equal(b Labels) bool { | |
| if len(l) != len(b) { | |
| return false | |
| } | |
| for k := range l { | |
| if l[k] != b[k] { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| // A Printer prints a sequence of benchmark results. | |
| type Printer struct { | |
| w io.Writer | |
| labels Labels | |
| } | |
| // NewPrinter constructs a BenchmarkPrinter writing to w. | |
| func NewPrinter(w io.Writer) *Printer { | |
| return &Printer{w: w} | |
| } | |
| // Print writes the lines necessary to recreate r. | |
| func (p *Printer) Print(r *Result) error { | |
| var keys []string | |
| // Print removed keys first. | |
| for k := range p.labels { | |
| if r.Labels[k] == "" { | |
| keys = append(keys, k) | |
| } | |
| } | |
| sort.Strings(keys) | |
| for _, k := range keys { | |
| if _, err := fmt.Fprintf(p.w, "%s:\n", k); err != nil { | |
| return err | |
| } | |
| } | |
| // Then print new or changed keys. | |
| keys = keys[:0] | |
| for k, v := range r.Labels { | |
| if v != "" && p.labels[k] != v { | |
| keys = append(keys, k) | |
| } | |
| } | |
| sort.Strings(keys) | |
| for _, k := range keys { | |
| if _, err := fmt.Fprintf(p.w, "%s: %s\n", k, r.Labels[k]); err != nil { | |
| return err | |
| } | |
| } | |
| // Finally print the actual line itself. | |
| if _, err := fmt.Fprintf(p.w, "%s\n", r.Content); err != nil { | |
| return err | |
| } | |
| p.labels = r.Labels | |
| return nil | |
| } | |
| // parseNameLabels extracts extra labels from a benchmark name and sets them in labels. | |
| func parseNameLabels(name string, labels Labels) { | |
| dash := strings.LastIndex(name, "-") | |
| if dash >= 0 { | |
| // Accept -N as an alias for /gomaxprocs=N | |
| _, err := strconv.Atoi(name[dash+1:]) | |
| if err == nil { | |
| labels["gomaxprocs"] = name[dash+1:] | |
| name = name[:dash] | |
| } | |
| } | |
| parts := strings.Split(name, "/") | |
| labels["name"] = parts[0] | |
| for i, sub := range parts[1:] { | |
| equals := strings.Index(sub, "=") | |
| var key string | |
| if equals >= 0 { | |
| key, sub = sub[:equals], sub[equals+1:] | |
| } else { | |
| key = fmt.Sprintf("sub%d", i+1) | |
| } | |
| labels[key] = sub | |
| } | |
| } | |
| // newResult parses a line and returns a Result object for the line. | |
| func (r *Reader) newResult(labels Labels, lineNum int, name, content string) *Result { | |
| res := &Result{ | |
| Labels: labels, | |
| LineNum: lineNum, | |
| Content: content, | |
| } | |
| if r.lastName != name { | |
| r.lastName = name | |
| r.lastNameLabels = make(Labels) | |
| parseNameLabels(name, r.lastNameLabels) | |
| } | |
| res.NameLabels = r.lastNameLabels | |
| return res | |
| } | |
| // Copy returns a new copy of the labels map, to protect against | |
| // future modifications to labels. | |
| func (l Labels) Copy() Labels { | |
| new := make(Labels) | |
| for k, v := range l { | |
| new[k] = v | |
| } | |
| return new | |
| } | |
| // TODO(quentin): How to represent and efficiently group multiple lines? | |
| // Next returns the next benchmark result from the file. If there are | |
| // no further results, it returns nil, io.EOF. | |
| func (r *Reader) Next() bool { | |
| if r.err != nil { | |
| return false | |
| } | |
| copied := false | |
| havePerm := r.permLabels != nil | |
| for r.s.Scan() { | |
| r.lineNum++ | |
| line := r.s.Text() | |
| if key, value, ok := parseKeyValueLine(line); ok { | |
| if _, ok := r.permLabels[key]; ok { | |
| continue | |
| } | |
| if !copied { | |
| copied = true | |
| r.labels = r.labels.Copy() | |
| } | |
| // TODO(quentin): Spec says empty value is valid, but | |
| // we need a way to cancel previous labels, so we'll | |
| // treat an empty value as a removal. | |
| if value == "" { | |
| delete(r.labels, key) | |
| } else { | |
| r.labels[key] = value | |
| } | |
| continue | |
| } | |
| // Blank line delimits the header. If we find anything else, the file must not have a header. | |
| if !havePerm { | |
| if line == "" { | |
| r.permLabels = r.labels.Copy() | |
| } else { | |
| r.permLabels = Labels{} | |
| } | |
| } | |
| if fullName, ok := parseBenchmarkLine(line); ok { | |
| r.result = r.newResult(r.labels, r.lineNum, fullName, line) | |
| return true | |
| } | |
| } | |
| if err := r.s.Err(); err != nil { | |
| r.err = err | |
| return false | |
| } | |
| r.err = io.EOF | |
| return false | |
| } | |
| // Result returns the most recent result generated by a call to Next. | |
| func (r *Reader) Result() *Result { | |
| return r.result | |
| } | |
| // Err returns the error state of the reader. | |
| func (r *Reader) Err() error { | |
| if r.err == io.EOF { | |
| return nil | |
| } | |
| return r.err | |
| } | |
| // parseKeyValueLine attempts to parse line as a key: value pair. ok | |
| // indicates whether the line could be parsed. | |
| func parseKeyValueLine(line string) (key, val string, ok bool) { | |
| for i, c := range line { | |
| if i == 0 && !unicode.IsLower(c) { | |
| return | |
| } | |
| if unicode.IsSpace(c) || unicode.IsUpper(c) { | |
| return | |
| } | |
| if i > 0 && c == ':' { | |
| key = line[:i] | |
| val = line[i+1:] | |
| break | |
| } | |
| } | |
| if key == "" { | |
| return | |
| } | |
| if val == "" { | |
| ok = true | |
| return | |
| } | |
| for len(val) > 0 && (val[0] == ' ' || val[0] == '\t') { | |
| val = val[1:] | |
| ok = true | |
| } | |
| return | |
| } | |
| // parseBenchmarkLine attempts to parse line as a benchmark result. If | |
| // successful, fullName is the name of the benchmark with the | |
| // "Benchmark" prefix stripped, and ok is true. | |
| func parseBenchmarkLine(line string) (fullName string, ok bool) { | |
| space := strings.IndexFunc(line, unicode.IsSpace) | |
| if space < 0 { | |
| return | |
| } | |
| name := line[:space] | |
| if !strings.HasPrefix(name, "Benchmark") { | |
| return | |
| } | |
| return name[len("Benchmark"):], true | |
| } |