From 11accd4d71eb65671332016ce4a2b2b0148755d7 Mon Sep 17 00:00:00 2001 From: Sergio Lopez Date: Mon, 19 Nov 2018 15:18:10 +0100 Subject: [PATCH] proc/proc: Extend GoroutinesInfo to allow specifying a range Instead of unconditionally returning all present goroutines, GoroutinesInfo now allows specifying a range (start and count). In addition to the array of goroutines and the error, it now also returns the next goroutine to be processed, to be used as 'start' argument on the next call, or 0 if all present goroutines have already been processed. This way clients can avoid eating large amounts of RAM while debugging core dumps and processes with a exceptionally high amount of goroutines. Fixes #1403 --- pkg/proc/core/core_test.go | 6 ++-- pkg/proc/proc.go | 37 +++++++++++++-------- pkg/proc/proc_test.go | 14 ++++---- pkg/proc/variable_test.go | 2 +- pkg/terminal/command.go | 53 +++++++++++++++++++++---------- service/client.go | 2 +- service/debugger/debugger.go | 8 ++--- service/rpc1/server.go | 2 +- service/rpc2/client.go | 6 ++-- service/rpc2/server.go | 6 +++- service/test/integration2_test.go | 4 +-- 11 files changed, 87 insertions(+), 53 deletions(-) diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go index 0ecbd0ab..11766934 100644 --- a/pkg/proc/core/core_test.go +++ b/pkg/proc/core/core_test.go @@ -190,7 +190,7 @@ func TestCore(t *testing.T) { } p := withCoreFile(t, "panic", "") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) if err != nil || len(gs) == 0 { t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err) } @@ -260,7 +260,7 @@ func TestCoreFpRegisters(t *testing.T) { p := withCoreFile(t, "fputest/", "panic") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) if err != nil || len(gs) == 0 { t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err) } @@ -337,7 +337,7 @@ func TestCoreWithEmptyString(t *testing.T) { } p := withCoreFile(t, "coreemptystring", "") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo") var mainFrame *proc.Stackframe diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go index 0fe9db56..a845187d 100644 --- a/pkg/proc/proc.go +++ b/pkg/proc/proc.go @@ -488,14 +488,22 @@ func StepOut(dbp Process) error { return Continue(dbp) } -// GoroutinesInfo returns an array of G structures representing the information -// Delve cares about from the internal runtime G structure. -func GoroutinesInfo(dbp Process) ([]*G, error) { +// GoroutinesInfo searches for goroutines starting at index 'start', and +// returns an array of up to 'count' (or all found elements, if 'count' is 0) +// G structures representing the information Delve care about from the internal +// runtime G structure. +// GoroutinesInfo also returns the next index to be used as 'start' argument +// while scanning for all available goroutines, or -1 if there was an error +// or if the index already reached the last possible value. +func GoroutinesInfo(dbp Process, start, count int) ([]*G, int, error) { if _, err := dbp.Valid(); err != nil { - return nil, err + return nil, -1, err } if dbp.Common().allGCache != nil { - return dbp.Common().allGCache, nil + // We can't use the cached array to fulfill a subrange request + if start == 0 && (count == 0 || count >= len(dbp.Common().allGCache)) { + return dbp.Common().allGCache, -1, nil + } } var ( @@ -517,12 +525,12 @@ func GoroutinesInfo(dbp Process) ([]*G, error) { addr, err := rdr.AddrFor("runtime.allglen", dbp.BinInfo().staticBase) if err != nil { - return nil, err + return nil, -1, err } allglenBytes := make([]byte, 8) _, err = dbp.CurrentThread().ReadMemory(allglenBytes, uintptr(addr)) if err != nil { - return nil, err + return nil, -1, err } allglen := binary.LittleEndian.Uint64(allglenBytes) @@ -532,17 +540,20 @@ func GoroutinesInfo(dbp Process) ([]*G, error) { // try old name (pre Go 1.6) allgentryaddr, err = rdr.AddrFor("runtime.allg", dbp.BinInfo().staticBase) if err != nil { - return nil, err + return nil, -1, err } } faddr := make([]byte, dbp.BinInfo().Arch.PtrSize()) _, err = dbp.CurrentThread().ReadMemory(faddr, uintptr(allgentryaddr)) if err != nil { - return nil, err + return nil, -1, err } allgptr := binary.LittleEndian.Uint64(faddr) - for i := uint64(0); i < allglen; i++ { + for i := uint64(start); i < allglen; i++ { + if count != 0 && len(allg) >= count { + return allg, int(i), nil + } gvar, err := newGVariable(dbp.CurrentThread(), uintptr(allgptr+(i*uint64(dbp.BinInfo().Arch.PtrSize()))), true) if err != nil { allg = append(allg, &G{Unreadable: err}) @@ -556,7 +567,7 @@ func GoroutinesInfo(dbp Process) ([]*G, error) { if thg, allocated := threadg[g.ID]; allocated { loc, err := thg.Thread.Location() if err != nil { - return nil, err + return nil, -1, err } g.Thread = thg.Thread // Prefer actual thread location information. @@ -569,7 +580,7 @@ func GoroutinesInfo(dbp Process) ([]*G, error) { } dbp.Common().allGCache = allg - return allg, nil + return allg, -1, nil } // FindGoroutine returns a G struct representing the goroutine @@ -579,7 +590,7 @@ func FindGoroutine(dbp Process, gid int) (*G, error) { return dbp.SelectedGoroutine(), nil } - gs, err := GoroutinesInfo(dbp) + gs, _, err := GoroutinesInfo(dbp, 0, 0) if err != nil { return nil, err } diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 3177bebf..4a5ce8d9 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -920,7 +920,7 @@ func TestStacktraceGoroutine(t *testing.T) { assertNoError(proc.Continue(p), t, "Continue()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo") agoroutineCount := 0 @@ -1239,7 +1239,7 @@ func TestFrameEvaluation(t *testing.T) { t.Logf("stopped on thread %d, goroutine: %#v", p.CurrentThread().ThreadID(), p.SelectedGoroutine()) // Testing evaluation on goroutines - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo") found := make([]bool, 10) for _, g := range gs { @@ -1523,7 +1523,7 @@ func BenchmarkGoroutinesInfo(b *testing.B) { assertNoError(proc.Continue(p), b, "Continue()") for i := 0; i < b.N; i++ { p.Common().ClearAllGCache() - _, err := proc.GoroutinesInfo(p) + _, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, b, "GoroutinesInfo") } }) @@ -1953,7 +1953,7 @@ func TestNextParked(t *testing.T) { } assertNoError(err, t, "Continue()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo()") // Search for a parked goroutine that we know for sure will have to be @@ -2005,7 +2005,7 @@ func TestStepParked(t *testing.T) { } assertNoError(err, t, "Continue()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo()") for _, g := range gs { @@ -2729,7 +2729,7 @@ func TestStacktraceWithBarriers(t *testing.T) { return } assertNoError(err, t, "Continue()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo()") for _, th := range p.ThreadList() { if bp := th.Breakpoint(); bp.Breakpoint == nil { @@ -2760,7 +2760,7 @@ func TestStacktraceWithBarriers(t *testing.T) { assertNoError(proc.StepOut(p), t, "StepOut()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo()") for _, goid := range stackBarrierGoids { diff --git a/pkg/proc/variable_test.go b/pkg/proc/variable_test.go index 631f5b75..3718afe1 100644 --- a/pkg/proc/variable_test.go +++ b/pkg/proc/variable_test.go @@ -15,7 +15,7 @@ func TestGoroutineCreationLocation(t *testing.T) { assertNoError(err, t, "BreakByLocation()") assertNoError(proc.Continue(p), t, "Continue()") - gs, err := proc.GoroutinesInfo(p) + gs, _, err := proc.GoroutinesInfo(p, 0, 0) assertNoError(err, t, "GoroutinesInfo") for _, g := range gs { diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 3c702c4c..be1ce43d 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -570,6 +570,27 @@ 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 + +func printGoroutines(t *Term, gs []*api.Goroutine, fgl formatGoroutineLoc, bPrintStack bool, state *api.DebuggerState) error { + for _, g := range gs { + prefix := " " + if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID { + prefix = "* " + } + fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g, fgl)) + if bPrintStack { + stack, err := t.client.Stacktrace(g.ID, 10, false, nil) + if err != nil { + return err + } + printStack(stack, "\t", false) + } + } + return nil +} + func goroutines(t *Term, ctx callContext, argstr string) error { args := strings.Split(argstr, " ") var fgl = fglUserCurrent @@ -604,26 +625,24 @@ func goroutines(t *Term, ctx callContext, argstr string) error { if err != nil { return err } - gs, err := t.client.ListGoroutines() - if err != nil { - return err - } - sort.Sort(byGoroutineID(gs)) - fmt.Printf("[%d goroutines]\n", len(gs)) - for _, g := range gs { - prefix := " " - if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID { - prefix = "* " + var ( + start = 0 + gslen = 0 + gs []*api.Goroutine + ) + for start >= 0 { + gs, start, err = t.client.ListGoroutines(start, goroutineBatchSize) + if err != nil { + return err } - fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g, fgl)) - if bPrintStack { - stack, err := t.client.Stacktrace(g.ID, 10, false, nil) - if err != nil { - return err - } - printStack(stack, "\t", false) + sort.Sort(byGoroutineID(gs)) + err = printGoroutines(t, gs, fgl, bPrintStack, state) + if err != nil { + return err } + gslen += len(gs) } + fmt.Printf("[%d goroutines]\n", gslen) return nil } diff --git a/service/client.go b/service/client.go index f4211c3a..d0f9a04a 100644 --- a/service/client.go +++ b/service/client.go @@ -95,7 +95,7 @@ type Client interface { ListRegisters(threadID int, includeFp bool) (api.Registers, error) // ListGoroutines lists all goroutines. - ListGoroutines() ([]*api.Goroutine, error) + ListGoroutines(start, count int) ([]*api.Goroutine, int, error) // Returns stacktrace Stacktrace(goroutineID int, depth int, readDefers bool, cfg *api.LoadConfig) ([]api.Stackframe, error) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 0fffa13e..fefc4a2f 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -907,19 +907,19 @@ func (d *Debugger) SetVariableInScope(scope api.EvalScope, symbol, value string) } // Goroutines will return a list of goroutines in the target process. -func (d *Debugger) Goroutines() ([]*api.Goroutine, error) { +func (d *Debugger) Goroutines(start, count int) ([]*api.Goroutine, int, error) { d.processMutex.Lock() defer d.processMutex.Unlock() goroutines := []*api.Goroutine{} - gs, err := proc.GoroutinesInfo(d.target) + gs, nextg, err := proc.GoroutinesInfo(d.target, start, count) if err != nil { - return nil, err + return nil, 0, err } for _, g := range gs { goroutines = append(goroutines, api.ConvertGoroutine(g)) } - return goroutines, err + return goroutines, nextg, err } // Stacktrace returns a list of Stackframes for the given goroutine. The diff --git a/service/rpc1/server.go b/service/rpc1/server.go index eae6d87a..e71e1348 100644 --- a/service/rpc1/server.go +++ b/service/rpc1/server.go @@ -283,7 +283,7 @@ func (s *RPCServer) ListTypes(filter string, types *[]string) error { } func (s *RPCServer) ListGoroutines(arg interface{}, goroutines *[]*api.Goroutine) error { - gs, err := s.debugger.Goroutines() + gs, _, err := s.debugger.Goroutines(0, 0) if err != nil { return err } diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 5ada18bc..fc4501cb 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -298,10 +298,10 @@ func (c *RPCClient) ListFunctionArgs(scope api.EvalScope, cfg api.LoadConfig) ([ return out.Args, err } -func (c *RPCClient) ListGoroutines() ([]*api.Goroutine, error) { +func (c *RPCClient) ListGoroutines(start, count int) ([]*api.Goroutine, int, error) { var out ListGoroutinesOut - err := c.call("ListGoroutines", ListGoroutinesIn{}, &out) - return out.Goroutines, err + err := c.call("ListGoroutines", ListGoroutinesIn{start, count}, &out) + return out.Goroutines, out.Nextg, err } func (c *RPCClient) Stacktrace(goroutineId, depth int, readDefers bool, cfg *api.LoadConfig) ([]api.Stackframe, error) { diff --git a/service/rpc2/server.go b/service/rpc2/server.go index e9193cf5..62edb30e 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -508,19 +508,23 @@ func (s *RPCServer) ListTypes(arg ListTypesIn, out *ListTypesOut) error { } type ListGoroutinesIn struct { + Start int + Count int } type ListGoroutinesOut struct { Goroutines []*api.Goroutine + Nextg int } // ListGoroutines lists all goroutines. func (s *RPCServer) ListGoroutines(arg ListGoroutinesIn, out *ListGoroutinesOut) error { - gs, err := s.debugger.Goroutines() + gs, nextg, err := s.debugger.Goroutines(arg.Start, arg.Count) if err != nil { return err } out.Goroutines = gs + out.Nextg = nextg return nil } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 654c524e..50230ae7 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -801,7 +801,7 @@ func TestClientServer_FullStacktrace(t *testing.T) { t.Fatalf("Continue(): %v\n", state.Err) } - gs, err := c.ListGoroutines() + gs, _, err := c.ListGoroutines(0, 0) assertNoError(err, t, "GoroutinesInfo()") found := make([]bool, 10) for _, g := range gs { @@ -916,7 +916,7 @@ func TestIssue355(t *testing.T) { assertError(err, t, "ListFunctionArgs()") _, err = c.ListRegisters(0, false) assertError(err, t, "ListRegisters()") - _, err = c.ListGoroutines() + _, _, err = c.ListGoroutines(0, 0) assertError(err, t, "ListGoroutines()") _, err = c.Stacktrace(gid, 10, false, &normalLoadConfig) assertError(err, t, "Stacktrace()") -- GitLab