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  }