proc,service,terminal: add ways to list goroutines waiting on a channel (#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.
This commit is contained in:
Alessandro Arzilli 2023-08-23 22:02:34 +02:00 committed by GitHub
parent 80e6c28ab2
commit 0b35fe6d42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 313 additions and 23 deletions

@ -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)

@ -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)

@ -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
}

@ -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 '='" {

@ -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:
@ -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 <n>] [frame <m>] print [%format] <expression>
@ -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
}

@ -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)

@ -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:], " ")

@ -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

@ -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)

@ -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

@ -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
}

@ -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
}

@ -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")
}
})
}