Skip to content

Commit

Permalink
Merge pull request opensearch-project#101 from zecke/freyth/wip-perf
Browse files Browse the repository at this point in the history
Parse Linux perf's map and try to symbolize
  • Loading branch information
brancz authored Nov 1, 2021
2 parents e8c2253 + b0302cb commit 451109e
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
32 changes: 32 additions & 0 deletions pkg/agent/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"github.com/parca-dev/parca-agent/pkg/debuginfo"
"github.com/parca-dev/parca-agent/pkg/ksym"
"github.com/parca-dev/parca-agent/pkg/maps"
"github.com/parca-dev/parca-agent/pkg/perf"
)

//go:embed parca-agent.bpf.o
Expand Down Expand Up @@ -90,6 +91,8 @@ type CgroupProfiler struct {
mtx *sync.RWMutex
lastProfileTakenAt time.Time
lastError error

perfCache *perf.PerfCache
}

func NewCgroupProfiler(
Expand All @@ -111,6 +114,7 @@ func NewCgroupProfiler(
profilingDuration: profilingDuration,
sink: sink,
pidMappingFileCache: maps.NewPidMappingFileCache(logger),
perfCache: perf.NewPerfCache(logger),
writeClient: writeClient,
debugInfoExtractor: debuginfo.NewExtractor(
log.With(logger, "component", "debuginfoextractor"),
Expand Down Expand Up @@ -265,6 +269,7 @@ func (p *CgroupProfiler) profileLoop(ctx context.Context, now time.Time, counts,
File: "[kernel.kallsyms]",
}
kernelFunctions := map[uint64]*profile.Function{}
userFunctions := map[[2]uint64]*profile.Function{}

// 2 uint64 1 for PID and 1 for Addr
locations := []*profile.Location{}
Expand Down Expand Up @@ -370,6 +375,12 @@ func (p *CgroupProfiler) profileLoop(ctx context.Context, now time.Time, counts,
}

// User stack
perfMap, err := p.perfCache.CacheForPid(pid)
if err != nil {
// We expect only a minority of processes to have a JIT and produce
// the perf map.
level.Debug(p.logger).Log("msg", "no perfmap", "err", err)
}
for _, addr := range stack[:stackDepth] {
if addr != uint64(0) {
key := [2]uint64{uint64(pid), addr}
Expand All @@ -385,6 +396,22 @@ func (p *CgroupProfiler) profileLoop(ctx context.Context, now time.Time, counts,
Address: addr,
Mapping: m,
}

// Does this addr point to JITed code?
if perfMap != nil {
// TODO(zecke): Log errors other than perf.NoSymbolFound
jitFunction, ok := userFunctions[key]
if !ok {
if sym, err := perfMap.Lookup(addr); err == nil {
jitFunction = &profile.Function{Name: sym}
userFunctions[key] = jitFunction
}
}
if jitFunction != nil {
l.Line = []profile.Line{{Function: jitFunction}}
}
}

locations = append(locations, l)
locationIndices[key] = locationIndex
}
Expand Down Expand Up @@ -440,6 +467,11 @@ func (p *CgroupProfiler) profileLoop(ctx context.Context, now time.Time, counts,
kernelMapping.ID = uint64(len(prof.Mapping)) + 1
prof.Mapping = append(prof.Mapping, kernelMapping)

for _, f := range userFunctions {
f.ID = uint64(len(prof.Function)) + 1
prof.Function = append(prof.Function, f)
}

p.debugInfoExtractor.EnsureUploaded(ctx, buildIDFiles)

buf := bytes.NewBuffer(nil)
Expand Down
146 changes: 146 additions & 0 deletions pkg/perf/perf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2021 The Parca Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package perf

import (
"bufio"
"errors"
"fmt"
"io/fs"
"os"
"sort"
"strconv"
"strings"

"github.com/go-kit/log"

"github.com/parca-dev/parca-agent/pkg/hash"
)

type PerfCache struct {
fs fs.FS
logger log.Logger
cache map[uint32]*PerfMap
pidMapHash map[uint32]uint64
}

type PerfMapAddr struct {
Start uint64
End uint64
Symbol string
}

type PerfMap struct {
addrs []PerfMapAddr
}

type realfs struct{}

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

func (f *realfs) Open(name string) (fs.File, error) {
return os.Open(name)
}

func PerfReadMap(fs fs.FS, fileName string) (PerfMap, error) {
fd, err := fs.Open(fileName)
if err != nil {
return PerfMap{}, err
}
defer fd.Close()

s := bufio.NewScanner(fd)
addrs := make([]PerfMapAddr, 0)
for s.Scan() {
l := strings.SplitN(s.Text(), " ", 3)
if len(l) < 3 {
return PerfMap{}, fmt.Errorf("splitting failed: %v", l)

}

start, err := strconv.ParseUint(l[0], 16, 64)
if err != nil {
return PerfMap{}, fmt.Errorf("parsing start failed on %v: %w", l, err)
}
size, err := strconv.ParseUint(l[1], 16, 64)
if err != nil {
return PerfMap{}, fmt.Errorf("parsing end failed on %v: %w", l, err)
}
if start+size < start {
return PerfMap{}, fmt.Errorf("overflowed mapping: %v", l)
}
addrs = append(addrs, PerfMapAddr{start, start + size, l[2]})
}
// Sorted by end address to allow binary search during look-up. End to find
// the (closest) address _before_ the end. This could be an inlined instruction
// within a larger blob.
sort.Slice(addrs, func(i, j int) bool {
return addrs[i].End < addrs[j].End
})
return PerfMap{addrs: addrs}, s.Err()
}

func (p *PerfMap) 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 "", NoSymbolFound
}

return p.addrs[idx].Symbol, nil
}

func NewPerfCache(logger log.Logger) *PerfCache {
return &PerfCache{
fs: &realfs{},
logger: logger,
cache: map[uint32]*PerfMap{},
pidMapHash: map[uint32]uint64{},
}
}

// CacheForPid returns the PerfMap for the given pid if it exists.
func (p *PerfCache) CacheForPid(pid uint32) (*PerfMap, error) {
// NOTE(zecke): There are various limitations and things to note.
// 1st) The input file is "tainted" and under control by the user. By all
// means it could be an infinitely large.
// 2nd) There might be a file called /tmp/perf-${nspid}.txt but that might
// be in a different mount_namespace(7) and pid_namespace(7). We don't
// map these yet. Using /proc/$pid/tmp/perf-$pid.txt is not enough and
// hence containerized workloads are broken.

perfFile := fmt.Sprintf("/tmp/perf-%d.map", pid)
// TODO(zecke): Log other than file not found errors?
h, err := hash.File(p.fs, perfFile)
if err != nil {
return nil, err
}

if p.pidMapHash[pid] == h {
return p.cache[pid], nil
}

m, err := PerfReadMap(p.fs, perfFile)
if err != nil {
return nil, err
}

p.cache[pid] = &m
p.pidMapHash[pid] = h // TODO(zecke): Resolve time of check/time of use.
return &m, nil
}
85 changes: 85 additions & 0 deletions pkg/perf/perf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2021 The Parca Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package perf

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/parca-dev/parca-agent/pkg/testutil"
)

// See https://github.com/torvalds/linux/blob/master/tools/perf/Documentation/jit-interface.txt
const perfMap = `3ef414c0 398 RegExp:[{(]
3ef418a0 398 RegExp:[})]
59ed4102 26 LazyCompile:~REPLServer.self.writer repl.js:514
59ed44ea 146 LazyCompile:~inspect internal/util/inspect.js:152
59ed4e4a 148 LazyCompile:~formatValue internal/util/inspect.js:456
59ed558a 25f LazyCompile:~formatPrimitive internal/util/inspect.js:768
59ed5d62 35 LazyCompile:~formatNumber internal/util/inspect.js:761
59ed5fca 5d LazyCompile:~stylizeWithColor internal/util/inspect.js:267
4edd2e52 65 LazyCompile:~Domain.exit domain.js:284
4edd30ea 14b LazyCompile:~lastIndexOf native array.js:618
4edd3522 35 LazyCompile:~online internal/repl.js:157
4edd37f2 ec LazyCompile:~setTimeout timers.js:388
4edd3cca b0 LazyCompile:~Timeout internal/timers.js:55
4edd40ba 55 LazyCompile:~initAsyncResource internal/timers.js:45
4edd42da f LazyCompile:~exports.active timers.js:151
4edd457a cb LazyCompile:~insert timers.js:167
4edd4962 50 LazyCompile:~TimersList timers.js:195
4edd4cea 37 LazyCompile:~append internal/linkedlist.js:29
4edd4f12 35 LazyCompile:~remove internal/linkedlist.js:15
4edd5132 d LazyCompile:~isEmpty internal/linkedlist.js:44
4edd529a 21 LazyCompile:~ok assert.js:345
4edd555a 68 LazyCompile:~innerOk assert.js:317
4edd59a2 27 LazyCompile:~processTimers timers.js:220
4edd5d9a 197 LazyCompile:~listOnTimeout timers.js:226
4edd6352 15 LazyCompile:~peek internal/linkedlist.js:9
4edd66ca a1 LazyCompile:~tryOnTimeout timers.js:292
4edd6a02 86 LazyCompile:~ontimeout timers.js:429
4edd7132 d7 LazyCompile:~process.kill internal/process/per_thread.js:173`

func TestPerfMapParse(t *testing.T) {
fs := testutil.NewFakeFS(map[string][]byte{
"/tmp/perf-123.map": []byte(perfMap),
})

res, err := PerfReadMap(fs, "/tmp/perf-123.map")
require.NoError(t, err)
require.Len(t, res.addrs, 28)
// Check for 4edd3cca B0 LazyCompile:~Timeout internal/timers.js:55
require.Equal(t, res.addrs[12], PerfMapAddr{0x4edd4f12, 0x4edd4f47, "LazyCompile:~remove internal/linkedlist.js:15"})

// Look-up a symbol.
sym, err := res.Lookup(0x4edd4f12 + 4)
require.NoError(t, err)
require.Equal(t, sym, "LazyCompile:~remove internal/linkedlist.js:15")

_, err = res.Lookup(0xFFFFFFFF)
require.ErrorIs(t, err, NoSymbolFound)
}

func BenchmarkPerfMapParse(b *testing.B) {
fs := testutil.NewFakeFS(map[string][]byte{
"/tmp/perf-123.map": []byte(perfMap),
})
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, err := PerfReadMap(fs, "/tmp/perf-123.map")
require.NoError(b, err)
}
}

0 comments on commit 451109e

Please sign in to comment.