diff --git a/README.md b/README.md index 58c0193..daddcfc 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Options: -v, --verbose Debug logging -q, --quiet Errors only --cprofile [FILE] Enable cProfile, dump to FILE (default: s5p.prof) + --tracemalloc [N] Enable tracemalloc, show top N allocators on exit (default: 10) -V, --version Show version ``` diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index c338fcc..e6db104 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -17,6 +17,8 @@ s5p -m 512 # max concurrent connections s5p --api 127.0.0.1:1081 # enable control API s5p --cprofile # profile to s5p.prof s5p --cprofile out.prof # profile to custom file +s5p --tracemalloc # memory profile (top 10) +s5p --tracemalloc 20 # memory profile (top 20) ``` ## Container diff --git a/docs/USAGE.md b/docs/USAGE.md index eea824e..e0e0356 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -419,6 +419,15 @@ s5p --cprofile output.prof -c config/s5p.yaml # Analyze after stopping python -m pstats s5p.prof + +# Memory profiling with tracemalloc (top 10 allocators on exit) +s5p --tracemalloc -c config/s5p.yaml + +# Show top 20 allocators +s5p --tracemalloc 20 -c config/s5p.yaml + +# Both profilers simultaneously +s5p --cprofile --tracemalloc -c config/s5p.yaml ``` ## Testing the Proxy diff --git a/src/s5p/cli.py b/src/s5p/cli.py index 718d705..6991aed 100644 --- a/src/s5p/cli.py +++ b/src/s5p/cli.py @@ -62,6 +62,10 @@ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: "--cprofile", metavar="FILE", nargs="?", const="s5p.prof", help="enable cProfile, dump stats to FILE (default: s5p.prof)", ) + p.add_argument( + "--tracemalloc", metavar="N", nargs="?", const=10, type=int, + help="enable tracemalloc, show top N allocators on exit (default: 10)", + ) return p.parse_args(argv) @@ -112,6 +116,11 @@ def main(argv: list[str] | None = None) -> int: config.log_level = "error" _setup_logging(config.log_level) + logger = logging.getLogger("s5p") + + if args.tracemalloc: + import tracemalloc + tracemalloc.start() try: if args.cprofile: @@ -123,13 +132,21 @@ def main(argv: list[str] | None = None) -> int: finally: prof.disable() prof.dump_stats(args.cprofile) - logging.getLogger("s5p").info("profile saved to %s", args.cprofile) + logger.info("profile saved to %s", args.cprofile) else: asyncio.run(serve(config)) except KeyboardInterrupt: return 0 except Exception as e: - logging.getLogger("s5p").error("%s", e) + logger.error("%s", e) return 1 + finally: + if args.tracemalloc: + import tracemalloc + snapshot = tracemalloc.take_snapshot() + stats = snapshot.statistics("lineno") + logger.info("tracemalloc: top %d allocations", args.tracemalloc) + for stat in stats[:args.tracemalloc]: + logger.info(" %s", stat) return 0