this repo has no description
1#!/usr/bin/env python3
2# Copyright (c) Facebook, Inc. and its affiliates. (http://www.facebook.com)
3from _builtins import _int_check_exact, _profiler_exclude, _profiler_install
4
5
6next_id = 0
7
8
9class CodeInfo:
10 """Profile information about a single code object."""
11
12 def __init__(self):
13 global next_id
14 self.id = next_id
15 next_id += 1
16
17 self.called = 0
18 self.function = None
19 self.self = 0
20 self.inclusive = 0
21 # List of callees: This list alternates between code object and
22 # corresponding call count.
23 self.callees = []
24
25 def clear(self):
26 self.called = 0
27 self.function = None
28 self.self = 0
29 self.inclusive = 0
30 self.callees = []
31
32 def is_empty(self):
33 return (
34 self.called == 0
35 and self.self == 0
36 and self.inclusive == 0
37 and len(self.callees) == 0
38 )
39
40
41class CodeInfoDict(dict):
42 """Dictionary mapping code object to corresponding CodeInfo instances.
43 Note that this is per-thread information. There are code info dictionaries
44 for each thread, and one code object can have multiple CodeInfos associated;
45 at most 1 per thread."""
46
47 def __missing__(self, key):
48 value = CodeInfo()
49 dict.__setitem__(self, key, value)
50 return value
51
52
53class FrameInfo:
54 """Profiling information about a currently active call/callframe. Instances
55 of this type are pushed to the `ThreadInfo.shadow_stack` as the program
56 is executing."""
57
58 def __init__(self, begin):
59 self.begin = begin
60 self.self_begin = begin
61 self.self_amount = 0
62
63
64class ThreadInfo:
65 """Profiling information about a single thread."""
66
67 def __init__(self):
68 self.code_infos = CodeInfoDict()
69 self.shadow_stack = []
70
71
72_main_thread_info = None
73
74
75def _new_thread():
76 global _main_thread_info
77 assert _main_thread_info is None
78 _main_thread_info = ThreadInfo()
79 return _main_thread_info
80
81
82def _is_native(code):
83 return _int_check_exact(code.co_code)
84
85
86def _call(thread_data, from_function, to_function, opcodes):
87 to_code = to_function.__code__
88 code_infos = thread_data.code_infos
89 to_info = code_infos[to_code]
90 to_info.called += 1
91
92 to_info_function = to_info.function
93 if to_info_function is None:
94 to_info.function = to_function
95 elif to_info_function is not to_function:
96 # Multiple functions appear to share the same code object
97 to_info.function = "multiple"
98
99 if from_function is not None:
100 from_code = from_function.__code__
101 from_info = code_infos[from_code]
102 # Add entry with callee code and call count to callees list.
103 callees = from_info.callees
104 i = 0
105 callees_len = len(callees)
106 while i < callees_len:
107 if callees[i] is to_code:
108 callees[i + 1] += 1
109 break
110 i += 2
111 else:
112 callees.append(to_code)
113 callees.append(1)
114
115 if _is_native(to_code):
116 return
117
118 shadow_stack = thread_data.shadow_stack
119 if shadow_stack:
120 last = shadow_stack[-1]
121 assert last.self_begin >= 0
122 last.self_amount += opcodes - last.self_begin
123 last.self_begin = -1
124
125 frame_info = FrameInfo(opcodes)
126 shadow_stack.append(frame_info)
127
128
129def _return(thread_data, from_function, to_function, opcodes):
130 from_code = from_function.__code__
131 if _is_native(from_code):
132 return
133
134 shadow_stack = thread_data.shadow_stack
135 if not shadow_stack:
136 return
137
138 frame_info = shadow_stack.pop()
139
140 from_info = thread_data.code_infos[from_code]
141 from_info.inclusive += opcodes - frame_info.begin
142 from_info.self += frame_info.self_amount + (opcodes - frame_info.self_begin)
143
144 if shadow_stack:
145 to_frame_info = shadow_stack[-1]
146 to_frame_info.self_begin = opcodes
147
148
149def install():
150 _profiler_install(_new_thread, _call, _return)
151
152
153def uninstall():
154 _profiler_install(None, None, None)
155
156
157def _info_name(code, code_info):
158 name = code.co_name
159 function = code_info.function
160 if function is not None and "multiple" != function:
161 module = getattr(function, "__module__", "?")
162 qualname = getattr(function, "__qualname__", name)
163 return f"{module}.{qualname}"
164 return name
165
166
167def _clear():
168 code_infos = _main_thread_info.code_infos
169 for info in code_infos.values():
170 info.clear()
171
172
173class _DumpCallgrind:
174 def __init__(self, fp):
175 self.fp = fp
176 self.current_file = {}
177 self.file_ids = {}
178 self.func_ids = {}
179
180 def file(self, prefix, filename):
181 if filename.strip() == "":
182 filename = "<empty>"
183 file_ids = self.file_ids
184 file_id = file_ids.get(filename)
185 if file_id is None:
186 file_id = len(file_ids)
187 file_ids[filename] = file_id
188 self.fp.write(f"{prefix}=({file_id}) {filename}\n")
189 elif self.current_file.get(prefix) != file_id:
190 self.fp.write(f"{prefix}=({file_id})\n")
191 self.current_file[prefix] = file_id
192
193 def func(self, prefix, code, info):
194 func_ids = self.func_ids
195 func_id = func_ids.get(info.id)
196 if func_id is None:
197 func_id = len(func_ids)
198 func_ids[info.id] = func_id
199 name = _info_name(code, info)
200 self.fp.write(f"{prefix}=({func_id}) {name}\n")
201 else:
202 self.fp.write(f"{prefix}=({func_id})\n")
203
204 def write(self, s):
205 self.fp.write(s)
206
207
208def _dump_callgrind_to_fp(fp):
209 code_infos = _main_thread_info.code_infos
210 to_print = []
211 for code, info in code_infos.items():
212 if info.is_empty():
213 continue
214 to_print.append((code, info))
215 to_print.sort(key=lambda d: d[0].co_name)
216
217 env = _DumpCallgrind(fp)
218 env.write(
219 """\
220# callgrind format
221version: 1
222creator: _profiler
223positions: line
224events: Op
225
226"""
227 )
228 for code, info in to_print:
229 env.file("fl", code.co_filename)
230 env.func("fn", code, info)
231 if not _is_native(code):
232 value = info.self
233 else:
234 value = info.called
235 lineno = code.co_firstlineno
236 env.write(f"{lineno} {value}\n")
237
238 callees = info.callees
239 callees_len = len(callees)
240 # Iterate through callees: Note that the list alternates between
241 # code object and corresponding call count (which is more efficient
242 # than storing tuples).
243 for c in range(0, callees_len, 2):
244 callee = callees[c]
245 called = callees[c + 1]
246 callee_info = code_infos[callee]
247 env.file("cfi", callee.co_filename)
248 env.func("cfn", callee, callee_info)
249 env.write(f"calls={called} {callee.co_firstlineno}\n")
250 if not _is_native(callee):
251 inclusive = callee_info.inclusive
252 else:
253 inclusive = callee_info.called
254 if inclusive > 0:
255 inclusive = int(called * (inclusive / callee_info.called))
256 env.write(f"{lineno} {inclusive}\n")
257 env.write("\n")
258
259
260def _dump_callgrind_impl(filename, clear):
261 with open(filename, "w") as fp:
262 _dump_callgrind_to_fp(fp)
263 if clear:
264 _clear()
265
266
267def dump_callgrind(filename, clear=True):
268 _profiler_exclude(lambda: _dump_callgrind_impl(filename, clear))