instrumentation_tracing.py (6186B)
1 # Copyright 2017 The Chromium Authors 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Functions to instrument all Python function calls. 6 7 This generates a JSON file readable by Chrome's about:tracing. To use it, 8 either call start_instrumenting and stop_instrumenting at the appropriate times, 9 or use the Instrument context manager. 10 11 A function is only traced if it is from a Python module that matches at least 12 one regular expression object in to_include, and does not match any in 13 to_exclude. In between the start and stop events, every function call of a 14 function from such a module will be added to the trace. 15 """ 16 17 import contextlib 18 import functools 19 import inspect 20 import os 21 import re 22 import sys 23 import threading 24 25 from py_trace_event import trace_event 26 27 28 # Modules to exclude by default (to avoid problems like infinite loops) 29 DEFAULT_EXCLUDE = [r'py_trace_event\..*'] 30 31 32 class _TraceArguments: 33 def __init__(self): 34 """Wraps a dictionary to ensure safe evaluation of repr().""" 35 self._arguments = {} 36 37 @staticmethod 38 def _safeStringify(item): 39 try: 40 item_str = repr(item) 41 except Exception: # pylint: disable=broad-except 42 try: 43 item_str = str(item) 44 except Exception: # pylint: disable=broad-except 45 item_str = "<ERROR>" 46 return item_str 47 48 def add(self, key, val): 49 key_str = _TraceArguments._safeStringify(key) 50 val_str = _TraceArguments._safeStringify(val) 51 52 self._arguments[key_str] = val_str 53 54 def __repr__(self): 55 return repr(self._arguments) 56 57 58 saved_thread_ids = set() 59 60 def _shouldTrace(frame, to_include, to_exclude, included, excluded): 61 """ 62 Decides whether or not the function called in frame should be traced. 63 64 Args: 65 frame: The Python frame object of this function call. 66 to_include: Set of regex objects for modules which should be traced. 67 to_exclude: Set of regex objects for modules which should not be traced. 68 included: Set of module names we've determined should be traced. 69 excluded: Set of module names we've determined should not be traced. 70 """ 71 if not inspect.getmodule(frame): 72 return False 73 74 module_name = inspect.getmodule(frame).__name__ 75 76 if module_name in included: 77 includes = True 78 elif to_include: 79 includes = any(pattern.match(module_name) for pattern in to_include) 80 else: 81 includes = True 82 83 if includes: 84 included.add(module_name) 85 else: 86 return False 87 88 # Find the modules of every function in the stack trace. 89 frames = inspect.getouterframes(frame) 90 calling_module_names = [inspect.getmodule(fr[0]).__name__ for fr in frames] 91 92 # Return False for anything with an excluded module's function anywhere in the 93 # stack trace (even if the function itself is in an included module). 94 if to_exclude: 95 for calling_module in calling_module_names: 96 if calling_module in excluded: 97 return False 98 for pattern in to_exclude: 99 if pattern.match(calling_module): 100 excluded.add(calling_module) 101 return False 102 103 return True 104 105 def _generate_trace_function(to_include, to_exclude): 106 to_include = {re.compile(item) for item in to_include} 107 to_exclude = {re.compile(item) for item in to_exclude} 108 to_exclude.update({re.compile(item) for item in DEFAULT_EXCLUDE}) 109 110 included = set() 111 excluded = set() 112 113 tracing_pid = os.getpid() 114 115 def traceFunction(frame, event, arg): 116 del arg 117 118 # Don't try to trace in subprocesses. 119 if os.getpid() != tracing_pid: 120 sys.settrace(None) 121 return None 122 123 # pylint: disable=unused-argument 124 if event not in ("call", "return"): 125 return None 126 127 function_name = frame.f_code.co_name 128 filename = frame.f_code.co_filename 129 line_number = frame.f_lineno 130 131 if _shouldTrace(frame, to_include, to_exclude, included, excluded): 132 if event == "call": 133 # This function is beginning; we save the thread name (if that hasn't 134 # been done), record the Begin event, and return this function to be 135 # used as the local trace function. 136 137 thread_id = threading.current_thread().ident 138 139 if thread_id not in saved_thread_ids: 140 thread_name = threading.current_thread().name 141 142 trace_event.trace_set_thread_name(thread_name) 143 144 saved_thread_ids.add(thread_id) 145 146 arguments = _TraceArguments() 147 # The function's argument values are stored in the frame's 148 # |co_varnames| as the first |co_argcount| elements. (Following that 149 # are local variables.) 150 for idx in range(frame.f_code.co_argcount): 151 arg_name = frame.f_code.co_varnames[idx] 152 arguments.add(arg_name, frame.f_locals[arg_name]) 153 trace_event.trace_begin(function_name, arguments=arguments, 154 module=inspect.getmodule(frame).__name__, 155 filename=filename, line_number=line_number) 156 157 # Return this function, so it gets used as the "local trace function" 158 # within this function's frame (and in particular, gets called for this 159 # function's "return" event). 160 return traceFunction 161 162 if event == "return": 163 trace_event.trace_end(function_name) 164 return None 165 return None 166 167 return traceFunction 168 169 170 def no_tracing(f): 171 @functools.wraps(f) 172 def wrapper(*args, **kwargs): 173 trace_func = sys.gettrace() 174 try: 175 sys.settrace(None) 176 threading.settrace(None) 177 return f(*args, **kwargs) 178 finally: 179 sys.settrace(trace_func) 180 threading.settrace(trace_func) 181 return wrapper 182 183 184 def start_instrumenting(output_file, to_include=(), to_exclude=()): 185 """Enable tracing of all function calls (from specified modules).""" 186 trace_event.trace_enable(output_file) 187 188 traceFunc = _generate_trace_function(to_include, to_exclude) 189 sys.settrace(traceFunc) 190 threading.settrace(traceFunc) 191 192 193 def stop_instrumenting(): 194 trace_event.trace_disable() 195 196 sys.settrace(None) 197 threading.settrace(None) 198 199 200 @contextlib.contextmanager 201 def Instrument(output_file, to_include=(), to_exclude=()): 202 try: 203 start_instrumenting(output_file, to_include, to_exclude) 204 yield None 205 finally: 206 stop_instrumenting()