From: David Finkel Date: Fri, 23 May 2025 20:04:08 +0000 (-0400) Subject: runtime: add GODEBUG=tracebacklabels=1 to include pprof labels in tracebacks X-Git-Tag: go1.26rc1~137 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=6851795fb6cda61e2c8396c36da187a2bd87b29e;p=gostls13.git runtime: add GODEBUG=tracebacklabels=1 to include pprof labels in tracebacks Copy LabelSet to an internal package as label.Set, and include (escaped) labels within goroutine stack dumps. Labels are added to the goroutine header as quoted key:value pairs, so the line may get long if there are a lot of labels. To handle escaping, we add a printescaped function to the runtime and hook it up to the print function in the compiler with a new runtime.quoted type that's a sibling to runtime.hex. (in fact, we leverage some of the machinery from printhex to generate escape sequences). The escaping can be improved for printable runes outside basic ASCII (particularly for languages using non-latin stripts). Additionally, invalid UTF-8 can be improved. So we can experiment with the output format make this opt-in via a a new tracebacklabels GODEBUG var. Updates #23458 Updates #76349 Change-Id: I08e78a40c55839a809236fff593ef2090c13c036 Reviewed-on: https://go-review.googlesource.com/c/go/+/694119 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt Auto-Submit: Michael Pratt Reviewed-by: Alan Donovan --- diff --git a/doc/godebug.md b/doc/godebug.md index d9ae462b98..0d1cd6b662 100644 --- a/doc/godebug.md +++ b/doc/godebug.md @@ -168,6 +168,12 @@ allows malformed hostnames containing colons outside of a bracketed IPv6 address The default `urlstrictcolons=1` rejects URLs such as `http://localhost:1:2` or `http://::1/`. Colons are permitted as part of a bracketed IPv6 address, such as `http://[::1]/`. +Go 1.26 added a new `tracebacklabels` setting that controls the inclusion of +goroutine labels set through the the `runtime/pprof` package. Setting `tracebacklabels=1` +includes these key/value pairs in the goroutine status header of runtime +tracebacks and debug=2 runtime/pprof stack dumps. This format may change in the future. +(see go.dev/issue/76349) + ### Go 1.25 Go 1.25 added a new `decoratemappings` setting that controls whether the Go diff --git a/src/cmd/compile/internal/typecheck/_builtin/runtime.go b/src/cmd/compile/internal/typecheck/_builtin/runtime.go index d43a9e5bf2..35fbbb6b12 100644 --- a/src/cmd/compile/internal/typecheck/_builtin/runtime.go +++ b/src/cmd/compile/internal/typecheck/_builtin/runtime.go @@ -57,6 +57,7 @@ func printuint(uint64) func printcomplex128(complex128) func printcomplex64(complex64) func printstring(string) +func printquoted(string) func printpointer(any) func printuintptr(uintptr) func printiface(any) diff --git a/src/cmd/compile/internal/typecheck/builtin.go b/src/cmd/compile/internal/typecheck/builtin.go index dd9f1593f3..8a505073f7 100644 --- a/src/cmd/compile/internal/typecheck/builtin.go +++ b/src/cmd/compile/internal/typecheck/builtin.go @@ -64,6 +64,7 @@ var runtimeDecls = [...]struct { {"printcomplex128", funcTag, 27}, {"printcomplex64", funcTag, 29}, {"printstring", funcTag, 31}, + {"printquoted", funcTag, 31}, {"printpointer", funcTag, 32}, {"printuintptr", funcTag, 33}, {"printiface", funcTag, 32}, diff --git a/src/cmd/compile/internal/walk/builtin.go b/src/cmd/compile/internal/walk/builtin.go index 2f2a2c62f1..c698caddce 100644 --- a/src/cmd/compile/internal/walk/builtin.go +++ b/src/cmd/compile/internal/walk/builtin.go @@ -729,13 +729,18 @@ func walkPrint(nn *ir.CallExpr, init *ir.Nodes) ir.Node { if ir.IsConst(n, constant.String) { cs = ir.StringVal(n) } - switch cs { - case " ": - on = typecheck.LookupRuntime("printsp") - case "\n": - on = typecheck.LookupRuntime("printnl") - default: - on = typecheck.LookupRuntime("printstring") + // Print values of the named type `quoted` using printquoted. + if types.RuntimeSymName(n.Type().Sym()) == "quoted" { + on = typecheck.LookupRuntime("printquoted") + } else { + switch cs { + case " ": + on = typecheck.LookupRuntime("printsp") + case "\n": + on = typecheck.LookupRuntime("printnl") + default: + on = typecheck.LookupRuntime("printstring") + } } default: badtype(ir.OPRINT, n.Type(), nil) diff --git a/src/cmd/internal/goobj/builtinlist.go b/src/cmd/internal/goobj/builtinlist.go index b3320808f1..918ade191d 100644 --- a/src/cmd/internal/goobj/builtinlist.go +++ b/src/cmd/internal/goobj/builtinlist.go @@ -43,6 +43,7 @@ var builtins = [...]struct { {"runtime.printcomplex128", 1}, {"runtime.printcomplex64", 1}, {"runtime.printstring", 1}, + {"runtime.printquoted", 1}, {"runtime.printpointer", 1}, {"runtime.printuintptr", 1}, {"runtime.printiface", 1}, diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index 2ee5114fd7..9a6b86b65c 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -58,6 +58,7 @@ var depsRules = ` internal/nettrace, internal/platform, internal/profilerecord, + internal/runtime/pprof/label, internal/syslist, internal/trace/tracev2, internal/trace/traceviewer/format, @@ -85,6 +86,7 @@ var depsRules = ` internal/goos, internal/itoa, internal/profilerecord, + internal/runtime/pprof/label, internal/strconv, internal/trace/tracev2, math/bits, @@ -672,7 +674,8 @@ var depsRules = ` < net/http/fcgi; # Profiling - FMT, compress/gzip, encoding/binary, sort, text/tabwriter + internal/runtime/pprof/label, runtime, context < internal/runtime/pprof; + FMT, compress/gzip, encoding/binary, sort, text/tabwriter, internal/runtime/pprof, internal/runtime/pprof/label < runtime/pprof; OS, compress/gzip, internal/lazyregexp diff --git a/src/internal/runtime/pprof/label/labelset.go b/src/internal/runtime/pprof/label/labelset.go new file mode 100644 index 0000000000..d3046d407c --- /dev/null +++ b/src/internal/runtime/pprof/label/labelset.go @@ -0,0 +1,25 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package label provides common declarations used by both the [runtime] and [runtime/pprof] packages. +// The [Set] type is used for goroutine labels, and is duplicated as +// [runtime/pprof.LabelSet]. The type is duplicated due to go.dev/issue/65437 +// preventing the use of a type-alias in an existing public interface. +package label + +// Label is a key/value pair of strings. +type Label struct { + Key string + Value string +} + +// Set is a set of labels. +type Set struct { + List []Label +} + +// NewSet constructs a LabelSet that wraps the provided labels. +func NewSet(list []Label) Set { + return Set{List: list} +} diff --git a/src/runtime/export_test.go b/src/runtime/export_test.go index 6e0360aaca..26341c4300 100644 --- a/src/runtime/export_test.go +++ b/src/runtime/export_test.go @@ -2064,3 +2064,15 @@ func HexdumpWords(p, bytes uintptr) string { } return string(buf[:n]) } + +// DumpPrintQuoted provides access to print(quoted()) for the tests in +// runtime/print_quoted_test.go, allowing us to test that implementation. +func DumpPrintQuoted(s string) string { + gp := getg() + gp.writebuf = make([]byte, 0, 1<<20) + print(quoted(s)) + buf := gp.writebuf + gp.writebuf = nil + + return string(buf) +} diff --git a/src/runtime/pprof/label.go b/src/runtime/pprof/label.go index 4c1d8d38ce..09dd1de651 100644 --- a/src/runtime/pprof/label.go +++ b/src/runtime/pprof/label.go @@ -7,18 +7,14 @@ package pprof import ( "context" "fmt" + "internal/runtime/pprof/label" "slices" "strings" ) -type label struct { - key string - value string -} - // LabelSet is a set of labels. type LabelSet struct { - list []label + list []label.Label } // labelContextKey is the type of contextKeys used for profiler labels. @@ -36,7 +32,7 @@ func labelValue(ctx context.Context) labelMap { // This is an initial implementation, but it will be replaced with something // that admits incremental immutable modification more efficiently. type labelMap struct { - LabelSet + label.Set } // String satisfies Stringer and returns key, value pairs in a consistent @@ -45,10 +41,10 @@ func (l *labelMap) String() string { if l == nil { return "" } - keyVals := make([]string, 0, len(l.list)) + keyVals := make([]string, 0, len(l.Set.List)) - for _, lbl := range l.list { - keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.key, lbl.value)) + for _, lbl := range l.Set.List { + keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.Key, lbl.Value)) } slices.Sort(keyVals) @@ -59,38 +55,39 @@ func (l *labelMap) String() string { // A label overwrites a prior label with the same key. func WithLabels(ctx context.Context, labels LabelSet) context.Context { parentLabels := labelValue(ctx) - return context.WithValue(ctx, labelContextKey{}, &labelMap{mergeLabelSets(parentLabels.LabelSet, labels)}) + return context.WithValue(ctx, labelContextKey{}, &labelMap{mergeLabelSets(parentLabels.Set, labels)}) } -func mergeLabelSets(left, right LabelSet) LabelSet { - if len(left.list) == 0 { - return right +func mergeLabelSets(left label.Set, right LabelSet) label.Set { + if len(left.List) == 0 { + return label.NewSet(right.list) } else if len(right.list) == 0 { return left } + lList, rList := left.List, right.list l, r := 0, 0 - result := make([]label, 0, len(right.list)) - for l < len(left.list) && r < len(right.list) { - switch strings.Compare(left.list[l].key, right.list[r].key) { + result := make([]label.Label, 0, len(rList)) + for l < len(lList) && r < len(rList) { + switch strings.Compare(lList[l].Key, rList[r].Key) { case -1: // left key < right key - result = append(result, left.list[l]) + result = append(result, lList[l]) l++ case 1: // right key < left key - result = append(result, right.list[r]) + result = append(result, rList[r]) r++ case 0: // keys are equal, right value overwrites left value - result = append(result, right.list[r]) + result = append(result, rList[r]) l++ r++ } } // Append the remaining elements - result = append(result, left.list[l:]...) - result = append(result, right.list[r:]...) + result = append(result, lList[l:]...) + result = append(result, rList[r:]...) - return LabelSet{list: result} + return label.NewSet(result) } // Labels takes an even number of strings representing key-value pairs @@ -103,20 +100,20 @@ func Labels(args ...string) LabelSet { if len(args)%2 != 0 { panic("uneven number of arguments to pprof.Labels") } - list := make([]label, 0, len(args)/2) + list := make([]label.Label, 0, len(args)/2) sortedNoDupes := true for i := 0; i+1 < len(args); i += 2 { - list = append(list, label{key: args[i], value: args[i+1]}) + list = append(list, label.Label{Key: args[i], Value: args[i+1]}) sortedNoDupes = sortedNoDupes && (i < 2 || args[i] > args[i-2]) } if !sortedNoDupes { // slow path: keys are unsorted, contain duplicates, or both - slices.SortStableFunc(list, func(a, b label) int { - return strings.Compare(a.key, b.key) + slices.SortStableFunc(list, func(a, b label.Label) int { + return strings.Compare(a.Key, b.Key) }) - deduped := make([]label, 0, len(list)) + deduped := make([]label.Label, 0, len(list)) for i, lbl := range list { - if i == 0 || lbl.key != list[i-1].key { + if i == 0 || lbl.Key != list[i-1].Key { deduped = append(deduped, lbl) } else { deduped[len(deduped)-1] = lbl @@ -131,9 +128,9 @@ func Labels(args ...string) LabelSet { // whether that label exists. func Label(ctx context.Context, key string) (string, bool) { ctxLabels := labelValue(ctx) - for _, lbl := range ctxLabels.list { - if lbl.key == key { - return lbl.value, true + for _, lbl := range ctxLabels.Set.List { + if lbl.Key == key { + return lbl.Value, true } } return "", false @@ -143,8 +140,8 @@ func Label(ctx context.Context, key string) (string, bool) { // The function f should return true to continue iteration or false to stop iteration early. func ForLabels(ctx context.Context, f func(key, value string) bool) { ctxLabels := labelValue(ctx) - for _, lbl := range ctxLabels.list { - if !f(lbl.key, lbl.value) { + for _, lbl := range ctxLabels.Set.List { + if !f(lbl.Key, lbl.Value) { break } } diff --git a/src/runtime/pprof/label_test.go b/src/runtime/pprof/label_test.go index 3018693c24..ded8b29575 100644 --- a/src/runtime/pprof/label_test.go +++ b/src/runtime/pprof/label_test.go @@ -7,19 +7,20 @@ package pprof import ( "context" "fmt" + "internal/runtime/pprof/label" "reflect" "slices" "strings" "testing" ) -func labelsSorted(ctx context.Context) []label { - ls := []label{} +func labelsSorted(ctx context.Context) []label.Label { + ls := []label.Label{} ForLabels(ctx, func(key, value string) bool { - ls = append(ls, label{key, value}) + ls = append(ls, label.Label{Key: key, Value: value}) return true }) - slices.SortFunc(ls, func(a, b label) int { return strings.Compare(a.key, b.key) }) + slices.SortFunc(ls, func(a, b label.Label) int { return strings.Compare(a.Key, b.Key) }) return ls } @@ -39,7 +40,7 @@ func TestContextLabels(t *testing.T) { t.Errorf(`Label(ctx, "key"): got %v, %v; want "value", ok`, v, ok) } gotLabels := labelsSorted(ctx) - wantLabels := []label{{"key", "value"}} + wantLabels := []label.Label{{Key: "key", Value: "value"}} if !reflect.DeepEqual(gotLabels, wantLabels) { t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) } @@ -51,7 +52,7 @@ func TestContextLabels(t *testing.T) { t.Errorf(`Label(ctx, "key2"): got %v, %v; want "value2", ok`, v, ok) } gotLabels = labelsSorted(ctx) - wantLabels = []label{{"key", "value"}, {"key2", "value2"}} + wantLabels = []label.Label{{Key: "key", Value: "value"}, {Key: "key2", Value: "value2"}} if !reflect.DeepEqual(gotLabels, wantLabels) { t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) } @@ -63,7 +64,7 @@ func TestContextLabels(t *testing.T) { t.Errorf(`Label(ctx, "key3"): got %v, %v; want "value3", ok`, v, ok) } gotLabels = labelsSorted(ctx) - wantLabels = []label{{"key", "value3"}, {"key2", "value2"}} + wantLabels = []label.Label{{Key: "key", Value: "value3"}, {Key: "key2", Value: "value2"}} if !reflect.DeepEqual(gotLabels, wantLabels) { t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) } @@ -75,7 +76,7 @@ func TestContextLabels(t *testing.T) { t.Errorf(`Label(ctx, "key4"): got %v, %v; want "value4b", ok`, v, ok) } gotLabels = labelsSorted(ctx) - wantLabels = []label{{"key", "value3"}, {"key2", "value2"}, {"key4", "value4b"}} + wantLabels = []label.Label{{Key: "key", Value: "value3"}, {Key: "key2", Value: "value2"}, {Key: "key4", Value: "value4b"}} if !reflect.DeepEqual(gotLabels, wantLabels) { t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) } @@ -93,18 +94,18 @@ func TestLabelMapStringer(t *testing.T) { expected: "{}", }, { m: labelMap{ - Labels("foo", "bar"), + label.NewSet(Labels("foo", "bar").list), }, expected: `{"foo":"bar"}`, }, { m: labelMap{ - Labels( + label.NewSet(Labels( "foo", "bar", "key1", "value1", "key2", "value2", "key3", "value3", "key4WithNewline", "\nvalue4", - ), + ).list), }, expected: `{"foo":"bar", "key1":"value1", "key2":"value2", "key3":"value3", "key4WithNewline":"\nvalue4"}`, }, diff --git a/src/runtime/pprof/pprof.go b/src/runtime/pprof/pprof.go index c617a8b26a..c27df22897 100644 --- a/src/runtime/pprof/pprof.go +++ b/src/runtime/pprof/pprof.go @@ -547,8 +547,8 @@ func printCountProfile(w io.Writer, debug int, name string, p countProfile) erro var labels func() if p.Label(idx) != nil { labels = func() { - for _, lbl := range p.Label(idx).list { - b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0) + for _, lbl := range p.Label(idx).Set.List { + b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0) } } } diff --git a/src/runtime/pprof/pprof_test.go b/src/runtime/pprof/pprof_test.go index 4c9279c5a6..e46e4f9d27 100644 --- a/src/runtime/pprof/pprof_test.go +++ b/src/runtime/pprof/pprof_test.go @@ -12,6 +12,7 @@ import ( "fmt" "internal/abi" "internal/profile" + "internal/runtime/pprof/label" "internal/syscall/unix" "internal/testenv" "io" @@ -1462,11 +1463,11 @@ func TestGoroutineCounts(t *testing.T) { goroutineProf.WriteTo(&w, 1) prof := w.String() - labels := labelMap{Labels("label", "value")} + labels := labelMap{label.NewSet(Labels("label", "value").list)} labelStr := "\n# labels: " + labels.String() - selfLabel := labelMap{Labels("self-label", "self-value")} + selfLabel := labelMap{label.NewSet(Labels("self-label", "self-value").list)} selfLabelStr := "\n# labels: " + selfLabel.String() - fingLabel := labelMap{Labels("fing-label", "fing-value")} + fingLabel := labelMap{label.NewSet(Labels("fing-label", "fing-value").list)} fingLabelStr := "\n# labels: " + fingLabel.String() orderedPrefix := []string{ "\n50 @ ", diff --git a/src/runtime/pprof/proto.go b/src/runtime/pprof/proto.go index 28ceb81542..5ad917f14a 100644 --- a/src/runtime/pprof/proto.go +++ b/src/runtime/pprof/proto.go @@ -367,8 +367,8 @@ func (b *profileBuilder) build() error { var labels func() if e.tag != nil { labels = func() { - for _, lbl := range (*labelMap)(e.tag).list { - b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0) + for _, lbl := range (*labelMap)(e.tag).Set.List { + b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0) } } } diff --git a/src/runtime/pprof/runtime_test.go b/src/runtime/pprof/runtime_test.go index 353ed8a3f1..acdd4c8d15 100644 --- a/src/runtime/pprof/runtime_test.go +++ b/src/runtime/pprof/runtime_test.go @@ -92,9 +92,10 @@ func getProfLabel() map[string]string { if l == nil { return map[string]string{} } - m := make(map[string]string, len(l.list)) - for _, lbl := range l.list { - m[lbl.key] = lbl.value + ls := l.Set.List + m := make(map[string]string, len(ls)) + for _, lbl := range ls { + m[lbl.Key] = lbl.Value } return m } diff --git a/src/runtime/print.go b/src/runtime/print.go index d2733fb266..5d1bc22809 100644 --- a/src/runtime/print.go +++ b/src/runtime/print.go @@ -13,6 +13,10 @@ import ( // should use printhex instead of printuint (decimal). type hex uint64 +// The compiler knows that a print of a value of this type should use +// printquoted instead of printstring. +type quoted string + func bytes(s string) (ret []byte) { rp := (*slice)(unsafe.Pointer(&ret)) sp := stringStructOf(&s) @@ -169,24 +173,67 @@ func printint(v int64) { var minhexdigits = 0 // protected by printlock -func printhex(v uint64) { +func printhexopts(include0x bool, mindigits int, v uint64) { const dig = "0123456789abcdef" var buf [100]byte i := len(buf) for i--; i > 0; i-- { buf[i] = dig[v%16] - if v < 16 && len(buf)-i >= minhexdigits { + if v < 16 && len(buf)-i >= mindigits { break } v /= 16 } - i-- - buf[i] = 'x' - i-- - buf[i] = '0' + if include0x { + i-- + buf[i] = 'x' + i-- + buf[i] = '0' + } gwrite(buf[i:]) } +func printhex(v uint64) { + printhexopts(true, minhexdigits, v) +} + +func printquoted(s string) { + printlock() + gwrite([]byte(`"`)) + for _, r := range s { + switch r { + case '\n': + gwrite([]byte(`\n`)) + continue + case '\r': + gwrite([]byte(`\r`)) + continue + case '\t': + gwrite([]byte(`\t`)) + print() + continue + case '\\', '"': + gwrite([]byte{byte('\\'), byte(r)}) + continue + } + // For now, only allow basic printable ascii through unescaped + if r >= ' ' && r <= '~' { + gwrite([]byte{byte(r)}) + } else if r < 127 { + gwrite(bytes(`\x`)) + printhexopts(false, 2, uint64(r)) + } else if r < 0x1_0000 { + gwrite(bytes(`\u`)) + printhexopts(false, 4, uint64(r)) + } else { + gwrite(bytes(`\U`)) + printhexopts(false, 8, uint64(r)) + } + } + gwrite([]byte{byte('"')}) + printunlock() +} + func printpointer(p unsafe.Pointer) { printhex(uint64(uintptr(p))) } diff --git a/src/runtime/print_quoted_test.go b/src/runtime/print_quoted_test.go new file mode 100644 index 0000000000..f9e947b569 --- /dev/null +++ b/src/runtime/print_quoted_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package runtime_test + +import ( + "runtime" + "testing" +) + +func TestPrintQuoted(t *testing.T) { + for _, tbl := range []struct { + in, expected string + }{ + {in: "baz", expected: `"baz"`}, + {in: "foobar", expected: `"foobar"`}, + // make sure newlines get escaped + {in: "baz\n", expected: `"baz\n"`}, + // make sure null and escape bytes are properly escaped + {in: "b\033it", expected: `"b\x1bit"`}, + {in: "b\000ar", expected: `"b\x00ar"`}, + // verify that simple 16-bit unicode runes are escaped with \u, including a greek upper-case sigma and an arbitrary unicode character. + {in: "\u1234Σ", expected: `"\u1234\u03a3"`}, + // verify that 32-bit unicode runes are escaped with \U along with tabs + {in: "fizz\tle", expected: `"fizz\tle"`}, + {in: "\U00045678boop", expected: `"\U00045678boop"`}, + // verify carriage returns and backslashes get escaped along with our nulls, newlines and a 32-bit unicode character + {in: "fiz\\zl\re", expected: `"fiz\\zl\re"`}, + } { + t.Run(tbl.in, func(t *testing.T) { + out := runtime.DumpPrintQuoted(tbl.in) + if out != tbl.expected { + t.Errorf("unexpected output for print(escaped(%q));\n got: %s\nwant: %s", tbl.in, out, tbl.expected) + } + }) + } +} diff --git a/src/runtime/runtime1.go b/src/runtime/runtime1.go index 64ee4c8d2e..965ff8ab51 100644 --- a/src/runtime/runtime1.go +++ b/src/runtime/runtime1.go @@ -360,6 +360,10 @@ var debug struct { // but allowing it is convenient for testing and for programs // that do an os.Setenv in main.init or main.main. asynctimerchan atomic.Int32 + + // tracebacklabels controls the inclusion of goroutine labels in the + // goroutine status header line. + tracebacklabels atomic.Int32 } var dbgvars = []*dbgVar{ @@ -394,6 +398,7 @@ var dbgvars = []*dbgVar{ {name: "traceallocfree", atomic: &debug.traceallocfree}, {name: "tracecheckstackownership", value: &debug.traceCheckStackOwnership}, {name: "tracebackancestors", value: &debug.tracebackancestors}, + {name: "tracebacklabels", atomic: &debug.tracebacklabels, def: 0}, {name: "tracefpunwindoff", value: &debug.tracefpunwindoff}, {name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1}, } diff --git a/src/runtime/traceback.go b/src/runtime/traceback.go index 74aaeba876..1c6f24c033 100644 --- a/src/runtime/traceback.go +++ b/src/runtime/traceback.go @@ -8,6 +8,7 @@ import ( "internal/abi" "internal/bytealg" "internal/goarch" + "internal/runtime/pprof/label" "internal/runtime/sys" "internal/stringslite" "unsafe" @@ -1270,6 +1271,19 @@ func goroutineheader(gp *g) { if bubble := gp.bubble; bubble != nil { print(", synctest bubble ", bubble.id) } + if gp.labels != nil && debug.tracebacklabels.Load() == 1 { + labels := (*label.Set)(gp.labels).List + if len(labels) > 0 { + print(" labels:{") + for i, kv := range labels { + print(quoted(kv.Key), ": ", quoted(kv.Value)) + if i < len(labels)-1 { + print(", ") + } + } + print("}") + } + } print("]:\n") } diff --git a/src/runtime/traceback_test.go b/src/runtime/traceback_test.go index 1dac91311c..d47f4ab745 100644 --- a/src/runtime/traceback_test.go +++ b/src/runtime/traceback_test.go @@ -6,6 +6,7 @@ package runtime_test import ( "bytes" + "context" "fmt" "internal/abi" "internal/asan" @@ -15,6 +16,7 @@ import ( "regexp" "runtime" "runtime/debug" + "runtime/pprof" "strconv" "strings" "sync" @@ -882,3 +884,71 @@ func TestSetCgoTracebackNoCgo(t *testing.T) { t.Fatalf("want %s, got %s\n", want, output) } } + +func TestTracebackGoroutineLabels(t *testing.T) { + t.Setenv("GODEBUG", "tracebacklabels=1") + for _, tbl := range []struct { + l pprof.LabelSet + expTB string + }{ + {l: pprof.Labels("foobar", "baz"), expTB: `{"foobar": "baz"}`}, + // Make sure the keys are sorted because the runtime/pprof package sorts for consistency + {l: pprof.Labels("foobar", "baz", "fizzle", "bit"), expTB: `{"fizzle": "bit", "foobar": "baz"}`}, + // make sure newlines get escaped + {l: pprof.Labels("fizzle", "bit", "foobar", "baz\n"), expTB: `{"fizzle": "bit", "foobar": "baz\n"}`}, + // make sure null and escape bytes are properly escaped + {l: pprof.Labels("fizzle", "b\033it", "foo\"ba\x00r", "baz\n"), expTB: `{"fizzle": "b\x1bit", "foo\"ba\x00r": "baz\n"}`}, + // verify that simple 16-bit unicode runes are escaped with \u, including a greek upper-case sigma and an arbitrary unicode character. + {l: pprof.Labels("fizzle", "\u1234Σ", "fooba\x00r", "baz\n"), expTB: `{"fizzle": "\u1234\u03a3", "fooba\x00r": "baz\n"}`}, + // verify that 32-bit unicode runes are escaped with \U along with tabs + {l: pprof.Labels("fizz\tle", "\U00045678boop", "fooba\x00r", "baz\n"), expTB: `{"fizz\tle": "\U00045678boop", "fooba\x00r": "baz\n"}`}, + // verify carriage returns and backslashes get escaped along with our nulls, newlines and a 32-bit unicode character + {l: pprof.Labels("fiz\\zl\re", "\U00045678boop", "fooba\x00r", "baz\n"), expTB: `{"fiz\\zl\re": "\U00045678boop", "fooba\x00r": "baz\n"}`}, + } { + t.Run(tbl.expTB, func(t *testing.T) { + verifyLabels := func() { + t.Helper() + buf := make([]byte, 1<<10) + // We collect the stack only for this goroutine (by passing + // false to runtime.Stack). We expect to see the parent's goroutine labels in the traceback. + stack := string(buf[:runtime.Stack(buf, false)]) + if !strings.Contains(stack, "labels:"+tbl.expTB) { + t.Errorf("failed to find goroutine labels with labels %s (as %s) got:\n%s\n---", tbl.l, tbl.expTB, stack) + } + } + // Use a clean context so the testing package can add whatever goroutine labels it wants to the testing.T context. + lblCtx := pprof.WithLabels(context.Background(), tbl.l) + pprof.SetGoroutineLabels(lblCtx) + var wg sync.WaitGroup + // make sure the labels are visible in a child goroutine + wg.Go(verifyLabels) + // and in this parent goroutine + verifyLabels() + wg.Wait() + }) + } +} + +func TestTracebackGoroutineLabelsDisabledGODEBUG(t *testing.T) { + t.Setenv("GODEBUG", "tracebacklabels=0") + lbls := pprof.Labels("foobar", "baz") + verifyLabels := func() { + t.Helper() + buf := make([]byte, 1<<10) + // We collect the stack only for this goroutine (by passing + // false to runtime.Stack). + stack := string(buf[:runtime.Stack(buf, false)]) + if strings.Contains(stack, "labels:") { + t.Errorf("found goroutine labels with labels %s got:\n%s\n---", lbls, stack) + } + } + // Use a clean context so the testing package can add whatever goroutine labels it wants to the testing.T context. + lblCtx := pprof.WithLabels(context.Background(), lbls) + pprof.SetGoroutineLabels(lblCtx) + var wg sync.WaitGroup + // make sure the labels are visible in a child goroutine + wg.Go(verifyLabels) + // and in this parent goroutine + verifyLabels() + wg.Wait() +}