├── .gitignore ├── setup.py ├── demo.py ├── README.md └── speedscope.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | build 3 | dist 4 | speedscope.egg-info 5 | speedscope.json 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="speedscope", 5 | description="A python package for using speedscope.app", 6 | author="Windel Bouwman", 7 | author_email="windel.bouwman@gmail.com", 8 | version="1.0", 9 | py_modules=["speedscope"], 10 | ) 11 | -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | 2 | import speedscope 3 | 4 | import time 5 | 6 | def func1(x): 7 | for y in range(15): 8 | # print(f'func1 {x} - {y}') 9 | time.sleep(0.001) 10 | 11 | def func2(x): 12 | for y in range(4): 13 | # print(f'func2 {x} - {y}') 14 | func1(x) 15 | time.sleep(0.002) 16 | 17 | def main(): 18 | for x in range(3): 19 | time.sleep(0.003) 20 | print(f'standby! {x}') 21 | func1(x) 22 | func2(x) 23 | 24 | 25 | with speedscope.track('speedscope.json'): 26 | main() 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Speedscope for python 3 | 4 | [Speedscope](https://www.speedscope.app/) is a nice web-app to view a flame graph of your application. 5 | 6 | This project contains a python recorder for speedscope. 7 | 8 | ## Installation 9 | 10 | $ pip install speedscope 11 | 12 | ## Usage 13 | 14 | ```python 15 | 16 | import speedscope 17 | from my_code import slow_function 18 | 19 | with speedscope.track('speedscope.json'): 20 | slow_function() 21 | 22 | ```` 23 | 24 | Next, upload the file `speedscope.json` into the [webapp](https://www.speedscope.app/). 25 | -------------------------------------------------------------------------------- /speedscope.py: -------------------------------------------------------------------------------- 1 | """ Speedscope recorder for python. 2 | 3 | See also: https://www.speedscope.app 4 | """ 5 | 6 | import argparse 7 | import time 8 | import json 9 | import sys 10 | from collections import namedtuple 11 | import contextlib 12 | 13 | 14 | @contextlib.contextmanager 15 | def track(filename): 16 | recorder = Recorder() 17 | recorder.start() 18 | yield 19 | recorder.stop() 20 | recorder.export_to_json(filename) 21 | 22 | 23 | Record = namedtuple("Record", ["timestamp", "typ", "filename", "line", "name"]) 24 | 25 | 26 | class Recorder: 27 | def __init__(self): 28 | self.records = [] 29 | 30 | def get_nanos(self): 31 | return int(time.perf_counter() * 1e9) 32 | 33 | def start(self): 34 | self._begin_time = self.get_nanos() 35 | sys.setprofile(self._trace_func) 36 | 37 | def stop(self): 38 | sys.setprofile(None) 39 | self._end_time = self.get_nanos() 40 | 41 | def _trace_func(self, frame, event, arg): 42 | filename = frame.f_code.co_filename 43 | if filename == __file__: 44 | # Ignore this file itself 45 | return 46 | 47 | if event == "call": 48 | typ = "O" 49 | elif event == "return": 50 | typ = "C" 51 | else: 52 | # Ignore everything else. 53 | return 54 | 55 | timestamp = self.get_nanos() 56 | line = frame.f_code.co_firstlineno 57 | name = frame.f_code.co_name 58 | self.records.append(Record(timestamp, typ, filename, line, name)) 59 | 60 | def export_to_json(self, filename): 61 | data = self._make_speed_scope_dict() 62 | with open(filename, "w") as f: 63 | json.dump(data, f, indent=2) 64 | 65 | def _make_speed_scope_dict(self): 66 | events = [] 67 | frames = [] 68 | frame_cache = {} 69 | 70 | # Strip off first and last events from entering and leaving this library. 71 | while self.records[0][1] == "C": 72 | self.records.pop(0) 73 | 74 | while self.records[-1][1] == "O": 75 | self.records.pop(-1) 76 | 77 | for timestamp, event, filename, line, name in self.records: 78 | key = (filename, line, name) 79 | if key not in frame_cache: 80 | frame_cache[key] = len(frames) 81 | frames.append({"name": name, "file": filename, "line": line, "col": 1}) 82 | frame_index = frame_cache[key] 83 | events.append({"type": event, "at": timestamp, "frame": frame_index}) 84 | 85 | data = { 86 | "$schema": "https://www.speedscope.app/file-format-schema.json", 87 | "profiles": [ 88 | { 89 | "type": "evented", 90 | "name": "python", 91 | "unit": "nanoseconds", 92 | "startValue": self._begin_time, 93 | "endValue": self._end_time, 94 | "events": events, 95 | } 96 | ], 97 | "shared": {"frames": frames}, 98 | "activeProfileIndex": 0, 99 | "exporter": "pyspeedscope", 100 | "name": "profile for python script", 101 | } 102 | return data 103 | --------------------------------------------------------------------------------