Skip to content

Commit

Permalink
Add pprofLabelForThreadNames config
Browse files Browse the repository at this point in the history
The config is a string value that indicates the key of a pprof label whose value
should be shown as a goroutine name in the threads view.
  • Loading branch information
stefanhaller committed Nov 23, 2023
1 parent c1e5f4b commit e5d5e6e
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 10 deletions.
1 change: 1 addition & 0 deletions Documentation/api/dap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ In addition to the general [DAP spec](https://microsoft.github.io/debug-adapter-
stackTraceDepth<br>
showGlobalVariables<br>
showRegisters<br>
showPprofLabels<br>
hideSystemGoroutines<br>
goroutineFilters
</tr>
Expand Down
11 changes: 9 additions & 2 deletions service/dap/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ Type "help" followed by the name of a command for more information about it.`
dlv config substitutePath -clear
Adds or removes a path substitution rule. If -clear is used all substitutePath rules are removed.
See also Documentation/cli/substitutepath.md.`
See also Documentation/cli/substitutepath.md.
dlv config showPprofLabels <label>
dlv config showPprofLabels -clear <label>
dlv config showPprofLabels -clear
Adds or removes a label key to show in the callstack view. If -clear is used without an argument,
all labels are removed.`
msgSources = `Print list of source files.
dlv sources [<regex>]
Expand Down Expand Up @@ -138,7 +145,7 @@ func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
Areas: []dap.InvalidatedAreas{"variables"},
},
})
case "goroutineFilters", "hideSystemGoroutines":
case "goroutineFilters", "hideSystemGoroutines", "showPprofLabels":
// Thread related data has become invalidated.
s.send(&dap.InvalidatedEvent{
Event: *newEvent("invalidated"),
Expand Down
43 changes: 43 additions & 0 deletions service/dap/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func configureSet(sargs *launchAttachArgs, args string) (bool, string, error) {
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
}

if cfgname == "showPprofLabels" {
err := configureSetShowPprofLabels(sargs, rest)
if err != nil {
return false, "", err
}
// Print the updated labels
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
}

err := config.ConfigureSetSimple(rest, cfgname, field)
if err != nil {
return false, "", err
Expand Down Expand Up @@ -85,3 +94,37 @@ func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {
}
return nil
}

func configureSetShowPprofLabels(args *launchAttachArgs, rest string) error {
if strings.TrimSpace(rest) == "-clear" {
args.ShowPprofLabels = args.ShowPprofLabels[:0]
return nil
}
delete := false
argv := config.SplitQuotedFields(rest, '"')
if len(argv) == 2 && argv[0] == "-clear" {
argv = argv[1:]
delete = true
}
switch len(argv) {
case 0:
// do nothing, let caller show the current list of labels
return nil
case 1:
if delete {
for i := range args.ShowPprofLabels {
if args.ShowPprofLabels[i] == argv[0] {
copy(args.ShowPprofLabels[i:], args.ShowPprofLabels[i+1:])
args.ShowPprofLabels = args.ShowPprofLabels[:len(args.ShowPprofLabels)-1]
return nil
}
}
return fmt.Errorf("could not find label %q", argv[0])
} else {
args.ShowPprofLabels = append(args.ShowPprofLabels, argv[0])
}
default:
return fmt.Errorf("too many arguments to \"config showPprofLabels\"")
}
return nil
}
8 changes: 5 additions & 3 deletions service/dap/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,28 @@ func TestListConfig(t *testing.T) {
args: args{
args: &launchAttachArgs{},
},
want: formatConfig(0, false, false, "", false, [][2]string{}),
want: formatConfig(0, false, false, "", []string{}, false, [][2]string{}),
},
{
name: "default values",
args: args{
args: &defaultArgs,
},
want: formatConfig(50, false, false, "", false, [][2]string{}),
want: formatConfig(50, false, false, "", []string{}, false, [][2]string{}),
},
{
name: "custom values",
args: args{
args: &launchAttachArgs{
StackTraceDepth: 35,
ShowGlobalVariables: true,
GoroutineFilters: "SomeFilter",
ShowPprofLabels: []string{"SomeLabel"},
substitutePathClientToServer: [][2]string{{"hello", "world"}},
substitutePathServerToClient: [][2]string{{"world", "hello"}},
},
},
want: formatConfig(35, true, false, "", false, [][2]string{{"hello", "world"}}),
want: formatConfig(35, true, false, "SomeFilter", []string{"SomeLabel"}, false, [][2]string{{"hello", "world"}}),
},
}
for _, tt := range tests {
Expand Down
40 changes: 39 additions & 1 deletion service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ type launchAttachArgs struct {
ShowRegisters bool `cfgName:"showRegisters"`
// GoroutineFilters are the filters used when loading goroutines.
GoroutineFilters string `cfgName:"goroutineFilters"`
// ShowPprofLabels is an array of keys of pprof labels to show as a
// goroutine name in the threads view. If the array has one element, only
// that label's value will be shown; otherwise, each of the labels will be
// shown as "key:value". To show all labels, specify the single element "*".
ShowPprofLabels []string `cfgName:"showPprofLabels"`
// HideSystemGoroutines indicates if system goroutines should be removed from threads
// responses.
HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
Expand All @@ -241,6 +246,7 @@ var defaultArgs = launchAttachArgs{
HideSystemGoroutines: false,
ShowRegisters: false,
GoroutineFilters: "",
ShowPprofLabels: []string{},
substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{},
}
Expand Down Expand Up @@ -353,6 +359,7 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) {
s.args.ShowRegisters = args.ShowRegisters
s.args.HideSystemGoroutines = args.HideSystemGoroutines
s.args.GoroutineFilters = args.GoroutineFilters
s.args.ShowPprofLabels = args.ShowPprofLabels
if paths := args.SubstitutePath; len(paths) > 0 {
clientToServer := make([][2]string, 0, len(paths))
serverToClient := make([][2]string, 0, len(paths))
Expand Down Expand Up @@ -1815,10 +1822,41 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
if g.Thread != nil && g.Thread.ThreadID() != 0 {
thread = fmt.Sprintf(" (Thread %d)", g.Thread.ThreadID())
}
var labels strings.Builder
writeLabelsForKeys := func(keys []string) {
for _, k := range keys {
labelValue := g.Labels()[k]
if labelValue != "" {
labels.WriteByte(' ')
labels.WriteString(k)
labels.WriteByte(':')
labels.WriteString(labelValue)
}
}
}
if len(s.args.ShowPprofLabels) == 1 {
labelKey := s.args.ShowPprofLabels[0]
if labelKey == "*" {
keys := make([]string, 0, len(g.Labels()))
for k := range g.Labels() {
keys = append(keys, k)
}
sort.Strings(keys)
writeLabelsForKeys(keys)
} else {
labelValue := g.Labels()[labelKey]
if labelValue != "" {
labels.WriteByte(' ')
labels.WriteString(labelValue)
}
}
} else {
writeLabelsForKeys(s.args.ShowPprofLabels)
}
// File name and line number are communicated via `stackTrace`
// so no need to include them here.
loc := g.UserCurrent()
threads[i].Name = fmt.Sprintf("%s[Go %d] %s%s", selected, g.ID, fnName(&loc), thread)
threads[i].Name = fmt.Sprintf("%s[Go %d%s] %s%s", selected, g.ID, labels.String(), fnName(&loc), thread)
threads[i].Id = int(g.ID)
}
}
Expand Down
71 changes: 67 additions & 4 deletions service/dap/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1396,6 +1396,68 @@ func TestSelectedThreadsRequest(t *testing.T) {
})
}

