From e8f34b4d80f6543b342173828b89b3f1c29f5127 Mon Sep 17 00:00:00 2001 From: Username Date: Sat, 28 Feb 2026 13:55:18 +0100 Subject: [PATCH] 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). --- src/tuimble/__main__.py | 43 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/tuimble/__main__.py b/src/tuimble/__main__.py index 725b421..15f3650 100644 --- a/src/tuimble/__main__.py +++ b/src/tuimble/__main__.py @@ -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__":