Skip to content

Commit 0ae59d2

Browse files
committed
runtime: refactor goroutine profilerecord types
Previously, goroutine profiles were collected into two adjacent slices each of length n, with the i'th index of the first holding the captured stack of the i'th goroutine and the i'th index of the second holding its captured labels. This changes the representation used during collection to be a single slice of length n, with the stack and labels of the i'th goroutine now residing in two fields of each struct in that slice. Switching from multiple slices each of a single attribute each to one slice of multiple attributes avoids allocating multiple slices, as well as needing to pass them both around, side-by-side, in all goroutine profile collection code. While maintaining and passing two slices was workable -- if perhaps slightly cumbersome -- passing side-by-side around would quickly become unwieldy if or when any additional attributes such as wait reasons or wait times are collected during goroutine profiling, for example as proposed in #74954. Regardless of the user-facing shape that any such extension to goroutine profiling may end up taking, this updated internal representation should be substantially easier to extend and maintain than side-by-side slices. This is a pure refactor of this internal representation; it should have no user-facing behavior change. While in the area: concurrent goroutine collection has been the only mechanism used for some time now, so the disused legacy implementation goroutineProfileWithLabelsSync is removed and the single remaining implementation is renamed to drop its 'concurrent' qualifier. Updates #74954. Change-Id: I3fd14834b2f3aae73317d3fad3294d539302713f
1 parent 99b724f commit 0ae59d2

File tree

4 files changed

+64
-153
lines changed

4 files changed

+64
-153
lines changed

src/internal/profilerecord/profilerecord.go

Lines changed: 15 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,12 @@ type BlockProfileRecord struct {
2632
Cycles int64
2733
Stack []uintptr
2834
}
35+
36+
type GoroutineRecord struct {
37+
Labels unsafe.Pointer
38+
Stack []uintptr
39+
}
40+
41+
func (r GoroutineRecord) GetStack() []uintptr { return r.Stack }
42+
func (r GoroutineRecord) GetLabels() unsafe.Pointer { return r.Labels }
43+
func (r GoroutineRecord) GetGoroutine() GoroutineRecord { return r }

src/runtime/mprof.go

Lines changed: 18 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,40 +1245,21 @@ func pprof_threadCreateInternal(p []profilerecord.StackRecord) (n int, ok bool)
12451245
})
12461246
}
12471247

1248-
//go:linkname pprof_goroutineProfileWithLabels
1249-
func pprof_goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1250-
return goroutineProfileWithLabels(p, labels)
1248+
//go:linkname pprof_goroutineProfile
1249+
func pprof_goroutineProfile(p []profilerecord.GoroutineRecord) (n int, ok bool) {
1250+
return goroutineProfileInternal(p)
12511251
}
12521252

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)
1260-
}
1261-
1262-
//go:linkname pprof_goroutineLeakProfileWithLabels
1263-
func pprof_goroutineLeakProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1264-
return goroutineLeakProfileWithLabelsConcurrent(p, labels)
1265-
}
1266-
1267-
// 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)
1253+
//go:linkname pprof_goroutineLeakProfile
1254+
func pprof_goroutineLeakProfile(p []profilerecord.GoroutineRecord) (n int, ok bool) {
1255+
return goroutineLeakProfileInternal(p)
12741256
}
12751257

12761258
var goroutineProfile = struct {
12771259
sema uint32
12781260
active bool
12791261
offset atomic.Int64
1280-
records []profilerecord.StackRecord
1281-
labels []unsafe.Pointer
1262+
records []profilerecord.GoroutineRecord
12821263
}{
12831264
sema: 1,
12841265
}
@@ -1316,18 +1297,18 @@ func (p *goroutineProfileStateHolder) CompareAndSwap(old, new goroutineProfileSt
13161297
return (*atomic.Uint32)(p).CompareAndSwap(uint32(old), uint32(new))
13171298
}
13181299

