Skip to content
This repository was archived by the owner on Aug 17, 2020. It is now read-only.

Commit eea4455

Browse files
committed
sub test and sub benchmark auto instrumentation
1 parent 0179c6c commit eea4455

File tree

7 files changed

+349
-10
lines changed

7 files changed

+349
-10
lines changed

instrumentation/testing/benchmark.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func StartBenchmark(b *testing.B, pc uintptr, benchFunc func(b *testing.B)) {
4343
// Runs an auto instrumented sub benchmark
4444
func (bench *Benchmark) Run(name string, f func(b *testing.B)) bool {
4545
pc, _, _, _ := runtime.Caller(1)
46-
return bench.b.Run(name, func(innerB *testing.B) {
46+
return FromTestingB(bench.b).Run(name, func(innerB *testing.B) {
4747
startBenchmark(innerB, pc, f)
4848
})
4949
}
@@ -78,7 +78,7 @@ func startBenchmark(b *testing.B, pc uintptr, benchFunc func(b *testing.B)) {
7878
b.ReportAllocs()
7979
b.ResetTimer()
8080
startTime := time.Now()
81-
result := b.Run("*&", func(b1 *testing.B) {
81+
result := FromTestingB(b).Run("*&", func(b1 *testing.B) {
8282
addBenchmark(b1, &Benchmark{b: b1})
8383
benchFunc(b1)
8484
bChild = b1
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
The purpose with this file is to clone the struct alignment of the testing.B struct so we can assign a *testing.B
3+
pointer to the *goB to have access to the internal private fields.
4+
5+
We use this to create a Run clone method to be called from the subtest auto instrumentation
6+
*/
7+
8+
package testing
9+
10+
import (
11+
"runtime"
12+
"sync"
13+
"sync/atomic"
14+
"testing"
15+
"time"
16+
"unsafe"
17+
)
18+
19+
// clone of testing.B struct
20+
type goB struct {
21+
goCommon
22+
importPath string
23+
context *goBenchContext
24+
N int
25+
previousN int
26+
previousDuration time.Duration
27+
benchFunc func(b *testing.B)
28+
benchTime goBenchTimeFlag
29+
bytes int64
30+
missingBytes bool
31+
timerOn bool
32+
showAllocResult bool
33+
result testing.BenchmarkResult
34+
parallelism int
35+
startAllocs uint64
36+
startBytes uint64
37+
netAllocs uint64
38+
netBytes uint64
39+
extra map[string]float64
40+
}
41+
42+
// clone of testing.benchContext struct
43+
type goBenchContext struct {
44+
match *goMatcher
45+
maxLen int
46+
extLen int
47+
}
48+
49+
// clone of testing.benchTimeFlag struct
50+
type goBenchTimeFlag struct {
51+
d time.Duration
52+
n int
53+
}
54+
55+
// Convert *goB to *testing.B
56+
func (b *goB) ToTestingB() *testing.B {
57+
return *(**testing.B)(unsafe.Pointer(&b))
58+
}
59+
60+
// Convert *testing.B to *goB
61+
func FromTestingB(b *testing.B) *goB {
62+
return *(**goB)(unsafe.Pointer(&b))
63+
}
64+
65+
//go:linkname benchmarkLock testing.benchmarkLock
66+
var benchmarkLock sync.Mutex
67+
68+
//go:linkname (*goB).run1 testing.(*B).run1
69+
func (b *goB) run1() bool
70+
71+
//go:linkname (*goB).run testing.(*B).run
72+
func (b *goB) run() bool
73+
74+
//go:linkname (*goB).add testing.(*B).add
75+
func (b *goB) add(other testing.BenchmarkResult)
76+
77+
// we clone the same (*testing.B).Run implementation because the Patch overwrite the original implementation with the jump
78+
func (b *goB) Run(name string, f func(b *testing.B)) bool {
79+
atomic.StoreInt32(&b.hasSub, 1)
80+
benchmarkLock.Unlock()
81+
defer benchmarkLock.Lock()
82+
83+
benchName, ok, partial := b.name, true, false
84+
if b.context != nil {
85+
benchName, ok, partial = b.context.match.fullName(&b.goCommon, name)
86+
}
87+
if !ok {
88+
return true
89+
}
90+
var pc [maxStackLen]uintptr
91+
n := runtime.Callers(2, pc[:])
92+
sub := &goB{
93+
goCommon: goCommon{
94+
signal: make(chan bool),
95+
name: benchName,
96+
parent: &b.goCommon,
97+
level: b.level + 1,
98+
creator: pc[:n],
99+
w: b.w,
100+
chatty: b.chatty,
101+
},
102+
importPath: b.importPath,
103+
benchFunc: f,
104+
benchTime: b.benchTime,
105+
context: b.context,
106+
}
107+
if partial {
108+
atomic.StoreInt32(&sub.hasSub, 1)
109+
}
110+
if sub.run1() {
111+
sub.run()
112+
}
113+
b.add(sub.result)
114+
return !sub.failed
115+
}

instrumentation/testing/go_testing.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
The purpose with this file is to clone the struct alignment of the testing.T struct so we can assign a *testing.T
3+
pointer to the *goT to have access to the internal private fields.
4+
5+
We use this to create a Run clone method to be called from the subtest auto instrumentation
6+
*/
7+
package testing
8+
9+
import (
10+
"bytes"
11+
"fmt"
12+
"runtime"
13+
"sync"
14+
"sync/atomic"
15+
"testing"
16+
"unsafe"
17+
)
18+
19+
// clone of testing.T struct
20+
type goT struct {
21+
goCommon
22+
isParallel bool
23+
context *goTestContext
24+
}
25+
26+
// clone of testing.testContext struct
27+
type goTestContext struct {
28+
match *goMatcher
29+
mu sync.Mutex
30+
startParallel chan bool
31+
running int
32+
numWaiting int
33+
maxParallel int
34+
}
35+
36+
// clone of testing.matcher struct
37+
type goMatcher struct {
38+
filter []string
39+
matchFunc func(pat, str string) (bool, error)
40+
mu sync.Mutex
41+
subNames map[string]int64
42+
}
43+
44+
// clone of testing.indenter struct
45+
type goIndenter struct {
46+
c *goCommon
47+
}
48+
49+
// Convert *goT to *testing.T
50+
func (t *goT) ToTestingT() *testing.T {
51+
return *(**testing.T)(unsafe.Pointer(&t))
52+
}
53+
54+
// Convert *testing.T to *goT
55+
func FromTestingT(t *testing.T) *goT {
56+
return *(**goT)(unsafe.Pointer(&t))
57+
}
58+
59+
const maxStackLen = 50
60+
61+
//go:linkname matchMutex testing.matchMutex
62+
var matchMutex sync.Mutex
63+
64+
//go:linkname goTRunner testing.tRunner
65+
func goTRunner(t *testing.T, fn func(t *testing.T))
66+
67+
//go:linkname rewrite testing.rewrite
68+
func rewrite(s string) string
69+
70+
//go:linkname shouldFailFast testing.shouldFailFast
71+
func shouldFailFast() bool
72+
73+
//go:linkname (*goMatcher).fullName testing.(*matcher).fullName
74+
func (m *goMatcher) fullName(c *goCommon, subname string) (name string, ok, partial bool)
75+
76+
// this method calls the original testing.tRunner by converting *goT to *testing.T
77+
func tRunner(t *goT, fn func(t *goT)) {
78+
goTRunner(t.ToTestingT(), func(t *testing.T) { fn(FromTestingT(t)) })
79+
}
80+
81+
// we clone the same (*testing.T).Run implementation because the Patch overwrite the original implementation with the jump
82+
func (t *goT) Run(name string, f func(t *goT)) bool {
83+
atomic.StoreInt32(&t.hasSub, 1)
84+
testName, ok, _ := t.context.match.fullName(&t.goCommon, name)
85+
if !ok || shouldFailFast() {
86+
return true
87+
}
88+
var pc [maxStackLen]uintptr
89+
n := runtime.Callers(2, pc[:])
90+
t = &goT{
91+
goCommon: goCommon{
92+
barrier: make(chan bool),
93+
signal: make(chan bool),
94+
name: testName,
95+
parent: &t.goCommon,
96+
level: t.level + 1,
97+
creator: pc[:n],
98+
chatty: t.chatty,
99+
},
100+
context: t.context,
101+
}
102+
t.w = goIndenter{&t.goCommon}
103+
104+
if t.chatty {
105+
root := t.parent
106+
for ; root.parent != nil; root = root.parent {
107+
}
108+
root.mu.Lock()
109+
fmt.Fprintf(root.w, "=== RUN %s\n", t.name)
110+
root.mu.Unlock()
111+
}
112+
go tRunner(t, f)
113+
if !<-t.signal {
114+
runtime.Goexit()
115+
}
116+
return !t.failed
117+
}
118+
119+
// we can't link an instance method without a struct pointer
120+
func (w goIndenter) Write(b []byte) (n int, err error) {
121+
n = len(b)
122+
for len(b) > 0 {
123+
end := bytes.IndexByte(b, '\n')
124+
if end == -1 {
125+
end = len(b)
126+
} else {
127+
end++
128+
}
129+
const indent = " "
130+
w.c.output = append(w.c.output, indent...)
131+
w.c.output = append(w.c.output, b[:end]...)
132+
b = b[end:]
133+
}
134+
return
135+
}

instrumentation/testing/go_testing.s

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// The testing package uses //go:linkname to push a few functions into this
2+
// package but we still need a .s file so the Go tool does not pass -complete
3+
// to the go tool compile so the latter does not complain about Go functions
4+
// with no bodies.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// +build !go1.14
2+
3+
package testing
4+
5+
import (
6+
"io"
7+
"sync"
8+
"time"
9+
)
10+
11+
// clone of testing.common struct
12+
type goCommon struct {
13+
mu sync.RWMutex
14+
output []byte
15+
w io.Writer
16+
ran bool
17+
failed bool
18+
skipped bool
19+
done bool
20+
helpers map[string]struct{}
21+
chatty bool
22+
finished bool
23+
hasSub int32
24+
raceErrors int
25+
runner string
26+
parent *goCommon
27+
level int
28+
creator []uintptr
29+
name string
30+
start time.Time
31+
duration time.Duration
32+
barrier chan bool
33+
signal chan bool
34+
sub []*goT
35+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// +build go1.14
2+
3+
package testing
4+
5+
import (
6+
"io"
7+
"sync"
8+
"time"
9+
)
10+
11+
// clone of testing.common struct
12+
type goCommon struct {
13+
mu sync.RWMutex
14+
output []byte
15+
w io.Writer
16+
ran bool
17+
failed bool
18+
skipped bool
19+
done bool
20+
helpers map[string]struct{}
21+
cleanup func() // New in golang 1.14
22+
chatty bool
23+
finished bool
24+
hasSub int32
25+
raceErrors int
26+
runner string
27+
parent *goCommon
28+
level int
29+
creator []uintptr
30+
name string
31+
start time.Time
32+
duration time.Duration
33+
barrier chan bool
34+
signal chan bool
35+
sub []*goT
36+
}

instrumentation/testing/init.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ func Init(m *testing.M) {
5454
benchmarks = append(benchmarks, testing.InternalBenchmark{
5555
Name: benchmark.Name,
5656
F: func(b *testing.B) { // Indirection of the original benchmark
57-
startBenchmark(b, funcPointer, funcValue)
57+
if envDMPatch, set := os.LookupEnv("SCOPE_DISABLE_MONKEY_PATCHING"); !set || envDMPatch == "" {
58+
funcValue(b)
59+
} else {
60+
startBenchmark(b, funcPointer, funcValue)
61+
}
5862
},
5963
})
6064
}
@@ -66,14 +70,11 @@ func Init(m *testing.M) {
6670
var t *testing.T
6771
tType := reflect.TypeOf(t)
6872
if tRunMethod, ok := tType.MethodByName("Run"); ok {
69-
var runPatch *mpatch.Patch
70-
var err error
71-
runPatch, err = mpatch.PatchMethodByReflect(tRunMethod, func(t *testing.T, name string, f func(t *testing.T)) bool {
73+
_, err := mpatch.PatchMethodByReflect(tRunMethod, func(t *testing.T, name string, f func(t *testing.T)) bool {
7274
pc, _, _, _ := runtime.Caller(1)
73-
logOnError(runPatch.Unpatch())
74-
defer runPatch.Patch()
75-
return t.Run(name, func(childT *testing.T) {
76-
_ = runPatch.Patch()
75+
gT := FromTestingT(t)
76+
return gT.Run(name, func(childGoT *goT) {
77+
childT := childGoT.ToTestingT()
7778
addAutoInstrumentedTest(childT)
7879
childTest := StartTestFromCaller(childT, pc)
7980
defer childTest.end()
@@ -82,5 +83,18 @@ func Init(m *testing.M) {
8283
})
8384
logOnError(err)
8485
}
86+
87+
// We monkey patch the `testing.B.Run()` func to auto instrument sub benchmark
88+
var b *testing.B
89+
bType := reflect.TypeOf(b)
90+
if bRunMethod, ok := bType.MethodByName("Run"); ok {
91+
_, err := mpatch.PatchMethodByReflect(bRunMethod, func(b *testing.B, name string, f func(b *testing.B)) bool {
92+
pc, _, _, _ := runtime.Caller(1)
93+
return FromTestingB(b).Run(name, func(b *testing.B) {
94+
StartBenchmark(b, pc, f)
95+
})
96+
})
97+
logOnError(err)
98+
}
8599
}
86100
}

0 commit comments

Comments
 (0)