this repo has no description
at trunk 268 lines 7.5 kB view raw
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))