/
parser.go
137 lines (121 loc) 路 3.3 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package bench
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"go.bobheadxi.dev/gobenchdata/internal"
)
// LineReader defines the API surface of bufio.Reader used by the parser
type LineReader interface {
ReadLine() (line []byte, isPrefix bool, err error)
}
// Parser is gobenchdata's benchmark output parser
type Parser struct {
in LineReader
}
// NewParser instantiates a new benchmark parser that reads from the given buffer
func NewParser(in *bufio.Reader) *Parser {
return &Parser{in}
}
func (p *Parser) Read() ([]Suite, error) {
suites := make([]Suite, 0)
for {
line, _, err := p.in.ReadLine()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if strings.HasPrefix(string(line), "goos:") {
// TODO: is it possible to set and rewind the reader?
suite, err := p.readBenchmarkSuite(string(line))
if err != nil {
return nil, err
}
suites = append(suites, *suite)
}
}
return suites, nil
}
func (p *Parser) readBenchmarkSuite(first string) (*Suite, error) {
var (
split = strings.Split(first, ": ")
suite = Suite{
Goos: split[1],
Benchmarks: make([]Benchmark, 0),
}
)
for {
l, _, err := p.in.ReadLine()
if err != nil {
return nil, err
}
line := string(l)
if strings.HasPrefix(line, "PASS") || strings.HasPrefix(line, "FAIL") {
break
} else if strings.HasPrefix(line, "goarch:") {
split = strings.Split(line, ": ")
suite.Goarch = split[1]
} else if strings.HasPrefix(line, "pkg:") {
split = strings.Split(line, ": ")
suite.Pkg = split[1]
} else if strings.HasPrefix(line, "Benchmark") {
bench, err := p.readBenchmark(line)
if err != nil {
return nil, fmt.Errorf("%w: %q", err, line)
}
suite.Benchmarks = append(suite.Benchmarks, *bench)
}
}
return &suite, nil
}
// readBenchmark parses a single line from a benchmark.
//
// Benchmarks take the following format:
// BenchmarkRegex 300000 5160 ns/op 5408 B/op 69 allocs/op
func (p *Parser) readBenchmark(line string) (*Benchmark, error) {
var (
bench Benchmark
err error
tmp string
)
// split out name
split := strings.Split(line, "\t")
bench.Name, split = internal.Popleft(split)
// runs - doesn't include units
tmp, split = internal.Popleft(split)
if bench.Runs, err = strconv.Atoi(tmp); err != nil {
return nil, fmt.Errorf("%s: could not parse run: %w (line: %s)", bench.Name, err, line)
}
// parse metrics with units
for len(split) > 0 {
tmp, split = internal.Popleft(split)
valueAndUnits := strings.Split(tmp, " ")
if len(valueAndUnits) < 2 {
return nil, fmt.Errorf("expected two parts in value '%s', got %d", tmp, len(valueAndUnits))
}
var value, units = valueAndUnits[0], valueAndUnits[1]
switch units {
case "ns/op":
bench.NsPerOp, err = strconv.ParseFloat(value, 64)
case "B/op":
bench.Mem.BytesPerOp, err = strconv.ParseFloat(value, 64)
case "allocs/op":
bench.Mem.AllocsPerOp, err = strconv.ParseFloat(value, 64)
case "MB/s":
bench.Mem.MBPerSec, err = strconv.ParseFloat(value, 64)
default:
if bench.Custom == nil {
bench.Custom = make(map[string]float64)
}
bench.Custom[units], err = strconv.ParseFloat(value, 64)
}
if err != nil {
return nil, fmt.Errorf("%s: could not parse %s: %v", bench.Name, units, err)
}
}
return &bench, nil
}