]> Cypherpunks repositories - gostls13.git/commitdiff
runtime: add GODEBUG=tracebacklabels=1 to include pprof labels in tracebacks
authorDavid Finkel <davidf@vimeo.com>
Fri, 23 May 2025 20:04:08 +0000 (16:04 -0400)
committerGopher Robot <gobot@golang.org>
Tue, 25 Nov 2025 04:07:56 +0000 (20:07 -0800)
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 <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
19 files changed:
doc/godebug.md
src/cmd/compile/internal/typecheck/_builtin/runtime.go
src/cmd/compile/internal/typecheck/builtin.go
src/cmd/compile/internal/walk/builtin.go
src/cmd/internal/goobj/builtinlist.go
src/go/build/deps_test.go
src/internal/runtime/pprof/label/labelset.go [new file with mode: 0644]
src/runtime/export_test.go
src/runtime/pprof/label.go
src/runtime/pprof/label_test.go
src/runtime/pprof/pprof.go
src/runtime/pprof/pprof_test.go
src/runtime/pprof/proto.go
src/runtime/pprof/runtime_test.go
src/runtime/print.go
src/runtime/print_quoted_test.go [new file with mode: 0644]
src/runtime/runtime1.go
src/runtime/traceback.go
src/runtime/traceback_test.go

index d9ae462b980d88bd223c3c0586cf103dc28557d9..0d1cd6b6627af9edc2d35012f1b89e367f390027 100644 (file)
@@ -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
index d43a9e5bf2deaa276fe54f22b7834cc1f709a630..35fbbb6b12015cb6b2f1f1af4091834d81d8f4e8 100644 (file)
@@ -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)
index dd9f1593f38febd5b6b8d2fb11a1d7b1721e0331..8a505073f7af6e51d7af35422d14cc0c1ce567c9 100644 (file)
@@ -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},
index 2f2a2c62f1664d0bc102f042503bd784e79fa9ad..c698caddce91f2da4bb4248af77d41973b9238bb 100644 (file)
@@ -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)
index b3320808f11d297d990f7bf362361fea4b767283..918ade191dd99d5fd50cc4474edccd8cd7aae82d 100644 (file)
@@ -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},
index 2ee5114fd7de5a1be65255fca29842ccfaf509df..9a6b86b65c86a9b5999b03e73a1146e8909a71a3 100644 (file)
@@ -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 (file)
index 0000000..d3046d4
--- /dev/null
@@ -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}
+}
index 6e0360aacabaa2f64f90ef2e30f3e7bcac859754..26341c43001689ad25c15323261bf0acf04b908a 100644 (file)
@@ -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)
+}
index 4c1d8d38ce5ea2474abdd030feb762ac9410eab2..09dd1de65159232a416532543e4bf391552a7afb 100644 (file)
@@ -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
                }
        }
index 3018693c247453ff4a5f9a47646946a9718a9595..ded8b29575053c222236eed012071bc54373d4f0 100644 (file)
@@ -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"}`,
                },
index c617a8b26a4968a44aeecafa20d710488b4da8eb..c27df2289783157a9bf7eca9362526c8627dd367 100644 (file)
@@ -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)
                                }
                        }
                }
index 4c9279c5a6f032b0d6fe5b55b2ddc47d236b7606..e46e4f9d273f8dd00c694623ec7ef0f32775f96e 100644 (file)
@@ -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 @ ",
index 28ceb81542110a7795f26b009202140fe874ab4f..5ad917f14a7292e1e8882e49573256b518ffa4cd 100644 (file)
@@ -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)
                                }
                        }
                }
index 353ed8a3f1d0272a45e3b3018d2c6248a71dd181..acdd4c8d15a7124ef6be735d9de0aebdd1805aa5 100644 (file)
@@ -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
 }
index d2733fb2661f5eebabe05f59f9e08e437b4cd8a5..5d1bc22809b92c049cfee99333e7c11dac42a420 100644 (file)
@@ -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 (file)
index 0000000..f9e947b
--- /dev/null
@@ -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)
+                       }
+               })
+       }
+}
index 64ee4c8d2e9b137111d50b26d207a98625f3a042..965ff8ab5167d5f106ed6d153ee883c150fca4b4 100644 (file)
@@ -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},
 }
index 74aaeba876709608a623c4a2fae3973fe66c6a4d..1c6f24c0332a75dcfaee51b582084dbe9743ed97 100644 (file)
@@ -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")
 }
 
index 1dac91311ca9a34ac0c0b52df40ccadbc96b4efb..d47f4ab7455263bac1e85e46df5e9651869c8bcc 100644 (file)
@@ -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()
+}