├── .gitignore ├── LICENSE ├── README.md ├── pytracing ├── __init__.py └── pytracing.py ├── setup.cfg ├── setup.py └── test_pytracing.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | trace.out 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kris Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytracing 2 | a python trace profiler that outputs to chrome trace-viewer format (about://tracing). 3 | 4 | # usage 5 | 6 | from pytracing import TraceProfiler 7 | tp = TraceProfiler(output=open('/tmp/trace.out', 'wb')) 8 | with tp.traced(): 9 | ...execute something here... 10 | 11 | # screenshots 12 | 13 | ## click to view detail 14 | 15 | ![click to view detail](http://kwlzn.github.io/img/pytracing-screenshots/screen1.png "click to view detail") 16 | 17 | 18 | ## selection and aggregate view 19 | 20 | ![selection and aggregate view](http://kwlzn.github.io/img/pytracing-screenshots/screen2.png "selection and aggregate view") 21 | 22 | 23 | ## zooming 24 | 25 | ![zoom](http://kwlzn.github.io/img/pytracing-screenshots/screen3.png "zoom") 26 | -------------------------------------------------------------------------------- /pytracing/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from .pytracing import TraceProfiler 8 | 9 | __all__ = ['TraceProfiler'] 10 | -------------------------------------------------------------------------------- /pytracing/pytracing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import os 8 | import sys 9 | import json 10 | import time 11 | import threading 12 | from contextlib import contextmanager 13 | 14 | try: 15 | from queue import Queue 16 | except ImportError: 17 | from Queue import Queue 18 | 19 | 20 | def to_microseconds(s): 21 | return 1000000 * float(s) 22 | 23 | 24 | class TraceWriter(threading.Thread): 25 | 26 | def __init__(self, terminator, input_queue, output_stream): 27 | threading.Thread.__init__(self) 28 | self.daemon = True 29 | self.terminator = terminator 30 | self.input = input_queue 31 | self.output = output_stream 32 | 33 | def _open_collection(self): 34 | """Write the opening of a JSON array to the output.""" 35 | self.output.write(b'[') 36 | 37 | def _close_collection(self): 38 | """Write the closing of a JSON array to the output.""" 39 | self.output.write(b'{}]') # empty {} so the final entry doesn't end with a comma 40 | 41 | def run(self): 42 | self._open_collection() 43 | while not self.terminator.is_set() or not self.input.empty(): 44 | item = self.input.get() 45 | self.output.write((json.dumps(item) + ',\n').encode('ascii')) 46 | self._close_collection() 47 | 48 | 49 | class TraceProfiler(object): 50 | """A python trace profiler that outputs Chrome Trace-Viewer format (about://tracing). 51 | 52 | Usage: 53 | 54 | from pytracing import TraceProfiler 55 | tp = TraceProfiler(output=open('/tmp/trace.out', 'wb')) 56 | with tp.traced(): 57 | ... 58 | 59 | """ 60 | TYPES = {'call': 'B', 'return': 'E'} 61 | 62 | def __init__(self, output, clock=None): 63 | self.output = output 64 | self.clock = clock or time.time 65 | self.pid = os.getpid() 66 | self.queue = Queue() 67 | self.terminator = threading.Event() 68 | self.writer = TraceWriter(self.terminator, self.queue, self.output) 69 | 70 | @property 71 | def thread_id(self): 72 | return threading.current_thread().name 73 | 74 | @contextmanager 75 | def traced(self): 76 | """Context manager for install/shutdown in a with block.""" 77 | self.install() 78 | try: 79 | yield 80 | finally: 81 | self.shutdown() 82 | 83 | def install(self): 84 | """Install the trace function and open the JSON output stream.""" 85 | self.writer.start() # Start the writer thread. 86 | sys.setprofile(self.tracer) # Set the trace/profile function. 87 | threading.setprofile(self.tracer) # Set the trace/profile function for threads. 88 | 89 | def shutdown(self): 90 | sys.setprofile(None) # Clear the trace/profile function. 91 | threading.setprofile(None) # Clear the trace/profile function for threads. 92 | self.terminator.set() # Stop the writer thread. 93 | self.writer.join() # Join the writer thread. 94 | 95 | def fire_event(self, event_type, func_name, func_filename, func_line_no, 96 | caller_filename, caller_line_no): 97 | """Write a trace event to the output stream.""" 98 | timestamp = to_microseconds(self.clock()) 99 | # https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview 100 | 101 | event = dict( 102 | name=func_name, # Event Name. 103 | cat=func_filename, # Event Category. 104 | tid=self.thread_id, # Thread ID. 105 | ph=self.TYPES[event_type], # Event Type. 106 | pid=self.pid, # Process ID. 107 | ts=timestamp, # Timestamp. 108 | args=dict( 109 | function=':'.join([str(x) for x in (func_filename, func_line_no, func_name)]), 110 | caller=':'.join([str(x) for x in (caller_filename, caller_line_no)]), 111 | ) 112 | ) 113 | self.queue.put(event) 114 | 115 | def tracer(self, frame, event_type, arg): 116 | """Bound tracer function for sys.settrace().""" 117 | try: 118 | if event_type in self.TYPES.keys() and frame.f_code.co_name != 'write': 119 | self.fire_event( 120 | event_type=event_type, 121 | func_name=frame.f_code.co_name, 122 | func_filename=frame.f_code.co_filename, 123 | func_line_no=frame.f_lineno, 124 | caller_filename=frame.f_back.f_code.co_filename, 125 | caller_line_no=frame.f_back.f_lineno, 126 | ) 127 | except Exception: 128 | pass # Don't disturb execution if we can't log the trace. 129 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='pytracing', 7 | version='0.4', 8 | description='a python trace profiler that outputs to chrome trace-viewer format (about://tracing).', 9 | author='Kris Wilson', 10 | author_email='kwilson@twitter.com', 11 | url='https://www.github.com/kwlzn/pytracing', 12 | packages=['pytracing'] 13 | ) 14 | -------------------------------------------------------------------------------- /test_pytracing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import 4 | from __future__ import division 5 | from __future__ import print_function 6 | from __future__ import unicode_literals 7 | 8 | import io 9 | import json 10 | import time 11 | 12 | from pytracing import TraceProfiler 13 | 14 | 15 | def function_a(x): 16 | print('sleeping {}'.format(x)) 17 | time.sleep(x) 18 | return 19 | 20 | 21 | def function_b(x): 22 | function_a(x) 23 | 24 | 25 | def main(): 26 | function_a(1) 27 | function_b(2) 28 | 29 | 30 | if __name__ == '__main__': 31 | with io.open('./trace.out', mode='w', encoding='utf-8') as fh: 32 | tp = TraceProfiler(output=fh) 33 | tp.install() 34 | main() 35 | tp.shutdown() 36 | print('wrote trace.out') 37 | 38 | # ensure the output is at least valid JSON 39 | with io.open('./trace.out', encoding='utf-8') as fh: 40 | json.load(fh) 41 | 42 | --------------------------------------------------------------------------------