func TestGoroutineLabels(t *testing.T) {
tests := []struct {
showPprofLabelsConfig []string
expectedPrefixWithLabel string
}{
{[]string{}, "* [Go 1]"},
{[]string{"k1"}, "* [Go 1 v1]"},
{[]string{"k2"}, "* [Go 1 v2]"},
{[]string{"k2", "k1"}, "* [Go 1 k2:v2 k1:v1]"}, // When passing keys explicitly, we show them in the given order
{[]string{"unknown"}, "* [Go 1]"},
{[]string{"unknown", "k1"}, "* [Go 1 k1:v1]"},
{[]string{"*"}, "* [Go 1 k1:v1 k2:v2]"}, // Special case for showing all labels; labels are shown sorted by key
}
for _, tc := range tests {
runTest(t, "goroutineLabels", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "exec",
"program": fixture.Path,
"hideSystemGoroutines": true,
"showPprofLabels": tc.showPprofLabelsConfig,
"stopOnEntry": !stopOnEntry,
})
},
// Breakpoints are set within the program
"", []int{},
[]onBreakpoint{{
execute: func() {
client.ThreadsRequest()
tr := client.ExpectThreadsResponse(t)
if len(tr.Body.Threads) != 1 {
t.Errorf("got %d threads, expected 1\n", len(tr.Body.Threads))
}
// The first breakpoint is before the call to pprof.Do; no labels yet:
expectedPrefix := "* [Go 1]"
if !strings.HasPrefix(tr.Body.Threads[0].Name, expectedPrefix) {
t.Errorf("got %s, expected %s\n", tr.Body.Threads[0].Name, expectedPrefix)
}

client.ContinueRequest(1)
client.ExpectContinueResponse(t)
client.ExpectStoppedEvent(t)
checkStop(t, client, 1, "main.f", 21)
client.ThreadsRequest()
tr = client.ExpectThreadsResponse(t)
if len(tr.Body.Threads) != 1 {
t.Errorf("got %d threads, expected 1\n", len(tr.Body.Threads))
}
// The second breakpoint is inside pprof.Do, so there are labels:
if !strings.HasPrefix(tr.Body.Threads[0].Name, tc.expectedPrefixWithLabel) {
t.Errorf("got %s, expected %s\n", tr.Body.Threads[0].Name, tc.expectedPrefixWithLabel)
}
},
disconnect: true,
}},
)
})
}
}

