runtime/pprof: plumb labels for goroutine profiles

Goroutines are directly associated with labels. It's relatively easy to
plumb those through without creating goroutine-locals in the wild.

This is accomplished by splitting out most of the code from the public
`runtime.GoroutineProfile` into a new unexported
`runtime.goroutineProfileWithLabels`, which then has a thin wrapper
linked into the `runtime/pprof` package as
`runtime_goroutineProfileWithLabels`. (mirroring the way labels get
associated with the `g` for a goroutine in the first place)

Per-#6104, OS-thread creation profiles are a bit useless, as `M`s tend
to be created be created by a background goroutine. As such, I decided
not to add support for capturing the labels at `M`-creation-time, since
the stack-traces seem to always come out `nil` for my simple test
binaries.

This change currently provides labels for debug=0 and debug=1, as
debug=2 is currently entirely generated by the runtime package and I
don't see a clean way of getting the `labelMap` type handled properly
within the `runtime` package.

Update the comment added in cl/131275 to mention goroutine support for
labels.

Updates #23458

Change-Id: Ia4b558893d7d10156b77121cd9b70c4ccd9e1889
Reviewed-on: https://go-review.googlesource.com/c/go/+/189318
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
This commit is contained in:
David Finkel 2019-08-04 15:14:48 -04:00 committed by Hyang-Ah Hana Kim
parent 0329c915a0
commit 38c2c12bc1
5 changed files with 193 additions and 29 deletions

View File

