Skip to content

Commit

Permalink
proc,service,terminal: add ways to list goroutines waiting on a chann…
Browse files Browse the repository at this point in the history
…el (#3481)

Adds -chan option to the goroutines command to list only the goroutines
running on a specified channel.
Also when printing a variable if it is a channel also print the list of
goroutines that are waiting on it.
  • Loading branch information
aarzilli authored Aug 23, 2023
1 parent 80e6c28 commit 0b35fe6
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 23 deletions.
10 changes: 9 additions & 1 deletion Documentation/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ Aliases: gr
## goroutines
List program goroutines.

goroutines [-u|-r|-g|-s] [-t [depth]] [-l] [-with loc expr] [-without loc expr] [-group argument] [-exec command]
goroutines [-u|-r|-g|-s] [-t [depth]] [-l] [-with loc expr] [-without loc expr] [-group argument] [-chan expr] [-exec command]

Print out info for every goroutine. The flag controls what information is shown along with each goroutine:

Expand Down Expand Up @@ -437,6 +437,14 @@ To only display user (or runtime) goroutines, use:
goroutines -with user
goroutines -without user

CHANNELS

To only show goroutines waiting to send to or receive from a specific channel use:

goroutines -chan expr

Note that 'expr' must not contain spaces.

GROUPING

goroutines -group (userloc|curloc|goloc|startloc|running|user)
Expand Down
2 changes: 1 addition & 1 deletion Documentation/cli/starlark.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ checkpoints() | Equivalent to API call [ListCheckpoints](https://godoc.org/githu
dynamic_libraries() | Equivalent to API call [ListDynamicLibraries](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListDynamicLibraries)
function_args(Scope, Cfg) | Equivalent to API call [ListFunctionArgs](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListFunctionArgs)
functions(Filter) | Equivalent to API call [ListFunctions](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListFunctions)
goroutines(Start, Count, Filters, GoroutineGroupingOptions) | Equivalent to API call [ListGoroutines](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListGoroutines)
goroutines(Start, Count, Filters, GoroutineGroupingOptions, EvalScope) | Equivalent to API call [ListGoroutines](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListGoroutines)
local_vars(Scope, Cfg) | Equivalent to API call [ListLocalVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListLocalVars)
package_vars(Filter, Cfg) | Equivalent to API call [ListPackageVars](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackageVars)
packages_build_info(IncludeFiles) | Equivalent to API call [ListPackagesBuildInfo](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackagesBuildInfo)
Expand Down
26 changes: 26 additions & 0 deletions _fixtures/changoroutines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"runtime"
"time"
)

func main() {
blockingchan1 := make(chan int)
blockingchan2 := make(chan int)

go sendToChan("one", blockingchan1)
go sendToChan("two", blockingchan1)
go recvFromChan(blockingchan2)
time.Sleep(time.Second)

runtime.Breakpoint()
}

func sendToChan(name string, ch chan<- int) {
ch <- 1
}

func recvFromChan(ch <-chan int) {
<-ch
}
83 changes: 83 additions & 0 deletions pkg/proc/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,89 @@ func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable,
return ev, nil
}

// ChanGoroutines returns the list of goroutines waiting to receive from or
// send to the channel.
func (scope *EvalScope) ChanGoroutines(expr string, start, count int) ([]int64, error) {
t, err := parser.ParseExpr(expr)
if err != nil {
return nil, err
}
v, err := scope.evalAST(t)
if err != nil {
return nil, err
}
if v.Kind != reflect.Chan {
return nil, nil
}

structMemberMulti := func(v *Variable, names ...string) *Variable {
for _, name := range names {
var err error
v, err = v.structMember(name)
if err != nil {
return nil
}
}
return v
}

waitqFirst := func(qname string) *Variable {
qvar := structMemberMulti(v, qname, "first")
if qvar == nil {
return nil
}
return qvar.maybeDereference()
}

var goids []int64

waitqToGoIDSlice := func(qvar *Variable) error {
if qvar == nil {
return nil
}
for {
if qvar.Addr == 0 {
return nil
}
if len(goids) > count {
return nil
}
goidVar := structMemberMulti(qvar, "g", "goid")
if goidVar == nil {
return nil
}
goidVar.loadValue(loadSingleValue)
if goidVar.Unreadable != nil {
return goidVar.Unreadable
}
goid, _ := constant.Int64Val(goidVar.Value)
if start > 0 {
start--
} else {
goids = append(goids, goid)
}

nextVar, err := qvar.structMember("next")
if err != nil {
return err
}
qvar = nextVar.maybeDereference()
}
}

recvqVar := waitqFirst("recvq")
err = waitqToGoIDSlice(recvqVar)
if err != nil {
return nil, err
}
sendqVar := waitqFirst("sendq")
err = waitqToGoIDSlice(sendqVar)
if err != nil {
return nil, err
}
return goids, nil
}

func isAssignment(err error) (int, bool) {
el, isScannerErr := err.(scanner.ErrorList)
if isScannerErr && el[0].Msg == "expected '==', found '='" {
Expand Down
34 changes: 30 additions & 4 deletions pkg/terminal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ If called with the locspec argument it will delete all the breakpoints matching
toggle <breakpoint name or id>`},
{aliases: []string{"goroutines", "grs"}, group: goroutineCmds, cmdFn: c.goroutines, helpMsg: `List program goroutines.
goroutines [-u|-r|-g|-s] [-t [depth]] [-l] [-with loc expr] [-without loc expr] [-group argument] [-exec command]
goroutines [-u|-r|-g|-s] [-t [depth]] [-l] [-with loc expr] [-without loc expr] [-group argument] [-chan expr] [-exec command]
Print out info for every goroutine. The flag controls what information is shown along with each goroutine:
Expand Down Expand Up @@ -279,6 +279,14 @@ To only display user (or runtime) goroutines, use:
goroutines -with user
goroutines -without user
CHANNELS
To only show goroutines waiting to send to or receive from a specific channel use:
goroutines -chan expr
Note that 'expr' must not contain spaces.
GROUPING
goroutines -group (userloc|curloc|goloc|startloc|running|user)
Expand Down Expand Up @@ -318,7 +326,7 @@ Called with more arguments it will execute a command on the specified goroutine.
breakpoints [-a]
Specifying -a prints all physical breakpoint, including internal breakpoints.`},
{aliases: []string{"print", "p"}, group: dataCmds, allowedPrefixes: onPrefix | deferredPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression.
{aliases: []string{"print", "p"}, group: dataCmds, allowedPrefixes: onPrefix | deferredPrefix, cmdFn: c.printVar, helpMsg: `Evaluate an expression.
[goroutine <n>] [frame <m>] print [%format] <expression>
Expand Down Expand Up @@ -902,7 +910,7 @@ func (c *Commands) goroutines(t *Term, ctx callContext, argstr string) error {
fmt.Fprintf(t.stdout, "interrupted\n")
return nil
}
gs, groups, start, tooManyGroups, err = t.client.ListGoroutinesWithFilter(start, batchSize, filters, &group)
gs, groups, start, tooManyGroups, err = t.client.ListGoroutinesWithFilter(start, batchSize, filters, &group, &api.EvalScope{GoroutineID: -1, Frame: c.frame})
if err != nil {
return err
}
Expand Down Expand Up @@ -2090,7 +2098,9 @@ func parseFormatArg(args string) (fmtstr, argsOut string) {
return v[0], v[1]
}

func printVar(t *Term, ctx callContext, args string) error {
const maxPrintVarChanGoroutines = 100

func (c *Commands) printVar(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
Expand All @@ -2105,6 +2115,22 @@ func printVar(t *Term, ctx callContext, args string) error {
}

fmt.Fprintln(t.stdout, val.MultilineString("", fmtstr))

if val.Kind == reflect.Chan {
fmt.Fprintln(t.stdout)
gs, _, _, _, err := t.client.ListGoroutinesWithFilter(0, maxPrintVarChanGoroutines, []api.ListGoroutinesFilter{{Kind: api.GoroutineWaitingOnChannel, Arg: fmt.Sprintf("*(*%q)(%#x)", val.Type, val.Addr)}}, nil, &ctx.Scope)
if err != nil {
fmt.Fprintf(t.stdout, "Error reading channel wait queue: %v", err)
} else {
fmt.Fprintln(t.stdout, "Goroutines waiting on this channel:")
state, err := t.client.GetState()
if err != nil {
fmt.Fprintf(t.stdout, "Error printing channel wait queue: %v", err)
}
var done bool
c.printGoroutines(t, ctx, "", gs, api.FglUserCurrent, 0, 0, "", &done, state)
}
}
return nil
}

Expand Down
13 changes: 12 additions & 1 deletion pkg/terminal/starbind/starlark_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -1143,6 +1143,15 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) {
return starlark.None, decorateError(thread, err)
}
}
if len(args) > 4 && args[4] != starlark.None {
err := unmarshalStarlarkValue(args[4], &rpcArgs.EvalScope, "EvalScope")
if err != nil {
return starlark.None, decorateError(thread, err)
}
} else {
scope := env.ctx.Scope()
rpcArgs.EvalScope = &scope
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
Expand All @@ -1154,6 +1163,8 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) {
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Filters, "Filters")
case "GoroutineGroupingOptions":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.GoroutineGroupingOptions, "GoroutineGroupingOptions")
case "EvalScope":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.EvalScope, "EvalScope")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}
Expand All @@ -1167,7 +1178,7 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) {
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
doc["goroutines"] = "builtin goroutines(Start, Count, Filters, GoroutineGroupingOptions)\n\ngoroutines lists all goroutines.\nIf Count is specified ListGoroutines will return at the first Count\ngoroutines and an index in Nextg, that can be passed as the Start\nparameter, to get more goroutines from ListGoroutines.\nPassing a value of Start that wasn't returned by ListGoroutines will skip\nan undefined number of goroutines.\n\nIf arg.Filters are specified the list of returned goroutines is filtered\napplying the specified filters.\nFor example:\n\n\tListGoroutinesFilter{ Kind: ListGoroutinesFilterUserLoc, Negated: false, Arg: \"afile.go\" }\n\nwill only return goroutines whose UserLoc contains \"afile.go\" as a substring.\nMore specifically a goroutine matches a location filter if the specified\nlocation, formatted like this:\n\n\tfilename:lineno in function\n\ncontains Arg[0] as a substring.\n\nFilters can also be applied to goroutine labels:\n\n\tListGoroutineFilter{ Kind: ListGoroutinesFilterLabel, Negated: false, Arg: \"key=value\" }\n\nthis filter will only return goroutines that have a key=value label.\n\nIf arg.GroupBy is not GoroutineFieldNone then the goroutines will\nbe grouped with the specified criterion.\nIf the value of arg.GroupBy is GoroutineLabel goroutines will\nbe grouped by the value of the label with key GroupByKey.\nFor each group a maximum of MaxGroupMembers example goroutines are\nreturned, as well as the total number of goroutines in the group."
doc["goroutines"] = "builtin goroutines(Start, Count, Filters, GoroutineGroupingOptions, EvalScope)\n\ngoroutines lists all goroutines.\nIf Count is specified ListGoroutines will return at the first Count\ngoroutines and an index in Nextg, that can be passed as the Start\nparameter, to get more goroutines from ListGoroutines.\nPassing a value of Start that wasn't returned by ListGoroutines will skip\nan undefined number of goroutines.\n\nIf arg.Filters are specified the list of returned goroutines is filtered\napplying the specified filters.\nFor example:\n\n\tListGoroutinesFilter{ Kind: ListGoroutinesFilterUserLoc, Negated: false, Arg: \"afile.go\" }\n\nwill only return goroutines whose UserLoc contains \"afile.go\" as a substring.\nMore specifically a goroutine matches a location filter if the specified\nlocation, formatted like this:\n\n\tfilename:lineno in function\n\ncontains Arg[0] as a substring.\n\nFilters can also be applied to goroutine labels:\n\n\tListGoroutineFilter{ Kind: ListGoroutinesFilterLabel, Negated: false, Arg: \"key=value\" }\n\nthis filter will only return goroutines that have a key=value label.\n\nIf arg.GroupBy is not GoroutineFieldNone then the goroutines will\nbe grouped with the specified criterion.\nIf the value of arg.GroupBy is GoroutineLabel goroutines will\nbe grouped by the value of the label with key GroupByKey.\nFor each group a maximum of MaxGroupMembers example goroutines are\nreturned, as well as the total number of goroutines in the group."
r["local_vars"] = starlark.NewBuiltin("local_vars", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, decorateError(thread, err)
Expand Down
8 changes: 8 additions & 0 deletions service/api/command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"errors"
"fmt"
"strconv"
"strings"
Expand Down Expand Up @@ -99,6 +100,13 @@ func ParseGoroutineArgs(argstr string) ([]ListGoroutinesFilter, GoroutineGroupin
}
batchSize = 0 // grouping only works well if run on all goroutines

case "-chan":
i++
if i >= len(args) {
return nil, GoroutineGroupingOptions{}, 0, 0, 0, 0, "", errors.New("not enough arguments after -chan")
}
filters = append(filters, ListGoroutinesFilter{Kind: GoroutineWaitingOnChannel, Arg: args[i]})

case "-exec":
flags |= PrintGoroutinesExec
cmd = strings.Join(args[i+1:], " ")
Expand Down
17 changes: 9 additions & 8 deletions service/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,14 +635,15 @@ type ListGoroutinesFilter struct {
type GoroutineField uint8

const (
GoroutineFieldNone GoroutineField = iota
GoroutineCurrentLoc // the goroutine's CurrentLoc
GoroutineUserLoc // the goroutine's UserLoc
GoroutineGoLoc // the goroutine's GoStatementLoc
GoroutineStartLoc // the goroutine's StartLoc
GoroutineLabel // the goroutine's label
GoroutineRunning // the goroutine is running
GoroutineUser // the goroutine is a user goroutine
GoroutineFieldNone GoroutineField = iota
GoroutineCurrentLoc // the goroutine's CurrentLoc
GoroutineUserLoc // the goroutine's UserLoc
GoroutineGoLoc // the goroutine's GoStatementLoc
GoroutineStartLoc // the goroutine's StartLoc
GoroutineLabel // the goroutine's label
GoroutineRunning // the goroutine is running
GoroutineUser // the goroutine is a user goroutine
GoroutineWaitingOnChannel // the goroutine is waiting on the channel specified by the argument
)

// GoroutineGroup represents a group of goroutines in the return value of
Expand Down
2 changes: 1 addition & 1 deletion service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ type Client interface {
// ListGoroutines lists all goroutines.
ListGoroutines(start, count int) ([]*api.Goroutine, int, error)
// ListGoroutinesWithFilter lists goroutines matching the filters
ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error)
ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions, scope *api.EvalScope) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error)

// Stacktrace returns stacktrace
Stacktrace(goroutineID int64, depth int, opts api.StacktraceOptions, cfg *api.LoadConfig) ([]api.Stackframe, error)
Expand Down
27 changes: 27 additions & 0 deletions service/debugger/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,8 @@ func matchGoroutineFilter(tgt *proc.Target, g *proc.G, filter *api.ListGoroutine
val = g.Thread != nil
case api.GoroutineUser:
val = !g.System(tgt)
case api.GoroutineWaitingOnChannel:
val = true // handled elsewhere
}
if filter.Negated {
val = !val
Expand Down Expand Up @@ -2325,6 +2327,31 @@ func (d *Debugger) DebugInfoDirectories() []string {
return d.target.Selected.BinInfo().DebugInfoDirectories
}

// ChanGoroutines returns the list of goroutines waiting on the channel specified by expr.
func (d *Debugger) ChanGoroutines(goid int64, frame, deferredCall int, expr string, start, count int) ([]*proc.G, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
s, err := proc.ConvertEvalScope(d.target.Selected, goid, frame, deferredCall)
if err != nil {
return nil, err
}

goids, err := s.ChanGoroutines(expr, start, count)
if err != nil {
return nil, err
}

gs := make([]*proc.G, len(goids))
for i := range goids {
g, err := proc.FindGoroutine(d.target.Selected, goids[i])
if g == nil {
g = &proc.G{Unreadable: err}
}
gs[i] = g
}
return gs, nil
}

func go11DecodeErrorCheck(err error) error {
if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr {
return err
Expand Down
6 changes: 3 additions & 3 deletions service/rpc2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,16 @@ func (c *RPCClient) ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([

func (c *RPCClient) ListGoroutines(start, count int) ([]*api.Goroutine, int, error) {
var out ListGoroutinesOut
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, nil, api.GoroutineGroupingOptions{}}, &out)
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, nil, api.GoroutineGroupingOptions{}, nil}, &out)
return out.Goroutines, out.Nextg, err
}

func (c *RPCClient) ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error) {
func (c *RPCClient) ListGoroutinesWithFilter(start, count int, filters []api.ListGoroutinesFilter, group *api.GoroutineGroupingOptions, scope *api.EvalScope) ([]*api.Goroutine, []api.GoroutineGroup, int, bool, error) {
if group == nil {
group = &api.GoroutineGroupingOptions{}
}
var out ListGoroutinesOut
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, filters, *group}, &out)
err := c.call("ListGoroutines", ListGoroutinesIn{start, count, filters, *group, scope}, &out)
return out.Goroutines, out.Groups, out.Nextg, out.TooManyGroups, err
}

Expand Down
Loading

0 comments on commit 0b35fe6

Please sign in to comment.