forked from opensearch-project/observability
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request opensearch-project#101 from zecke/freyth/wip-perf
Parse Linux perf's map and try to symbolize
- Loading branch information
Showing
3 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |