Skip to content

Commit d229162

Browse files
committed
runtime/pprof: add debug=3 goroutine profile with goid and labels
This adds a new goroutine profile debug mode, debug=3. This mode emits in the same binary proto output format as debug=0, with the only difference being that it does not aggregate matching stack/label combinations into a single count, and instead mits a sample per goroutine with additional synthesized labels to communicate some of the details of each goroutine, specifically: - go::goroutine: the goroutine's ID - go::goroutine_created_by: ID of the goroutine's creator (if any) - go::goroutine_state: current state of the goroutine (e.g. runnable) - go::goroutine_wait_minutes: approximate minutes goroutine has spent waiting (if applicable) These are synthesized as labels, side-by-side with any user-set labels thus the 'go::' prefix to namespace these synthetic labels somewhat. We aim to minimize collisions with user-set labels as while the format does allow for multi-valued labels, meaning a collision would not immediately overwrite a user-set label, the upstream proto does warn that not all tools handle multi-valued labels well, so the namespace prefix aims to avoid them if possible. The form 'go::' is chosen as the upstream proto reserves 'pprof::', which seems to establish this format as a convention. Previously the debug=2 mode was the only way to get this kind of per-goroutine information that is sometimes vital to understanding the state of a process. However debug=2 had two major drawbacks: 1) its collection incurs a potentially lengthy and disruptive stop-the-world pause due to it not (and perhaps being unable to) utilizing the concurrent collection mechanism used by debug=0/1, and 2) it does not (currently) include user-set labels in the same profile. This new mode that uses the same concurrent collection mechanism as 0/1, with its minimal STW penalty, but also includes the per-goroutine details that make debug=2 so useful. Changing debug=2, which already included this per-goroutine detail, in-place to use the concurrent collection mechanism could avoid introducing a new mode, but it is not clear that this is possible with breaking changes to its output: debug=2 includes argument _values_ in its output would would require collection in both a concurrent and consistent manner. Thus the use of a new mode instead. The difference in service latency observed by running goroutines during profile collection is demonstrated by BenchmarkGoroutineProfileLatencyImpact: │ debug=2 │ debug=3 │ │ max_latency_ns │ max_latency_ns vs base │ goroutines=100x3-14 508.4k ± 17% 166.7k ± 45% -67.21% (p=0.002 n=6) goroutines=100x10-14 727.4k ± 9% 176.3k ± 34% -75.76% (p=0.002 n=6) goroutines=100x50-14 1979.1k ± 6% 201.7k ± 18% -89.81% (p=0.002 n=6) goroutines=1000x3-14 6454.9k ± 3% 235.6k ± 36% -96.35% (p=0.002 n=6) goroutines=1000x10-14 10620.8k ± 47% 259.8k ± 165% -97.55% (p=0.002 n=6) goroutines=1000x50-14 35170.8k ± 1% 228.7k ± 108% -99.35% (p=0.002 n=6) goroutines=10000x3-14 63.836M ± 49% 1.961M ± 470% -96.93% (p=0.002 n=6) goroutines=10000x10-14 58.962M ± 82% 4.073M ± 134% -93.09% (p=0.002 n=6) goroutines=10000x50-14 259.723M ± 33% 4.730M ± 91% -98.18% (p=0.002 n=6) geomean 10.62M 527.0k -95.04%
1 parent c1e6e49 commit d229162

File tree

7 files changed

+430
-154
lines changed

7 files changed

+430
-154
lines changed

src/internal/profilerecord/profilerecord.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@
88
// TODO: Consider moving this to internal/runtime, see golang.org/issue/65355.
99
package profilerecord
1010

11+
import "unsafe"
12+
1113
type StackRecord struct {
1214
Stack []uintptr
1315
}
1416

17+
func (r StackRecord) GetStack() []uintptr { return r.Stack }
18+
func (r StackRecord) GetLabels() unsafe.Pointer { return nil }
19+
func (r StackRecord) GetGoroutine() GoroutineRecord { return GoroutineRecord{} }
20+
1521
type MemProfileRecord struct {
1622
AllocBytes, FreeBytes int64
1723
AllocObjects, FreeObjects int64
@@ -26,3 +32,18 @@ type BlockProfileRecord struct {
2632
Cycles int64
2733
Stack []uintptr
2834
}
35+
36+
type GoroutineRecord struct {
37+
ID uint64
38+
State uint32
39+
WaitReason uint8
40+
CreatorID uint64
41+
CreationPC uintptr
42+
WaitSince int64 // approx time when the g became blocked, in nanoseconds
43+
Labels unsafe.Pointer
44+
Stack []uintptr
45+
}
46+
47+
func (r GoroutineRecord) GetStack() []uintptr { return r.Stack }
48+
func (r GoroutineRecord) GetLabels() unsafe.Pointer { return r.Labels }
49+
func (r GoroutineRecord) GetGoroutine() GoroutineRecord { return r }

src/runtime/mprof.go

Lines changed: 27 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1246,39 +1246,25 @@ func pprof_threadCreateInternal(p []profilerecord.StackRecord) (n int, ok bool)
12461246
}
12471247

