terminal,proc: Improved goroutine printing

Three locations are returned for goroutines: its current location,
its current location excluding unexported runtime functions and
the location of its go instruction.
The command 'goroutines' takes a new parameter to select which
location to print (defaulting to current location w/o runtime)
This commit is contained in:
aarzilli
2015-10-16 08:42:02 +02:00
committed by Derek Parker
parent 2b4fef44a5
commit cb529eafab
6 changed files with 174 additions and 50 deletions

View File

@ -487,9 +487,7 @@ func (dbp *Process) GoroutinesInfo() ([]*G, error) {
} }
g.thread = thread g.thread = thread
// Prefer actual thread location information. // Prefer actual thread location information.
g.File = loc.File g.Current = *loc
g.Line = loc.Line
g.Func = loc.Fn
} }
if g.Status != Gdead { if g.Status != Gdead {
allg = append(allg, g) allg = append(allg, g)

View File

@ -70,6 +70,59 @@ func (n NullAddrError) Error() string {
return "NULL address" return "NULL address"
} }
type StackIterator struct {
pc, sp uint64
top bool
frame Stackframe
dbp *Process
atend bool
err error
}
func newStackIterator(dbp *Process, pc, sp uint64) *StackIterator {
return &StackIterator{pc: pc, sp: sp, top: true, dbp: dbp, err: nil, atend: false}
}
func (it *StackIterator) Next() bool {
if it.err != nil || it.atend {
return false
}
it.frame, it.err = it.dbp.frameInfo(it.pc, it.sp, it.top)
if it.err != nil {
return false
}
if it.frame.Current.Fn == nil {
return false
}
if it.frame.Ret <= 0 {
it.atend = true
return true
}
// Look for "top of stack" functions.
if it.frame.Current.Fn.Name == "runtime.goexit" || it.frame.Current.Fn.Name == "runtime.rt0_go" {
it.atend = true
return true
}
it.top = false
it.pc = it.frame.Ret
it.sp = uint64(it.frame.CFA)
return true
}
func (it *StackIterator) Frame() Stackframe {
if it.err != nil {
panic(it.err)
}
return it.frame
}
func (it *StackIterator) Err() error {
return it.err
}
func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) { func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) {
f, l, fn := dbp.PCToLine(pc) f, l, fn := dbp.PCToLine(pc)
fde, err := dbp.frameEntries.FDEForPC(pc) fde, err := dbp.frameEntries.FDEForPC(pc)
@ -99,26 +152,15 @@ func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) {
func (dbp *Process) stacktrace(pc, sp uint64, depth int) ([]Stackframe, error) { func (dbp *Process) stacktrace(pc, sp uint64, depth int) ([]Stackframe, error) {
frames := make([]Stackframe, 0, depth+1) frames := make([]Stackframe, 0, depth+1)
it := newStackIterator(dbp, pc, sp)
for i := 0; i < depth+1; i++ { for it.Next() {
frame, err := dbp.frameInfo(pc, sp, i == 0) frames = append(frames, it.Frame())
if err != nil { if len(frames) >= depth+1 {
return nil, err
}
if frame.Current.Fn == nil {
break break
} }
frames = append(frames, frame) }
if frame.Ret <= 0 { if err := it.Err(); err != nil {
break return nil, err
}
// Look for "top of stack" functions.
if frame.Current.Fn.Name == "runtime.goexit" || frame.Current.Fn.Name == "runtime.rt0_go" {
break
}
pc = frame.Ret
sp = uint64(frame.CFA)
} }
return frames, nil return frames, nil
} }

View File

@ -3,7 +3,6 @@ package proc
import ( import (
"bytes" "bytes"
"debug/dwarf" "debug/dwarf"
"debug/gosym"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"go/ast" "go/ast"
@ -73,16 +72,16 @@ type G struct {
WaitReason string // Reason for goroutine being parked. WaitReason string // Reason for goroutine being parked.
Status uint64 Status uint64
// Information on goroutine location. // Information on goroutine location
File string Current Location
Line int
Func *gosym.Func
// PC of entry to top-most deferred function. // PC of entry to top-most deferred function.
DeferPC uint64 DeferPC uint64
// Thread that this goroutine is currently allocated to // Thread that this goroutine is currently allocated to
thread *Thread thread *Thread
dbp *Process
} }
// Scope for variable evaluation // Scope for variable evaluation
@ -292,16 +291,48 @@ func parseG(thread *Thread, gaddr uint64, deref bool) (*G, error) {
GoPC: gopc, GoPC: gopc,
PC: pc, PC: pc,
SP: sp, SP: sp,
File: f, Current: Location{PC: pc, File: f, Line: l, Fn: fn},
Line: l,
Func: fn,
WaitReason: waitreason, WaitReason: waitreason,
DeferPC: deferPC, DeferPC: deferPC,
Status: atomicStatus, Status: atomicStatus,
dbp: thread.dbp,
} }
return g, nil return g, nil
} }
// From $GOROOT/src/runtime/traceback.go:597
// isExportedRuntime reports whether name is an exported runtime function.
// It is only for runtime functions, so ASCII A-Z is fine.
func isExportedRuntime(name string) bool {
const n = len("runtime.")
return len(name) > n && name[:n] == "runtime." && 'A' <= name[n] && name[n] <= 'Z'
}
func (g *G) UserCurrent() Location {
pc, sp := g.PC, g.SP
if g.thread != nil {
regs, err := g.thread.Registers()
if err != nil {
return g.Current
}
pc, sp = regs.PC(), regs.SP()
}
it := newStackIterator(g.dbp, pc, sp)
for it.Next() {
frame := it.Frame()
name := frame.Call.Fn.Name
if (strings.Index(name, ".") >= 0) && (!strings.HasPrefix(name, "runtime.") || isExportedRuntime(name)) {
return frame.Call
}
}
return g.Current
}
func (g *G) Go() Location {
f, l, fn := g.dbp.goSymTable.PCToLine(g.GoPC)
return Location{PC: g.GoPC, File: f, Line: l, Fn: fn}
}
// Returns information for the named variable. // Returns information for the named variable.
func (scope *EvalScope) ExtractVariableInfo(name string) (*Variable, error) { func (scope *EvalScope) ExtractVariableInfo(name string) (*Variable, error) {
parts := strings.Split(name, ".") parts := strings.Split(name, ".")

View File

@ -81,11 +81,10 @@ func ConvertFunction(fn *gosym.Func) *Function {
// convertGoroutine converts an internal Goroutine to an API Goroutine. // convertGoroutine converts an internal Goroutine to an API Goroutine.
func ConvertGoroutine(g *proc.G) *Goroutine { func ConvertGoroutine(g *proc.G) *Goroutine {
return &Goroutine{ return &Goroutine{
ID: g.Id, ID: g.Id,
PC: g.PC, Current: ConvertLocation(g.Current),
File: g.File, UserCurrent: ConvertLocation(g.UserCurrent()),
Line: g.Line, Go: ConvertLocation(g.Go()),
Function: ConvertFunction(g.Func),
} }
} }

View File

@ -114,14 +114,12 @@ type Variable struct {
type Goroutine struct { type Goroutine struct {
// ID is a unique identifier for the goroutine. // ID is a unique identifier for the goroutine.
ID int `json:"id"` ID int `json:"id"`
// PC is the current program counter for the goroutine. // Current location of the goroutine
PC uint64 `json:"pc"` Current Location
// File is the file for the program counter. // Current location of the goroutine, excluding calls inside runtime
File string `json:"file"` UserCurrent Location
// Line is the line number for the program counter. // Location of the go instruction that started this goroutine
Line int `json:"line"` Go Location
// Function is function information at the program counter. May be nil.
Function *Function `json:"function,omitempty"`
} }
// DebuggerCommand is a command which changes the debugger's execution state. // DebuggerCommand is a command which changes the debugger's execution state.

View File

@ -5,6 +5,7 @@ package terminal
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io"
"math" "math"
"os" "os"
"regexp" "regexp"
@ -223,6 +224,25 @@ 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 } func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func goroutines(t *Term, args ...string) error { func goroutines(t *Term, args ...string) error {
var fgl = fglUserCurrent
switch len(args) {
case 0:
// nothing to do
case 1:
switch args[0] {
case "-u":
fgl = fglUserCurrent
case "-r":
fgl = fglRuntimeCurrent
case "-g":
fgl = fglGo
default:
fmt.Errorf("wrong argument: '%s'", args[0])
}
default:
return fmt.Errorf("too many arguments")
}
state, err := t.client.GetState() state, err := t.client.GetState()
if err != nil { if err != nil {
return err return err
@ -238,7 +258,7 @@ func goroutines(t *Term, args ...string) error {
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID { if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = "* " prefix = "* "
} }
fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g)) fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g, fgl))
} }
return nil return nil
} }
@ -355,7 +375,10 @@ func printscope(t *Term) error {
return err return err
} }
fmt.Printf("Thread %s\nGoroutine %s\n", formatThread(state.CurrentThread), formatGoroutine(state.SelectedGoroutine)) fmt.Printf("Thread %s\n", formatThread(state.CurrentThread))
if state.SelectedGoroutine != nil {
writeGoroutineLong(os.Stdout, state.SelectedGoroutine, "")
}
return nil return nil
} }
@ -366,15 +389,48 @@ func formatThread(th *api.Thread) string {
return fmt.Sprintf("%d at %s:%d", th.ID, shortenFilePath(th.File), th.Line) return fmt.Sprintf("%d at %s:%d", th.ID, shortenFilePath(th.File), th.Line)
} }
func formatGoroutine(g *api.Goroutine) string { type formatGoroutineLoc int
const (
fglRuntimeCurrent = formatGoroutineLoc(iota)
fglUserCurrent
fglGo
)
func formatLocation(loc api.Location) string {
fname := ""
if loc.Function != nil {
fname = loc.Function.Name
}
return fmt.Sprintf("%s:%d %s (%#v)", shortenFilePath(loc.File), loc.Line, fname, loc.PC)
}
func formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string {
if g == nil { if g == nil {
return "<nil>" return "<nil>"
} }
fname := "" var locname string
if g.Function != nil { var loc api.Location
fname = g.Function.Name switch fgl {
case fglRuntimeCurrent:
locname = "Runtime"
loc = g.Current
case fglUserCurrent:
locname = "User"
loc = g.UserCurrent
case fglGo:
locname = "Go"
loc = g.Go
} }
return fmt.Sprintf("%d - %s:%d %s (%#v)", g.ID, shortenFilePath(g.File), g.Line, fname, g.PC) return fmt.Sprintf("%d - %s: %s", g.ID, locname, formatLocation(loc))
}
func writeGoroutineLong(w io.Writer, g *api.Goroutine, prefix string) {
fmt.Fprintf(w, "%sGoroutine %d:\n%s\tRuntime: %s\n%s\tUser: %s\n%s\tGo: %s\n",
prefix, g.ID,
prefix, formatLocation(g.Current),
prefix, formatLocation(g.UserCurrent),
prefix, formatLocation(g.Go))
} }
func restart(t *Term, args ...string) error { func restart(t *Term, args ...string) error {
@ -803,7 +859,7 @@ func printcontext(t *Term, state *api.DebuggerState) error {
bpi := state.BreakpointInfo bpi := state.BreakpointInfo
if bpi.Goroutine != nil { if bpi.Goroutine != nil {
fmt.Printf("\tGoroutine %s\n", formatGoroutine(bpi.Goroutine)) writeGoroutineLong(os.Stdout, bpi.Goroutine, "\t")
} }
ss := make([]string, len(bpi.Variables)) ss := make([]string, len(bpi.Variables))