Skip to content

Commit 450ecbe

Browse files
rhyshgopherbot
authored andcommitted
runtime: profile contended lock calls
Add runtime-internal locks to the mutex contention profile. Store up to one call stack responsible for lock contention on the M, until it's safe to contribute its value to the mprof table. Try to use that limited local storage space for a relatively large source of contention, and attribute any contention in stacks we're not able to store to a sentinel _LostContendedLock function. Avoid ballooning lock contention while manipulating the mprof table by attributing to that sentinel function any lock contention experienced while reporting lock contention. Guard collecting real call stacks with GODEBUG=profileruntimelocks=1, since the available data has mixed semantics; we can easily capture an M's own wait time, but we'd prefer for the profile entry of each critical section to describe how long it made the other Ms wait. It's too late in the Go 1.22 cycle to make the required changes to futex-based locks. When not enabled, attribute the time to the sentinel function instead. Fixes #57071 This is a roll-forward of https://go.dev/cl/528657, which was reverted in https://go.dev/cl/543660 Reason for revert: de-flakes tests (reduces dependence on fine-grained timers, correctly identifies contention on big-endian futex locks, attempts to measure contention in the semaphore implementation but only uses that secondary measurement to finish the test early, skips tests on single-processor systems) Change-Id: I31389f24283d85e46ad9ba8d4f514cb9add8dfb0 Reviewed-on: https://go-review.googlesource.com/c/go/+/544195 Reviewed-by: Michael Pratt <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Than McIntosh <[email protected]> Auto-Submit: Rhys Hiltner <[email protected]> Run-TryBot: Rhys Hiltner <[email protected]>
1 parent 539b4d8 commit 450ecbe

13 files changed

+648
-23
lines changed

src/runtime/debug.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ func NumCgoCall() int64 {
5252
return n
5353
}
5454

