diff --git a/docs/USAGE.md b/docs/USAGE.md index 35234f8..22ba188 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -22,6 +22,26 @@ tuimble --host mumble.example.com --user myname - **toggle** — press to start, press again to stop (default) - **hold** — hold key to transmit, release to stop (requires evdev) +## Profiling + +```sh +tuimble --cprofile # saves to ~/.config/tuimble/profile.prof +tuimble --cprofile /tmp/tuimble.prof # saves to custom path +``` + +Profile data is dumped every 30 seconds and on exit, so snapshots +are available even after a crash or `kill`. Output is standard `.prof` +format: + +```sh +python3 -m pstats /tmp/tuimble.prof # interactive explorer +# or install snakeviz for a browser-based flamegraph: +# pip install snakeviz && snakeviz /tmp/tuimble.prof +``` + +Note: cProfile captures the main thread only. Background workers +started with `@work(thread=True)` are not included. + ## Configuration See `~/.config/tuimble/config.toml`. All fields are optional; diff --git a/src/tuimble/__main__.py b/src/tuimble/__main__.py index 57304f5..725b421 100644 --- a/src/tuimble/__main__.py +++ b/src/tuimble/__main__.py @@ -1,13 +1,67 @@ """Entry point for tuimble.""" +import argparse import sys def main(): + parser = argparse.ArgumentParser( + prog="tuimble", + description="TUI client for Mumble", + ) + parser.add_argument( + "--cprofile", + nargs="?", + const=None, + default=False, + metavar="FILE", + help="run under cProfile, saving to FILE " + "(default: ~/.config/tuimble/profile.prof)", + ) + args = parser.parse_args() + from tuimble.app import TuimbleApp app = TuimbleApp() - app.run() + + if args.cprofile is not False: + _run_profiled(app, args.cprofile) + else: + app.run() + + +def _run_profiled(app, dest): + """Run the app under cProfile with periodic 30s dumps.""" + import cProfile + from pathlib import Path + from threading import Event, Thread + + if dest is None: + from tuimble.config import CONFIG_DIR + + dest = CONFIG_DIR / "profile.prof" + else: + dest = Path(dest) + + dest.parent.mkdir(parents=True, exist_ok=True) + + prof = cProfile.Profile() + stop = Event() + + def _periodic_dump(): + while not stop.wait(30): + prof.dump_stats(str(dest)) + + dumper = Thread(target=_periodic_dump, daemon=True) + dumper.start() + + try: + prof.enable() + app.run() + finally: + prof.disable() + stop.set() + prof.dump_stats(str(dest)) if __name__ == "__main__":