service/dap: support goroutine filters in dap (#2759)

* service/dap: filter goroutines

* adjust defaults

* add tests

* remove label change

* fix typos

* send invalidated areas

* respond to review, and allow to clear goroutineFilters
This commit is contained in:
Suzy Mueller
2021-12-07 12:23:55 -05:00
committed by GitHub
parent d0898e4de1
commit a4ac69e87d
8 changed files with 348 additions and 183 deletions

View File

@ -772,27 +772,17 @@ func (a byGoroutineID) Len() int { return len(a) }
func (a byGoroutineID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
// The number of goroutines we're going to request on each RPC call
const goroutineBatchSize = 10000
type printGoroutinesFlags uint8
const (
printGoroutinesStack printGoroutinesFlags = 1 << iota
printGoroutinesLabels
)
func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl formatGoroutineLoc, flags printGoroutinesFlags, depth int, state *api.DebuggerState) error {
func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl api.FormatGoroutineLoc, flags api.PrintGoroutinesFlags, depth int, state *api.DebuggerState) error {
for _, g := range gs {
prefix := indent + " "
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = indent + "* "
}
fmt.Printf("%sGoroutine %s\n", prefix, t.formatGoroutine(g, fgl))
if flags&printGoroutinesLabels != 0 {
if flags&api.PrintGoroutinesLabels != 0 {
writeGoroutineLabels(os.Stdout, g, indent+"\t")
}
if flags&printGoroutinesStack != 0 {
if flags&api.PrintGoroutinesStack != 0 {
stack, err := t.client.Stacktrace(g.ID, depth, 0, nil)
if err != nil {
return err
@ -803,83 +793,10 @@ func printGoroutines(t *Term, indent string, gs []*api.Goroutine, fgl formatGoro
return nil
}
const (
maxGroupMembers = 5
maxGoroutineGroups = 50
)
func goroutines(t *Term, ctx callContext, argstr string) error {
args := strings.Split(argstr, " ")
var filters []api.ListGoroutinesFilter
var group api.GoroutineGroupingOptions
var fgl = fglUserCurrent
var flags printGoroutinesFlags
var depth = 10
var batchSize = goroutineBatchSize
group.MaxGroupMembers = maxGroupMembers
group.MaxGroups = maxGoroutineGroups
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "-u":
fgl = fglUserCurrent
case "-r":
fgl = fglRuntimeCurrent
case "-g":
fgl = fglGo
case "-s":
fgl = fglStart
case "-l":
flags |= printGoroutinesLabels
case "-t":
flags |= printGoroutinesStack
// optional depth argument
if i+1 < len(args) && len(args[i+1]) > 0 {
n, err := strconv.Atoi(args[i+1])
if err == nil {
depth = n
i++
}
}
case "-w", "-with":
filter, err := readGoroutinesFilter(args, &i)
if err != nil {
return err
}
filters = append(filters, *filter)
case "-wo", "-without":
filter, err := readGoroutinesFilter(args, &i)
if err != nil {
return err
}
filter.Negated = true
filters = append(filters, *filter)
case "-group":
var err error
group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
if err != nil {
return err
}
i++
if group.GroupBy == api.GoroutineLabel {
if i+1 >= len(args) {
return errors.New("-group label must be followed by an argument")
}
group.GroupByKey = args[i+1]
i++
}
batchSize = 0 // grouping only works well if run on all goroutines
case "":
// nothing to do
default:
return fmt.Errorf("wrong argument: '%s'", arg)
}
filters, group, fgl, flags, depth, batchSize, err := api.ParseGoroutineArgs(argstr)
if err != nil {
return err
}
state, err := t.client.GetState()
@ -933,52 +850,6 @@ func goroutines(t *Term, ctx callContext, argstr string) error {
return nil
}
func readGoroutinesFilterKind(args []string, i int) (api.GoroutineField, error) {
if i >= len(args) {
return api.GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
}
switch args[i] {
case "curloc":
return api.GoroutineCurrentLoc, nil
case "userloc":
return api.GoroutineUserLoc, nil
case "goloc":
return api.GoroutineGoLoc, nil
case "startloc":
return api.GoroutineStartLoc, nil
case "label":
return api.GoroutineLabel, nil
case "running":
return api.GoroutineRunning, nil
case "user":
return api.GoroutineUser, nil
default:
return api.GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
}
}
func readGoroutinesFilter(args []string, pi *int) (*api.ListGoroutinesFilter, error) {
r := new(api.ListGoroutinesFilter)
var err error
r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
if err != nil {
return nil, err
}
*pi++
switch r.Kind {
case api.GoroutineRunning, api.GoroutineUser:
return r, nil
}
if *pi+1 >= len(args) {
return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
}
r.Arg = args[*pi+1]
*pi++
return r, nil
}
func selectedGID(state *api.DebuggerState) int {
if state.SelectedGoroutine == nil {
return 0
@ -1113,20 +984,11 @@ func (t *Term) formatThread(th *api.Thread) string {
return fmt.Sprintf("%d at %s:%d", th.ID, t.formatPath(th.File), th.Line)
}
type formatGoroutineLoc int
const (
fglRuntimeCurrent = formatGoroutineLoc(iota)
fglUserCurrent
fglGo
fglStart
)
func (t *Term) formatLocation(loc api.Location) string {
return fmt.Sprintf("%s:%d %s (%#v)", t.formatPath(loc.File), loc.Line, loc.Function.Name(), loc.PC)
}
func (t *Term) formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string {
func (t *Term) formatGoroutine(g *api.Goroutine, fgl api.FormatGoroutineLoc) string {
if g == nil {
return "<nil>"
}
@ -1136,16 +998,16 @@ func (t *Term) formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string
var locname string
var loc api.Location
switch fgl {
case fglRuntimeCurrent:
case api.FglRuntimeCurrent:
locname = "Runtime"
loc = g.CurrentLoc
case fglUserCurrent:
case api.FglUserCurrent:
locname = "User"
loc = g.UserCurrentLoc
case fglGo:
case api.FglGo:
locname = "Go"
loc = g.GoStatementLoc
case fglStart:
case api.FglStart:
locname = "Start"
loc = g.StartLoc
}

153
service/api/command.go Normal file
View File

@ -0,0 +1,153 @@
package api
import (
"fmt"
"strconv"
"strings"
)
type PrintGoroutinesFlags uint8
const (
PrintGoroutinesStack PrintGoroutinesFlags = 1 << iota
PrintGoroutinesLabels
)
type FormatGoroutineLoc int
const (
FglRuntimeCurrent = FormatGoroutineLoc(iota)
FglUserCurrent
FglGo
FglStart
)
const (
maxGroupMembers = 5
maxGoroutineGroups = 50
)
// The number of goroutines we're going to request on each RPC call
const goroutineBatchSize = 10000
func ParseGoroutineArgs(argstr string) ([]ListGoroutinesFilter, GoroutineGroupingOptions, FormatGoroutineLoc, PrintGoroutinesFlags, int, int, error) {
args := strings.Split(argstr, " ")
var filters []ListGoroutinesFilter
var group GoroutineGroupingOptions
var fgl = FglUserCurrent
var flags PrintGoroutinesFlags
var depth = 10
var batchSize = goroutineBatchSize
group.MaxGroupMembers = maxGroupMembers
group.MaxGroups = maxGoroutineGroups
for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "-u":
fgl = FglUserCurrent
case "-r":
fgl = FglRuntimeCurrent
case "-g":
fgl = FglGo
case "-s":
fgl = FglStart
case "-l":
flags |= PrintGoroutinesLabels
case "-t":
flags |= PrintGoroutinesStack
// optional depth argument
if i+1 < len(args) && len(args[i+1]) > 0 {
n, err := strconv.Atoi(args[i+1])
if err == nil {
depth = n
i++
}
}
case "-w", "-with":
filter, err := readGoroutinesFilter(args, &i)
if err != nil {
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
}
filters = append(filters, *filter)
case "-wo", "-without":
filter, err := readGoroutinesFilter(args, &i)
if err != nil {
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
}
filter.Negated = true
filters = append(filters, *filter)
case "-group":
var err error
group.GroupBy, err = readGoroutinesFilterKind(args, i+1)
if err != nil {
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
}
i++
if group.GroupBy == GoroutineLabel {
if i+1 >= len(args) {
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
}
group.GroupByKey = args[i+1]
i++
}
batchSize = 0 // grouping only works well if run on all goroutines
case "":
// nothing to do
default:
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, fmt.Errorf("wrong argument: '%s'", arg)
}
}
return filters, group, fgl, flags, depth, batchSize, nil
}
func readGoroutinesFilterKind(args []string, i int) (GoroutineField, error) {
if i >= len(args) {
return GoroutineFieldNone, fmt.Errorf("%s must be followed by an argument", args[i-1])
}
switch args[i] {
case "curloc":
return GoroutineCurrentLoc, nil
case "userloc":
return GoroutineUserLoc, nil
case "goloc":
return GoroutineGoLoc, nil
case "startloc":
return GoroutineStartLoc, nil
case "label":
return GoroutineLabel, nil
case "running":
return GoroutineRunning, nil
case "user":
return GoroutineUser, nil
default:
return GoroutineFieldNone, fmt.Errorf("unrecognized argument to %s %s", args[i-1], args[i])
}
}
func readGoroutinesFilter(args []string, pi *int) (*ListGoroutinesFilter, error) {
r := new(ListGoroutinesFilter)
var err error
r.Kind, err = readGoroutinesFilterKind(args, *pi+1)
if err != nil {
return nil, err
}
*pi++
switch r.Kind {
case GoroutineRunning, GoroutineUser:
return r, nil
}
if *pi+1 >= len(args) {
return nil, fmt.Errorf("%s %s needs to be followed by an expression", args[*pi-1], args[*pi])
}
r.Arg = args[*pi+1]
*pi++
return r, nil
}

View File

@ -8,6 +8,7 @@ import (
"strings"
"github.com/go-delve/delve/pkg/config"
"github.com/google/go-dap"
)
func (s *Session) delveCmd(goid, frame int, cmdstr string) (string, error) {
@ -48,6 +49,10 @@ Type "help" followed by the name of a command for more information about it.`
Show all configuration parameters.
config -list <parameter>
Show value of a configuration parameter.
dlv config <parameter> <value>
Changes the value of a configuration parameter.
@ -109,16 +114,40 @@ func (s *Session) helpMessage(_, _ int, args string) (string, error) {
func (s *Session) evaluateConfig(_, _ int, expr string) (string, error) {
argv := config.Split2PartsBySpace(expr)
name := argv[0]
switch name {
case "-list":
return listConfig(&s.args), nil
default:
res, err := configureSet(&s.args, expr)
if err != nil {
return "", err
if name == "-list" {
if len(argv) > 1 {
return config.ConfigureListByName(&s.args, argv[1], "cfgName"), nil
}
return res, nil
return listConfig(&s.args), nil
}
updated, res, err := configureSet(&s.args, expr)
if err != nil {
return "", err
}
if updated {
// Send invalidated events for areas that are affected by configuration changes.
switch name {
case "showGlobalVariables", "showRegisters":
// Variable data has become invalidated.
s.send(&dap.InvalidatedEvent{
Event: *newEvent("invalidated"),
Body: dap.InvalidatedEventBody{
Areas: []dap.InvalidatedAreas{"variables"},
},
})
case "goroutineFilters", "hideSystemGoroutines":
// Thread related data has become invalidated.
s.send(&dap.InvalidatedEvent{
Event: *newEvent("invalidated"),
Body: dap.InvalidatedEventBody{
Areas: []dap.InvalidatedAreas{"threads"},
},
})
}
res += "\nUpdated"
}
return res, nil
}
func (s *Session) sources(_, _ int, filter string) (string, error) {

View File

@ -13,7 +13,7 @@ func listConfig(args *launchAttachArgs) string {
return buf.String()
}
func configureSet(sargs *launchAttachArgs, args string) (string, error) {
func configureSet(sargs *launchAttachArgs, args string) (bool, string, error) {
v := config.Split2PartsBySpace(args)
cfgname := v[0]
@ -24,28 +24,23 @@ func configureSet(sargs *launchAttachArgs, args string) (string, error) {
field := config.ConfigureFindFieldByName(sargs, cfgname, "cfgName")
if !field.CanAddr() {
return "", fmt.Errorf("%q is not a configuration parameter", cfgname)
}
// If there were no arguments provided, just list the value.
if len(v) == 1 {
return config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
return false, "", fmt.Errorf("%q is not a configuration parameter", cfgname)
}
if cfgname == "substitutePath" {
err := configureSetSubstitutePath(sargs, rest)
if err != nil {
return "", err
return false, "", err
}
// Print the updated client to server and server to client maps.
return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
}
err := config.ConfigureSetSimple(rest, cfgname, field)
if err != nil {
return "", err
return false, "", err
}
return fmt.Sprintf("%s\nUpdated", config.ConfigureListByName(sargs, cfgname, "cfgName")), nil
return true, config.ConfigureListByName(sargs, cfgname, "cfgName"), nil
}
func configureSetSubstitutePath(args *launchAttachArgs, rest string) error {

View File

@ -18,14 +18,14 @@ func TestListConfig(t *testing.T) {
args: args{
args: &launchAttachArgs{},
},
want: formatConfig(0, false, false, false, [][2]string{}),
want: formatConfig(0, false, false, "", false, [][2]string{}),
},
{
name: "default values",
args: args{
args: &defaultArgs,
},
want: formatConfig(50, false, false, false, [][2]string{}),
want: formatConfig(50, false, false, "", false, [][2]string{}),
},
{
name: "custom values",
@ -37,7 +37,7 @@ func TestListConfig(t *testing.T) {
substitutePathServerToClient: [][2]string{{"world", "hello"}},
},
},
want: formatConfig(35, true, false, false, [][2]string{{"hello", "world"}}),
want: formatConfig(35, true, false, "", false, [][2]string{{"hello", "world"}}),
},
}
for _, tt := range tests {

View File

@ -213,6 +213,8 @@ type launchAttachArgs struct {
ShowGlobalVariables bool `cfgName:"showGlobalVariables"`
// ShowRegisters indicates if register values should be loaded.
ShowRegisters bool `cfgName:"showRegisters"`
// GoroutineFilters are the filters used when loading goroutines.
GoroutineFilters string `cfgName:"goroutineFilters"`
// HideSystemGoroutines indicates if system goroutines should be removed from threads
// responses.
HideSystemGoroutines bool `cfgName:"hideSystemGoroutines"`
@ -233,6 +235,7 @@ var defaultArgs = launchAttachArgs{
ShowGlobalVariables: false,
HideSystemGoroutines: false,
ShowRegisters: false,
GoroutineFilters: "",
substitutePathClientToServer: [][2]string{},
substitutePathServerToClient: [][2]string{},
}
@ -345,6 +348,7 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
s.args.ShowGlobalVariables = args.ShowGlobalVariables
s.args.ShowRegisters = args.ShowRegisters
s.args.HideSystemGoroutines = args.HideSystemGoroutines
s.args.GoroutineFilters = args.GoroutineFilters
if paths := args.SubstitutePath; len(paths) > 0 {
clientToServer := make([][2]string, 0, len(paths))
serverToClient := make([][2]string, 0, len(paths))
@ -1659,11 +1663,19 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) {
var next int
if s.debugger != nil {
gs, next, err = s.debugger.Goroutines(0, maxGoroutines)
if err == nil && s.args.HideSystemGoroutines {
gs = s.debugger.FilterGoroutines(gs, []api.ListGoroutinesFilter{{
Kind: api.GoroutineUser,
Negated: false,
}})
if err == nil {
// Parse the goroutine arguments.
filters, _, _, _, _, _, parseErr := api.ParseGoroutineArgs(s.args.GoroutineFilters)
if parseErr != nil {
s.logToConsole(parseErr.Error())
}
if s.args.HideSystemGoroutines {
filters = append(filters, api.ListGoroutinesFilter{
Kind: api.GoroutineUser,
Negated: false,
})
}
gs = s.debugger.FilterGoroutines(gs, filters)
}
}

View File

@ -805,6 +805,113 @@ func checkStackFramesExact(t *testing.T, got *dap.StackTraceResponse,
checkStackFramesNamed("", t, got, wantStartName, wantStartLine, wantStartID, wantFrames, wantTotalFrames, true)
}
func TestFilterGoroutines(t *testing.T) {
tt := []struct {
name string
filter string
want []string
wantLen int
wantErr bool
}{
{
name: "user goroutines",
filter: "-with user",
want: []string{"main.main", "main.agoroutine"},
wantLen: 11,
},
{
name: "filter by user loc",
filter: "-with userloc main.main",
want: []string{"main.main"},
wantLen: 1,
},
{
name: "multiple filters",
filter: "-with user -with userloc main.agoroutine",
want: []string{"main.agoroutine"},
wantLen: 10,
},
{
name: "system goroutines",
filter: "-without user",
want: []string{"runtime."},
},
// Filters that should return all goroutines.
{
name: "empty filter string",
filter: "",
want: []string{"main.main", "main.agoroutine", "runtime."},
wantLen: -1,
},
{
name: "bad filter string",
filter: "not parsable to filters",
want: []string{"main.main", "main.agoroutine", "runtime."},
wantLen: -1,
wantErr: true,
},
// Filters that should produce none.
{
name: "no match to user loc",
filter: "-with userloc main.NotAUserFrame",
want: []string{"Dummy"},
wantLen: 1,
},
{
name: "no match to user and not user",
filter: "-with user -without user",
want: []string{"Dummy"},
wantLen: 1,
},
}
runTest(t, "goroutinestackprog", func(client *daptest.Client, fixture protest.Fixture) {
runDebugSessionWithBPs(t, client, "launch",
// Launch
func() {
client.LaunchRequestWithArgs(map[string]interface{}{
"mode": "exec",
"program": fixture.Path,
"stopOnEntry": !stopOnEntry})
},
// Set breakpoints
fixture.Source, []int{30},
[]onBreakpoint{{
// Stop at line 30
execute: func() {
for _, tc := range tt {
command := fmt.Sprintf("dlv config goroutineFilters %s", tc.filter)
client.EvaluateRequest(command, 1000, "repl")
client.ExpectInvalidatedEvent(t)
client.ExpectEvaluateResponse(t)
client.ThreadsRequest()
if tc.wantErr {
client.ExpectOutputEvent(t)
}
tr := client.ExpectThreadsResponse(t)
if tc.wantLen > 0 && len(tr.Body.Threads) != tc.wantLen {
t.Errorf("got Threads=%#v, want Len=%d\n", tr.Body.Threads, tc.wantLen)
}
for i, frame := range tr.Body.Threads {
var found bool
for _, wantName := range tc.want {
if strings.Contains(frame.Name, wantName) {
found = true
break
}
}
if !found {
t.Errorf("got Threads[%d]=%#v, want Name=%v\n", i, frame, tc.want)
}
}
}
},
disconnect: false,
}})
})
}
func checkStackFramesHasMore(t *testing.T, got *dap.StackTraceResponse,
wantStartName string, wantStartLine, wantStartID, wantFrames, wantTotalFrames int) {
t.Helper()
@ -3822,14 +3929,15 @@ func TestEvaluateRequest(t *testing.T) {
})
}
func formatConfig(depth int, showGlobals, showRegisters, hideSystemGoroutines bool, substitutePath [][2]string) string {
func formatConfig(depth int, showGlobals, showRegisters bool, goroutineFilters string, hideSystemGoroutines bool, substitutePath [][2]string) string {
formatStr := `stackTraceDepth %d
showGlobalVariables %v
showRegisters %v
goroutineFilters %q
hideSystemGoroutines %v
substitutePath %v
`
return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, hideSystemGoroutines, substitutePath)
return fmt.Sprintf(formatStr, depth, showGlobals, showRegisters, goroutineFilters, hideSystemGoroutines, substitutePath)
}
func TestEvaluateCommandRequest(t *testing.T) {
@ -3863,10 +3971,10 @@ Type 'dlv help' followed by a command for full documentation.
// Test config.
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, "", false, [][2]string{}), noChildren)
// Read and modify showGlobalVariables.
client.EvaluateRequest("dlv config showGlobalVariables", 1000, "repl")
client.EvaluateRequest("dlv config -list showGlobalVariables", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "showGlobalVariables\tfalse\n", noChildren)
@ -3878,12 +3986,13 @@ Type 'dlv help' followed by a command for full documentation.
checkScope(t, scopes, 0, "Locals", -1)
client.EvaluateRequest("dlv config showGlobalVariables true", 1000, "repl")
client.ExpectInvalidatedEvent(t)
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "showGlobalVariables\ttrue\n\nUpdated", noChildren)
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, "", false, [][2]string{}), noChildren)
client.ScopesRequest(1000)
scopes = client.ExpectScopesResponse(t)
@ -3894,7 +4003,7 @@ Type 'dlv help' followed by a command for full documentation.
checkScope(t, scopes, 1, "Globals (package main)", -1)
// Read and modify substitutePath.
client.EvaluateRequest("dlv config substitutePath", 1000, "repl")
client.EvaluateRequest("dlv config -list substitutePath", 1000, "repl")
got = client.ExpectEvaluateResponse(t)
checkEval(t, got, "substitutePath\t[]\n", noChildren)

View File

@ -154,9 +154,14 @@ type LaunchAttachCommonConfig struct {
ShowRegisters bool `json:"showRegisters,omitempty"`
// Boolean value to indicate whether system goroutines
// should be should be hidden from the call stack view.
// should be hidden from the call stack view.
HideSystemGoroutines bool `json:"hideSystemGoroutines,omitempty"`
// String value to indicate which system goroutines should be
// shown in the call stack view. See filtering documentation:
// https://github.com/go-delve/delve/blob/master/Documentation/cli/README.md#goroutines
GoroutineFilters string `json:"goroutineFilters,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.