Skip to content

Commit 170760b

Browse files
committed
Address feedback
1 parent 817fdc0 commit 170760b

File tree

2 files changed

+80
-83
lines changed

2 files changed

+80
-83
lines changed

Lib/profiling/sampling/cli.py

Lines changed: 76 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -29,40 +29,32 @@
2929
LiveStatsCollector = None
3030

3131

32+
class CustomFormatter(
33+
argparse.ArgumentDefaultsHelpFormatter,
34+
argparse.RawDescriptionHelpFormatter,
35+
):
36+
"""Custom formatter that combines default values display with raw description formatting."""
37+
pass
38+
39+
3240
_HELP_DESCRIPTION = """Sample a process's stack frames and generate profiling data.
3341
3442
Commands:
3543
run Run and profile a script or module
3644
attach Attach to and profile a running process
37-
live Interactive TUI profiler (top-like interface)
3845
3946
Examples:
40-
# Run and profile a script (default: pstats to stdout)
47+
# Run and profile a script
4148
python -m profiling.sampling run script.py arg1 arg2
4249
43-
# Run and profile a module
44-
python -m profiling.sampling run -m mymodule arg1 arg2
45-
4650
# Attach to a running process
4751
python -m profiling.sampling attach 1234
4852
49-
# Live interactive mode for a process
50-
python -m profiling.sampling live 1234
51-
52-
# Live mode for a script
53-
python -m profiling.sampling live script.py
54-
55-
# Generate flamegraph from a script
56-
python -m profiling.sampling run --flamegraph -o output.html script.py
53+
# Live interactive mode for a script
54+
python -m profiling.sampling run --live script.py
5755
58-
# Profile with custom interval and duration
59-
python -m profiling.sampling run -i 50 -d 30 script.py
60-
61-
# Profile all threads, sort by total time
62-
python -m profiling.sampling attach -a --sort tottime 1234
63-
64-
# Save collapsed stacks to file
65-
python -m profiling.sampling run --collapsed -o stacks.txt script.py
56+
# Live interactive mode for a running process
57+
python -m profiling.sampling attach --live 1234
6658
6759
Use 'python -m profiling.sampling <command> --help' for command-specific help."""
6860

@@ -167,14 +159,16 @@ def _add_sampling_options(parser):
167159
"--interval",
168160
type=int,
169161
default=100,
170-
help="Sampling interval in microseconds (default: 100)",
162+
metavar="MICROSECONDS",
163+
help="Sampling interval",
171164
)
172165
sampling_group.add_argument(
173166
"-d",
174167
"--duration",
175168
type=int,
176169
default=10,
177-
help="Sampling duration in seconds (default: 10)",
170+
metavar="SECONDS",
171+
help="Sampling duration",
178172
)
179173
sampling_group.add_argument(
180174
"-a",
@@ -208,7 +202,7 @@ def _add_mode_options(parser):
208202
choices=["wall", "cpu", "gil"],
209203
default="wall",
210204
help="Sampling mode: wall (all samples), cpu (only samples when thread is on CPU), "
211-
"gil (only samples when thread holds the GIL) (default: wall)",
205+
"gil (only samples when thread holds the GIL)",
212206
)
213207

214208

