diff --git a/doc/godebug.md b/doc/godebug.md index d9ae462b980..0d1cd6b6627 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 d43a9e5bf2d..35fbbb6b120 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 dd9f1593f38..8a505073f7a 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 2f2a2c62f16..c698caddce9 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 b3320808f11..918ade191dd 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 2ee5114fd7d..9a6b86b65c8 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 00000000000..d3046d407c2 --- /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 6e0360aacab..26341c43001 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 4c1d8d38ce5..09dd1de6515 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 3018693c247..ded8b295750 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 c617a8b26a4..c27df228978 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 4c9279c5a6f..e46e4f9d273 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 28ceb815421..5ad917f14a7 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 353ed8a3f1d..acdd4c8d15a 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 d2733fb2661..5d1bc22809b 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 00000000000..f9e947b569c --- /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 64ee4c8d2e9..965ff8ab516 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 74aaeba8767..1c6f24c0332 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 1dac91311ca..d47f4ab7455 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() +}