proc,debugger,terminal: read goroutine ancestors

Add options to the stack command to read the goroutine ancestors.
Ancestor tracking was added to Go 1.12 with CL:
https://go-review.googlesource.com/c/go/+/70993/

Implements #1491
This commit is contained in:
aarzilli 2019-03-16 14:50:18 +01:00 committed by Alessandro Arzilli
parent 48f1f51ef9
commit 9826531597
10 changed files with 293 additions and 7 deletions

@ -357,11 +357,13 @@ If regex is specified only the source files matching it will be returned.
## stack
Print stack trace.
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-offsets] [-defer]
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-offsets] [-defer] [-a <n>] [-adepth <depth>]
-full every stackframe is decorated with the value of its local variables and arguments.
-offsets prints frame offset of each frame.
-defer prints deferred function call stack for each frame.
-a <n> prints stacktrace of n ancestors of the selected goroutine (target process must have tracebackancestors enabled)
-adepth <depth> configures depth of ancestor stacktrace
Aliases: bt

@ -4251,3 +4251,38 @@ func TestListImages(t *testing.T) {
}
})
}
func TestAncestors(t *testing.T) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) {
t.Skip("not supported on Go <= 1.10")
}
savedGodebug := os.Getenv("GODEBUG")
os.Setenv("GODEBUG", "tracebackancestors=100")
defer os.Setenv("GODEBUG", savedGodebug)
withTestProcess("testnextprog", t, func(p proc.Process, fixture protest.Fixture) {
_, err := setFunctionBreakpoint(p, "main.testgoroutine")
assertNoError(err, t, "setFunctionBreakpoint()")
assertNoError(proc.Continue(p), t, "Continue()")
as, err := p.SelectedGoroutine().Ancestors(1000)
assertNoError(err, t, "Ancestors")
t.Logf("ancestors: %#v\n", as)
if len(as) != 1 {
t.Fatalf("expected only one ancestor got %d", len(as))
}
mainFound := false
for i, a := range as {
astack, err := a.Stack(100)
assertNoError(err, t, fmt.Sprintf("Ancestor %d stack", i))
t.Logf("ancestor %d\n", i)
logStacktrace(t, p.BinInfo(), astack)
for _, frame := range astack {
if frame.Current.Fn != nil && frame.Current.Fn.Name == "main.main" {
mainFound = true
}
}
}
if !mainFound {
t.Fatal("could not find main.main function in ancestors")
}
})
}

