Skip to content

Commit f80efa5

Browse files
dkegel-fastlydeadprogram
authored andcommitted
testing: support b.SetBytes(); implement sub-benchmarks.
1 parent 29f7ebc commit f80efa5

File tree

4 files changed

+156
-20
lines changed

4 files changed

+156
-20
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,12 @@ TEST_PACKAGES_WASI = \
254254
.PHONY: tinygo-test
255255
tinygo-test:
256256
$(TINYGO) test $(TEST_PACKAGES)
257+
tinygo-bench:
258+
$(TINYGO) test -bench . $(TEST_PACKAGES)
257259
tinygo-test-wasi:
258260
$(TINYGO) test -target wasi $(TEST_PACKAGES_WASI)
261+
tinygo-bench-wasi:
262+
$(TINYGO) test -target wasi -bench . $(TEST_PACKAGES_WASI)
259263

260264
.PHONY: smoketest
261265
smoketest:

src/testing/benchmark.go

Lines changed: 146 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ package testing
88

99
import (
1010
"fmt"
11+
"io"
12+
"math"
13+
"strings"
1114
"time"
1215
)
1316

@@ -41,14 +44,17 @@ type InternalBenchmark struct {
4144
// affecting benchmark results.
4245
type B struct {
4346
common
44-
hasSub bool // TODO: should be in common, and atomic
45-
start time.Time // TODO: should be in common
46-
duration time.Duration // TODO: should be in common
47-
N int
48-
benchFunc func(b *B)
49-
benchTime benchTimeFlag
50-
timerOn bool
51-
result BenchmarkResult
47+
hasSub bool // TODO: should be in common, and atomic
48+
start time.Time // TODO: should be in common
49+
duration time.Duration // TODO: should be in common
50+
context *benchContext
51+
N int
52+
benchFunc func(b *B)
53+
bytes int64
54+
missingBytes bool // one of the subbenchmarks does not have bytes set.
55+
benchTime benchTimeFlag
56+
timerOn bool
57+
result BenchmarkResult
5258
}
5359

5460
// StartTimer starts timing a test. This function is called automatically
@@ -82,15 +88,13 @@ func (b *B) ResetTimer() {
8288

8389
// SetBytes records the number of bytes processed in a single operation.
8490
// If this is called, the benchmark will report ns/op and MB/s.
85-
func (b *B) SetBytes(n int64) {
86-
panic("testing: unimplemented: B.SetBytes")
87-
}
91+
func (b *B) SetBytes(n int64) { b.bytes = n }
8892

8993
// ReportAllocs enables malloc statistics for this benchmark.
9094
// It is equivalent to setting -test.benchmem, but it only affects the
9195
// benchmark function that calls ReportAllocs.
9296
func (b *B) ReportAllocs() {
93-
panic("testing: unimplemented: B.ReportAllocs")
97+
return // TODO: implement
9498
}
9599

96100
// runN runs a single benchmark for the specified number of iterations.
@@ -119,13 +123,31 @@ func max(x, y int64) int64 {
119123
// run1 runs the first iteration of benchFunc. It reports whether more
120124
// iterations of this benchmarks should be run.
121125
func (b *B) run1() bool {
126+
if ctx := b.context; ctx != nil {
127+
// Extend maxLen, if needed.
128+
if n := len(b.name); n > ctx.maxLen {
129+
ctx.maxLen = n + 8 // Add additional slack to avoid too many jumps in size.
130+
}
131+
}
122132
b.runN(1)
123133
return !b.hasSub
124134
}
125135

126136
// run executes the benchmark.
127137
func (b *B) run() {
138+
if b.context != nil {
139+
// Running go test --test.bench
140+
b.processBench(b.context) // calls doBench and prints results
141+
} else {
142+
// Running func Benchmark.
143+
b.doBench()
144+
}
145+
}
146+
147+
func (b *B) doBench() BenchmarkResult {
148+
// in upstream, this uses a goroutine
128149
b.launch()
150+
return b.result
129151
}
130152

131153
// launch launches the benchmark function. It gradually increases the number
@@ -159,13 +181,14 @@ func (b *B) launch() {
159181
n = min(n, 1e9)
160182
b.runN(int(n))
161183
}
162-
b.result = BenchmarkResult{b.N, b.duration}
184+
b.result = BenchmarkResult{b.N, b.duration, b.bytes}
163185
}
164186

165187
// BenchmarkResult contains the results of a benchmark run.
166188
type BenchmarkResult struct {
167-
N int // The number of iterations.
168-
T time.Duration // The total time taken.
189+
N int // The number of iterations.
190+
T time.Duration // The total time taken.
191+
Bytes int64 // Bytes processed in one iteration.
169192
}
170193

171194
// NsPerOp returns the "ns/op" metric.
@@ -176,6 +199,14 @@ func (r BenchmarkResult) NsPerOp() int64 {
176199
return r.T.Nanoseconds() / int64(r.N)
177200
}
178201

202+
// mbPerSec returns the "MB/s" metric.
203+
func (r BenchmarkResult) mbPerSec() float64 {
204+
if r.Bytes <= 0 || r.T <= 0 || r.N <= 0 {
205+
return 0
206+
}
207+
return (float64(r.Bytes) * float64(r.N) / 1e6) / r.T.Seconds()
208+
}
209+
179210
// AllocsPerOp returns the "allocs/op" metric,
180211
// which is calculated as r.MemAllocs / r.N.
181212
func (r BenchmarkResult) AllocsPerOp() int64 {
@@ -188,41 +219,127 @@ func (r BenchmarkResult) AllocedBytesPerOp() int64 {
188219
return 0 // Dummy version to allow running e.g. golang.org/test/fibo.go
189220
}
190221

222+
// String returns a summary of the benchmark results.
223+
// It follows the benchmark result line format from
224+
// https://golang.org/design/14313-benchmark-format, not including the
225+
// benchmark name.
226+
// Extra metrics override built-in metrics of the same name.
227+
// String does not include allocs/op or B/op, since those are reported
228+
// by MemString.
229+
func (r BenchmarkResult) String() string {
230+
buf := new(strings.Builder)
231+
fmt.Fprintf(buf, "%8d", r.N)
232+
233+
// Get ns/op as a float.
234+
ns := float64(r.T.Nanoseconds()) / float64(r.N)
235+
if ns != 0 {
236+
buf.WriteByte('\t')
237+
prettyPrint(buf, ns, "ns/op")
238+
}
239+
240+
if mbs := r.mbPerSec(); mbs != 0 {
241+
fmt.Fprintf(buf, "\t%7.2f MB/s", mbs)
242+
}
243+
return buf.String()
244+
}
245+
246+
func prettyPrint(w io.Writer, x float64, unit string) {
247+
// Print all numbers with 10 places before the decimal point
248+
// and small numbers with four sig figs. Field widths are
249+
// chosen to fit the whole part in 10 places while aligning
250+
// the decimal point of all fractional formats.
251+
var format string
252+
switch y := math.Abs(x); {
253+
case y == 0 || y >= 999.95:
254+
format = "%10.0f %s"
255+
case y >= 99.995:
256+
format = "%12.1f %s"
257+
case y >= 9.9995:
258+
format = "%13.2f %s"
259+
case y >= 0.99995:
260+
format = "%14.3f %s"
261+
case y >= 0.099995:
262+
format = "%15.4f %s"
263+
case y >= 0.0099995:
264+
format = "%16.5f %s"
265+
case y >= 0.00099995:
266+
format = "%17.6f %s"
267+
default:
268+
format = "%18.7f %s"
269+
}
270+
fmt.Fprintf(w, format, x, unit)
271+
}
272+
273+
type benchContext struct {
274+
maxLen int // The largest recorded benchmark name.
275+
}
276+
191277
func runBenchmarks(benchmarks []InternalBenchmark) bool {
192278
if len(benchmarks) == 0 {
193279
return true
194280
}
281+
ctx := &benchContext{}
282+
for _, Benchmark := range benchmarks {
283+
if l := len(Benchmark.Name); l > ctx.maxLen {
284+
ctx.maxLen = l
285+
}
286+
}
195287
main := &B{
196288
common: common{
197289
name: "Main",
198290
},
199291
benchTime: benchTime,
200292
benchFunc: func(b *B) {
201293
for _, Benchmark := range benchmarks {
202-
if flagVerbose {
203-
fmt.Printf("=== RUN %s\n", Benchmark.Name)
204-
}
205294
b.Run(Benchmark.Name, Benchmark.F)
206-
fmt.Printf("--- Result: %d ns/op\n", b.result.NsPerOp())
207295
}
208296
},
297+
context: ctx,
209298
}
210299

211300
main.runN(1)
212301
return true
213302
}
214303

304+
// processBench runs bench b and prints the results.
305+
func (b *B) processBench(ctx *benchContext) {
306+
benchName := b.name
307+
308+
if ctx != nil {
309+
fmt.Printf("%-*s\t", ctx.maxLen, benchName)
310+
}
311+
r := b.doBench()
312+
if b.failed {
313+
// The output could be very long here, but probably isn't.
314+
// We print it all, regardless, because we don't want to trim the reason
315+
// the benchmark failed.
316+
fmt.Printf("--- FAIL: %s\n%s", benchName, "") // b.output)
317+
return
318+
}
319+
if ctx != nil {
320+
results := r.String()
321+
fmt.Println(results)
322+
}
323+
}
324+
215325
// Run benchmarks f as a subbenchmark with the given name. It reports
216326
// true if the subbenchmark succeeded.
217327
//
218328
// A subbenchmark is like any other benchmark. A benchmark that calls Run at
219329
// least once will not be measured itself and will be called once with N=1.
220330
func (b *B) Run(name string, f func(b *B)) bool {
331+
if b.level > 0 {
332+
name = b.name + "/" + name
333+
}
221334
b.hasSub = true
222335
sub := &B{
223-
common: common{name: name},
336+
common: common{
337+
name: name,
338+
level: b.level + 1,
339+
},
224340
benchFunc: f,
225341
benchTime: b.benchTime,
342+
context: b.context,
226343
}
227344
if sub.run1() {
228345
sub.run()
@@ -240,6 +357,15 @@ func (b *B) add(other BenchmarkResult) {
240357
// in sequence in a single benchmark.
241358
r.N = 1
242359
r.T += time.Duration(other.NsPerOp())
360+
if other.Bytes == 0 {
361+
// Summing Bytes is meaningless in aggregate if not all subbenchmarks
362+
// set it.
363+
b.missingBytes = true
364+
r.Bytes = 0
365+
}
366+
if !b.missingBytes {
367+
r.Bytes += other.Bytes
368+
}
243369
}
244370

245371
// A PB is used by RunParallel for running parallel benchmarks.

src/testing/benchmark_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ func TestBenchmark(t *testing.T) {
4848
t.Errorf("Expected speedup >= 0.3, got %f", speedup)
4949
}
5050
}
51+
52+
func BenchmarkSub(b *testing.B) {
53+
b.Run("Fast", func(b *testing.B) { BenchmarkFastNonASCII(b) })
54+
b.Run("Slow", func(b *testing.B) { BenchmarkSlowNonASCII(b) })
55+
}

src/testing/testing.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type common struct {
4848
failed bool // Test or benchmark has failed.
4949
skipped bool // Test of benchmark has been skipped.
5050
finished bool // Test function has completed.
51+
level int // Nesting depth of test or benchmark.
5152
name string // Name of test or benchmark.
5253
}
5354

0 commit comments

Comments
 (0)