1319-
func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1300+
func goroutineLeakProfileInternal(p []profilerecord.GoroutineRecord) (n int, ok bool) {
13201301
if len(p) == 0 {
13211302
// An empty slice is obviously too small. Return a rough
13221303
// allocation estimate.
13231304
return work.goroutineLeak.count, false
13241305
}
13251306

1326-
// Use the same semaphore as goroutineProfileWithLabelsConcurrent,
1307+
// Use the same semaphore as goroutineProfileInternal,
13271308
// because ultimately we still use goroutine profiles.
13281309
semacquire(&goroutineProfile.sema)
13291310

1330-
// Unlike in goroutineProfileWithLabelsConcurrent, we don't need to
1311+
// Unlike in goroutineProfileInternal, we don't need to
13311312
// save the current goroutine stack, because it is obviously not leaked.
13321313

13331314
pcbuf := makeProfStack() // see saveg() for explanation
@@ -1358,7 +1339,7 @@ func goroutineLeakProfileWithLabelsConcurrent(p []profilerecord.StackRecord, lab
13581339
return n, true
13591340
}
13601341

1361-
func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1342+
func goroutineProfileInternal(p []profilerecord.GoroutineRecord) (n int, ok bool) {
13621343
if len(p) == 0 {
13631344
// An empty slice is obviously too small. Return a rough
13641345
// allocation estimate without bothering to STW. As long as
@@ -1401,9 +1382,8 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14011382
systemstack(func() {
14021383
saveg(pc, sp, ourg, &p[0], pcbuf)
14031384
})
1404-
if labels != nil {
1405-
labels[0] = ourg.labels
1406-
}
1385+
1386+
p[0].Labels = ourg.labels
14071387
ourg.goroutineProfiled.Store(goroutineProfileSatisfied)
14081388
goroutineProfile.offset.Store(1)
14091389

@@ -1414,7 +1394,6 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14141394
// field set to goroutineProfileSatisfied.
14151395
goroutineProfile.active = true
14161396
goroutineProfile.records = p
1417-
goroutineProfile.labels = labels
14181397
startTheWorld(stw)
14191398

14201399
// Visit each goroutine that existed as of the startTheWorld call above.
@@ -1436,7 +1415,6 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
14361415
endOffset := goroutineProfile.offset.Swap(0)
14371416
goroutineProfile.active = false
14381417
goroutineProfile.records = nil
1439-
goroutineProfile.labels = nil
14401418
startTheWorld(stw)
14411419

14421420
// Restore the invariant that every goroutine struct in allgs has its
@@ -1528,7 +1506,7 @@ func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) {
15281506
// System goroutines should not appear in the profile.
15291507
// Check this here and not in tryRecordGoroutineProfile because isSystemGoroutine
15301508
// may change on a goroutine while it is executing, so while the scheduler might
1531-
// see a system goroutine, goroutineProfileWithLabelsConcurrent might not, and
1509+
// see a system goroutine, goroutineProfileInternal might not, and
15321510
// this inconsistency could cause invariants to be violated, such as trying to
15331511
// record the stack of a running goroutine below. In short, we still want system
15341512
// goroutines to participate in the same state machine on gp1.goroutineProfiled as
@@ -1556,7 +1534,7 @@ func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) {
15561534
if offset >= len(goroutineProfile.records) {
15571535
// Should be impossible, but better to return a truncated profile than
15581536
// to crash the entire process at this point. Instead, deal with it in
1559-
// goroutineProfileWithLabelsConcurrent where we have more context.
1537+
// goroutineProfileInternal where we have more context.
15601538
return
15611539
}
15621540

@@ -1570,88 +1548,7 @@ func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) {
15701548
// to avoid schedule delays.
15711549
systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &goroutineProfile.records[offset], pcbuf) })
15721550

