Skip to content

Commit ca922b6

Browse files
committedOct 28, 2016
runtime: Profile goroutines holding contended mutexes.
runtime.SetMutexProfileFraction(n int) will capture 1/n-th of stack traces of goroutines holding contended mutexes if n > 0. From runtime/pprof, pprot.Lookup("mutex").WriteTo writes the accumulated stack traces to w (in essentially the same format that blocking profiling uses). Change-Id: Ie0b54fa4226853d99aa42c14cb529ae586a8335a Reviewed-on: https://go-review.googlesource.com/29650 Reviewed-by: Austin Clements <austin@google.com>
1 parent b679665 commit ca922b6

File tree

15 files changed

+286
-39
lines changed

15 files changed

+286
-39
lines changed
 

‎src/cmd/go/test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,15 @@ profile the tests during execution::
238238
To profile all memory allocations, use -test.memprofilerate=1
239239
and pass --alloc_space flag to the pprof tool.
240240
241+
-mutexprofile mutex.out
242+
Write a mutex contention profile to the specified file
243+
when all tests are complete.
244+
Writes test binary as -c would.
245+
246+
-mutexprofilefraction n
247+
Sample 1 in n stack traces of goroutines holding a
248+
contended mutex.
249+
241250
-outputdir directory
242251
Place output files from profiling in the specified directory,
243252
by default the directory in which "go test" is running.