func TestHideSystemGoroutinesRequest(t *testing.T) {
tests := []struct{ hideSystemGoroutines bool }{
{hideSystemGoroutines: true},
Expand Down Expand Up @@ -4054,15 +4116,16 @@ func TestEvaluateRequest(t *testing.T) {
})
}

func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, hideSystemGoroutines bool, substitutePath [][2]string) string {
func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, showPprofLabels []string, hideSystemGoroutines bool, substitutePath [][2]string) string {
formatStr := `stackTraceDepth %d
showGlobalVariables %v
showRegisters %v
goroutineFilters %q
showPprofLabels %v
hideSystemGoroutines %v
substitutePath %v
`
return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, hideSystemGoroutines, substitutePath)
return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, showPprofLabels, hideSystemGoroutines, substitutePath)
}

func TestEvaluateCommandRequest(t *testing.T) {
Expand Down Expand Up @@ -4099,7 +4162,7 @@ Type 'dlv help' followed by a command for full documentation.

client.EvaluateRequest("dlv config -list", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, formatConfig(50, false, false, "", false, [][2]string{}), noChildren)
checkEval(t, got, formatConfig(50, false, false, "", []string{}, false, [][2]string{}), noChildren)

// Read and modify showGlobalVariables.
client.EvaluateRequest("dlv config -list showGlobalVariables", 1000, "repl")
Expand All @@ -4120,7 +4183,7 @@ Type 'dlv help' followed by a command for full documentation.

client.EvaluateRequest("dlv config -list", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, formatConfig(50, true, false, "", false, [][2]string{}), noChildren)
checkEval(t, got, formatConfig(50, true, false, "", []string{}, false, [][2]string{}), noChildren)

client.ScopesRequest(1000)
scopes = client.ExpectScopesResponse(t)
Expand Down
6 changes: 6 additions & 0 deletions service/dap/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ type LaunchAttachCommonConfig struct {
// https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md#goroutines
GoroutineFilters string `json:"goroutineFilters,omitempty"`

// Array of string values indicating the keys of pprof labels to show as a
// goroutine name in the threads view. If the array has one element, only
// that label's value will be shown; otherwise, each of the labels will be
// shown as "key:value". To show all labels, specify the single element "*".
ShowPprofLabels []string `json:"showPprofLabels,omitempty"`

// An array of mappings from a local path (client) to the remote path (debugger).
// This setting is useful when working in a file system with symbolic links,
// running remote debugging, or debugging an executable compiled externally.
Expand Down

0 comments on commit e5d5e6e

Please sign in to comment.