From 86c74a358063ed9888e262e6854aaff6c1b0ba48 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Tue, 1 Aug 2023 12:46:55 +0100 Subject: [PATCH] faster Run for known types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an alternative to PRs #160 and #165. It's essentially the same as PR #165 except that it uses generics to reduce the amount of duplicated code. Instead of just amortizing the checking of the type, when the argument type of the function passed to `Run` is known, it bypasses the reflect-based code altogether. We don't bother implementing the optimization on pre-generics Go versions because those are end-of-lifetime anyway. I've added an implementation-independent benchmark. ``` goos: linux goarch: amd64 pkg: github.com/frankban/quicktest cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz │ base │ thisPR │ │ sec/op │ sec/op vs base │ CNewAndRunWithCustomType-8 1077.5n ± 5% 136.8n ± 6% -87.30% (p=0.002 n=6) CRunWithCustomType-8 1035.00n ± 11% 66.43n ± 3% -93.58% (p=0.002 n=6) geomean 1.056µ 95.33n -90.97% ``` --- bench_test.go | 29 ++++++++++++++++++++++ go.mod | 10 +++++++- quicktest.go | 4 ++++ run_go1.18.go | 41 +++++++++++++++++++++++++++++++ run_go1.18_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++ run_legacy.go | 10 ++++++++ 6 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 bench_test.go create mode 100644 run_go1.18.go create mode 100644 run_go1.18_test.go create mode 100644 run_legacy.go diff --git a/bench_test.go b/bench_test.go new file mode 100644 index 0000000..e971268 --- /dev/null +++ b/bench_test.go @@ -0,0 +1,29 @@ +package quicktest_test + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func BenchmarkCNewAndRunWithCustomType(b *testing.B) { + for i := 0; i < b.N; i++ { + c := qt.New(customTForBenchmark{}) + c.Run("test", func(c *qt.C) {}) + } +} + +func BenchmarkCRunWithCustomType(b *testing.B) { + c := qt.New(customTForBenchmark{}) + for i := 0; i < b.N; i++ { + c.Run("test", func(c *qt.C) {}) + } +} + +type customTForBenchmark struct { + testing.TB +} + +func (customTForBenchmark) Run(name string, f func(testing.TB)) bool { + return true +} diff --git a/go.mod b/go.mod index d5ba542..21f2cd5 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,12 @@ require ( github.com/kr/pretty v0.3.1 ) -go 1.13 +require ( + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect +) + +// We do actually support go 1.14, but until go 1.21 +// we can't have any generics code even if guarded +// by a build tag. +go 1.18 diff --git a/quicktest.go b/quicktest.go index 65243cf..e944435 100644 --- a/quicktest.go +++ b/quicktest.go @@ -243,6 +243,10 @@ var ( // A panic is raised when Run is called and the embedded concrete type does not // implement a Run method with a correct signature. func (c *C) Run(name string, f func(c *C)) bool { + r, ok := fastRun(c, name, f) + if ok { + return r + } badType := func(m string) { panic(fmt.Sprintf("cannot execute Run with underlying concrete type %T (%s)", c.TB, m)) } diff --git a/run_go1.18.go b/run_go1.18.go new file mode 100644 index 0000000..76cab73 --- /dev/null +++ b/run_go1.18.go @@ -0,0 +1,41 @@ +// Licensed under the MIT license, see LICENSE file for details. + +//go:build go1.18 +// +build go1.18 + +package quicktest + +import "testing" + +// fastRun implements c.Run for some known types. +// It returns the result of calling c.Run and also reports +// whether it was able to do so. +func fastRun(c *C, name string, f func(c *C)) (bool, bool) { + switch t := c.TB.(type) { + case runner[*testing.T]: + return fastRun1(c, name, f, t), true + case runner[*testing.B]: + return fastRun1(c, name, f, t), true + case runner[*C]: + return fastRun1(c, name, f, t), true + case runner[testing.TB]: + // This case is here mostly for benchmarking, because + // it's hard to create a working concrete instance of *testing.T + // that isn't part of the outer tests. + return fastRun1(c, name, f, t), true + } + return false, false +} + +type runner[T any] interface { + Run(name string, f func(T)) bool +} + +func fastRun1[T testing.TB](c *C, name string, f func(*C), t runner[T]) bool { + return t.Run(name, func(t2 T) { + c2 := New(t2) + defer c2.Done() + c2.SetFormat(c.getFormat()) + f(c2) + }) +} diff --git a/run_go1.18_test.go b/run_go1.18_test.go new file mode 100644 index 0000000..847e87a --- /dev/null +++ b/run_go1.18_test.go @@ -0,0 +1,60 @@ +// Licensed under the MIT license, see LICENSE file for details. + +//go:build go1.18 +// +build go1.18 + +package quicktest_test + +import ( + "reflect" + "testing" + + qt "github.com/frankban/quicktest" +) + +type customT2[T testing.TB] struct { + testing.TB +} + +func (t *customT2[T]) Run(name string, f func(T)) bool { + f(*new(T)) + return true +} + +func (t *customT2[T]) rtype() reflect.Type { + return reflect.TypeOf((*T)(nil)).Elem() +} + +type otherTB struct { + testing.TB +} + +func TestCRunCustomTypeWithNonMatchingRunSignature(t *testing.T) { + // Note: this test runs only on >=go1.18 because there isn't any + // code that specializes on this types that's enabled on versions before that. + tests := []interface { + testing.TB + rtype() reflect.Type + }{ + &customT2[*testing.T]{}, + &customT2[*testing.B]{}, + &customT2[*qt.C]{}, + &customT2[testing.TB]{}, + &customT2[otherTB]{}, + } + for _, test := range tests { + t.Run(test.rtype().String(), func(t *testing.T) { + c := qt.New(test) + called := 0 + c.Run("test", func(c *qt.C) { + called++ + if test.rtype().Kind() != reflect.Interface && reflect.TypeOf(c.TB) != test.rtype() { + t.Errorf("TB isn't expected type (want %v got %T)", test.rtype(), c.TB) + } + }) + if got, want := called, 1; got != want { + t.Errorf("subtest was called %d times, not once", called) + } + }) + } +} diff --git a/run_legacy.go b/run_legacy.go new file mode 100644 index 0000000..d4eddfd --- /dev/null +++ b/run_legacy.go @@ -0,0 +1,10 @@ +// Licensed under the MIT license, see LICENSE file for details. + +//go:build !go1.18 +// +build !go1.18 + +package quicktest + +func fastRun(c *C, name string, f func(c *C)) (bool, bool) { + return false, false +}