Skip to content

Commit

Permalink
pkg/perf: Prevent intermediate perfmap/jitdump entry lists
Browse files Browse the repository at this point in the history
Previously we maintained multiple lists of perfmap and jitdump entries
as well as all related strings. This caused somewhat large memory spikes
when a lot of these have to be loaded frequently.

Now strings are never on heap and are directly written to the optimized
symtab file, and only entries are loaded into memory once so they can be
sorted and deduplicated easily and then without any further intermediate
lists directly written to the optimized symtab file.

If this is also not sufficient the next optimization would be to read
perfmap files from the back and directly write entries that are read if
they don't conflict with any already written memory ranges.
  • Loading branch information
brancz committed Nov 30, 2023
1 parent 963005a commit 5632b66
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 292 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.4
require (
buf.build/gen/go/prometheus/prometheus/protocolbuffers/go v1.31.0-20231019191021-98a368fa6cc9.2
github.com/Masterminds/semver/v3 v3.2.1
github.com/RoaringBitmap/roaring v1.6.0
github.com/alecthomas/kong v0.8.1
github.com/aquasecurity/libbpfgo v0.5.1-libbpf-1.2.0.20231128112859-0f5b9c71492d
github.com/aquasecurity/libbpfgo/helpers v0.4.5
Expand Down Expand Up @@ -78,7 +79,6 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/RoaringBitmap/roaring v1.6.0 // indirect
github.com/aliyun/aliyun-oss-go-sdk v3.0.1+incompatible // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/apache/arrow/go/v14 v14.0.1 // indirect
Expand Down
73 changes: 54 additions & 19 deletions pkg/perf/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ type jitdumpCacheValue struct {

var ErrJITDumpNotFound = errors.New("jitdump not found")

func ReadJITdump(logger log.Logger, fileName string) (Map, error) {
func ReadJITdump(
logger log.Logger,
fileName string,
w *symtab.FileWriter,
) (Map, error) {
fd, err := os.Open(fileName)
if err != nil {
return Map{}, err
Expand All @@ -72,9 +76,17 @@ func ReadJITdump(logger log.Logger, fileName string) (Map, error) {
}

addrs := make([]MapAddr, 0, len(dump.CodeLoads))
st := NewStringTable(16*1024, 1024)
for _, cl := range dump.CodeLoads {
addrs = append(addrs, MapAddr{cl.CodeAddr, cl.CodeAddr + cl.CodeSize, st.GetOrAdd([]byte(cl.Name))})
offset, err := w.AddString(cl.Name)
if err != nil {
return Map{}, fmt.Errorf("writing string: %w", err)
}
addrs = append(addrs, MapAddr{
Start: cl.CodeAddr,
End: cl.CodeAddr + cl.CodeSize,
SymbolOffset: offset,
SymbolLen: uint16(len(cl.Name)),
})
}

// Sorted by end address to allow binary search during look-up. End to find
Expand All @@ -84,7 +96,7 @@ func ReadJITdump(logger log.Logger, fileName string) (Map, error) {
return addrs[i].End < addrs[j].End
})

return Map{Path: fileName, addrs: addrs, stringTable: st}, nil
return Map{Path: fileName, addrs: addrs}, nil
}

func NewJITDumpCache(
Expand Down Expand Up @@ -152,41 +164,64 @@ func (p *JITDumpCache) JITDumpForPID(pid int, path string) (*symtab.FileReader,
}
}

m, err := ReadJITdump(p.logger, jitdumpFile)
filePath := p.path(pid, path)
if err := os.MkdirAll(filepath.Dir(filePath), 0o644); err != nil {
return nil, err
}
f, err := optimizeAndOpenJitdump(p.logger, jitdumpFile, filePath)
if err != nil {
return nil, err
}

filePath := p.path(pid, path)
if err := os.MkdirAll(filepath.Dir(filePath), 0o644); err != nil {
p.cache.Add(jitdumpFile, jitdumpCacheValue{
f: f,
fileModTime: info.ModTime(),
fileSize: info.Size(),
})

return f, nil
}

func optimizeAndOpenJitdump(
logger log.Logger,
perfMapFile string,
outFile string,
) (*symtab.FileReader, error) {
w, err := symtab.NewWriter(outFile, 0)
if err != nil {
return nil, err
}
w, err := symtab.NewWriter(filePath, len(m.addrs))

m, err := ReadJITdump(logger, perfMapFile, w)
if err != nil {
return nil, err
}

for _, addr := range m.addrs {
sym := m.stringTable.GetBytes(addr.Symbol)
if err := w.AddSymbol(unsafeString(sym), addr.Start); err != nil {
indices := m.DeduplicatedIndices()
i := indices.Iterator()
for i.HasNext() {
e := m.addrs[i.Next()]
if err := w.WriteEntry(symtab.Entry{
Address: e.Start,
Offset: e.SymbolOffset,
Len: e.SymbolLen,
}); err != nil {
return nil, err
}
}

if err := w.Write(); err != nil {
if err := w.WriteHeader(); err != nil {
return nil, err
}

f, err := symtab.NewReader(filePath)
if err != nil {
if err := w.Close(); err != nil {
return nil, err
}

p.cache.Add(jitdumpFile, jitdumpCacheValue{
f: f,
fileModTime: info.ModTime(),
fileSize: info.Size(),
})
f, err := symtab.NewReader(outFile)
if err != nil {
return nil, err
}

return f, nil
}
77 changes: 13 additions & 64 deletions pkg/perf/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,89 +15,38 @@ package perf

import (
"errors"
"sort"

"github.com/RoaringBitmap/roaring"
)

var ErrNoSymbolFound = errors.New("no symbol found")

type MapAddr struct {
Start uint64
End uint64
Symbol int
Start uint64
End uint64
SymbolOffset uint32
SymbolLen uint16
}

type Map struct {
Path string

addrs []MapAddr
stringTable *StringTable
addrs []MapAddr
}

func (p *Map) Deduplicate() *Map {
newAddrs := make([]MapAddr, len(p.addrs))

// For deduplication to be most effective we need to also remove entries
// from the string table.
stringTableUsage := make([]int, p.stringTable.Len())
func (p *Map) DeduplicatedIndices() *roaring.Bitmap {
bm := roaring.NewBitmap()

j := len(p.addrs) - 1
for i := len(p.addrs) - 1; i >= 0; i-- {
// The last symbol is the most up to date one, so if any earlier ones
// intersect with it we only keep the latest one.
// intersect with it we only keep the latest one. This works because
// the list has been previously sorted.
if i > 0 && p.addrs[i-1].End > p.addrs[i].Start {
continue
}

stringTableUsage[p.addrs[i].Symbol]++

newAddrs[j] = p.addrs[i]
j--
}

newStringTableSize := uint32(0)
newStringTableEntries := 0
for i, usage := range stringTableUsage {
if usage > 0 {
newStringTableSize += p.stringTable.LengthOf(i)
newStringTableEntries++
}
}

newStringTable := NewStringTable(int(newStringTableSize), newStringTableEntries)
translation := make([]int, p.stringTable.Len())
for i, usage := range stringTableUsage {
if usage > 0 {
translation[i] = newStringTable.GetOrAdd(p.stringTable.GetBytes(i))
}
}

newAddrs = newAddrs[j+1:]

// We need to do this so we can free the memory used by the old slice that
// contains duplicates.
compacted := make([]MapAddr, len(newAddrs))
for i, addr := range newAddrs {
compacted[i] = MapAddr{
Start: addr.Start,
End: addr.End,
Symbol: translation[addr.Symbol],
}
}

return &Map{
Path: p.Path,
addrs: compacted,
stringTable: newStringTable,
}
}

func (p *Map) Lookup(addr uint64) (string, error) {
idx := sort.Search(len(p.addrs), func(i int) bool {
return addr < p.addrs[i].End
})
if idx == len(p.addrs) || p.addrs[idx].Start > addr {
return "", ErrNoSymbolFound
bm.Add(uint32(i))
}

return p.stringTable.Get(p.addrs[idx].Symbol), nil
return bm
}
Loading

0 comments on commit 5632b66

Please sign in to comment.