@ -711,13 +711,16 @@ func ThreadCreateProfile(p []StackRecord) (n int, ok bool) {
return return
} }
// GoroutineProfile returns n, the number of records in the active goroutine stack profile. //go:linkname runtime_goroutineProfileWithLabels runtime/pprof.runtime_goroutineProfileWithLabels
// If len(p) >= n, GoroutineProfile copies the profile into p and returns n, true. func runtime_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
// If len(p) < n, GoroutineProfile does not change p and returns n, false. return goroutineProfileWithLabels(p, labels)
// }
// Most clients should use the runtime/pprof package instead
// of calling GoroutineProfile directly. // labels may be nil. If labels is non-nil, it must have the same length as p.
func GoroutineProfile(p []StackRecord) (n int, ok bool) { func goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) {
if labels != nil && len(labels) != len(p) {
labels = nil
}
gp := getg() gp := getg()
isOK := func(gp1 *g) bool { isOK := func(gp1 *g) bool {
@ -737,7 +740,7 @@ func GoroutineProfile(p []StackRecord) (n int, ok bool) {
if n <= len(p) { if n <= len(p) {
ok = true ok = true
r := p r, lbl := p, labels
// Save current goroutine. // Save current goroutine.
sp := getcallersp() sp := getcallersp()
@ -747,6 +750,12 @@ func GoroutineProfile(p []StackRecord) (n int, ok bool) {
}) })
r = r[1:] r = r[1:]
// If we have a place to put our goroutine labelmap, insert it there.
if labels != nil {
lbl[0] = gp.labels
lbl = lbl[1:]
}
// Save other goroutines. // Save other goroutines.
for _, gp1 := range allgs { for _, gp1 := range allgs {
if isOK(gp1) { if isOK(gp1) {
@ -756,16 +765,30 @@ func GoroutineProfile(p []StackRecord) (n int, ok bool) {
break break
} }
saveg(^uintptr(0), ^uintptr(0), gp1, &r[0]) saveg(^uintptr(0), ^uintptr(0), gp1, &r[0])
if labels != nil {
lbl[0] = gp1.labels
lbl = lbl[1:]
}
r = r[1:] r = r[1:]
} }
} }
} }
startTheWorld() startTheWorld()
return n, ok return n, ok
} }
// GoroutineProfile returns n, the number of records in the active goroutine stack profile.
// If len(p) >= n, GoroutineProfile copies the profile into p and returns n, true.
// If len(p) < n, GoroutineProfile does not change p and returns n, false.
//
// Most clients should use the runtime/pprof package instead
// of calling GoroutineProfile directly.
func GoroutineProfile(p []StackRecord) (n int, ok bool) {
return goroutineProfileWithLabels(p, nil)
}
func saveg(pc, sp uintptr, gp *g, r *StackRecord) { func saveg(pc, sp uintptr, gp *g, r *StackRecord) {
n := gentraceback(pc, sp, 0, gp, 0, &r.Stack0[0], len(r.Stack0), nil, nil, 0) n := gentraceback(pc, sp, 0, gp, 0, &r.Stack0[0], len(r.Stack0), nil, nil, 0)
if n < len(r.Stack0) { if n < len(r.Stack0) {

View File

@ -6,6 +6,9 @@ package pprof
import ( import (
"context" "context"
"fmt"
"sort"
"strings"
) )
type label struct { type label struct {
@ -34,6 +37,23 @@ func labelValue(ctx context.Context) labelMap {
// that admits incremental immutable modification more efficiently. // that admits incremental immutable modification more efficiently.
type labelMap map[string]string type labelMap map[string]string
// String statisfies Stringer and returns key, value pairs in a consistent
// order.
func (l *labelMap) String() string {
if l == nil {
return ""
}
keyVals := make([]string, 0, len(*l))
for k, v := range *l {
keyVals = append(keyVals, fmt.Sprintf("%q:%q", k, v))
}
sort.Strings(keyVals)
return "{" + strings.Join(keyVals, ", ") + "}"
}
// WithLabels returns a new context.Context with the given labels added. // WithLabels returns a new context.Context with the given labels added.
// A label overwrites a prior label with the same key. // A label overwrites a prior label with the same key.
func WithLabels(ctx context.Context, labels LabelSet) context.Context { func WithLabels(ctx context.Context, labels LabelSet) context.Context {
@ -54,7 +74,8 @@ func WithLabels(ctx context.Context, labels LabelSet) context.Context {
// Labels takes an even number of strings representing key-value pairs // Labels takes an even number of strings representing key-value pairs
// and makes a LabelSet containing them. // and makes a LabelSet containing them.
// A label overwrites a prior label with the same key. // A label overwrites a prior label with the same key.
// Currently only CPU profile utilizes labels information. // Currently only the CPU and goroutine profiles utilize any labels
// information.
// See https://golang.org/issue/23458 for details. // See https://golang.org/issue/23458 for details.
func Labels(args ...string) LabelSet { func Labels(args ...string) LabelSet {
if len(args)%2 != 0 { if len(args)%2 != 0 {

View File

@ -80,3 +80,35 @@ func TestContextLabels(t *testing.T) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
} }
} }
func TestLabelMapStringer(t *testing.T) {
for _, tbl := range []struct {
m labelMap
expected string
}{
{
m: labelMap{
// empty map
},
expected: "{}",
}, {
m: labelMap{
"foo": "bar",
},
expected: `{"foo":"bar"}`,
}, {
m: labelMap{
"foo": "bar",
"key1": "value1",
"key2": "value2",
"key3": "value3",
"key4WithNewline": "\nvalue4",
},
expected: `{"foo":"bar", "key1":"value1", "key2":"value2", "key3":"value3", "key4WithNewline":"\nvalue4"}`,
},
} {
if got := tbl.m.String(); tbl.expected != got {
t.Errorf("%#v.String() = %q; want %q", tbl.m, got, tbl.expected)
}
}
}

View File

@ -357,6 +357,7 @@ type stackProfile [][]uintptr
func (x stackProfile) Len() int { return len(x) } func (x stackProfile) Len() int { return len(x) }
func (x stackProfile) Stack(i int) []uintptr { return x[i] } func (x stackProfile) Stack(i int) []uintptr { return x[i] }
func (x stackProfile) Label(i int) *labelMap { return nil }
// A countProfile is a set of stack traces to be printed as counts // A countProfile is a set of stack traces to be printed as counts
// grouped by stack trace. There are multiple implementations: // grouped by stack trace. There are multiple implementations:
@ -365,6 +366,7 @@ func (x stackProfile) Stack(i int) []uintptr { return x[i] }
type countProfile interface { type countProfile interface {
Len() int Len() int
Stack(i int) []uintptr Stack(i int) []uintptr
Label(i int) *labelMap
} }
// printCountCycleProfile outputs block profile records (for block or mutex profiles) // printCountCycleProfile outputs block profile records (for block or mutex profiles)
@ -402,12 +404,16 @@ func printCountCycleProfile(w io.Writer, countName, cycleName string, scaler fun
func printCountProfile(w io.Writer, debug int, name string, p countProfile) error { func printCountProfile(w io.Writer, debug int, name string, p countProfile) error {
// Build count of each stack. // Build count of each stack.
var buf bytes.Buffer var buf bytes.Buffer
key := func(stk []uintptr) string { key := func(stk []uintptr, lbls *labelMap) string {
buf.Reset() buf.Reset()
fmt.Fprintf(&buf, "@") fmt.Fprintf(&buf, "@")
for _, pc := range stk { for _, pc := range stk {
fmt.Fprintf(&buf, " %#x", pc) fmt.Fprintf(&buf, " %#x", pc)
} }
if lbls != nil {
buf.WriteString("\n# labels: ")
buf.WriteString(lbls.String())
}
return buf.String() return buf.String()
} }
count := map[string]int{} count := map[string]int{}
@ -415,7 +421,7 @@ func printCountProfile(w io.Writer, debug int, name string, p countProfile) erro
var keys []string var keys []string
n := p.Len() n := p.Len()
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
k := key(p.Stack(i)) k := key(p.Stack(i), p.Label(i))
if count[k] == 0 { if count[k] == 0 {
index[k] = i index[k] = i
keys = append(keys, k) keys = append(keys, k)
@ -449,7 +455,16 @@ func printCountProfile(w io.Writer, debug int, name string, p countProfile) erro
// For count profiles, all stack addresses are // For count profiles, all stack addresses are
// return PCs, which is what appendLocsForStack expects. // return PCs, which is what appendLocsForStack expects.
locs = b.appendLocsForStack(locs[:0], p.Stack(index[k])) locs = b.appendLocsForStack(locs[:0], p.Stack(index[k]))
b.pbSample(values, locs, nil) idx := index[k]
var labels func()
if p.Label(idx) != nil {
labels = func() {
for k, v := range *p.Label(idx) {
b.pbLabel(tagSample_Label, k, v, 0)
}
}
}
b.pbSample(values, locs, labels)
} }
b.build() b.build()
return nil return nil
@ -645,7 +660,12 @@ func countThreadCreate() int {
// writeThreadCreate writes the current runtime ThreadCreateProfile to w. // writeThreadCreate writes the current runtime ThreadCreateProfile to w.
func writeThreadCreate(w io.Writer, debug int) error { func writeThreadCreate(w io.Writer, debug int) error {
return writeRuntimeProfile(w, debug, "threadcreate", runtime.ThreadCreateProfile) // Until https://golang.org/issues/6104 is addressed, wrap
// ThreadCreateProfile because there's no point in tracking labels when we
// don't get any stack-traces.
return writeRuntimeProfile(w, debug, "threadcreate", func(p []runtime.StackRecord, _ []unsafe.Pointer) (n int, ok bool) {
return runtime.ThreadCreateProfile(p)
})
} }
// countGoroutine returns the number of goroutines. // countGoroutine returns the number of goroutines.
@ -653,12 +673,15 @@ func countGoroutine() int {
return runtime.NumGoroutine() return runtime.NumGoroutine()
} }
// runtime_goroutineProfileWithLabels is defined in runtime/mprof.go
func runtime_goroutineProfileWithLabels(p []runtime.StackRecord, labels []unsafe.Pointer) (n int, ok bool)
// writeGoroutine writes the current runtime GoroutineProfile to w. // writeGoroutine writes the current runtime GoroutineProfile to w.
func writeGoroutine(w io.Writer, debug int) error { func writeGoroutine(w io.Writer, debug int) error {
if debug >= 2 { if debug >= 2 {
return writeGoroutineStacks(w) return writeGoroutineStacks(w)
} }
return writeRuntimeProfile(w, debug, "goroutine", runtime.GoroutineProfile) return writeRuntimeProfile(w, debug, "goroutine", runtime_goroutineProfileWithLabels)
} }
func writeGoroutineStacks(w io.Writer) error { func writeGoroutineStacks(w io.Writer) error {
@ -682,7 +705,7 @@ func writeGoroutineStacks(w io.Writer) error {
return err return err
} }
func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord) (int, bool)) error { func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord, []unsafe.Pointer) (int, bool)) error {
// Find out how many records there are (fetch(nil)), // Find out how many records there are (fetch(nil)),
// allocate that many records, and get the data. // allocate that many records, and get the data.
// There's a race—more records might be added between // There's a race—more records might be added between
@ -690,13 +713,15 @@ func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runti
// and also try again if we're very unlucky. // and also try again if we're very unlucky.
// The loop should only execute one iteration in the common case. // The loop should only execute one iteration in the common case.
var p []runtime.StackRecord var p []runtime.StackRecord
n, ok := fetch(nil) var labels []unsafe.Pointer
n, ok := fetch(nil, nil)
for { for {
// Allocate room for a slightly bigger profile, // Allocate room for a slightly bigger profile,
// in case a few more entries have been added // in case a few more entries have been added
// since the call to ThreadProfile. // since the call to ThreadProfile.
p = make([]runtime.StackRecord, n+10) p = make([]runtime.StackRecord, n+10)
n, ok = fetch(p) labels = make([]unsafe.Pointer, n+10)
n, ok = fetch(p, labels)
if ok { if ok {
p = p[0:n] p = p[0:n]
break break
@ -704,13 +729,17 @@ func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runti
// Profile grew; try again. // Profile grew; try again.
} }
return printCountProfile(w, debug, name, runtimeProfile(p)) return printCountProfile(w, debug, name, &runtimeProfile{p, labels})
} }
type runtimeProfile []runtime.StackRecord type runtimeProfile struct {
stk []runtime.StackRecord
labels []unsafe.Pointer
}
func (p runtimeProfile) Len() int { return len(p) } func (p *runtimeProfile) Len() int { return len(p.stk) }
func (p runtimeProfile) Stack(i int) []uintptr { return p[i].Stack() } func (p *runtimeProfile) Stack(i int) []uintptr { return p.stk[i].Stack() }
func (p *runtimeProfile) Label(i int) *labelMap { return (*labelMap)(p.labels[i]) }
var cpu struct { var cpu struct {
sync.Mutex sync.Mutex

View File

@ -977,6 +977,26 @@ func TestGoroutineCounts(t *testing.T) {
runtime.Gosched() runtime.Gosched()
} }
} }
ctx := context.Background()
// ... and again, with labels this time (just with fewer iterations to keep
// sorting deterministic).
Do(ctx, Labels("label", "value"), func(context.Context) {
for i := 0; i < 89; i++ {
switch {
case i%10 == 0:
go func1(c)
case i%2 == 0:
go func2(c)
default:
go func3(c)
}
// Let goroutines block on channel
for j := 0; j < 5; j++ {
runtime.Gosched()
}
}
})
var w bytes.Buffer var w bytes.Buffer
goroutineProf := Lookup("goroutine") goroutineProf := Lookup("goroutine")
@ -985,8 +1005,11 @@ func TestGoroutineCounts(t *testing.T) {
goroutineProf.WriteTo(&w, 1) goroutineProf.WriteTo(&w, 1)
prof := w.String() prof := w.String()
if !containsInOrder(prof, "\n50 @ ", "\n40 @", "\n10 @", "\n1 @") { labels := labelMap{"label": "value"}
t.Errorf("expected sorted goroutine counts:\n%s", prof) labelStr := "\n# labels: " + labels.String()
if !containsInOrder(prof, "\n50 @ ", "\n44 @", labelStr,
"\n40 @", "\n36 @", labelStr, "\n10 @", "\n9 @", labelStr, "\n1 @") {
t.Errorf("expected sorted goroutine counts with Labels:\n%s", prof)
} }
// Check proto profile // Check proto profile
@ -999,9 +1022,18 @@ func TestGoroutineCounts(t *testing.T) {
if err := p.CheckValid(); err != nil { if err := p.CheckValid(); err != nil {
t.Errorf("protobuf profile is invalid: %v", err) t.Errorf("protobuf profile is invalid: %v", err)
} }
if !containsCounts(p, []int64{50, 40, 10, 1}) { expectedLabels := map[int64]map[string]string{
t.Errorf("expected count profile to contain goroutines with counts %v, got %v", 50: map[string]string{},
[]int64{50, 40, 10, 1}, p) 44: map[string]string{"label": "value"},
40: map[string]string{},
36: map[string]string{"label": "value"},
10: map[string]string{},
9: map[string]string{"label": "value"},
1: map[string]string{},
}
if !containsCountsLabels(p, expectedLabels) {
t.Errorf("expected count profile to contain goroutines with counts and labels %v, got %v",
expectedLabels, p)
} }
close(c) close(c)
@ -1020,10 +1052,23 @@ func containsInOrder(s string, all ...string) bool {
return true return true
} }
func containsCounts(prof *profile.Profile, counts []int64) bool { func containsCountsLabels(prof *profile.Profile, countLabels map[int64]map[string]string) bool {
m := make(map[int64]int) m := make(map[int64]int)
for _, c := range counts { type nkey struct {
count int64
key, val string
}
n := make(map[nkey]int)
for c, kv := range countLabels {
m[c]++ m[c]++
for k, v := range kv {
n[nkey{
count: c,
key: k,
val: v,
}]++
}
} }
for _, s := range prof.Sample { for _, s := range prof.Sample {
// The count is the single value in the sample // The count is the single value in the sample
@ -1031,12 +1076,26 @@ func containsCounts(prof *profile.Profile, counts []int64) bool {
return false return false
} }
m[s.Value[0]]-- m[s.Value[0]]--
for k, vs := range s.Label {
for _, v := range vs {
n[nkey{
count: s.Value[0],
key: k,
val: v,
}]--
}
}
} }
for _, n := range m { for _, n := range m {
if n > 0 { if n > 0 {
return false return false
} }
} }
for _, ncnt := range n {
if ncnt != 0 {
return false
}
}
return true return true
} }