diff options
Diffstat (limited to 'src/pkg/testing')
-rw-r--r-- | src/pkg/testing/benchmark.go | 102 | ||||
-rw-r--r-- | src/pkg/testing/benchmark_test.go | 53 | ||||
-rw-r--r-- | src/pkg/testing/testing.go | 47 |
3 files changed, 188 insertions, 14 deletions
diff --git a/src/pkg/testing/benchmark.go b/src/pkg/testing/benchmark.go index 3473c5b2c..1fbf5c861 100644 --- a/src/pkg/testing/benchmark.go +++ b/src/pkg/testing/benchmark.go @@ -10,6 +10,7 @@ import ( "os" "runtime" "sync" + "sync/atomic" "time" ) @@ -34,12 +35,15 @@ type InternalBenchmark struct { // timing and to specify the number of iterations to run. type B struct { common - N int - benchmark InternalBenchmark - bytes int64 - timerOn bool - showAllocResult bool - result BenchmarkResult + N int + previousN int // number of iterations in the previous run + previousDuration time.Duration // total duration of the previous run + benchmark InternalBenchmark + bytes int64 + timerOn bool + showAllocResult bool + result BenchmarkResult + parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines // The initial states of memStats.Mallocs and memStats.TotalAlloc. startAllocs uint64 startBytes uint64 @@ -74,7 +78,7 @@ func (b *B) StopTimer() { } } -// ResetTimer sets the elapsed benchmark time to zero. +// ResetTimer zeros the elapsed benchmark time and memory allocation counters. // It does not affect whether the timer is running. func (b *B) ResetTimer() { if b.timerOn { @@ -114,10 +118,13 @@ func (b *B) runN(n int) { // by clearing garbage from previous runs. runtime.GC() b.N = n + b.parallelism = 1 b.ResetTimer() b.StartTimer() b.benchmark.F(b) b.StopTimer() + b.previousN = n + b.previousDuration = b.duration } func min(x, y int) int { @@ -343,6 +350,87 @@ func (b *B) trimOutput() { } } +// A PB is used by RunParallel for running parallel benchmarks. +type PB struct { + globalN *uint64 // shared between all worker goroutines iteration counter + grain uint64 // acquire that many iterations from globalN at once + cache uint64 // local cache of acquired iterations + bN uint64 // total number of iterations to execute (b.N) +} + +// Next reports whether there are more iterations to execute. +func (pb *PB) Next() bool { + if pb.cache == 0 { + n := atomic.AddUint64(pb.globalN, pb.grain) + if n <= pb.bN { + pb.cache = pb.grain + } else if n < pb.bN+pb.grain { + pb.cache = pb.bN + pb.grain - n + } else { + return false + } + } + pb.cache-- + return true +} + +// RunParallel runs a benchmark in parallel. +// It creates multiple goroutines and distributes b.N iterations among them. +// The number of goroutines defaults to GOMAXPROCS. To increase parallelism for +// non-CPU-bound benchmarks, call SetParallelism before RunParallel. +// RunParallel is usually used with the go test -cpu flag. +// +// The body function will be run in each goroutine. It should set up any +// goroutine-local state and then iterate until pb.Next returns false. +// It should not use the StartTimer, StopTimer, or ResetTimer functions, +// because they have global effect. +func (b *B) RunParallel(body func(*PB)) { + // Calculate grain size as number of iterations that take ~100µs. + // 100µs is enough to amortize the overhead and provide sufficient + // dynamic load balancing. + grain := uint64(0) + if b.previousN > 0 && b.previousDuration > 0 { + grain = 1e5 * uint64(b.previousN) / uint64(b.previousDuration) + } + if grain < 1 { + grain = 1 + } + // We expect the inner loop and function call to take at least 10ns, + // so do not do more than 100µs/10ns=1e4 iterations. + if grain > 1e4 { + grain = 1e4 + } + + n := uint64(0) + numProcs := b.parallelism * runtime.GOMAXPROCS(0) + var wg sync.WaitGroup + wg.Add(numProcs) + for p := 0; p < numProcs; p++ { + go func() { + defer wg.Done() + pb := &PB{ + globalN: &n, + grain: grain, + bN: uint64(b.N), + } + body(pb) + }() + } + wg.Wait() + if n <= uint64(b.N) && !b.Failed() { + b.Fatal("RunParallel: body exited without pb.Next() == false") + } +} + +// SetParallelism sets the number of goroutines used by RunParallel to p*GOMAXPROCS. +// There is usually no need to call SetParallelism for CPU-bound benchmarks. +// If p is less than 1, this call will have no effect. +func (b *B) SetParallelism(p int) { + if p >= 1 { + b.parallelism = p + } +} + // Benchmark benchmarks a single function. Useful for creating // custom benchmarks that do not use the "go test" command. func Benchmark(f func(b *B)) BenchmarkResult { diff --git a/src/pkg/testing/benchmark_test.go b/src/pkg/testing/benchmark_test.go index 94e994dfa..f7ea64e7f 100644 --- a/src/pkg/testing/benchmark_test.go +++ b/src/pkg/testing/benchmark_test.go @@ -5,7 +5,11 @@ package testing_test import ( + "bytes" + "runtime" + "sync/atomic" "testing" + "text/template" ) var roundDownTests = []struct { @@ -56,3 +60,52 @@ func TestRoundUp(t *testing.T) { } } } + +func TestRunParallel(t *testing.T) { + testing.Benchmark(func(b *testing.B) { + procs := uint32(0) + iters := uint64(0) + b.SetParallelism(3) + b.RunParallel(func(pb *testing.PB) { + atomic.AddUint32(&procs, 1) + for pb.Next() { + atomic.AddUint64(&iters, 1) + } + }) + if want := uint32(3 * runtime.GOMAXPROCS(0)); procs != want { + t.Errorf("got %v procs, want %v", procs, want) + } + if iters != uint64(b.N) { + t.Errorf("got %v iters, want %v", iters, b.N) + } + }) +} + +func TestRunParallelFail(t *testing.T) { + testing.Benchmark(func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + // The function must be able to log/abort + // w/o crashing/deadlocking the whole benchmark. + b.Log("log") + b.Error("error") + }) + }) +} + +func ExampleB_RunParallel() { + // Parallel benchmark for text/template.Template.Execute on a single object. + testing.Benchmark(func(b *testing.B) { + templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) + // RunParallel will create GOMAXPROCS goroutines + // and distribute work among them. + b.RunParallel(func(pb *testing.PB) { + // Each goroutine has its own bytes.Buffer. + var buf bytes.Buffer + for pb.Next() { + // The loop body is executed b.N times total across all goroutines. + buf.Reset() + templ.Execute(&buf, "World") + } + }) + }) +} diff --git a/src/pkg/testing/testing.go b/src/pkg/testing/testing.go index 52dc166dd..8078ba7cc 100644 --- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -8,9 +8,17 @@ // func TestXxx(*testing.T) // where Xxx can be any alphanumeric string (but the first letter must not be in // [a-z]) and serves to identify the test routine. -// These TestXxx routines should be declared within the package they are testing. // -// Tests and benchmarks may be skipped if not applicable like this: +// Within these functions, use the Error, Fail or related methods to signal failure. +// +// To write a new test suite, create a file whose name ends _test.go that +// contains the TestXxx functions as described here. Put the file in the same +// package as the one being tested. The file will be excluded from regular +// package builds but will be included when the ``go test'' command is run. +// For more detail, run ``go help test'' and ``go help testflag''. +// +// Tests and benchmarks may be skipped if not applicable with a call to +// the Skip method of *T and *B: // func TestTimeConsuming(t *testing.T) { // if testing.Short() { // t.Skip("skipping test in short mode.") @@ -43,6 +51,7 @@ // // If a benchmark needs some expensive setup before running, the timer // may be reset: +// // func BenchmarkBigLen(b *testing.B) { // big := NewBig() // b.ResetTimer() @@ -51,6 +60,21 @@ // } // } // +// If a benchmark needs to test performance in a parallel setting, it may use +// the RunParallel helper function; such benchmarks are intended to be used with +// the go test -cpu flag: +// +// func BenchmarkTemplateParallel(b *testing.B) { +// templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) +// b.RunParallel(func(pb *testing.PB) { +// var buf bytes.Buffer +// for pb.Next() { +// buf.Reset() +// templ.Execute(&buf, "World") +// } +// }) +// } +// // Examples // // The package also runs and verifies example code. Example functions may @@ -143,10 +167,11 @@ var ( // common holds the elements common between T and B and // captures common methods such as Errorf. type common struct { - mu sync.RWMutex // guards output and failed - output []byte // Output generated by test or benchmark. - failed bool // Test or benchmark has failed. - skipped bool // Test of benchmark has been skipped. + mu sync.RWMutex // guards output and failed + output []byte // Output generated by test or benchmark. + failed bool // Test or benchmark has failed. + skipped bool // Test of benchmark has been skipped. + finished bool start time.Time // Time test or benchmark started duration time.Duration @@ -275,6 +300,7 @@ func (c *common) FailNow() { // it would run on a test failure. Because we send on c.signal during // a top-of-stack deferred function now, we know that the send // only happens after any other stacked defers have completed. + c.finished = true runtime.Goexit() } @@ -338,6 +364,7 @@ func (c *common) Skipf(format string, args ...interface{}) { // those other goroutines. func (c *common) SkipNow() { c.skip() + c.finished = true runtime.Goexit() } @@ -379,7 +406,11 @@ func tRunner(t *T, test *InternalTest) { defer func() { t.duration = time.Now().Sub(t.start) // If the test panicked, print any test output before dying. - if err := recover(); err != nil { + err := recover() + if !t.finished && err == nil { + err = fmt.Errorf("test executed panic(nil) or runtime.Goexit") + } + if err != nil { t.Fail() t.report() panic(err) @@ -389,6 +420,7 @@ func tRunner(t *T, test *InternalTest) { t.start = time.Now() test.F(t) + t.finished = true } // An internal function but exported because it is cross-package; part of the implementation @@ -405,6 +437,7 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest, stopAlarm() if !testOk || !exampleOk { fmt.Println("FAIL") + after() os.Exit(1) } fmt.Println("PASS") |