# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals import os import sys import json import time import threading from contextlib import contextmanager try: from queue import Queue except ImportError: from Queue import Queue def to_microseconds(s): return 1000000 * float(s) class TraceWriter(threading.Thread): def __init__(self, terminator, input_queue, output_stream): threading.Thread.__init__(self) self.daemon = True self.terminator = terminator self.input = input_queue self.output = output_stream def _open_collection(self): """Write the opening of a JSON array to the output.""" self.output.write(b'[') def _close_collection(self): """Write the closing of a JSON array to the output.""" self.output.write(b'{}]') # empty {} so the final entry doesn't end with a comma def run(self): self._open_collection() while not self.terminator.is_set() or not self.input.empty(): item = self.input.get() self.output.write((json.dumps(item) + ',\n').encode('ascii')) self._close_collection() class TraceProfiler(object): """A python trace profiler that outputs Chrome Trace-Viewer format (about://tracing). Usage: from pytracing import TraceProfiler tp = TraceProfiler(output=open('/tmp/trace.out', 'wb')) with tp.traced(): ... """ TYPES = {'call': 'B', 'return': 'E'} def __init__(self, output, clock=None): self.output = output self.clock = clock or time.time self.pid = os.getpid() self.queue = Queue() self.terminator = threading.Event() self.writer = TraceWriter(self.terminator, self.queue, self.output) @property def thread_id(self): return threading.current_thread().name @contextmanager def traced(self): """Context manager for install/shutdown in a with block.""" self.install() try: yield finally: self.shutdown() def install(self): """Install the trace function and open the JSON output stream.""" self.writer.start() # Start the writer thread. sys.setprofile(self.tracer) # Set the trace/profile function. threading.setprofile(self.tracer) # Set the trace/profile function for threads. def shutdown(self): sys.setprofile(None) # Clear the trace/profile function. threading.setprofile(None) # Clear the trace/profile function for threads. self.terminator.set() # Stop the writer thread. self.writer.join() # Join the writer thread. def fire_event(self, event_type, func_name, func_filename, func_line_no, caller_filename, caller_line_no): """Write a trace event to the output stream.""" timestamp = to_microseconds(self.clock()) # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview event = dict( name=func_name, # Event Name. cat=func_filename, # Event Category. tid=self.thread_id, # Thread ID. ph=self.TYPES[event_type], # Event Type. pid=self.pid, # Process ID. ts=timestamp, # Timestamp. args=dict( function=':'.join([str(x) for x in (func_filename, func_line_no, func_name)]), caller=':'.join([str(x) for x in (caller_filename, caller_line_no)]), ) ) self.queue.put(event) def tracer(self, frame, event_type, arg): """Bound tracer function for sys.settrace().""" try: if event_type in self.TYPES.keys() and frame.f_code.co_name != 'write': self.fire_event( event_type=event_type, func_name=frame.f_code.co_name, func_filename=frame.f_code.co_filename, func_line_no=frame.f_lineno, caller_filename=frame.f_back.f_code.co_filename, caller_line_no=frame.f_back.f_lineno, ) except Exception: pass # Don't disturb execution if we can't log the trace.