12481248
//go:linkname pprof_goroutineProfileWithLabels
1249-
func pprof_goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1250-
return goroutineProfileWithLabels(p, labels)
1251-
}
1252-
1253-
// labels may be nil. If labels is non-nil, it must have the same length as p.
1254-
func goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1255-
if labels != nil && len(labels) != len(p) {
1256-
labels = nil
1257-
}
1258-
1259-
return goroutineProfileWithLabelsConcurrent(p, labels)
1249+
func pprof_goroutineProfileWithLabels(p []profilerecord.GoroutineRecord) (n int, ok bool) {
1250+
return goroutineProfileInternal(p)
12601251
}
12611252

12621253
//go:linkname pprof_goroutineLeakProfileWithLabels
1263-
func pprof_goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1264-
return goroutineLeakProfileWithLabelsConcurrent(p, labels)
1254+
func pprof_goroutineLeakProfileWithLabels(p []profilerecord.GoroutineRecord) (n int, ok bool) {
1255+
return goroutineLeakProfileWithLabelsConcurrent(p)
12651256
}
12661257

12671258
// labels may be nil. If labels is non-nil, it must have the same length as p.
1268-
func goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1269-
if labels != nil && len(labels) != len(p) {
1270-
labels = nil
1271-
}
1272-
1273-
return goroutineLeakProfileWithLabelsConcurrent(p, labels)
1259+
func goroutineLeakProfileWithLabels(p []profilerecord.GoroutineRecord) (n int, ok bool) {
1260+
return goroutineLeakProfileWithLabelsConcurrent(p)
12741261
}
12751262

12761263
var goroutineProfile = struct {
12771264
sema uint32
12781265
active bool
12791266
offset atomic.Int64
1280-
records []profilerecord.StackRecord
1281-
labels []unsafe.Pointer
1267+
records []profilerecord.GoroutineRecord
12821268
}{
12831269
sema: 1,
12841270
}
@@ -1316,7 +1302,7 @@ func (p *goroutineProfileStateHolder) CompareAndSwap(old, new goroutineProfileSt
13161302
return (*atomic.Uint32)(p).CompareAndSwap(uint32(old), uint32(new))
13171303
}
13181304

1319-
func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1305+
func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.GoroutineRecord) (n int, ok bool) {
13201306
if len(p) == 0 {
13211307
// An empty slice is obviously too small. Return a rough
13221308
// allocation estimate.
@@ -1358,7 +1344,7 @@ func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, lab
13581344
return n, true
13591345
}
13601346

1361-
func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1347+
func goroutineProfileInternal(p []profilerecord.GoroutineRecord) (n int, ok bool) {
13621348
if len(p) == 0 {
13631349
// An empty slice is obviously too small. Return a rough
13641350
// allocation estimate without bothering to STW. As long as
@@ -1401,9 +1387,15 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14011387
systemstack(func() {
14021388
saveg(pc, sp, ourg, &p[0], pcbuf)
14031389
})
1404-
if labels != nil {
1405-
labels[0] = ourg.labels
1406-
}
1390+
1391+
p[0].ID = ourg.goid
1392+
p[0].CreatorID = ourg.parentGoid
1393+
p[0].CreationPC = ourg.gopc
1394+
p[0].Labels = ourg.labels
1395+
p[0].State = readgstatus(ourg) &^ _Gscan
1396+
p[0].WaitReason = uint8(ourg.waitreason)
1397+
p[0].WaitSince = ourg.waitsince
1398+
14071399
ourg.goroutineProfiled.Store(goroutineProfileSatisfied)
14081400
goroutineProfile.offset.Store(1)
14091401

@@ -1414,7 +1406,6 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14141406
// field set to goroutineProfileSatisfied.
14151407
goroutineProfile.active = true
14161408
goroutineProfile.records = p
1417-
goroutineProfile.labels = labels
14181409
startTheWorld(stw)
14191410

14201411
// Visit each goroutine that existed as of the startTheWorld call above.
@@ -1436,7 +1427,6 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14361427
endOffset := goroutineProfile.offset.Swap(0)
14371428
goroutineProfile.active = false
14381429
goroutineProfile.records = nil
1439-
goroutineProfile.labels = nil
14401430
startTheWorld(stw)
14411431

14421432
// Restore the invariant that every goroutine struct in allgs has its
@@ -1559,79 +1549,13 @@ func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) {
15591549
// to avoid schedule delays.
15601550
systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &goroutineProfile.records[offset], pcbuf) })
15611551

1562-
if goroutineProfile.labels != nil {
1563-
goroutineProfile.labels[offset] = gp1.labels
1564-
}
1565-
}
1566-
1567-
func goroutineProfileWithLabelsSync(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1568-
gp := getg()
1569-
1570-
isOK := func(gp1 *g) bool {
1571-
// Checking isSystemGoroutine here makes GoroutineProfile
1572-
// consistent with both NumGoroutine and Stack.
1573-
return gp1 != gp && readgstatus(gp1) != _Gdead && !isSystemGoroutine(gp1, false)
1574-
}
1575-
1576-
pcbuf := makeProfStack() // see saveg() for explanation
1577-
stw := stopTheWorld(stwGoroutineProfile)
1578-
1579-
// World is stopped, no locking required.
1580-
n = 1
1581-
forEachGRace(func(gp1 *g) {
1582-
if isOK(gp1) {
1583-
n++
1584-
}
1585-
})
1586-
1587-
if n <= len(p) {
1588-
ok = true
1589-
r, lbl := p, labels
1590-
1591-
// Save current goroutine.
1592-
sp := sys.GetCallerSP()
1593-
pc := sys.GetCallerPC()
1594-
systemstack(func() {
1595-
saveg(pc, sp, gp, &r[0], pcbuf)
1596-
})
1597-
r = r[1:]
1598-
1599-
// If we have a place to put our goroutine labelmap, insert it there.
1600-
if labels != nil {
1601-
lbl[0] = gp.labels
1602-
lbl = lbl[1:]
1603-
}
1604-
1605-
// Save other goroutines.
1606-
forEachGRace(func(gp1 *g) {
1607-
if !isOK(gp1) {
1608-
return
1609-
}
1610-
1611-
if len(r) == 0 {
1612-
// Should be impossible, but better to return a
1613-
// truncated profile than to crash the entire process.
1614-
return
1615-
}
1616-
// saveg calls gentraceback, which may call cgo traceback functions.
1617-
// The world is stopped, so it cannot use cgocall (which will be
1618-
// blocked at exitsyscall). Do it on the system stack so it won't
1619-
// call into the schedular (see traceback.go:cgoContextPCs).
1620-
systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &r[0], pcbuf) })
1621-
if labels != nil {
1622-
lbl[0] = gp1.labels
1623-
lbl = lbl[1:]
1624-
}
1625-
r = r[1:]
1626-
})
1627-
}
1628-
1629-
if raceenabled {
1630-
raceacquire(unsafe.Pointer(&labelSync))
1631-
}
1632-
1633-
startTheWorld(stw)
1634-
return n, ok
1552+
goroutineProfile.records[offset].Labels = gp1.labels
1553+
goroutineProfile.records[offset].ID = gp1.goid
1554+
goroutineProfile.records[offset].CreatorID = gp1.parentGoid
1555+
goroutineProfile.records[offset].CreationPC = gp1.gopc
1556+
goroutineProfile.records[offset].State = readgstatus(gp1) &^ _Gscan
1557+
goroutineProfile.records[offset].WaitReason = uint8(gp1.waitreason)
1558+
goroutineProfile.records[offset].WaitSince = gp1.waitsince
16351559
}
16361560

16371561
// GoroutineProfile returns n, the number of records in the active goroutine stack profile.
@@ -1641,7 +1565,7 @@ func goroutineProfileWithLabelsSync(p []profilerecord.StackRecord, labels []unsa
16411565
// Most clients should use the [runtime/pprof] package instead
16421566
// of calling GoroutineProfile directly.
16431567
func GoroutineProfile(p []StackRecord) (n int, ok bool) {
1644-
records := make([]profilerecord.StackRecord, len(p))
1568+
records := make([]profilerecord.GoroutineRecord, len(p))
16451569
n, ok = goroutineProfileInternal(records)
16461570
if !ok {
16471571
return
@@ -1653,11 +1577,7 @@ func GoroutineProfile(p []StackRecord) (n int, ok bool) {
16531577
return
16541578
}
16551579

1656-
func goroutineProfileInternal(p []profilerecord.StackRecord) (n int, ok bool) {
1657-
return goroutineProfileWithLabels(p, nil)
1658-
}
1659-
1660-
func saveg(pc, sp uintptr, gp *g, r *profilerecord.StackRecord, pcbuf []uintptr) {
1580+
func saveg(pc, sp uintptr, gp *g, r *profilerecord.GoroutineRecord, pcbuf []uintptr) {
16611581
// To reduce memory usage, we want to allocate a r.Stack that is just big
16621582
// enough to hold gp's stack trace. Naively we might achieve this by
16631583
// recording our stack trace into mp.profStack, and then allocating a

0 commit comments

Comments
 (0)