‎src/cmd/go/testflag.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ var testFlagDefn = []*testFlagSpec{
5050
{name: "memprofilerate", passToTest: true},
5151
{name: "blockprofile", passToTest: true},
5252
{name: "blockprofilerate", passToTest: true},
53+
{name: "mutexprofile", passToTest: true},
54+
{name: "mutexprofilefraction", passToTest: true},
5355
{name: "outputdir", passToTest: true},
5456
{name: "parallel", passToTest: true},
5557
{name: "run", passToTest: true},
@@ -152,7 +154,7 @@ func testFlags(args []string) (packageNames, passToTest []string) {
152154
case "blockprofile", "cpuprofile", "memprofile":
153155
testProfile = true
154156
testNeedBinary = true
155-
case "trace":
157+
case "mutexprofile", "trace":
156158
testProfile = true
157159
case "coverpkg":
158160
testCover = true

‎src/cmd/internal/pprof/profile/legacy_profile.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -679,20 +679,32 @@ func scaleHeapSample(count, size, rate int64) (int64, int64) {
679679
return int64(float64(count) * scale), int64(float64(size) * scale)
680680
}
681681

682-
// parseContention parses a contentionz profile and returns a newly
683-
// populated Profile.
684-
func parseContention(b []byte) (p *Profile, err error) {
682+
// parseContention parses a mutex or contention profile. There are 2 cases:
683+
// "--- contentionz " for legacy C++ profiles (and backwards compatibility)
684+
// "--- mutex:" or "--- contention:" for profiles generated by the Go runtime.
685+
// This code converts the text output from runtime into a *Profile. (In the future
686+
// the runtime might write a serialized Profile directly making this unnecessary.)
687+
func parseContention(b []byte) (*Profile, error) {
685688
r := bytes.NewBuffer(b)
686689
l, err := r.ReadString('\n')
687690
if err != nil {
688691
return nil, errUnrecognized
689692
}
690-
691-
if !strings.HasPrefix(l, "--- contention") {
692-
return nil, errUnrecognized
693+
if strings.HasPrefix(l, "--- contentionz ") {
694+
return parseCppContention(r)
695+
} else if strings.HasPrefix(l, "--- mutex:") {
696+
return parseCppContention(r)
697+
} else if strings.HasPrefix(l, "--- contention:") {
698+
return parseCppContention(r)
693699
}
700+
return nil, errUnrecognized
701+
}
694702

695-
p = &Profile{
703+
// parseCppContention parses the output from synchronization_profiling.cc
704+
// for backward compatibility, and the compatible (non-debug) block profile
705+
// output from the Go runtime.
706+
func parseCppContention(r *bytes.Buffer) (*Profile, error) {
707+
p := &Profile{
696708
PeriodType: &ValueType{Type: "contentions", Unit: "count"},
697709
Period: 1,
698710
SampleType: []*ValueType{
@@ -702,6 +714,8 @@ func parseContention(b []byte) (p *Profile, err error) {
702714
}
703715

704716
var cpuHz int64
717+
var l string
718+
var err error
705719
// Parse text of the form "attribute = value" before the samples.
706720
const delimiter = "="
707721
for {

‎src/runtime/mgc.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -932,7 +932,7 @@ func gcStart(mode gcMode, forceTrigger bool) {
932932
// another thread.
933933
useStartSema := mode == gcBackgroundMode
934934
if useStartSema {
935-
semacquire(&work.startSema, false)
935+
semacquire(&work.startSema, 0)
936936
// Re-check transition condition under transition lock.
937937
if !gcShouldStart(forceTrigger) {
938938
semrelease(&work.startSema)
@@ -953,7 +953,7 @@ func gcStart(mode gcMode, forceTrigger bool) {
953953
}
954954

955955
// Ok, we're doing it! Stop everybody else
956-
semacquire(&worldsema, false)
956+
semacquire(&worldsema, 0)
957957

958958
if trace.enabled {
959959
traceGCStart()
@@ -1063,7 +1063,7 @@ func gcStart(mode gcMode, forceTrigger bool) {
10631063
// by mark termination.
10641064
func gcMarkDone() {
10651065
top:
1066-
semacquire(&work.markDoneSema, false)
1066+
semacquire(&work.markDoneSema, 0)
10671067

10681068
// Re-check transition condition under transition lock.
10691069
if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {

‎src/runtime/mprof.go

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
// profile types
2323
memProfile bucketType = 1 + iota
2424
blockProfile
25+
mutexProfile
2526

2627
// size of bucket hash table
2728
buckHashSize = 179999
@@ -47,7 +48,7 @@ type bucketType int
4748
type bucket struct {
4849
next *bucket
4950
allnext *bucket
50-
typ bucketType // memBucket or blockBucket
51+
typ bucketType // memBucket or blockBucket (includes mutexProfile)
5152
hash uintptr
5253
size uintptr
5354
nstk uintptr
@@ -87,7 +88,7 @@ type memRecord struct {
8788
}
8889

8990
// A blockRecord is the bucket data for a bucket of type blockProfile,
90-
// part of the blocking profile.
91+
// which is used in blocking and mutex profiles.
9192
type blockRecord struct {
9293
count int64
9394
cycles int64
@@ -96,6 +97,7 @@ type blockRecord struct {
9697
var (
9798
mbuckets *bucket // memory profile buckets
9899
bbuckets *bucket // blocking profile buckets
100+
xbuckets *bucket // mutex profile buckets
99101
buckhash *[179999]*bucket
100102
bucketmem uintptr
101103
)
@@ -108,7 +110,7 @@ func newBucket(typ bucketType, nstk int) *bucket {
108110
throw("invalid profile bucket type")
109111
case memProfile:
110112
size += unsafe.Sizeof(memRecord{})
111-
case blockProfile:
113+
case blockProfile, mutexProfile:
112114
size += unsafe.Sizeof(blockRecord{})
113115
}
114116

@@ -136,7 +138,7 @@ func (b *bucket) mp() *memRecord {
136138

137139
// bp returns the blockRecord associated with the blockProfile bucket b.
138140
func (b *bucket) bp() *blockRecord {
139-
if b.typ != blockProfile {
141+
if b.typ != blockProfile && b.typ != mutexProfile {
140142
throw("bad use of bucket.bp")
141143
}
142144
data := add(unsafe.Pointer(b), unsafe.Sizeof(*b)+b.nstk*unsafe.Sizeof(uintptr(0)))
@@ -188,6 +190,9 @@ func stkbucket(typ bucketType, size uintptr, stk []uintptr, alloc bool) *bucket
188190
if typ == memProfile {
189191
b.allnext = mbuckets
190192
mbuckets = b
193+
} else if typ == mutexProfile {
194+
b.allnext = xbuckets
195+
xbuckets = b
191196
} else {
192197
b.allnext = bbuckets
193198
bbuckets = b
@@ -292,10 +297,20 @@ func blockevent(cycles int64, skip int) {
292297
if cycles <= 0 {
293298
cycles = 1
294299
}
300+
if blocksampled(cycles) {
301+
saveblockevent(cycles, skip+1, blockProfile, &blockprofilerate)
302+
}
303+
}
304+
305+
func blocksampled(cycles int64) bool {
295306
rate := int64(atomic.Load64(&blockprofilerate))
296307
if rate <= 0 || (rate > cycles && int64(fastrand())%rate > cycles) {
297-
return
308+
return false
298309
}
310+
return true
311+
}
312+
313+
func saveblockevent(cycles int64, skip int, which bucketType, ratep *uint64) {
299314
gp := getg()
300315
var nstk int
301316
var stk [maxStack]uintptr
@@ -305,12 +320,40 @@ func blockevent(cycles int64, skip int) {
305320
nstk = gcallers(gp.m.curg, skip, stk[:])
306321
}
307322
lock(&proflock)
308-
b := stkbucket(blockProfile, 0, stk[:nstk], true)
323+
b := stkbucket(which, 0, stk[:nstk], true)
309324
b.bp().count++
310325
b.bp().cycles += cycles
311326
unlock(&proflock)
312327
}
313328

329+
var mutexprofilerate uint64 // fraction sampled
330+
331+
// SetMutexProfileFraction controls the fraction of mutex contention events
332+
// that are reported in the mutex profile. On average 1/rate events are
333+
// reported. The previous rate is returned.
334+
//
335+
// To turn off profiling entirely, pass rate 0.
336+
// To just read the current rate, pass rate -1.
337+
// (For n>1 the details of sampling may change.)
338+
func SetMutexProfileFraction(rate int) int {
339+
if rate < 0 {
340+
return int(mutexprofilerate)
341+
}
342+
old := mutexprofilerate
343+
atomic.Store64(&mutexprofilerate, uint64(rate))
344+
return int(old)
345+
}
346+
347+
//go:linkname mutexevent sync.event
348+
func mutexevent(cycles int64, skip int) {
349+
rate := int64(atomic.Load64(&mutexprofilerate))
350+
// TODO(pjw): measure impact of always calling fastrand vs using something
351+
// like malloc.go:nextSample()
352+
if rate > 0 && int64(fastrand())%rate == 0 {
353+
saveblockevent(cycles, skip+1, mutexProfile, &mutexprofilerate)
354+
}
355+
}
356+
314357
// Go interface to profile data.
315358

316359
// A StackRecord describes a single execution stack.
@@ -507,6 +550,35 @@ func BlockProfile(p []BlockProfileRecord) (n int, ok bool) {
507550
return
508551
}
509552

553+
// MutexProfile returns n, the number of records in the current mutex profile.
554+
// If len(p) >= n, MutexProfile copies the profile into p and returns n, true.
555+
// Otherwise, MutexProfile does not change p, and returns n, false.
556+
//
557+
// Most clients should use the runtime/pprof package
558+
// instead of calling MutexProfile directly.
559+
func MutexProfile(p []BlockProfileRecord) (n int, ok bool) {
560+
lock(&proflock)
561+
for b := xbuckets; b != nil; b = b.allnext {
562+
n++
563+
}
564+
if n <= len(p) {
565+
ok = true
566+
for b := xbuckets; b != nil; b = b.allnext {
567+
bp := b.bp()
568+
r := &p[0]
569+
r.Count = int64(bp.count)
570+
r.Cycles = bp.cycles
571+
i := copy(r.Stack0[:], b.stk())
572+
for ; i < len(r.Stack0); i++ {
573+
r.Stack0[i] = 0
574+
}
575+
p = p[1:]
576+
}
577+
}
578+
unlock(&proflock)
579+
return
580+
}
581+
510582
// ThreadCreateProfile returns n, the number of records in the thread creation profile.
511583
// If len(p) >= n, ThreadCreateProfile copies the profile into p and returns n, true.
512584
// If len(p) < n, ThreadCreateProfile does not change p and returns n, false.

‎src/runtime/pprof/pprof.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import (
9999
// heap - a sampling of all heap allocations
100100
// threadcreate - stack traces that led to the creation of new OS threads
101101
// block - stack traces that led to blocking on synchronization primitives
102+
// mutex - stack traces of holders of contended mutexes
102103
//
103104
// These predefined profiles maintain themselves and panic on an explicit
104105
// Add or Remove method call.
@@ -152,6 +153,12 @@ var blockProfile = &Profile{
152153
write: writeBlock,
153154
}
154155

156+
var mutexProfile = &Profile{
157+
name: "mutex",
158+
count: countMutex,
159+
write: writeMutex,
160+
}
161+
155162
func lockProfiles() {
156163
profiles.mu.Lock()
157164
if profiles.m == nil {
@@ -161,6 +168,7 @@ func lockProfiles() {
161168
"threadcreate": threadcreateProfile,
162169
"heap": heapProfile,
163170
"block": blockProfile,
171+
"mutex": mutexProfile,
164172
}
165173
}
166174
}
@@ -729,6 +737,12 @@ func countBlock() int {
729737
return n
730738
}
731739

740+
// countMutex returns the number of records in the mutex profile.
741+
func countMutex() int {
742+
n, _ := runtime.MutexProfile(nil)
743+
return n
744+
}
745+
732746
// writeBlock writes the current blocking profile to w.
733747
func writeBlock(w io.Writer, debug int) error {
734748
var p []runtime.BlockProfileRecord
@@ -772,4 +786,49 @@ func writeBlock(w io.Writer, debug int) error {
772786
return b.Flush()
773787
}
774788

789+
// writeMutex writes the current mutex profile to w.
790+
func writeMutex(w io.Writer, debug int) error {
791+
// TODO(pjw): too much common code with writeBlock. FIX!
792+
var p []runtime.BlockProfileRecord
793+
n, ok := runtime.MutexProfile(nil)
794+
for {
795+
p = make([]runtime.BlockProfileRecord, n+50)
796+
n, ok = runtime.MutexProfile(p)
797+
if ok {
798+
p = p[:n]
799+
break
800+
}
801+
}
802+
803+
sort.Slice(p, func(i, j int) bool { return p[i].Cycles > p[j].Cycles })
804+
805+
b := bufio.NewWriter(w)
806+
var tw *tabwriter.Writer
807+
w = b
808+
if debug > 0 {
809+
tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
810+
w = tw
811+
}
812+
813+
fmt.Fprintf(w, "--- mutex:\n")
814+
fmt.Fprintf(w, "cycles/second=%v\n", runtime_cyclesPerSecond())
815+
fmt.Fprintf(w, "sampling period=%d\n", runtime.SetMutexProfileFraction(-1))
816+
for i := range p {
817+
r := &p[i]
818+
fmt.Fprintf(w, "%v %v @", r.Cycles, r.Count)
819+
for _, pc := range r.Stack() {
820+
fmt.Fprintf(w, " %#x", pc)
821+
}
822+
fmt.Fprint(w, "\n")
823+
if debug > 0 {
824+
printStackRecord(w, r.Stack(), true)
825+
}
826+
}
827+
828+
if tw != nil {
829+
tw.Flush()
830+
}
831+
return b.Flush()
832+
}
833+
775834
func runtime_cyclesPerSecond() int64

‎src/runtime/pprof/pprof_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,42 @@ func blockCond() {
592592
mu.Unlock()
593593
}
594594

595+
func TestMutexProfile(t *testing.T) {
596+
old := runtime.SetMutexProfileFraction(1)
597+
defer runtime.SetMutexProfileFraction(old)
598+
if old != 0 {
599+
t.Fatalf("need MutexProfileRate 0, got %d", old)
600+
}
601+
602+
blockMutex()
603+
604+
var w bytes.Buffer
605+
Lookup("mutex").WriteTo(&w, 1)
606+
prof := w.String()
607+
608+
if !strings.HasPrefix(prof, "--- mutex:\ncycles/second=") {
609+
t.Errorf("Bad profile header:\n%v", prof)
610+
}
611+
prof = strings.Trim(prof, "\n")
612+
lines := strings.Split(prof, "\n")
613+
if len(lines) != 6 {
614+
t.Errorf("expected 6 lines, got %d %q\n%s", len(lines), prof, prof)
615+
}
616+
if len(lines) < 6 {
617+
return
618+
}
619+
// checking that the line is like "35258904 1 @ 0x48288d 0x47cd28 0x458931"
620+
r2 := `^\d+ 1 @(?: 0x[[:xdigit:]]+)+`
621+
//r2 := "^[0-9]+ 1 @ 0x[0-9a-f x]+$"
622+
if ok, err := regexp.MatchString(r2, lines[3]); err != nil || !ok {
623+
t.Errorf("%q didn't match %q", lines[3], r2)
624+
}
625+
r3 := "^#.*runtime/pprof_test.blockMutex.*$"
626+
if ok, err := regexp.MatchString(r3, lines[5]); err != nil || !ok {
627+
t.Errorf("%q didn't match %q", lines[5], r3)
628+
}
629+
}
630+
595631
func func1(c chan int) { <-c }
596632
func func2(c chan int) { <-c }
597633
func func3(c chan int) { <-c }

‎src/runtime/proc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,7 @@ func restartg(gp *g) {
923923
// in panic or being exited, this may not reliably stop all
924924
// goroutines.
925925
func stopTheWorld(reason string) {
926-
semacquire(&worldsema, false)
926+
semacquire(&worldsema, 0)
927927
getg().m.preemptoff = reason
928928
systemstack(stopTheWorldWithSema)
929929
}
@@ -946,7 +946,7 @@ var worldsema uint32 = 1
946946
// preemption first and then should stopTheWorldWithSema on the system
947947
// stack:
948948
//
949-
// semacquire(&worldsema, false)
949+
// semacquire(&worldsema, 0)
950950
// m.preemptoff = "reason"
951951
// systemstack(stopTheWorldWithSema)
952952
//

‎src/runtime/runtime2.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ type sudog struct {
256256
// The following fields are never accessed concurrently.
257257
// waitlink is only accessed by g.
258258

259+
acquiretime int64
259260
releasetime int64
260261
ticket uint32
261262
waitlink *sudog // g.waiting list

‎src/runtime/sema.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,24 @@ var semtable [semTabSize]struct {
4444

4545
//go:linkname sync_runtime_Semacquire sync.runtime_Semacquire
4646
func sync_runtime_Semacquire(addr *uint32) {
47-
semacquire(addr, true)
47+
semacquire(addr, semaBlockProfile)
4848
}
4949

5050
//go:linkname net_runtime_Semacquire net.runtime_Semacquire
5151
func net_runtime_Semacquire(addr *uint32) {
52-
semacquire(addr, true)
52+
semacquire(addr, semaBlockProfile)
5353
}
5454

5555
//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
5656
func sync_runtime_Semrelease(addr *uint32) {
5757
semrelease(addr)
5858
}
5959

60+
//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
61+
func sync_runtime_SemacquireMutex(addr *uint32) {
62+
semacquire(addr, semaBlockProfile|semaMutexProfile)
63+
}
64+
6065
//go:linkname net_runtime_Semrelease net.runtime_Semrelease
6166
func net_runtime_Semrelease(addr *uint32) {
6267
semrelease(addr)
@@ -69,8 +74,15 @@ func readyWithTime(s *sudog, traceskip int) {
6974
goready(s.g, traceskip)
7075
}
7176

77+
type semaProfileFlags int
78+
79+
const (
80+
semaBlockProfile semaProfileFlags = 1 << iota
81+
semaMutexProfile
82+
)
83+
7284
// Called from runtime.
73-
func semacquire(addr *uint32, profile bool) {
85+
func semacquire(addr *uint32, profile semaProfileFlags) {
7486
gp := getg()
7587
if gp != gp.m.curg {
7688
throw("semacquire not on the G stack")
@@ -91,10 +103,17 @@ func semacquire(addr *uint32, profile bool) {
91103
root := semroot(addr)
92104
t0 := int64(0)
93105
s.releasetime = 0
94-
if profile && blockprofilerate > 0 {
106+
s.acquiretime = 0
107+
if profile&semaBlockProfile != 0 && blockprofilerate > 0 {
95108
t0 = cputicks()
96109
s.releasetime = -1
97110
}
111+
if profile&semaMutexProfile != 0 && mutexprofilerate > 0 {
112+
if t0 == 0 {
113+
t0 = cputicks()
114+
}
115+
s.acquiretime = t0
116+
}
98117
for {
99118
lock(&root.lock)
100119
// Add ourselves to nwait to disable "easy case" in semrelease.
@@ -146,8 +165,19 @@ func semrelease(addr *uint32) {
146165
break
147166
}
148167
}
149-
unlock(&root.lock)
150168
if s != nil {
169+
if s.acquiretime != 0 {
170+
t0 := cputicks()
171+
for x := root.head; x != nil; x = x.next {
172+
if x.elem == unsafe.Pointer(addr) {
173+
x.acquiretime = t0
174+
}
175+
}
176+
mutexevent(t0-s.acquiretime, 3)
177+
}
178+
}
179+
unlock(&root.lock)
180+
if s != nil { // May be slow, so unlock first
151181
readyWithTime(s, 5)
152182
}
153183
}

‎src/runtime/trace.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ func StopTrace() {
292292

293293
// The world is started but we've set trace.shutdown, so new tracing can't start.
294294
// Wait for the trace reader to flush pending buffers and stop.
295-
semacquire(&trace.shutdownSema, false)
295+
semacquire(&trace.shutdownSema, 0)
296296
if raceenabled {
297297
raceacquire(unsafe.Pointer(&trace.shutdownSema))
298298
}

‎src/sync/mutex.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (m *Mutex) Lock() {
8484
if old&mutexLocked == 0 {
8585
break
8686
}
87-
runtime_Semacquire(&m.sema)
87+
runtime_SemacquireMutex(&m.sema)
8888
awoke = true
8989
iter = 0
9090
}

‎src/sync/mutex_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func HammerMutex(m *Mutex, loops int, cdone chan bool) {
6666
}
6767

6868
func TestMutex(t *testing.T) {
69+
if n := runtime.SetMutexProfileFraction(1); n != 0 {
70+
t.Logf("got mutexrate %d expected 0", n)
71+
}
72+
defer runtime.SetMutexProfileFraction(0)
6973
m := new(Mutex)
7074
c := make(chan bool)
7175
for i := 0; i < 10; i++ {

‎src/sync/runtime.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import "unsafe"
1313
// library and should not be used directly.
1414
func runtime_Semacquire(s *uint32)
1515

16+
// SemacquireMutex is like Semacquire, but for profiling contended Mutexes.
17+
func runtime_SemacquireMutex(*uint32)
18+
1619
// Semrelease atomically increments *s and notifies a waiting goroutine
1720
// if one is blocked in Semacquire.
1821
// It is intended as a simple wakeup primitive for use by the synchronization

‎src/testing/testing.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -233,19 +233,21 @@ var (
233233
outputDir = flag.String("test.outputdir", "", "write profiles to `dir`")
234234

235235
// Report as tests are run; default is silent for success.
236-
chatty = flag.Bool("test.v", false, "verbose: print additional output")
237-
count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times")
238-
coverProfile = flag.String("test.coverprofile", "", "write a coverage profile to `file`")
239-
match = flag.String("test.run", "", "run only tests and examples matching `regexp`")
240-
memProfile = flag.String("test.memprofile", "", "write a memory profile to `file`")
241-
memProfileRate = flag.Int("test.memprofilerate", 0, "set memory profiling `rate` (see runtime.MemProfileRate)")
242-
cpuProfile = flag.String("test.cpuprofile", "", "write a cpu profile to `file`")
243-
blockProfile = flag.String("test.blockprofile", "", "write a goroutine blocking profile to `file`")
244-
blockProfileRate = flag.Int("test.blockprofilerate", 1, "set blocking profile `rate` (see runtime.SetBlockProfileRate)")
245-
traceFile = flag.String("test.trace", "", "write an execution trace to `file`")
246-
timeout = flag.Duration("test.timeout", 0, "fail test binary execution after duration `d` (0 means unlimited)")
247-
cpuListStr = flag.String("test.cpu", "", "comma-separated `list` of cpu counts to run each test with")
248-
parallel = flag.Int("test.parallel", runtime.GOMAXPROCS(0), "run at most `n` tests in parallel")
236+
chatty = flag.Bool("test.v", false, "verbose: print additional output")
237+
count = flag.Uint("test.count", 1, "run tests and benchmarks `n` times")
238+
coverProfile = flag.String("test.coverprofile", "", "write a coverage profile to `file`")
239+
match = flag.String("test.run", "", "run only tests and examples matching `regexp`")
240+
memProfile = flag.String("test.memprofile", "", "write a memory profile to `file`")
241+
memProfileRate = flag.Int("test.memprofilerate", 0, "set memory profiling `rate` (see runtime.MemProfileRate)")
242+
cpuProfile = flag.String("test.cpuprofile", "", "write a cpu profile to `file`")
243+
blockProfile = flag.String("test.blockprofile", "", "write a goroutine blocking profile to `file`")
244+
blockProfileRate = flag.Int("test.blockprofilerate", 1, "set blocking profile `rate` (see runtime.SetBlockProfileRate)")
245+
mutexProfile = flag.String("test.mutexprofile", "", "write a mutex contention profile to the named file after execution")
246+
mutexProfileFraction = flag.Int("test.mutexprofilefraction", 1, "if >= 0, calls runtime.SetMutexProfileFraction()")
247+
traceFile = flag.String("test.trace", "", "write an execution trace to `file`")
248+
timeout = flag.Duration("test.timeout", 0, "fail test binary execution after duration `d` (0 means unlimited)")
249+
cpuListStr = flag.String("test.cpu", "", "comma-separated `list` of cpu counts to run each test with")
250+
parallel = flag.Int("test.parallel", runtime.GOMAXPROCS(0), "run at most `n` tests in parallel")
249251

250252
haveExamples bool // are there examples?
251253

@@ -874,6 +876,9 @@ func before() {
874876
if *blockProfile != "" && *blockProfileRate >= 0 {
875877
runtime.SetBlockProfileRate(*blockProfileRate)
876878
}
879+
if *mutexProfile != "" && *mutexProfileFraction >= 0 {
880+
runtime.SetMutexProfileFraction(*mutexProfileFraction)
881+
}
877882
if *coverProfile != "" && cover.Mode == "" {
878883
fmt.Fprintf(os.Stderr, "testing: cannot use -test.coverprofile because test binary was not built with coverage enabled\n")
879884
os.Exit(2)
@@ -913,6 +918,18 @@ func after() {
913918
}
914919
f.Close()
915920
}
921+
if *mutexProfile != "" && *mutexProfileFraction >= 0 {
922+
f, err := os.Create(toOutputDir(*mutexProfile))
923+
if err != nil {
924+
fmt.Fprintf(os.Stderr, "testing: %s\n", err)
925+
os.Exit(2)
926+
}
927+
if err = pprof.Lookup("mutex").WriteTo(f, 0); err != nil {
928+
fmt.Fprintf(os.Stderr, "testing: can't write %s: %s\n", *blockProfile, err)
929+
os.Exit(2)
930+
}
931+
f.Close()
932+
}
916933
if cover.Mode != "" {
917934
coverReport()
918935
}

0 commit comments

Comments
 (0)
Please sign in to comment.