@@ -21,6 +21,8 @@ package bigquery
21
21
import (
22
22
"context"
23
23
"fmt"
24
+ "regexp"
25
+ "sort"
24
26
"strconv"
25
27
"strings"
26
28
"time"
@@ -81,6 +83,31 @@ func (s *Suite) debugString(sb *strings.Builder, prefix string) {
81
83
sb .WriteString (fmt .Sprintf ("End of data for benchmark suite %s." , s .Name ))
82
84
}
83
85
86
+ // Benchstat returns a benchstat-formatted output string.
87
+ // See https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
88
+ // `includeConditions` contains names of `Condition`s that should be included
89
+ // as part of the benchmark name.
90
+ func (s * Suite ) Benchstat (includeConditions []string ) string {
91
+ var sb strings.Builder
92
+ benchmarkNames := make ([]string , 0 , len (s .Benchmarks ))
93
+ benchmarks := make (map [string ]* Benchmark , len (s .Benchmarks ))
94
+ for _ , bm := range s .Benchmarks {
95
+ if _ , found := benchmarks [bm .Name ]; ! found {
96
+ benchmarkNames = append (benchmarkNames , bm .Name )
97
+ benchmarks [bm .Name ] = bm
98
+ }
99
+ }
100
+ sort .Strings (benchmarkNames )
101
+ includeConditionsMap := make (map [string ]bool , len (includeConditions ))
102
+ for _ , condName := range includeConditions {
103
+ includeConditionsMap [condName ] = true
104
+ }
105
+ for _ , bmName := range benchmarkNames {
106
+ benchmarks [bmName ].benchstat (& sb , s .Name , includeConditionsMap , s .Conditions )
107
+ }
108
+ return sb .String ()
109
+ }
110
+
84
111
// Benchmark represents an individual benchmark in a suite.
85
112
type Benchmark struct {
86
113
Name string `bq:"name"`
@@ -117,6 +144,69 @@ func (bm *Benchmark) debugString(sb *strings.Builder, prefix string) {
117
144
}
118
145
}
119
146
147
+ // noSpaceRe is used to remove whitespace characters in `noSpace`.
148
+ var noSpaceRe = regexp .MustCompile ("\\ s+" )
149
+
150
+ // noSpace replaces whitespace characters from `s` with "_".
151
+ func noSpace (s string ) string {
152
+ return noSpaceRe .ReplaceAllString (s , "_" )
153
+ }
154
+
155
+ // benchstat produces benchmark-formatted output for this Benchmark.
156
+ func (bm * Benchmark ) benchstat (sb * strings.Builder , suiteName string , includeConditions map [string ]bool , suiteConditions []* Condition ) {
157
+ var conditionsStr string
158
+ conditionNames := make ([]string , 0 , len (suiteConditions )+ len (bm .Condition ))
159
+ conditionMap := make (map [string ]string , len (suiteConditions )+ len (bm .Condition ))
160
+ for _ , c := range suiteConditions {
161
+ cName := noSpace (c .Name )
162
+ if _ , found := conditionMap [cName ]; ! found && includeConditions [cName ] {
163
+ conditionNames = append (conditionNames , cName )
164
+ conditionMap [cName ] = noSpace (c .Value )
165
+ }
166
+ }
167
+ for _ , c := range bm .Condition {
168
+ cName := noSpace (c .Name )
169
+ if _ , found := conditionMap [cName ]; ! found && includeConditions [cName ] {
170
+ conditionNames = append (conditionNames , cName )
171
+ conditionMap [cName ] = noSpace (c .Value )
172
+ }
173
+ }
174
+ sort .Strings (conditionNames )
175
+ var conditionsBuilder strings.Builder
176
+ if len (conditionNames ) > 0 {
177
+ conditionsBuilder .WriteByte ('{' )
178
+ for i , condName := range conditionNames {
179
+ if i != 0 {
180
+ conditionsBuilder .WriteByte (',' )
181
+ }
182
+ conditionsBuilder .WriteString (condName )
183
+ conditionsBuilder .WriteByte ('=' )
184
+ conditionsBuilder .WriteString (conditionMap [condName ])
185
+ }
186
+ conditionsBuilder .WriteByte ('}' )
187
+ }
188
+ conditionsStr = conditionsBuilder .String ()
189
+ for _ , m := range bm .Metric {
190
+ if ! strings .HasPrefix (suiteName , "Benchmark" ) {
191
+ // benchstat format requires all benchmark names to start with "Benchmark".
192
+ sb .WriteString ("Benchmark" )
193
+ }
194
+ sb .WriteString (noSpace (suiteName ))
195
+ if suiteName != bm .Name {
196
+ sb .WriteByte ('/' )
197
+ sb .WriteString (noSpace (bm .Name ))
198
+ }
199
+ sb .WriteString (conditionsStr )
200
+ sb .WriteByte ('/' )
201
+ sb .WriteString (noSpace (m .Name ))
202
+ sb .WriteString (" 1 " ) // 1 sample
203
+ sb .WriteString (fmt .Sprintf ("%f" , m .Sample ))
204
+ sb .WriteByte (' ' )
205
+ sb .WriteString (noSpace (m .Unit ))
206
+ sb .WriteByte ('\n' )
207
+ }
208
+ }
209
+
120
210
// AddMetric adds a metric to an existing Benchmark.
121
211
func (bm * Benchmark ) AddMetric (metricName , unit string , sample float64 ) {
122
212
m := & Metric {
0 commit comments