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:
@@ -31,10 +31,17 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
def _run_profiled(app, dest):
|
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 cProfile
|
||||||
|
import pstats
|
||||||
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Event, Thread
|
from threading import Event, Lock, Thread
|
||||||
|
|
||||||
if dest is None:
|
if dest is None:
|
||||||
from tuimble.config import CONFIG_DIR
|
from tuimble.config import CONFIG_DIR
|
||||||
@@ -47,10 +54,34 @@ def _run_profiled(app, dest):
|
|||||||
|
|
||||||
prof = cProfile.Profile()
|
prof = cProfile.Profile()
|
||||||
stop = Event()
|
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():
|
def _periodic_dump():
|
||||||
while not stop.wait(30):
|
while not stop.wait(30):
|
||||||
prof.dump_stats(str(dest))
|
_dump_all()
|
||||||
|
|
||||||
dumper = Thread(target=_periodic_dump, daemon=True)
|
dumper = Thread(target=_periodic_dump, daemon=True)
|
||||||
dumper.start()
|
dumper.start()
|
||||||
@@ -60,8 +91,12 @@ def _run_profiled(app, dest):
|
|||||||
app.run()
|
app.run()
|
||||||
finally:
|
finally:
|
||||||
prof.disable()
|
prof.disable()
|
||||||
|
threading.setprofile(None)
|
||||||
|
with lock:
|
||||||
|
for tp in thread_profiles:
|
||||||
|
tp.disable()
|
||||||
stop.set()
|
stop.set()
|
||||||
prof.dump_stats(str(dest))
|
_dump_all()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user