1573-
if goroutineProfile.labels != nil {
1574-
goroutineProfile.labels[offset] = gp1.labels
1575-
}
1576-
}
1577-
1578-
func goroutineProfileWithLabelsSync(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
1579-
gp := getg()
1580-
1581-
isOK := func(gp1 *g) bool {
1582-
// Checking isSystemGoroutine here makes GoroutineProfile
1583-
// consistent with both NumGoroutine and Stack.
1584-
if gp1 == gp {
1585-
return false
1586-
}
1587-
if status := readgstatus(gp1); status == _Gdead || status == _Gdeadextra {
1588-
return false
1589-
}
1590-
if isSystemGoroutine(gp1, false) {
1591-
return false
1592-
}
1593-
return true
1594-
}
1595-
1596-
pcbuf := makeProfStack() // see saveg() for explanation
1597-
stw := stopTheWorld(stwGoroutineProfile)
1598-
1599-
// World is stopped, no locking required.
1600-
n = 1
1601-
forEachGRace(func(gp1 *g) {
1602-
if isOK(gp1) {
1603-
n++
1604-
}
1605-
})
1606-
1607-
if n <= len(p) {
1608-
ok = true
1609-
r, lbl := p, labels
1610-
1611-
// Save current goroutine.
1612-
sp := sys.GetCallerSP()
1613-
pc := sys.GetCallerPC()
1614-
systemstack(func() {
1615-
saveg(pc, sp, gp, &r[0], pcbuf)
1616-
})
1617-
r = r[1:]
1618-
1619-
// If we have a place to put our goroutine labelmap, insert it there.
1620-
if labels != nil {
1621-
lbl[0] = gp.labels
1622-
lbl = lbl[1:]
1623-
}
1624-
1625-
// Save other goroutines.
1626-
forEachGRace(func(gp1 *g) {
1627-
if !isOK(gp1) {
1628-
return
1629-
}
1630-
1631-
if len(r) == 0 {
1632-
// Should be impossible, but better to return a
1633-
// truncated profile than to crash the entire process.
1634-
return
1635-
}
1636-
// saveg calls gentraceback, which may call cgo traceback functions.
1637-
// The world is stopped, so it cannot use cgocall (which will be
1638-
// blocked at exitsyscall). Do it on the system stack so it won't
1639-
// call into the schedular (see traceback.go:cgoContextPCs).
1640-
systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &r[0], pcbuf) })
1641-
if labels != nil {
1642-
lbl[0] = gp1.labels
1643-
lbl = lbl[1:]
1644-
}
1645-
r = r[1:]
1646-
})
1647-
}
1648-
1649-
if raceenabled {
1650-
raceacquire(unsafe.Pointer(&labelSync))
1651-
}
1652-
1653-
startTheWorld(stw)
1654-
return n, ok
1551+
goroutineProfile.records[offset].Labels = gp1.labels
16551552
}
16561553

16571554
// GoroutineProfile returns n, the number of records in the active goroutine stack profile.
@@ -1661,7 +1558,7 @@ func goroutineProfileWithLabelsSync(p []profilerecord.StackRecord, labels []unsa
16611558
// Most clients should use the [runtime/pprof] package instead
16621559
// of calling GoroutineProfile directly.
16631560
func GoroutineProfile(p []StackRecord) (n int, ok bool) {
1664-
records := make([]profilerecord.StackRecord, len(p))
1561+
records := make([]profilerecord.GoroutineRecord, len(p))
16651562
n, ok = goroutineProfileInternal(records)
16661563
if !ok {
16671564
return
@@ -1673,11 +1570,7 @@ func GoroutineProfile(p []StackRecord) (n int, ok bool) {
16731570
return
16741571
}
16751572

1676-
func goroutineProfileInternal(p []profilerecord.StackRecord) (n int, ok bool) {
1677-
return goroutineProfileWithLabels(p, nil)
1678-
}
1679-
1680-
func saveg(pc, sp uintptr, gp *g, r *profilerecord.StackRecord, pcbuf []uintptr) {
1573+
func saveg(pc, sp uintptr, gp *g, r *profilerecord.GoroutineRecord, pcbuf []uintptr) {
16811574
// To reduce memory usage, we want to allocate a r.Stack that is just big
16821575
// enough to hold gp's stack trace. Naively we might achieve this by
16831576
// recording our stack trace into mp.profStack, and then allocating a

0 commit comments

Comments
 (0)