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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy