proc: cache goroutine objects

Adds a cache mapping goroutine IDs to goroutine objects, this allows
speeding up FindGoroutine and makes commands like 'goroutines -t' not
be accidentally quadratic in the number of goroutines.
This commit is contained in:
aarzilli 2020-01-14 16:22:33 +01:00 committed by Derek Parker
parent 666a618f47
commit 9af1eac341
2 changed files with 69 additions and 32 deletions

@ -114,12 +114,20 @@ type BreakpointManipulation interface {
// CommonProcess contains fields used by this package, common to all
// implementations of the Process interface.
type CommonProcess struct {
allGCache []*G
goroutineCache
fncallEnabled bool
fncallForG map[int]*callInjection
}
type goroutineCache struct {
partialGCache map[int]*G
allGCache []*G
allgentryAddr, allglenAddr uint64
}
type callInjection struct {
// if continueCompleted is not nil it means we are in the process of
// executing an injected function call, see comments throughout
@ -133,8 +141,3 @@ type callInjection struct {
func NewCommonProcess(fncallEnabled bool) CommonProcess {
return CommonProcess{fncallEnabled: fncallEnabled, fncallForG: make(map[int]*callInjection)}
}
// ClearAllGCache clears the cached contents of the cache for runtime.allgs.
func (p *CommonProcess) ClearAllGCache() {
p.allGCache = nil
}

@ -18,6 +18,8 @@ var ErrNotExecutable = errors.New("not an executable file")
// only possible on recorded (traced) programs.
var ErrNotRecorded = errors.New("not a recording")
var ErrNoRuntimeAllG = errors.New("could not find goroutine array")
const (
// UnrecoveredPanic is the name given to the unrecovered panic breakpoint.
UnrecoveredPanic = "unrecovered-panic"
@ -72,6 +74,8 @@ func PostInitializationSetup(p Process, path string, debugInfoDirs []string, wri
createUnrecoveredPanicBreakpoint(p, writeBreakpoint)
createFatalThrowBreakpoint(p, writeBreakpoint)
p.Common().goroutineCache.init(p.BinInfo())
return nil
}
@ -507,12 +511,9 @@ func GoroutinesInfo(dbp Process, start, count int) ([]*G, int, error) {
}
}
exeimage := dbp.BinInfo().Images[0] // Image corresponding to the executable file
var (
threadg = map[int]*G{}
allg []*G
rdr = exeimage.DwarfReader()
)
threads := dbp.ThreadList()
@ -526,32 +527,10 @@ func GoroutinesInfo(dbp Process, start, count int) ([]*G, int, error) {
}
}
addr, err := rdr.AddrFor("runtime.allglen", exeimage.StaticBase)
allgptr, allglen, err := dbp.Common().getRuntimeAllg(dbp.BinInfo(), dbp.CurrentThread())
if err != nil {
return nil, -1, err
}
allglenBytes := make([]byte, 8)
_, err = dbp.CurrentThread().ReadMemory(allglenBytes, uintptr(addr))
if err != nil {
return nil, -1, err
}
allglen := binary.LittleEndian.Uint64(allglenBytes)
rdr.Seek(0)
allgentryaddr, err := rdr.AddrFor("runtime.allgs", exeimage.StaticBase)
if err != nil {
// try old name (pre Go 1.6)
allgentryaddr, err = rdr.AddrFor("runtime.allg", exeimage.StaticBase)
if err != nil {
return nil, -1, err
}
}
faddr := make([]byte, dbp.BinInfo().Arch.PtrSize())
_, err = dbp.CurrentThread().ReadMemory(faddr, uintptr(allgentryaddr))
if err != nil {
return nil, -1, err
}
allgptr := binary.LittleEndian.Uint64(faddr)
for i := uint64(start); i < allglen; i++ {
if count != 0 && len(allg) >= count {
@ -580,6 +559,7 @@ func GoroutinesInfo(dbp Process, start, count int) ([]*G, int, error) {
if g.Status != Gdead {
allg = append(allg, g)
}
dbp.Common().addGoroutine(g)
}
if start == 0 {
dbp.Common().allGCache = allg
@ -588,6 +568,56 @@ func GoroutinesInfo(dbp Process, start, count int) ([]*G, int, error) {
return allg, -1, nil
}
func (gcache *goroutineCache) init(bi *BinaryInfo) {
var err error
exeimage := bi.Images[0]
rdr := exeimage.DwarfReader()
gcache.allglenAddr, _ = rdr.AddrFor("runtime.allglen", exeimage.StaticBase)
rdr.Seek(0)
gcache.allgentryAddr, err = rdr.AddrFor("runtime.allgs", exeimage.StaticBase)
if err != nil {
// try old name (pre Go 1.6)
gcache.allgentryAddr, _ = rdr.AddrFor("runtime.allg", exeimage.StaticBase)
}
}
func (gcache *goroutineCache) getRuntimeAllg(bi *BinaryInfo, mem MemoryReadWriter) (uint64, uint64, error) {
if gcache.allglenAddr == 0 || gcache.allgentryAddr == 0 {
return 0, 0, ErrNoRuntimeAllG
}
allglenBytes := make([]byte, 8)
_, err := mem.ReadMemory(allglenBytes, uintptr(gcache.allglenAddr))
if err != nil {
return 0, 0, err
}
allglen := binary.LittleEndian.Uint64(allglenBytes)
faddr := make([]byte, bi.Arch.PtrSize())
_, err = mem.ReadMemory(faddr, uintptr(gcache.allgentryAddr))
if err != nil {
return 0, 0, err
}
allgptr := binary.LittleEndian.Uint64(faddr)
return allgptr, allglen, nil
}
func (gcache *goroutineCache) addGoroutine(g *G) {
if gcache.partialGCache == nil {
gcache.partialGCache = make(map[int]*G)
}
gcache.partialGCache[g.ID] = g
}
// ClearAllGCache clears the cached contents of the cache for runtime.allgs.
func (gcache *goroutineCache) ClearAllGCache() {
gcache.partialGCache = nil
gcache.allGCache = nil
}
// FindGoroutine returns a G struct representing the goroutine
// specified by `gid`.
func FindGoroutine(dbp Process, gid int) (*G, error) {
@ -627,6 +657,10 @@ func FindGoroutine(dbp Process, gid int) (*G, error) {
}
}
if g := dbp.Common().partialGCache[gid]; g != nil {
return g, nil
}
const goroutinesInfoLimit = 10
nextg := 0
for nextg >= 0 {