@@ -270,14 +264,14 @@ def _add_pstats_options(parser):
270264
"name",
271265
],
272266
default="nsamples",
273-
help="Sort order for pstats output (default: nsamples)",
267+
help="Sort order for pstats output",
274268
)
275269
pstats_group.add_argument(
276270
"-l",
277271
"--limit",
278272
type=int,
279273
default=15,
280-
help="Limit the number of rows in the output (default: 15)",
274+
help="Limit the number of rows in the output",
281275
)
282276
pstats_group.add_argument(
283277
"--no-summary",
@@ -368,13 +362,18 @@ def _validate_args(args, parser):
368362
parser: ArgumentParser instance for error reporting
369363
"""
370364
# Check if live mode is available
371-
if args.command == "live" and LiveStatsCollector is None:
365+
if hasattr(args, 'live') and args.live and LiveStatsCollector is None:
372366
parser.error(
373367
"Live mode requires the curses module, which is not available."
374368
)
375369

376-
# Only validate format options for run/attach commands (live doesn't have format option)
377-
if args.command not in ("run", "attach"):
370+
# Live mode is incompatible with format options
371+
if hasattr(args, 'live') and args.live:
372+
if args.format != "pstats":
373+
format_flag = f"--{args.format}"
374+
parser.error(
375+
f"--live is incompatible with {format_flag}. Live mode uses a TUI interface."
376+
)
378377
return
379378

380379
# Validate gecko mode doesn't use non-wall mode
@@ -406,7 +405,7 @@ def main():
406405
# Create the main parser
407406
parser = argparse.ArgumentParser(
408407
description=_HELP_DESCRIPTION,
409-
formatter_class=argparse.RawDescriptionHelpFormatter,
408+
formatter_class=CustomFormatter,
410409
)
411410

412411
# Create subparsers for commands
@@ -418,8 +417,24 @@ def main():
418417
run_parser = subparsers.add_parser(
419418
"run",
420419
help="Run and profile a script or module",
421-
formatter_class=argparse.RawDescriptionHelpFormatter,
422-
description="Run and profile a Python script or module",
420+
formatter_class=CustomFormatter,
421+
description="""Run and profile a Python script or module
422+
423+
Examples:
424+
# Run and profile a module
425+
python -m profiling.sampling run -m mymodule arg1 arg2
426+
427+
# Generate flamegraph from a script
428+
python -m profiling.sampling run --flamegraph -o output.html script.py
429+
430+
# Profile with custom interval and duration
431+
python -m profiling.sampling run -i 50 -d 30 script.py
432+
433+
# Save collapsed stacks to file
434+
python -m profiling.sampling run --collapsed -o stacks.txt script.py
435+
436+
# Live interactive mode for a script
437+
python -m profiling.sampling run --live script.py""",
423438
)
424439
run_parser.add_argument(
425440
"-m",
@@ -436,6 +451,11 @@ def main():
436451
nargs=argparse.REMAINDER,
437452
help="Arguments to pass to the script or module",
438453
)
454+
run_parser.add_argument(
455+
"--live",
456+
action="store_true",
457+
help="Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)",
458+
)
439459
_add_sampling_options(run_parser)
440460
_add_mode_options(run_parser)
441461
_add_format_options(run_parser)
@@ -445,44 +465,31 @@ def main():
445465
attach_parser = subparsers.add_parser(
446466
"attach",
447467
help="Attach to and profile a running process",
448-
formatter_class=argparse.RawDescriptionHelpFormatter,
449-
description="Attach to a running process and profile it",
468+
formatter_class=CustomFormatter,
469+
description="""Attach to a running process and profile it
470+
471+
Examples:
472+
# Profile all threads, sort by total time
473+
python -m profiling.sampling attach -a --sort tottime 1234
474+
475+
# Live interactive mode for a running process
476+
python -m profiling.sampling attach --live 1234""",
450477
)
451478
attach_parser.add_argument(
452479
"pid",
453480
type=int,
454481
help="Process ID to attach to",
455482
)
483+
attach_parser.add_argument(
484+
"--live",
485+
action="store_true",
486+
help="Interactive TUI profiler (top-like interface, press 'q' to quit, 's' to cycle sort)",
487+
)
456488
_add_sampling_options(attach_parser)
457489
_add_mode_options(attach_parser)
458490
_add_format_options(attach_parser)
459491
_add_pstats_options(attach_parser)
460492

461-
# === LIVE COMMAND ===
462-
live_parser = subparsers.add_parser(
463-
"live",
464-
help="Interactive TUI profiler (top-like interface)",
465-
formatter_class=argparse.RawDescriptionHelpFormatter,
466-
description="Interactive live profiling with a terminal UI (press 'q' to quit, 's' to cycle sort)",
467-
)
468-
live_parser.add_argument(
469-
"-m",
470-
"--module",
471-
action="store_true",
472-
help="Run target as a module (like python -m)",
473-
)
474-
live_parser.add_argument(
475-
"target",
476-
help="Process ID, script file, or module name to profile",
477-
)
478-
live_parser.add_argument(
479-
"args",
480-
nargs=argparse.REMAINDER,
481-
help="Arguments to pass to the script or module (if not a PID)",
482-
)
483-
_add_sampling_options(live_parser)
484-
_add_mode_options(live_parser)
485-
486493
# Parse arguments
487494
args = parser.parse_args()
488495

@@ -493,7 +500,6 @@ def main():
493500
command_handlers = {
494501
"run": _handle_run,
495502
"attach": _handle_attach,
496-
"live": _handle_live,
497503
}
498504

499505
# Execute the appropriate command
@@ -506,6 +512,11 @@ def main():
506512

507513
def _handle_attach(args):
508514
"""Handle the 'attach' command."""
515+
# Check if live mode is requested
516+
if args.live:
517+
_handle_live_attach(args, args.pid)
518+
return
519+
509520
# Use PROFILING_MODE_ALL for gecko format
510521
mode = (
511522
PROFILING_MODE_ALL
@@ -539,6 +550,11 @@ def _handle_attach(args):
539550

540551
def _handle_run(args):
541552
"""Handle the 'run' command."""
553+
# Check if live mode is requested
554+
if args.live:
555+
_handle_live_run(args)
556+
return
557+
542558
# Build the command to run
543559
if args.module:
544560
cmd = (sys.executable, "-m", args.target, *args.args)
@@ -589,26 +605,6 @@ def _handle_run(args):
589605
process.wait()
590606

591607

592-
def _handle_live(args):
593-
"""Handle the 'live' command."""
594-
# Determine if target is a PID or a script/module
595-
try:
596-
# Try to parse as PID
597-
pid = int(args.target)
598-
is_pid = True
599-
except ValueError:
600-
# It's a script or module name
601-
is_pid = False
602-
pid = None
603-
604-
if is_pid:
605-
# Attach to existing process in live mode
606-
_handle_live_attach(args, pid)
607-
else:
608-
# Run script/module in live mode
609-
_handle_live_run(args)
610-
611-
612608
def _handle_live_attach(args, pid):
613609
"""Handle live mode for an existing process."""
614610
mode = _parse_mode(args.mode)

Lib/test/test_profiling/test_sampling_profiler/test_integration.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,13 @@ def test_sampling_basic_functionality(self):
405405
):
406406
try:
407407
# Sample for up to SHORT_TIMEOUT seconds, but process exits after fixed iterations
408+
collector = PstatsCollector(sample_interval_usec=1000, skip_idle=False)
408409
profiling.sampling.sample.sample(
409410
subproc.process.pid,
411+
collector,
410412
duration_sec=SHORT_TIMEOUT,
411-
sample_interval_usec=1000, # 1ms
412-
show_summary=False,
413413
)
414+
collector.print_stats(show_summary=False)
414415
except PermissionError:
415416
self.skipTest("Insufficient permissions for remote profiling")
416417

@@ -552,7 +553,7 @@ def test_sample_target_script(self):
552553
self.addCleanup(close_and_unlink, script_file)
553554

554555
# Sample for up to SHORT_TIMEOUT seconds, but process exits after fixed iterations
555-
test_args = ["profiling.sampling.sample", "-d", PROFILING_TIMEOUT, script_file.name]
556+
test_args = ["profiling.sampling.sample", "run", "-d", PROFILING_TIMEOUT, script_file.name]
556557

557558
with (
558559
mock.patch("sys.argv", test_args),

0 commit comments

Comments
 (0)