diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 01818a5036..2754e69551 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -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: @@ -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) diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 5b35dab759..da0b3d3a26 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -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) diff --git a/_fixtures/changoroutines.go b/_fixtures/changoroutines.go new file mode 100644 index 0000000000..7f8b884880 --- /dev/null +++ b/_fixtures/changoroutines.go @@ -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 +} diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go index 51161e4493..02e010106e 100644 --- a/pkg/proc/eval.go +++ b/pkg/proc/eval.go @@ -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 '='" { diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 430b9b12e4..886eb27a36 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -228,7 +228,7 @@ If called with the locspec argument it will delete all the breakpoints matching toggle `}, {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: @@ -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) @@ -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 ] [frame ] print [%format] @@ -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 } @@ -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") } @@ -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 } diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 5ff0433c37..967e1bf94c 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -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) { @@ -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]) } @@ -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) diff --git a/service/api/command.go b/service/api/command.go index 86e43cd039..922d14546d 100644 --- a/service/api/command.go +++ b/service/api/command.go @@ -1,6 +1,7 @@ package api import ( + "errors" "fmt" "strconv" "strings" @@ -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:], " ") diff --git a/service/api/types.go b/service/api/types.go index 13a1e8d2e7..6e893a5327 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -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 diff --git a/service/client.go b/service/client.go index f0cdc668b4..6e42224f0e 100644 --- a/service/client.go +++ b/service/client.go @@ -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) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index c07eb1b563..3e73621c67 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -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 @@ -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 diff --git a/service/rpc2/client.go b/service/rpc2/client.go index e05ae8fbb3..22f88382f7 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -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 } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index c6536e061b..ab18d23202 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -534,7 +534,8 @@ func (s *RPCServer) Eval(arg EvalIn, out *EvalOut) error { if cfg == nil { cfg = &api.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1} } - v, err := s.debugger.EvalVariableInScope(arg.Scope.GoroutineID, arg.Scope.Frame, arg.Scope.DeferredCall, arg.Expr, *api.LoadConfigToProc(cfg)) + pcfg := *api.LoadConfigToProc(cfg) + v, err := s.debugger.EvalVariableInScope(arg.Scope.GoroutineID, arg.Scope.Frame, arg.Scope.DeferredCall, arg.Expr, pcfg) if err != nil { return err } @@ -617,6 +618,8 @@ type ListGoroutinesIn struct { Filters []api.ListGoroutinesFilter api.GoroutineGroupingOptions + + EvalScope *api.EvalScope } type ListGoroutinesOut struct { @@ -663,7 +666,37 @@ func (s *RPCServer) ListGoroutines(arg ListGoroutinesIn, out *ListGoroutinesOut) //TODO(aarzilli): if arg contains a running goroutines filter (not negated) // and start == 0 and count == 0 then we can optimize this by just looking // at threads directly. - gs, nextg, err := s.debugger.Goroutines(arg.Start, arg.Count) + + var gs []*proc.G + var nextg int + var err error + var gsLoaded bool + + for _, filter := range arg.Filters { + if filter.Kind == api.GoroutineWaitingOnChannel { + if filter.Negated { + return errors.New("channel filter can not be negated") + } + if arg.Count == 0 { + return errors.New("count == 0 not allowed with a channel filter") + } + if arg.EvalScope == nil { + return errors.New("channel filter without eval scope") + } + gs, err = s.debugger.ChanGoroutines(arg.EvalScope.GoroutineID, arg.EvalScope.Frame, arg.EvalScope.DeferredCall, filter.Arg, arg.Start, arg.Count) + if len(gs) == arg.Count { + nextg = arg.Start + len(gs) + } else { + nextg = -1 + } + gsLoaded = true + break + } + } + + if !gsLoaded { + gs, nextg, err = s.debugger.Goroutines(arg.Start, arg.Count) + } if err != nil { return err } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 6aaa938ea2..2d7f64f77f 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -2577,7 +2577,7 @@ func TestGoroutinesGrouping(t *testing.T) { withTestClient2("goroutinegroup", t, func(c service.Client) { state := <-c.Continue() assertNoError(state.Err, t, "Continue") - _, ggrp, _, _, err := c.ListGoroutinesWithFilter(0, 0, nil, &api.GoroutineGroupingOptions{GroupBy: api.GoroutineLabel, GroupByKey: "name", MaxGroupMembers: 5, MaxGroups: 10}) + _, ggrp, _, _, err := c.ListGoroutinesWithFilter(0, 0, nil, &api.GoroutineGroupingOptions{GroupBy: api.GoroutineLabel, GroupByKey: "name", MaxGroupMembers: 5, MaxGroups: 10}, nil) assertNoError(err, t, "ListGoroutinesWithFilter (group by label)") t.Logf("%#v\n", ggrp) if len(ggrp) < 5 { @@ -2590,7 +2590,7 @@ func TestGoroutinesGrouping(t *testing.T) { break } } - gs, _, _, _, err := c.ListGoroutinesWithFilter(0, 0, []api.ListGoroutinesFilter{{Kind: api.GoroutineLabel, Arg: "name="}}, nil) + gs, _, _, _, err := c.ListGoroutinesWithFilter(0, 0, []api.ListGoroutinesFilter{{Kind: api.GoroutineLabel, Arg: "name="}}, nil, nil) assertNoError(err, t, "ListGoroutinesWithFilter (filter unnamed)") if len(gs) != unnamedCount { t.Errorf("wrong number of goroutines returned by filter: %d (expected %d)\n", len(gs), unnamedCount) @@ -3031,3 +3031,70 @@ func TestClientServer_breakpointOnFuncWithABIWrapper(t *testing.T) { } }) } + +var waitReasonStrings = [...]string{ + "", + "GC assist marking", + "IO wait", + "chan receive (nil chan)", + "chan send (nil chan)", + "dumping heap", + "garbage collection", + "garbage collection scan", + "panicwait", + "select", + "select (no cases)", + "GC assist wait", + "GC sweep wait", + "GC scavenge wait", + "chan receive", + "chan send", + "finalizer wait", + "force gc (idle)", + "semacquire", + "sleep", + "sync.Cond.Wait", + "timer goroutine (idle)", + "trace reader (blocked)", + "wait for GC cycle", + "GC worker (idle)", + "preempted", + "debug call", +} + +func TestClientServer_chanGoroutines(t *testing.T) { + protest.AllowRecording(t) + withTestClient2("changoroutines", t, func(c service.Client) { + state := <-c.Continue() + assertNoError(state.Err, t, "Continue()") + + countRecvSend := func(gs []*api.Goroutine) (recvq, sendq int) { + for _, g := range gs { + t.Logf("\tID: %d WaitReason: %s\n", g.ID, waitReasonStrings[g.WaitReason]) + switch waitReasonStrings[g.WaitReason] { + case "chan send": + sendq++ + case "chan receive": + recvq++ + } + } + return + } + + gs, _, _, _, err := c.ListGoroutinesWithFilter(0, 100, []api.ListGoroutinesFilter{{Kind: api.GoroutineWaitingOnChannel, Arg: "blockingchan1"}}, nil, &api.EvalScope{GoroutineID: -1}) + assertNoError(err, t, "ListGoroutinesWithFilter(blockingchan1)") + t.Logf("blockingchan1 gs:") + recvq, sendq := countRecvSend(gs) + if len(gs) != 2 || recvq != 0 || sendq != 2 { + t.Error("wrong number of goroutines for blockingchan1") + } + + gs, _, _, _, err = c.ListGoroutinesWithFilter(0, 100, []api.ListGoroutinesFilter{{Kind: api.GoroutineWaitingOnChannel, Arg: "blockingchan2"}}, nil, &api.EvalScope{GoroutineID: -1}) + assertNoError(err, t, "ListGoroutinesWithFilter(blockingchan2)") + t.Logf("blockingchan2 gs:") + recvq, sendq = countRecvSend(gs) + if len(gs) != 1 || recvq != 1 || sendq != 0 { + t.Error("wrong number of goroutines for blockingchan2") + } + }) +}