|
#!/usr/bin/env python3 |
|
""" |
|
A simple program to run a Python program and dump statistics of how many |
|
sys_trace events ("call", "return", "line", "opcode") it runs. |
|
|
|
Run like this: |
|
|
|
python3 run-python-program-with-count.py *your-python-program* *args* |
|
|
|
""" |
|
import atexit |
|
import os.path as osp |
|
import sys |
|
import types |
|
from collections import defaultdict |
|
from typing import Callable, Optional |
|
|
|
_globals = globals().copy() |
|
|
|
class TraceEvents: |
|
""" |
|
A simple sys.settrace event handler for counting sys_trace() events. |
|
""" |
|
def __init__(self): |
|
self.event_counts = defaultdict(int) |
|
|
|
def event_handler(self, call_frame: types.FrameType, event: str, _) -> Optional[Callable]: |
|
self.event_counts[event] += 1 |
|
|
|
# If you don't want to count opcode you can comment this out. |
|
# Actually, it probably only needs to be set once (per frame |
|
# call?, per trace session?) |
|
|
|
call_frame.f_trace_opcodes = True |
|
|
|
# print(f"event: {event} {call_frame.f_code.co_filename}:{call_frame.f_lineno}") |
|
|
|
# We need to return our method to make sure tracing keeps calling us. |
|
return self.event_handler |
|
|
|
def stop_trace_and_dump(self): |
|
sys.settrace(None) |
|
for event, count in self.event_counts.items(): |
|
print(f'Event "{event}" seen {count} times.') |
|
|
|
|
|
def run_and_trace_program(python_path: str): |
|
""" |
|
Compile `python_path` and evaluate it in a way to keep track |
|
of the instruction events it runs. |
|
""" |
|
|
|
if not osp.exists(python_path): |
|
print(f"Python file {python_path} does not exist") |
|
|
|
compiled = compile(open(python_path, "r").read(), filename=python_path, mode='exec') |
|
tracer = TraceEvents() |
|
atexit.register(tracer.stop_trace_and_dump) |
|
sys.argv.pop(0) |
|
sys.settrace(tracer.event_handler) |
|
exec(compiled, _globals) |
|
|
|
if __name__ == "__main__": |
|
traced_program = sys.argv[1] |
|
run_and_trace_program(traced_program) |