55+
func totalMutexWaitTimeNanos() int64 {
56+
total := sched.totalMutexWaitTime.Load()
57+
58+
total += sched.totalRuntimeLockWaitTime.Load()
59+
for mp := (*m)(atomic.Loadp(unsafe.Pointer(&allm))); mp != nil; mp = mp.alllink {
60+
total += mp.mLockProfile.waitTime.Load()
61+
}
62+
63+
return total
64+
}
65+
5566
// NumGoroutine returns the number of goroutines that currently exist.
5667
func NumGoroutine() int {
5768
return int(gcount())

src/runtime/export_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,18 @@ func PageCachePagesLeaked() (leaked uintptr) {
13401340
return
13411341
}
13421342

1343+
type Mutex = mutex
1344+
1345+
var Lock = lock
1346+
var Unlock = unlock
1347+
1348+
var MutexContended = mutexContended
1349+
1350+
func SemRootLock(addr *uint32) *mutex {
1351+
root := semtable.rootFor(addr)
1352+
return &root.lock
1353+
}
1354+
13431355
var Semacquire = semacquire
13441356
var Semrelease1 = semrelease1
13451357

src/runtime/extern.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ It is a comma-separated list of name=val pairs setting these named variables:
145145
risk in that scenario. Currently not supported on Windows, plan9 or js/wasm. Setting this
146146
option for some applications can produce large traces, so use with care.
147147
148+
profileruntimelocks: setting profileruntimelocks=1 includes call stacks related to
149+
contention on runtime-internal locks in the "mutex" profile, subject to the
150+
MutexProfileFraction setting. The call stacks will correspond to the unlock call that
151+
released the lock. But instead of the value corresponding to the amount of contention that
152+
call stack caused, it corresponds to the amount of time the caller of unlock had to wait
153+
in its original call to lock. A future release is expected to align those and remove this
154+
setting.
155+
148156
invalidptr: invalidptr=1 (the default) causes the garbage collector and stack
149157
copier to crash the program if an invalid pointer value (for example, 1)
150158
is found in a pointer-typed location. Setting invalidptr=0 disables this check.

src/runtime/lock_futex.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func key32(p *uintptr) *uint32 {
4444
return (*uint32)(unsafe.Pointer(p))
4545
}
4646

47+
func mutexContended(l *mutex) bool {
48+
return atomic.Load(key32(&l.key)) > mutex_locked
49+
}
50+
4751
func lock(l *mutex) {
4852
lockWithRank(l, getLockRank(l))
4953
}
@@ -71,6 +75,8 @@ func lock2(l *mutex) {
7175
// its wakeup call.
7276
wait := v
7377

78+
timer := &lockTimer{lock: l}
79+
timer.begin()
7480
// On uniprocessors, no point spinning.
7581
// On multiprocessors, spin for ACTIVE_SPIN attempts.
7682
spin := 0
@@ -82,6 +88,7 @@ func lock2(l *mutex) {
8288
for i := 0; i < spin; i++ {
8389
for l.key == mutex_unlocked {
8490
if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
91+
timer.end()
8592
return
8693
}
8794
}
@@ -92,6 +99,7 @@ func lock2(l *mutex) {
9299
for i := 0; i < passive_spin; i++ {
93100
for l.key == mutex_unlocked {
94101
if atomic.Cas(key32(&l.key), mutex_unlocked, wait) {
102+
timer.end()
95103
return
96104
}
97105
}
@@ -101,6 +109,7 @@ func lock2(l *mutex) {
101109
// Sleep.
102110
v = atomic.Xchg(key32(&l.key), mutex_sleeping)
103111
if v == mutex_unlocked {
112+
timer.end()
104113
return
105114
}
106115
wait = mutex_sleeping
@@ -122,6 +131,7 @@ func unlock2(l *mutex) {
122131
}
123132

124133
gp := getg()
134+
gp.m.mLockProfile.recordUnlock(l)
125135
gp.m.locks--
126136
if gp.m.locks < 0 {
127137
throw("runtime·unlock: lock count")

src/runtime/lock_js.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ const (
2323
passive_spin = 1
2424
)
2525

26+
func mutexContended(l *mutex) bool {
27+
return false
28+
}
29+
2630
func lock(l *mutex) {
2731
lockWithRank(l, getLockRank(l))
2832
}

src/runtime/lock_sema.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const (
3131
passive_spin = 1
3232
)
3333

34+
func mutexContended(l *mutex) bool {
35+
return atomic.Loaduintptr(&l.key) > locked
36+
}
37+
3438
func lock(l *mutex) {
3539
lockWithRank(l, getLockRank(l))
3640
}
@@ -48,6 +52,8 @@ func lock2(l *mutex) {
4852
}
4953
semacreate(gp.m)
5054

55+
timer := &lockTimer{lock: l}
56+
timer.begin()
5157
// On uniprocessor's, no point spinning.
5258
// On multiprocessors, spin for ACTIVE_SPIN attempts.
5359
spin := 0
@@ -60,6 +66,7 @@ Loop:
6066
if v&locked == 0 {
6167
// Unlocked. Try to lock.
6268
if atomic.Casuintptr(&l.key, v, v|locked) {
69+
timer.end()
6370
return
6471
}
6572
i = 0
@@ -119,6 +126,7 @@ func unlock2(l *mutex) {
119126
}
120127
}
121128
}
129+
gp.m.mLockProfile.recordUnlock(l)
122130
gp.m.locks--
123131
if gp.m.locks < 0 {
124132
throw("runtime·unlock: lock count")

src/runtime/lock_wasip1.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const (
1919
active_spin_cnt = 30
2020
)
2121

22+
func mutexContended(l *mutex) bool {
23+
return false
24+
}
25+
2226
func lock(l *mutex) {
2327
lockWithRank(l, getLockRank(l))
2428
}

src/runtime/metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ func initMetrics() {
470470
"/sync/mutex/wait/total:seconds": {
471471
compute: func(_ *statAggregate, out *metricValue) {
472472
out.kind = metricKindFloat64
473-
out.scalar = float64bits(nsToSec(sched.totalMutexWaitTime.Load()))
473+
out.scalar = float64bits(nsToSec(totalMutexWaitTimeNanos()))
474474
},
475475
},
476476
}

0 commit comments

Comments
 (0)