@ -200,6 +200,12 @@ type G struct {
Unreadable error // could not read the G struct
}
type Ancestor struct {
ID int64 // Goroutine ID
Unreadable error
pcsVar *Variable
}
// EvalScope is the scope for variable evaluation. Contains the thread,
// current location (PC), and canonical frame address.
type EvalScope struct {
@ -617,6 +623,96 @@ func (g *G) StartLoc() Location {
return Location{PC: g.StartPC, File: f, Line: l, Fn: fn}
}
var errTracebackAncestorsDisabled = errors.New("tracebackancestors is disabled")
// Ancestors returns the list of ancestors for g.
func (g *G) Ancestors(n int) ([]Ancestor, error) {
scope := globalScope(g.Thread.BinInfo(), g.Thread)
tbav, err := scope.EvalExpression("runtime.debug.tracebackancestors", loadSingleValue)
if err == nil && tbav.Unreadable == nil && tbav.Kind == reflect.Int {
tba, _ := constant.Int64Val(tbav.Value)
if tba == 0 {
return nil, errTracebackAncestorsDisabled
}
}
av, err := g.variable.structMember("ancestors")
if err != nil {
return nil, err
}
av = av.maybeDereference()
av.loadValue(LoadConfig{MaxArrayValues: n, MaxVariableRecurse: 1, MaxStructFields: -1})
if av.Unreadable != nil {
return nil, err
}
if av.Addr == 0 {
// no ancestors
return nil, nil
}
r := make([]Ancestor, len(av.Children))
for i := range av.Children {
if av.Children[i].Unreadable != nil {
r[i].Unreadable = av.Children[i].Unreadable
continue
}
goidv := av.Children[i].fieldVariable("goid")
if goidv.Unreadable != nil {
r[i].Unreadable = goidv.Unreadable
continue
}
r[i].ID, _ = constant.Int64Val(goidv.Value)
pcsVar := av.Children[i].fieldVariable("pcs")
if pcsVar.Unreadable != nil {
r[i].Unreadable = pcsVar.Unreadable
}
pcsVar.loaded = false
pcsVar.Children = pcsVar.Children[:0]
r[i].pcsVar = pcsVar
}
return r, nil
}
// Stack returns the stack trace of ancestor 'a' as saved by the runtime.
func (a *Ancestor) Stack(n int) ([]Stackframe, error) {
if a.Unreadable != nil {
return nil, a.Unreadable
}
pcsVar := a.pcsVar.clone()
pcsVar.loadValue(LoadConfig{MaxArrayValues: n})
if pcsVar.Unreadable != nil {
return nil, pcsVar.Unreadable
}
r := make([]Stackframe, len(pcsVar.Children))
for i := range pcsVar.Children {
if pcsVar.Children[i].Unreadable != nil {
r[i] = Stackframe{Err: pcsVar.Children[i].Unreadable}
continue
}
if pcsVar.Children[i].Kind != reflect.Uint {
return nil, fmt.Errorf("wrong type for pcs item %d: %v", i, pcsVar.Children[i].Kind)
}
pc, _ := constant.Int64Val(pcsVar.Children[i].Value)
fn := a.pcsVar.bi.PCToFunc(uint64(pc))
if fn == nil {
loc := Location{PC: uint64(pc)}
r[i] = Stackframe{Current: loc, Call: loc}
continue
}
pc2 := uint64(pc)
if pc2-1 >= fn.Entry {
pc2--
}
f, ln := fn.cu.lineInfo.PCToLine(fn.Entry, pc2)
loc := Location{PC: uint64(pc), File: f, Line: ln, Fn: fn}
r[i] = Stackframe{Current: loc, Call: loc}
}
r[len(r)-1].Bottom = pcsVar.Len == int64(len(pcsVar.Children))
return r, nil
}
// Returns the list of saved return addresses used by stack barriers
func (g *G) stkbar() ([]savedLR, error) {
if g.stkbarVar == nil { // stack barriers were removed in Go 1.9

@ -251,11 +251,13 @@ When connected to a headless instance started with the --accept-multiclient, pas
Show source around current point or provided linespec.`},
{aliases: []string{"stack", "bt"}, allowedPrefixes: onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace.
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-offsets] [-defer]
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-offsets] [-defer] [-a <n>] [-adepth <depth>]
-full every stackframe is decorated with the value of its local variables and arguments.
-offsets prints frame offset of each frame.
-defer prints deferred function call stack for each frame.
-a <n> prints stacktrace of n ancestors of the selected goroutine (target process must have tracebackancestors enabled)
-adepth <depth> configures depth of ancestor stacktrace
`},
{aliases: []string{"frame"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
@ -1412,6 +1414,20 @@ func stackCommand(t *Term, ctx callContext, args string) error {
return err
}
printStack(stack, "", sa.offsets)
if sa.ancestors > 0 {
ancestors, err := t.client.Ancestors(ctx.Scope.GoroutineID, sa.ancestors, sa.ancestorDepth)
if err != nil {
return err
}
for _, ancestor := range ancestors {
fmt.Printf("Created by Goroutine %d:\n", ancestor.ID)
if ancestor.Unreadable != "" {
fmt.Printf("\t%s\n", ancestor.Unreadable)
continue
}
printStack(ancestor.Stack, "\t", false)
}
}
return nil
}
@ -1420,6 +1436,9 @@ type stackArgs struct {
full bool
offsets bool
readDefers bool
ancestors int
ancestorDepth int
}
func parseStackArgs(argstr string) (stackArgs, error) {
@ -1429,7 +1448,18 @@ func parseStackArgs(argstr string) (stackArgs, error) {
}
if argstr != "" {
args := strings.Split(argstr, " ")
for i := range args {
for i := 0; i < len(args); i++ {
numarg := func(name string) (int, error) {
if i >= len(args) {
return 0, fmt.Errorf("expected number after %s", name)
}
n, err := strconv.Atoi(args[i])
if err != nil {
return 0, fmt.Errorf("expected number after %s: %v", name, err)
}
return n, nil
}
switch args[i] {
case "-full":
r.full = true
@ -1437,6 +1467,20 @@ func parseStackArgs(argstr string) (stackArgs, error) {
r.offsets = true
case "-defer":
r.readDefers = true
case "-a":
i++
n, err := numarg("-a")
if err != nil {
return stackArgs{}, err
}
r.ancestors = n
case "-adepth":
i++
n, err := numarg("-adepth")
if err != nil {
return stackArgs{}, err
}
r.ancestorDepth = n
default:
n, err := strconv.Atoi(args[i])
if err != nil {
@ -1446,6 +1490,9 @@ func parseStackArgs(argstr string) (stackArgs, error) {
}
}
}
if r.ancestors > 0 && r.ancestorDepth == 0 {
r.ancestorDepth = r.depth
}
return r, nil
}

@ -447,3 +447,11 @@ type Checkpoint struct {
type Image struct {
Path string
}
// Ancestor represents a goroutine ancestor
type Ancestor struct {
ID int64
Stack []Stackframe
Unreadable string
}

@ -100,6 +100,9 @@ type Client interface {
// Returns stacktrace
Stacktrace(goroutineID int, depth int, readDefers bool, cfg *api.LoadConfig) ([]api.Stackframe, error)
// Returns ancestor stacktraces
Ancestors(goroutineID int, numAncestors int, depth int) ([]api.Ancestor, error)
// Returns whether we attached to a running process or not
AttachedToExistingProcess() bool

@ -947,6 +947,48 @@ func (d *Debugger) Stacktrace(goroutineID, depth int, readDefers bool, cfg *proc
return d.convertStacktrace(rawlocs, cfg)
}
// Ancestors returns the stacktraces for the ancestors of a goroutine.
func (d *Debugger) Ancestors(goroutineID, numAncestors, depth int) ([]api.Ancestor, error) {
d.processMutex.Lock()
defer d.processMutex.Unlock()
if _, err := d.target.Valid(); err != nil {
return nil, err
}
g, err := proc.FindGoroutine(d.target, goroutineID)
if err != nil {
return nil, err
}
if g == nil {
return nil, errors.New("no selected goroutine")
}
ancestors, err := g.Ancestors(numAncestors)
if err != nil {
return nil, err
}
r := make([]api.Ancestor, len(ancestors))
for i := range ancestors {
r[i].ID = ancestors[i].ID
if ancestors[i].Unreadable != nil {
r[i].Unreadable = ancestors[i].Unreadable.Error()
continue
}
frames, err := ancestors[i].Stack(depth)
if err != nil {
r[i].Unreadable = fmt.Sprintf("could not read ancestor stacktrace: %v", err)
continue
}
r[i].Stack, err = d.convertStacktrace(frames, nil)
if err != nil {
r[i].Unreadable = fmt.Sprintf("could not read ancestor stacktrace: %v", err)
}
}
return r, nil
}
func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadConfig) ([]api.Stackframe, error) {
locations := make([]api.Stackframe, 0, len(rawlocs))
for i := range rawlocs {

@ -310,6 +310,12 @@ func (c *RPCClient) Stacktrace(goroutineId, depth int, readDefers bool, cfg *api
return out.Locations, err
}
func (c *RPCClient) Ancestors(goroutineID int, numAncestors int, depth int) ([]api.Ancestor, error) {
var out AncestorsOut
err := c.call("Ancestors", AncestorsIn{goroutineID, numAncestors, depth}, &out)
return out.Ancestors, err
}
func (c *RPCClient) AttachedToExistingProcess() bool {
out := new(AttachedToExistingProcessOut)
c.call("AttachedToExistingProcess", AttachedToExistingProcessIn{}, out)

@ -174,10 +174,24 @@ func (s *RPCServer) Stacktrace(arg StacktraceIn, out *StacktraceOut) error {
}
var err error
out.Locations, err = s.debugger.Stacktrace(arg.Id, arg.Depth, arg.Defers, api.LoadConfigToProc(cfg))
if err != nil {
return err
}
return nil
return err
}
type AncestorsIn struct {
GoroutineID int
NumAncestors int
Depth int
}
type AncestorsOut struct {
Ancestors []api.Ancestor
}
// Ancestors returns the stacktraces for the ancestors of a goroutine.
func (s *RPCServer) Ancestors(arg AncestorsIn, out *AncestorsOut) error {
var err error
out.Ancestors, err = s.debugger.Ancestors(arg.GoroutineID, arg.NumAncestors, arg.Depth)
return err
}
type ListBreakpointsIn struct {

@ -1640,3 +1640,36 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) {
}
})
}
func TestAncestors(t *testing.T) {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) {
t.Skip("not supported on Go <= 1.10")
}
savedGodebug := os.Getenv("GODEBUG")
os.Setenv("GODEBUG", "tracebackancestors=100")
defer os.Setenv("GODEBUG", savedGodebug)
withTestClient2("testnextprog", t, func(c service.Client) {
_, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.testgoroutine", Line: -1})
assertNoError(err, t, "CreateBreakpoin")
state := <-c.Continue()
assertNoError(state.Err, t, "Continue()")
ancestors, err := c.Ancestors(-1, 1000, 1000)
assertNoError(err, t, "Ancestors")
t.Logf("ancestors: %#v\n", ancestors)
if len(ancestors) != 1 {
t.Fatalf("expected only one ancestor got %d", len(ancestors))
}
mainFound := false
for _, ancestor := range ancestors {
for _, frame := range ancestor.Stack {
if frame.Function.Name() == "main.main" {
mainFound = true
}
}
}
if !mainFound {
t.Fatal("function main.main not found in any ancestor")
}
})
}