
Users can create sparse maps in two ways, either by: a) adding lots of entries to a map and then deleting most of them, or b) using the make(mapType, N) expression with a very large N When this happens reading the resulting map will be very slow because loadMap needs to scan many buckets for each entry it finds. Technically this is not a bug, the user just created a map that's very sparse and therefore very slow to read. However it's very annoying to have the debugger hang for several seconds when trying to read the local variables just because one of them (which you might not even be interested into) happens to be a very sparse map. There is an easy mitigation to this problem: not reading any additional buckets once we know that we have already read all entries of the map, or as many entries as we need to fulfill the MaxArrayValues parameter. Unfortunately this is mostly useless, a VLSM (Very Large Sparse Map) with a single entry will still be slow to access, because the single entry in the map could easily end up in the last bucket. The obvious solution to this problem is to set a limit to the number of buckets we read when loading a map. However there is no good way to set this limit. If we hardcode it there will be no way to print maps that are beyond whatever limit we pick. We could let users (or clients) specify it but the meaning of such knob would be arcane and they would have no way of picking a good value (because there is no objectively good value for it). The solution used in this commit is to set an arbirtray limit on the number of buckets we read but only when loadMap is invoked through API calls ListLocalVars and ListFunctionArgs. In this way `ListLocalVars` and `ListFunctionArgs` (which are often invoked automatically by GUI clients) remain fast even in presence of a VLSM, but the contents of the VLSM can still be inspected using `EvalVariable`.
317 lines
7.1 KiB
Go
317 lines
7.1 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"go/constant"
|
|
"go/printer"
|
|
"go/token"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
"github.com/derekparker/delve/pkg/dwarf/godwarf"
|
|
"github.com/derekparker/delve/pkg/proc"
|
|
)
|
|
|
|
// ConvertBreakpoint converts from a proc.Breakpoint to
|
|
// an api.Breakpoint.
|
|
func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
|
|
b := &Breakpoint{
|
|
Name: bp.Name,
|
|
ID: bp.ID,
|
|
FunctionName: bp.FunctionName,
|
|
File: bp.File,
|
|
Line: bp.Line,
|
|
Addr: bp.Addr,
|
|
Tracepoint: bp.Tracepoint,
|
|
TraceReturn: bp.TraceReturn,
|
|
Stacktrace: bp.Stacktrace,
|
|
Goroutine: bp.Goroutine,
|
|
Variables: bp.Variables,
|
|
LoadArgs: LoadConfigFromProc(bp.LoadArgs),
|
|
LoadLocals: LoadConfigFromProc(bp.LoadLocals),
|
|
TotalHitCount: bp.TotalHitCount,
|
|
}
|
|
|
|
b.HitCount = map[string]uint64{}
|
|
for idx := range bp.HitCount {
|
|
b.HitCount[strconv.Itoa(idx)] = bp.HitCount[idx]
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
printer.Fprint(&buf, token.NewFileSet(), bp.Cond)
|
|
b.Cond = buf.String()
|
|
|
|
return b
|
|
}
|
|
|
|
// ConvertThread converts a proc.Thread into an
|
|
// api thread.
|
|
func ConvertThread(th proc.Thread) *Thread {
|
|
var (
|
|
function *Function
|
|
file string
|
|
line int
|
|
pc uint64
|
|
gid int
|
|
)
|
|
|
|
loc, err := th.Location()
|
|
if err == nil {
|
|
pc = loc.PC
|
|
file = loc.File
|
|
line = loc.Line
|
|
function = ConvertFunction(loc.Fn)
|
|
}
|
|
|
|
var bp *Breakpoint
|
|
|
|
if b := th.Breakpoint(); b.Active {
|
|
bp = ConvertBreakpoint(b.Breakpoint)
|
|
}
|
|
|
|
if g, _ := proc.GetG(th); g != nil {
|
|
gid = g.ID
|
|
}
|
|
|
|
return &Thread{
|
|
ID: th.ThreadID(),
|
|
PC: pc,
|
|
File: file,
|
|
Line: line,
|
|
Function: function,
|
|
GoroutineID: gid,
|
|
Breakpoint: bp,
|
|
}
|
|
}
|
|
|
|
func prettyTypeName(typ godwarf.Type) string {
|
|
if typ == nil {
|
|
return ""
|
|
}
|
|
if typ.Common().Name != "" {
|
|
return typ.Common().Name
|
|
}
|
|
r := typ.String()
|
|
if r == "*void" {
|
|
return "unsafe.Pointer"
|
|
}
|
|
return r
|
|
}
|
|
|
|
func convertFloatValue(v *proc.Variable, sz int) string {
|
|
switch v.FloatSpecial {
|
|
case proc.FloatIsPosInf:
|
|
return "+Inf"
|
|
case proc.FloatIsNegInf:
|
|
return "-Inf"
|
|
case proc.FloatIsNaN:
|
|
return "NaN"
|
|
}
|
|
f, _ := constant.Float64Val(v.Value)
|
|
return strconv.FormatFloat(f, 'f', -1, sz)
|
|
}
|
|
|
|
// ConvertVar converts from proc.Variable to api.Variable.
|
|
func ConvertVar(v *proc.Variable) *Variable {
|
|
r := Variable{
|
|
Addr: v.Addr,
|
|
OnlyAddr: v.OnlyAddr,
|
|
Name: v.Name,
|
|
Kind: v.Kind,
|
|
Len: v.Len,
|
|
Cap: v.Cap,
|
|
Flags: VariableFlags(v.Flags),
|
|
Base: v.Base,
|
|
|
|
LocationExpr: v.LocationExpr,
|
|
DeclLine: v.DeclLine,
|
|
}
|
|
|
|
r.Type = prettyTypeName(v.DwarfType)
|
|
r.RealType = prettyTypeName(v.RealType)
|
|
|
|
if v.Unreadable != nil {
|
|
r.Unreadable = v.Unreadable.Error()
|
|
}
|
|
|
|
if v.Value != nil {
|
|
switch v.Kind {
|
|
case reflect.Float32:
|
|
r.Value = convertFloatValue(v, 32)
|
|
case reflect.Float64:
|
|
r.Value = convertFloatValue(v, 64)
|
|
case reflect.String, reflect.Func:
|
|
r.Value = constant.StringVal(v.Value)
|
|
default:
|
|
r.Value = v.ConstDescr()
|
|
if r.Value == "" {
|
|
r.Value = v.Value.String()
|
|
}
|
|
}
|
|
}
|
|
|
|
switch v.Kind {
|
|
case reflect.Complex64:
|
|
r.Children = make([]Variable, 2)
|
|
r.Len = 2
|
|
|
|
r.Children[0].Name = "real"
|
|
r.Children[0].Kind = reflect.Float32
|
|
|
|
r.Children[1].Name = "imaginary"
|
|
r.Children[1].Kind = reflect.Float32
|
|
|
|
if v.Value != nil {
|
|
real, _ := constant.Float64Val(constant.Real(v.Value))
|
|
r.Children[0].Value = strconv.FormatFloat(real, 'f', -1, 32)
|
|
|
|
imag, _ := constant.Float64Val(constant.Imag(v.Value))
|
|
r.Children[1].Value = strconv.FormatFloat(imag, 'f', -1, 32)
|
|
} else {
|
|
r.Children[0].Value = "nil"
|
|
r.Children[1].Value = "nil"
|
|
}
|
|
|
|
case reflect.Complex128:
|
|
r.Children = make([]Variable, 2)
|
|
r.Len = 2
|
|
|
|
r.Children[0].Name = "real"
|
|
r.Children[0].Kind = reflect.Float64
|
|
|
|
r.Children[1].Name = "imaginary"
|
|
r.Children[1].Kind = reflect.Float64
|
|
|
|
if v.Value != nil {
|
|
real, _ := constant.Float64Val(constant.Real(v.Value))
|
|
r.Children[0].Value = strconv.FormatFloat(real, 'f', -1, 64)
|
|
|
|
imag, _ := constant.Float64Val(constant.Imag(v.Value))
|
|
r.Children[1].Value = strconv.FormatFloat(imag, 'f', -1, 64)
|
|
} else {
|
|
r.Children[0].Value = "nil"
|
|
r.Children[1].Value = "nil"
|
|
}
|
|
|
|
default:
|
|
r.Children = make([]Variable, len(v.Children))
|
|
|
|
for i := range v.Children {
|
|
r.Children[i] = *ConvertVar(&v.Children[i])
|
|
}
|
|
}
|
|
|
|
return &r
|
|
}
|
|
|
|
// ConvertFunction converts from gosym.Func to
|
|
// api.Function.
|
|
func ConvertFunction(fn *proc.Function) *Function {
|
|
if fn == nil {
|
|
return nil
|
|
}
|
|
|
|
// fn here used to be a *gosym.Func, the fields Type and GoType below
|
|
// corresponded to the homonymous field of gosym.Func. Since the contents of
|
|
// those fields is not documented their value was replaced with 0 when
|
|
// gosym.Func was replaced by debug_info entries.
|
|
return &Function{
|
|
Name_: fn.Name,
|
|
Type: 0,
|
|
Value: fn.Entry,
|
|
GoType: 0,
|
|
Optimized: fn.Optimized(),
|
|
}
|
|
}
|
|
|
|
// ConvertGoroutine converts from proc.G to api.Goroutine.
|
|
func ConvertGoroutine(g *proc.G) *Goroutine {
|
|
th := g.Thread
|
|
tid := 0
|
|
if th != nil {
|
|
tid = th.ThreadID()
|
|
}
|
|
r := &Goroutine{
|
|
ID: g.ID,
|
|
CurrentLoc: ConvertLocation(g.CurrentLoc),
|
|
UserCurrentLoc: ConvertLocation(g.UserCurrent()),
|
|
GoStatementLoc: ConvertLocation(g.Go()),
|
|
StartLoc: ConvertLocation(g.StartLoc()),
|
|
ThreadID: tid,
|
|
}
|
|
if g.Unreadable != nil {
|
|
r.Unreadable = g.Unreadable.Error()
|
|
}
|
|
return r
|
|
}
|
|
|
|
// ConvertLocation converts from proc.Location to api.Location.
|
|
func ConvertLocation(loc proc.Location) Location {
|
|
return Location{
|
|
PC: loc.PC,
|
|
File: loc.File,
|
|
Line: loc.Line,
|
|
Function: ConvertFunction(loc.Fn),
|
|
}
|
|
}
|
|
|
|
// ConvertAsmInstruction converts from proc.AsmInstruction to api.AsmInstruction.
|
|
func ConvertAsmInstruction(inst proc.AsmInstruction, text string) AsmInstruction {
|
|
var destloc *Location
|
|
if inst.DestLoc != nil {
|
|
r := ConvertLocation(*inst.DestLoc)
|
|
destloc = &r
|
|
}
|
|
return AsmInstruction{
|
|
Loc: ConvertLocation(inst.Loc),
|
|
DestLoc: destloc,
|
|
Text: text,
|
|
Bytes: inst.Bytes,
|
|
Breakpoint: inst.Breakpoint,
|
|
AtPC: inst.AtPC,
|
|
}
|
|
}
|
|
|
|
// LoadConfigToProc converts an api.LoadConfig to proc.LoadConfig.
|
|
func LoadConfigToProc(cfg *LoadConfig) *proc.LoadConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
return &proc.LoadConfig{
|
|
cfg.FollowPointers,
|
|
cfg.MaxVariableRecurse,
|
|
cfg.MaxStringLen,
|
|
cfg.MaxArrayValues,
|
|
cfg.MaxStructFields,
|
|
0, // MaxMapBuckets is set internally by pkg/proc, read its documentation for an explanation.
|
|
}
|
|
}
|
|
|
|
// LoadConfigFromProc converts a proc.LoadConfig to api.LoadConfig.
|
|
func LoadConfigFromProc(cfg *proc.LoadConfig) *LoadConfig {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
return &LoadConfig{
|
|
cfg.FollowPointers,
|
|
cfg.MaxVariableRecurse,
|
|
cfg.MaxStringLen,
|
|
cfg.MaxArrayValues,
|
|
cfg.MaxStructFields,
|
|
}
|
|
}
|
|
|
|
// ConvertRegisters converts proc.Register to api.Register for a slice.
|
|
func ConvertRegisters(in []proc.Register) (out []Register) {
|
|
out = make([]Register, len(in))
|
|
for i := range in {
|
|
out[i] = Register{in[i].Name, in[i].Value}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ConvertCheckpoint converts proc.Chekcpoint to api.Checkpoint.
|
|
func ConvertCheckpoint(in proc.Checkpoint) (out Checkpoint) {
|
|
return Checkpoint(in)
|
|
}
|