Skip to content

Commit 5b88d22

Browse files
committed
Profile a module or script with sampling profiler
Add `-m` and `filename` arguments to the sampling profiler to launch the specified Python program in a subprocess and start profiling it. Previously only a PID was accepted, this can now be done by passing `-p PID`.
1 parent d995922 commit 5b88d22

File tree

2 files changed

+385
-39
lines changed

2 files changed

+385
-39
lines changed

Lib/profile/sample.py

Lines changed: 88 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import _remote_debugging
33
import os
44
import pstats
5+
import subprocess
56
import statistics
67
import sys
78
import sysconfig
@@ -542,46 +543,66 @@ def main():
542543
parser = argparse.ArgumentParser(
543544
description=(
544545
"Sample a process's stack frames and generate profiling data.\n"
545-
"Supports two output formats:\n"
546-
" - pstats: Detailed profiling statistics with sorting options\n"
547-
" - collapsed: Stack traces for generating flamegraphs\n"
546+
"Supports the following target modes:\n"
547+
" - -p PID: Profile an existing process by PID\n"
548+
" - -m MODULE [ARGS...]: Profile a module as python -m module ... \n"
549+
" - filename [ARGS...]: Profile the specified script by running it in a subprocess\n"
548550
"\n"
549551
"Examples:\n"
550552
" # Profile process 1234 for 10 seconds with default settings\n"
551-
" python -m profile.sample 1234\n"
553+
" python -m profile.sample -p 1234\n"
554+
"\n"
555+
" # Profile a script by running it in a subprocess\n"
556+
" python -m profile.sample myscript.py arg1 arg2\n"
557+
"\n"
558+
" # Profile a module by running it as python -m module in a subprocess\n"
559+
" python -m profile.sample -m mymodule arg1 arg2\n"
552560
"\n"
553561
" # Profile with custom interval and duration, save to file\n"
554-
" python -m profile.sample -i 50 -d 30 -o profile.stats 1234\n"
562+
" python -m profile.sample -i 50 -d 30 -o profile.stats -p 1234\n"
555563
"\n"
556564
" # Generate collapsed stacks for flamegraph\n"
557-
" python -m profile.sample --collapsed 1234\n"
565+
" python -m profile.sample --collapsed -p 1234\n"
558566
"\n"
559567
" # Profile all threads, sort by total time\n"
560-
" python -m profile.sample -a --sort-tottime 1234\n"
568+
" python -m profile.sample -a --sort-tottime -p 1234\n"
561569
"\n"
562570
" # Profile for 1 minute with 1ms sampling interval\n"
563-
" python -m profile.sample -i 1000 -d 60 1234\n"
571+
" python -m profile.sample -i 1000 -d 60 -p 1234\n"
564572
"\n"
565573
" # Show only top 20 functions sorted by direct samples\n"
566-
" python -m profile.sample --sort-nsamples -l 20 1234\n"
574+
" python -m profile.sample --sort-nsamples -l 20 -p 1234\n"
567575
"\n"
568576
" # Profile all threads and save collapsed stacks\n"
569-
" python -m profile.sample -a --collapsed -o stacks.txt 1234\n"
577+
" python -m profile.sample -a --collapsed -o stacks.txt -p 1234\n"
570578
"\n"
571579
" # Profile with real-time sampling statistics\n"
572-
" python -m profile.sample --realtime-stats 1234\n"
580+
" python -m profile.sample --realtime-stats -p 1234\n"
573581
"\n"
574582
" # Sort by sample percentage to find most sampled functions\n"
575-
" python -m profile.sample --sort-sample-pct 1234\n"
583+
" python -m profile.sample --sort-sample-pct -p 1234\n"
576584
"\n"
577585
" # Sort by cumulative samples to find functions most on call stack\n"
578-
" python -m profile.sample --sort-nsamples-cumul 1234"
586+
" python -m profile.sample --sort-nsamples-cumul -p 1234\n"
579587
),
580588
formatter_class=argparse.RawDescriptionHelpFormatter,
581589
)
582590

583-
# Required arguments
584-
parser.add_argument("pid", type=int, help="Process ID to sample")
591+
# Target selection
592+
target_group = parser.add_mutually_exclusive_group(required=True)
593+
target_group.add_argument(
594+
"-p", "--pid", type=int, help="Process ID to sample"
595+
)
596+
target_group.add_argument(
597+
"-m", "--module",
598+
nargs=argparse.REMAINDER,
599+
help="Run and profile a module as python -m module [ARGS...]"
600+
)
601+
target_group.add_argument(
602+
"script",
603+
nargs=argparse.REMAINDER,
604+
help="Script to run and profile, with optional arguments"
605+
)
585606

586607
# Sampling options
587608
sampling_group = parser.add_argument_group("Sampling configuration")
@@ -712,19 +733,59 @@ def main():
712733

713734
sort_value = args.sort if args.sort is not None else 2
714735

715-
sample(
716-
args.pid,
717-
sample_interval_usec=args.interval,
718-
duration_sec=args.duration,
719-
filename=args.outfile,
720-
all_threads=args.all_threads,
721-
limit=args.limit,
722-
sort=sort_value,
723-
show_summary=not args.no_summary,
724-
output_format=args.format,
725-
realtime_stats=args.realtime_stats,
726-
)
736+
if not(args.pid or args.module or args.script):
737+
parser.error(
738+
"You must specify either a process ID (-p), a module (-m), or a script to run."
739+
)
727740

741+
if args.pid:
742+
sample(
743+
args.pid,
744+
sample_interval_usec=args.interval,
745+
duration_sec=args.duration,
746+
filename=args.outfile,
747+
all_threads=args.all_threads,
748+
limit=args.limit,
749+
sort=sort_value,
750+
show_summary=not args.no_summary,
751+
output_format=args.format,
752+
realtime_stats=args.realtime_stats,
753+
)
754+
elif args.module or args.script:
755+
if args.module:
756+
cmd = [sys.executable, "-m", *args.module]
757+
else:
758+
cmd = [sys.executable, *args.script]
759+
760+
process = subprocess.Popen(cmd)
761+
762+
try:
763+
exit_code = process.wait(timeout=0.1)
764+
sys.exit(exit_code)
765+
except subprocess.TimeoutExpired:
766+
pass
767+
768+
try:
769+
sample(
770+
process.pid,
771+
sort=sort_value,
772+
sample_interval_usec=args.interval,
773+
duration_sec=args.duration,
774+
filename=args.outfile,
775+
all_threads=args.all_threads,
776+
limit=args.limit,
777+
show_summary=not args.no_summary,
778+
output_format=args.format,
779+
realtime_stats=args.realtime_stats,
780+
)
781+
finally:
782+
if process.poll() is None:
783+
process.terminate()
784+
try:
785+
process.wait(timeout=2)
786+
except subprocess.TimeoutExpired:
787+
process.kill()
788+
process.wait()
728789

729790
if __name__ == "__main__":
730791
main()

0 commit comments

Comments
 (0)