mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
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:
parent
0921e1db83
commit
6851795fb6
19 changed files with 297 additions and 69 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
25
src/internal/runtime/pprof/label/labelset.go
Normal file
25
src/internal/runtime/pprof/label/labelset.go
Normal 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}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @ ",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
38
src/runtime/print_quoted_test.go
Normal file
38
src/runtime/print_quoted_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue