Changeset View
Changeset View
Standalone View
Standalone View
converters/perf2calltree.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
1 | # perf script event handlers, generated by perf script -g python | ||||
---|---|---|---|---|---|
2 | # (c) 2016, Milian Wolff <milian.wolff@kdab.com> | ||||
3 | # (c) 2019, Lubos Lunak <l.lunak@kde.org> | ||||
4 | # Licensed under the terms of the GNU GPL License version 2 | ||||
5 | # | ||||
6 | # This script converts perf data into the callgrind format. | ||||
7 | # The output can then be visualized in kcachegrind. | ||||
8 | # | ||||
9 | # Usage: perf script -s perf2calltree.py > perf.out | ||||
10 | # | ||||
11 | # NOTE: This script currently does not support conversion of data files | ||||
12 | # that contain multiple event sources. | ||||
13 | | ||||
14 | import os | ||||
15 | import sys | ||||
16 | import subprocess | ||||
17 | from collections import defaultdict | ||||
18 | from subprocess import PIPE | ||||
19 | | ||||
20 | sys.path.append(os.environ['PERF_EXEC_PATH'] + \ | ||||
21 | '/scripts/python/Perf-Trace-Util/lib/Perf/Trace') | ||||
22 | | ||||
23 | from Core import * | ||||
24 | from perf_trace_context import * | ||||
25 | | ||||
26 | try: | ||||
27 | from subprocess import DEVNULL # py3k | ||||
28 | except ImportError: | ||||
29 | import os | ||||
30 | DEVNULL = open(os.devnull, 'wb') | ||||
31 | | ||||
32 | class Cost: | ||||
33 | def __init__(self): | ||||
34 | self.cost = 0 | ||||
35 | self.calls = 0 | ||||
36 | | ||||
37 | def add(self, cost): | ||||
38 | self.cost += cost | ||||
39 | self.calls += 1 | ||||
40 | | ||||
41 | class FileInfo: | ||||
42 | def __init__(self, file, line): | ||||
43 | self.file = file | ||||
44 | self.line = line | ||||
45 | | ||||
46 | class Function: | ||||
47 | def __init__(self, dsoName, name, sym): | ||||
48 | self.cost = Cost() | ||||
49 | self.calls = 0 | ||||
50 | self.dso = dsoName | ||||
51 | self.name = name | ||||
52 | self.sym = sym | ||||
53 | self.fileInfo = FileInfo("???", 0) | ||||
54 | | ||||
55 | self.callees = defaultdict(lambda: Cost()) | ||||
56 | | ||||
57 | class DSO: | ||||
58 | def __init__(self): | ||||
59 | self.functions = dict() | ||||
60 | self.name = "" | ||||
61 | | ||||
62 | def createFileInfo(self): | ||||
63 | # try | ||||
64 | addresses = "" | ||||
65 | for sym, function in self.functions.iteritems(): | ||||
66 | try: | ||||
67 | addresses += hex(function.sym['start']) + "\n" | ||||
68 | except: | ||||
69 | addresses += "\n" | ||||
70 | process = subprocess.Popen(["addr2line", "-e", self.name], stdin=PIPE, stdout=PIPE, stderr=DEVNULL, universal_newlines=True) | ||||
71 | output = process.communicate(input=addresses)[0].split('\n') | ||||
72 | pos = 0 | ||||
73 | for sym, function in self.functions.iteritems(): | ||||
74 | try: | ||||
75 | addressInfo = output[pos].split(':') | ||||
76 | file = addressInfo[0] | ||||
77 | except: | ||||
78 | file = None | ||||
79 | if not function.sym or not file or file == "??": | ||||
80 | file = "???" | ||||
81 | try: | ||||
82 | line = int(addressInfo[1]) | ||||
83 | except: | ||||
84 | line = 0 | ||||
85 | function.fileInfo = FileInfo(file, line) | ||||
86 | pos = pos + 1 | ||||
87 | | ||||
88 | # a map of all encountered dso's and the functions therein | ||||
89 | # this is done to prevent name clashes | ||||
90 | dsos = defaultdict(lambda: DSO()) | ||||
91 | | ||||
92 | def addFunction(dsoName, name, sym): | ||||
93 | global dsos | ||||
94 | dso = dsos[dsoName] | ||||
95 | if not dso.name: | ||||
96 | dso.name = dsoName | ||||
97 | function = dso.functions.get(name, None) | ||||
98 | # create function if it's not yet known | ||||
99 | if not function: | ||||
100 | function = Function(dsoName, name, sym) | ||||
101 | dso.functions[name] = function | ||||
102 | return function | ||||
103 | | ||||
104 | eventsType = "events: Samples" | ||||
105 | | ||||
106 | # write the callgrind file format to stdout | ||||
107 | def trace_end(): | ||||
108 | global dsos | ||||
109 | | ||||
110 | print("version: 1") | ||||
111 | print("creator: perf-callgrind 0.1") | ||||
112 | print("part: 1") | ||||
113 | # TODO: get access to command line, it's in the perf data header | ||||
114 | # but not accessible to the scripting backend, is it? | ||||
115 | print(eventsType) | ||||
116 | | ||||
117 | for dsoName, dso in dsos.iteritems(): | ||||
118 | dso.createFileInfo() | ||||
119 | | ||||
120 | for dsoName, dso in dsos.iteritems(): | ||||
121 | print("ob=%s" % dsoName) | ||||
122 | for sym, function in dso.functions.iteritems(): | ||||
123 | print("fl=%s" % function.fileInfo.file) | ||||
124 | print("fn=%s" % sym) | ||||
125 | print("%d %d" % (function.fileInfo.line, function.cost.cost)) | ||||
126 | for callee, cost in function.callees.iteritems(): | ||||
127 | print("cob=%s" % callee.dso) | ||||
128 | print("cfi=%s" % callee.fileInfo.file) | ||||
129 | print("cfn=%s" % callee.name) | ||||
130 | print("calls=%d %d" % (cost.calls, callee.fileInfo.line)) | ||||
131 | print("%d %d" % (function.fileInfo.line, cost.cost)) | ||||
132 | print("") | ||||
133 | | ||||
134 | def addSample(event, cost, callchain): | ||||
135 | caller = None | ||||
136 | if not callchain: | ||||
137 | # only add the single symbol where we got the sample, without a backtrace | ||||
138 | dsoName = event.get("dso", "???") | ||||
139 | name = event.get("symbol", "???") | ||||
140 | caller = addFunction(dsoName, name, None) | ||||
141 | else: | ||||
142 | # add a function for every frame in the callchain | ||||
143 | for item in reversed(callchain): | ||||
144 | dsoName = item.get("dso", "???") | ||||
145 | name = "???" | ||||
146 | if "sym" in item: | ||||
147 | name = item["sym"]["name"] | ||||
148 | function = addFunction(dsoName, name, item.get("sym", None)) | ||||
149 | # add current frame to parent's callee list | ||||
150 | if caller is not None: | ||||
151 | caller.callees[function].add(cost) | ||||
152 | caller = function | ||||
153 | | ||||
154 | # increase the self cost of the last frame | ||||
155 | # all other frames include it now and kcachegrind will automatically | ||||
156 | # take care of adapting their inclusive cost | ||||
157 | if caller is not None: | ||||
158 | caller.cost.add(cost) | ||||
159 | | ||||
160 | def process_event(event): | ||||
161 | global eventsType | ||||
162 | caller = addSample(event, 1, event["callchain"]) | ||||
163 | | ||||
164 | def trace_unhandled(event_name, context, sample, event): | ||||
165 | global eventsType | ||||
166 | cost = 1 | ||||
167 | if sample["period"] > 0: | ||||
168 | cost = sample["period"] | ||||
169 | eventsType = "event: ns: time in ns\nevents: ns" | ||||
170 | caller = addSample(event, cost, event['common_callchain']) |