profiler: add per-thread cProfile support

threading.setprofile installs a bootstrap that creates a per-thread
cProfile.Profile.  Stats from all threads are merged on periodic
dumps and at exit, capturing worker-thread hotspots (audio send
loop, pitch shifting).
This commit is contained in:
Username
2026-02-28 13:55:18 +01:00
parent b62993459a
commit e8f34b4d80

View File

@@ -31,10 +31,17 @@ def main():
def _run_profiled(app, dest):
"""Run the app under cProfile with periodic 30s dumps."""
"""Run the app under cProfile with periodic 30s dumps.
Profiles both the main thread and all worker threads (including
the audio send loop where PitchShifter.process runs). Each
thread gets its own cProfile.Profile; stats are merged on dump.
"""
import cProfile
import pstats
import threading
from pathlib import Path
from threading import Event, Thread
from threading import Event, Lock, Thread
if dest is None:
from tuimble.config import CONFIG_DIR
@@ -47,10 +54,34 @@ def _run_profiled(app, dest):
prof = cProfile.Profile()
stop = Event()
thread_profiles: list[cProfile.Profile] = []
lock = Lock()
def _thread_bootstrap(frame, event, arg):
"""Installed via threading.setprofile; creates a per-thread profiler.
Called once per new thread as the profile function. Immediately
creates and enables a cProfile.Profile whose C-level hook
replaces this Python-level one for the remainder of the thread.
"""
tp = cProfile.Profile()
with lock:
thread_profiles.append(tp)
tp.enable()
threading.setprofile(_thread_bootstrap)
def _dump_all():
"""Merge main + thread profiles and write to disk."""
stats = pstats.Stats(prof)
with lock:
for tp in thread_profiles:
stats.add(tp)
stats.dump_stats(str(dest))
def _periodic_dump():
while not stop.wait(30):
prof.dump_stats(str(dest))
_dump_all()
dumper = Thread(target=_periodic_dump, daemon=True)
dumper.start()
@@ -60,8 +91,12 @@ def _run_profiled(app, dest):
app.run()
finally:
prof.disable()
threading.setprofile(None)
with lock:
for tp in thread_profiles:
tp.disable()
stop.set()
prof.dump_stats(str(dest))
_dump_all()
if __name__ == "__main__":