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 <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>
This commit is contained in:
David Finkel 2025-05-23 16:04:08 -04:00 committed by Gopher Robot
parent 0921e1db83
commit 6851795fb6
19 changed files with 297 additions and 69 deletions

View 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/`. 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]/`. 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
Go 1.25 added a new `decoratemappings` setting that controls whether the Go Go 1.25 added a new `decoratemappings` setting that controls whether the Go

View file

@ -57,6 +57,7 @@ func printuint(uint64)
func printcomplex128(complex128) func printcomplex128(complex128)
func printcomplex64(complex64) func printcomplex64(complex64)
func printstring(string) func printstring(string)
func printquoted(string)
func printpointer(any) func printpointer(any)
func printuintptr(uintptr) func printuintptr(uintptr)
func printiface(any) func printiface(any)

View file

@ -64,6 +64,7 @@ var runtimeDecls = [...]struct {
{"printcomplex128", funcTag, 27}, {"printcomplex128", funcTag, 27},
{"printcomplex64", funcTag, 29}, {"printcomplex64", funcTag, 29},
{"printstring", funcTag, 31}, {"printstring", funcTag, 31},
{"printquoted", funcTag, 31},
{"printpointer", funcTag, 32}, {"printpointer", funcTag, 32},
{"printuintptr", funcTag, 33}, {"printuintptr", funcTag, 33},
{"printiface", funcTag, 32}, {"printiface", funcTag, 32},

View file

@ -729,6 +729,10 @@ func walkPrint(nn *ir.CallExpr, init *ir.Nodes) ir.Node {
if ir.IsConst(n, constant.String) { if ir.IsConst(n, constant.String) {
cs = ir.StringVal(n) cs = ir.StringVal(n)
} }
// Print values of the named type `quoted` using printquoted.
if types.RuntimeSymName(n.Type().Sym()) == "quoted" {
on = typecheck.LookupRuntime("printquoted")
} else {
switch cs { switch cs {
case " ": case " ":
on = typecheck.LookupRuntime("printsp") on = typecheck.LookupRuntime("printsp")
@ -737,6 +741,7 @@ func walkPrint(nn *ir.CallExpr, init *ir.Nodes) ir.Node {
default: default:
on = typecheck.LookupRuntime("printstring") on = typecheck.LookupRuntime("printstring")
} }
}
default: default:
badtype(ir.OPRINT, n.Type(), nil) badtype(ir.OPRINT, n.Type(), nil)
continue continue

View file

@ -43,6 +43,7 @@ var builtins = [...]struct {
{"runtime.printcomplex128", 1}, {"runtime.printcomplex128", 1},
{"runtime.printcomplex64", 1}, {"runtime.printcomplex64", 1},
{"runtime.printstring", 1}, {"runtime.printstring", 1},
{"runtime.printquoted", 1},
{"runtime.printpointer", 1}, {"runtime.printpointer", 1},
{"runtime.printuintptr", 1}, {"runtime.printuintptr", 1},
{"runtime.printiface", 1}, {"runtime.printiface", 1},

View file

@ -58,6 +58,7 @@ var depsRules = `
internal/nettrace, internal/nettrace,
internal/platform, internal/platform,
internal/profilerecord, internal/profilerecord,
internal/runtime/pprof/label,
internal/syslist, internal/syslist,
internal/trace/tracev2, internal/trace/tracev2,
internal/trace/traceviewer/format, internal/trace/traceviewer/format,
@ -85,6 +86,7 @@ var depsRules = `
internal/goos, internal/goos,
internal/itoa, internal/itoa,
internal/profilerecord, internal/profilerecord,
internal/runtime/pprof/label,
internal/strconv, internal/strconv,
internal/trace/tracev2, internal/trace/tracev2,
math/bits, math/bits,
@ -672,7 +674,8 @@ var depsRules = `
< net/http/fcgi; < net/http/fcgi;
# Profiling # 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; < runtime/pprof;
OS, compress/gzip, internal/lazyregexp OS, compress/gzip, internal/lazyregexp

View file

@ -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}
}

View file

@ -2064,3 +2064,15 @@ func HexdumpWords(p, bytes uintptr) string {
} }
return string(buf[:n]) 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)
}

View file

@ -7,18 +7,14 @@ package pprof
import ( import (
"context" "context"
"fmt" "fmt"
"internal/runtime/pprof/label"
"slices" "slices"
"strings" "strings"
) )
type label struct {
key string
value string
}
// LabelSet is a set of labels. // LabelSet is a set of labels.
type LabelSet struct { type LabelSet struct {
list []label list []label.Label
} }
// labelContextKey is the type of contextKeys used for profiler labels. // 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 // This is an initial implementation, but it will be replaced with something
// that admits incremental immutable modification more efficiently. // that admits incremental immutable modification more efficiently.
type labelMap struct { type labelMap struct {
LabelSet label.Set
} }
// String satisfies Stringer and returns key, value pairs in a consistent // String satisfies Stringer and returns key, value pairs in a consistent
@ -45,10 +41,10 @@ func (l *labelMap) String() string {
if l == nil { if l == nil {
return "" return ""
} }
keyVals := make([]string, 0, len(l.list)) keyVals := make([]string, 0, len(l.Set.List))
for _, lbl := range l.list { for _, lbl := range l.Set.List {
keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.key, lbl.value)) keyVals = append(keyVals, fmt.Sprintf("%q:%q", lbl.Key, lbl.Value))
} }
slices.Sort(keyVals) slices.Sort(keyVals)
@ -59,38 +55,39 @@ func (l *labelMap) String() string {
// 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 {
parentLabels := labelValue(ctx) 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 { func mergeLabelSets(left label.Set, right LabelSet) label.Set {
if len(left.list) == 0 { if len(left.List) == 0 {
return right return label.NewSet(right.list)
} else if len(right.list) == 0 { } else if len(right.list) == 0 {
return left return left
} }
lList, rList := left.List, right.list
l, r := 0, 0 l, r := 0, 0
result := make([]label, 0, len(right.list)) result := make([]label.Label, 0, len(rList))
for l < len(left.list) && r < len(right.list) { for l < len(lList) && r < len(rList) {
switch strings.Compare(left.list[l].key, right.list[r].key) { switch strings.Compare(lList[l].Key, rList[r].Key) {
case -1: // left key < right key case -1: // left key < right key
result = append(result, left.list[l]) result = append(result, lList[l])
l++ l++
case 1: // right key < left key case 1: // right key < left key
result = append(result, right.list[r]) result = append(result, rList[r])
r++ r++
case 0: // keys are equal, right value overwrites left value case 0: // keys are equal, right value overwrites left value
result = append(result, right.list[r]) result = append(result, rList[r])
l++ l++
r++ r++
} }
} }
// Append the remaining elements // Append the remaining elements
result = append(result, left.list[l:]...) result = append(result, lList[l:]...)
result = append(result, right.list[r:]...) result = append(result, rList[r:]...)
return LabelSet{list: result} return label.NewSet(result)
} }
// Labels takes an even number of strings representing key-value pairs // 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 { if len(args)%2 != 0 {
panic("uneven number of arguments to pprof.Labels") 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 sortedNoDupes := true
for i := 0; i+1 < len(args); i += 2 { 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]) sortedNoDupes = sortedNoDupes && (i < 2 || args[i] > args[i-2])
} }
if !sortedNoDupes { if !sortedNoDupes {
// slow path: keys are unsorted, contain duplicates, or both // slow path: keys are unsorted, contain duplicates, or both
slices.SortStableFunc(list, func(a, b label) int { slices.SortStableFunc(list, func(a, b label.Label) int {
return strings.Compare(a.key, b.key) 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 { 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) deduped = append(deduped, lbl)
} else { } else {
deduped[len(deduped)-1] = lbl deduped[len(deduped)-1] = lbl
@ -131,9 +128,9 @@ func Labels(args ...string) LabelSet {
// whether that label exists. // whether that label exists.
func Label(ctx context.Context, key string) (string, bool) { func Label(ctx context.Context, key string) (string, bool) {
ctxLabels := labelValue(ctx) ctxLabels := labelValue(ctx)
for _, lbl := range ctxLabels.list { for _, lbl := range ctxLabels.Set.List {
if lbl.key == key { if lbl.Key == key {
return lbl.value, true return lbl.Value, true
} }
} }
return "", false 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. // 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) { func ForLabels(ctx context.Context, f func(key, value string) bool) {
ctxLabels := labelValue(ctx) ctxLabels := labelValue(ctx)
for _, lbl := range ctxLabels.list { for _, lbl := range ctxLabels.Set.List {
if !f(lbl.key, lbl.value) { if !f(lbl.Key, lbl.Value) {
break break
} }
} }

View file

@ -7,19 +7,20 @@ package pprof
import ( import (
"context" "context"
"fmt" "fmt"
"internal/runtime/pprof/label"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
"testing" "testing"
) )
func labelsSorted(ctx context.Context) []label { func labelsSorted(ctx context.Context) []label.Label {
ls := []label{} ls := []label.Label{}
ForLabels(ctx, func(key, value string) bool { ForLabels(ctx, func(key, value string) bool {
ls = append(ls, label{key, value}) ls = append(ls, label.Label{Key: key, Value: value})
return true 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 return ls
} }
@ -39,7 +40,7 @@ func TestContextLabels(t *testing.T) {
t.Errorf(`Label(ctx, "key"): got %v, %v; want "value", ok`, v, ok) t.Errorf(`Label(ctx, "key"): got %v, %v; want "value", ok`, v, ok)
} }
gotLabels := labelsSorted(ctx) gotLabels := labelsSorted(ctx)
wantLabels := []label{{"key", "value"}} wantLabels := []label.Label{{Key: "key", Value: "value"}}
if !reflect.DeepEqual(gotLabels, wantLabels) { if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", 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) t.Errorf(`Label(ctx, "key2"): got %v, %v; want "value2", ok`, v, ok)
} }
gotLabels = labelsSorted(ctx) 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) { if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", 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) t.Errorf(`Label(ctx, "key3"): got %v, %v; want "value3", ok`, v, ok)
} }
gotLabels = labelsSorted(ctx) 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) { if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", 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) t.Errorf(`Label(ctx, "key4"): got %v, %v; want "value4b", ok`, v, ok)
} }
gotLabels = labelsSorted(ctx) 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) { if !reflect.DeepEqual(gotLabels, wantLabels) {
t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels) t.Errorf("(sorted) labels on context: got %v, want %v", gotLabels, wantLabels)
} }
@ -93,18 +94,18 @@ func TestLabelMapStringer(t *testing.T) {
expected: "{}", expected: "{}",
}, { }, {
m: labelMap{ m: labelMap{
Labels("foo", "bar"), label.NewSet(Labels("foo", "bar").list),
}, },
expected: `{"foo":"bar"}`, expected: `{"foo":"bar"}`,
}, { }, {
m: labelMap{ m: labelMap{
Labels( label.NewSet(Labels(
"foo", "bar", "foo", "bar",
"key1", "value1", "key1", "value1",
"key2", "value2", "key2", "value2",
"key3", "value3", "key3", "value3",
"key4WithNewline", "\nvalue4", "key4WithNewline", "\nvalue4",
), ).list),
}, },
expected: `{"foo":"bar", "key1":"value1", "key2":"value2", "key3":"value3", "key4WithNewline":"\nvalue4"}`, expected: `{"foo":"bar", "key1":"value1", "key2":"value2", "key3":"value3", "key4WithNewline":"\nvalue4"}`,
}, },

View file

@ -547,8 +547,8 @@ func printCountProfile(w io.Writer, debug int, name string, p countProfile) erro
var labels func() var labels func()
if p.Label(idx) != nil { if p.Label(idx) != nil {
labels = func() { labels = func() {
for _, lbl := range p.Label(idx).list { for _, lbl := range p.Label(idx).Set.List {
b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0) b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0)
} }
} }
} }

View file

@ -12,6 +12,7 @@ import (
"fmt" "fmt"
"internal/abi" "internal/abi"
"internal/profile" "internal/profile"
"internal/runtime/pprof/label"
"internal/syscall/unix" "internal/syscall/unix"
"internal/testenv" "internal/testenv"
"io" "io"
@ -1462,11 +1463,11 @@ func TestGoroutineCounts(t *testing.T) {
goroutineProf.WriteTo(&w, 1) goroutineProf.WriteTo(&w, 1)
prof := w.String() prof := w.String()
labels := labelMap{Labels("label", "value")} labels := labelMap{label.NewSet(Labels("label", "value").list)}
labelStr := "\n# labels: " + labels.String() 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() 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() fingLabelStr := "\n# labels: " + fingLabel.String()
orderedPrefix := []string{ orderedPrefix := []string{
"\n50 @ ", "\n50 @ ",

View file

@ -367,8 +367,8 @@ func (b *profileBuilder) build() error {
var labels func() var labels func()
if e.tag != nil { if e.tag != nil {
labels = func() { labels = func() {
for _, lbl := range (*labelMap)(e.tag).list { for _, lbl := range (*labelMap)(e.tag).Set.List {
b.pbLabel(tagSample_Label, lbl.key, lbl.value, 0) b.pbLabel(tagSample_Label, lbl.Key, lbl.Value, 0)
} }
} }
} }

View file

@ -92,9 +92,10 @@ func getProfLabel() map[string]string {
if l == nil { if l == nil {
return map[string]string{} return map[string]string{}
} }
m := make(map[string]string, len(l.list)) ls := l.Set.List
for _, lbl := range l.list { m := make(map[string]string, len(ls))
m[lbl.key] = lbl.value for _, lbl := range ls {
m[lbl.Key] = lbl.Value
} }
return m return m
} }

View file

@ -13,6 +13,10 @@ import (
// should use printhex instead of printuint (decimal). // should use printhex instead of printuint (decimal).
type hex uint64 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) { func bytes(s string) (ret []byte) {
rp := (*slice)(unsafe.Pointer(&ret)) rp := (*slice)(unsafe.Pointer(&ret))
sp := stringStructOf(&s) sp := stringStructOf(&s)
@ -169,24 +173,67 @@ func printint(v int64) {
var minhexdigits = 0 // protected by printlock var minhexdigits = 0 // protected by printlock
func printhex(v uint64) { func printhexopts(include0x bool, mindigits int, v uint64) {
const dig = "0123456789abcdef" const dig = "0123456789abcdef"
var buf [100]byte var buf [100]byte
i := len(buf) i := len(buf)
for i--; i > 0; i-- { for i--; i > 0; i-- {
buf[i] = dig[v%16] buf[i] = dig[v%16]
if v < 16 && len(buf)-i >= minhexdigits { if v < 16 && len(buf)-i >= mindigits {
break break
} }
v /= 16 v /= 16
} }
if include0x {
i-- i--
buf[i] = 'x' buf[i] = 'x'
i-- i--
buf[i] = '0' buf[i] = '0'
}
gwrite(buf[i:]) 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) { func printpointer(p unsafe.Pointer) {
printhex(uint64(uintptr(p))) printhex(uint64(uintptr(p)))
} }

View file

@ -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)
}
})
}
}

View file

@ -360,6 +360,10 @@ var debug struct {
// but allowing it is convenient for testing and for programs // but allowing it is convenient for testing and for programs
// that do an os.Setenv in main.init or main.main. // that do an os.Setenv in main.init or main.main.
asynctimerchan atomic.Int32 asynctimerchan atomic.Int32
// tracebacklabels controls the inclusion of goroutine labels in the
// goroutine status header line.
tracebacklabels atomic.Int32
} }
var dbgvars = []*dbgVar{ var dbgvars = []*dbgVar{
@ -394,6 +398,7 @@ var dbgvars = []*dbgVar{
{name: "traceallocfree", atomic: &debug.traceallocfree}, {name: "traceallocfree", atomic: &debug.traceallocfree},
{name: "tracecheckstackownership", value: &debug.traceCheckStackOwnership}, {name: "tracecheckstackownership", value: &debug.traceCheckStackOwnership},
{name: "tracebackancestors", value: &debug.tracebackancestors}, {name: "tracebackancestors", value: &debug.tracebackancestors},
{name: "tracebacklabels", atomic: &debug.tracebacklabels, def: 0},
{name: "tracefpunwindoff", value: &debug.tracefpunwindoff}, {name: "tracefpunwindoff", value: &debug.tracefpunwindoff},
{name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1}, {name: "updatemaxprocs", value: &debug.updatemaxprocs, def: 1},
} }

View file

@ -8,6 +8,7 @@ import (
"internal/abi" "internal/abi"
"internal/bytealg" "internal/bytealg"
"internal/goarch" "internal/goarch"
"internal/runtime/pprof/label"
"internal/runtime/sys" "internal/runtime/sys"
"internal/stringslite" "internal/stringslite"
"unsafe" "unsafe"
@ -1270,6 +1271,19 @@ func goroutineheader(gp *g) {
if bubble := gp.bubble; bubble != nil { if bubble := gp.bubble; bubble != nil {
print(", synctest bubble ", bubble.id) 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") print("]:\n")
} }

View file

@ -6,6 +6,7 @@ package runtime_test
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"internal/abi" "internal/abi"
"internal/asan" "internal/asan"
@ -15,6 +16,7 @@ import (
"regexp" "regexp"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"runtime/pprof"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -882,3 +884,71 @@ func TestSetCgoTracebackNoCgo(t *testing.T) {
t.Fatalf("want %s, got %s\n", want, output) 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()
}