mirror of
https://github.com/python/cpython.git
synced 2025-10-19 16:03:42 +00:00
gh-138709: Implement CPU time profiling in profiling.sample (#138710)
This commit is contained in:
parent
d06113c7a7
commit
67636f72d2
13 changed files with 971 additions and 38 deletions
|
@ -15,6 +15,21 @@
|
|||
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
|
||||
|
||||
_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None
|
||||
|
||||
# Profiling mode constants
|
||||
PROFILING_MODE_WALL = 0
|
||||
PROFILING_MODE_CPU = 1
|
||||
PROFILING_MODE_GIL = 2
|
||||
|
||||
|
||||
def _parse_mode(mode_string):
|
||||
"""Convert mode string to mode constant."""
|
||||
mode_map = {
|
||||
"wall": PROFILING_MODE_WALL,
|
||||
"cpu": PROFILING_MODE_CPU,
|
||||
"gil": PROFILING_MODE_GIL,
|
||||
}
|
||||
return mode_map[mode_string]
|
||||
_HELP_DESCRIPTION = """Sample a process's stack frames and generate profiling data.
|
||||
Supports the following target modes:
|
||||
- -p PID: Profile an existing process by PID
|
||||
|
@ -120,18 +135,18 @@ def _run_with_sync(original_cmd):
|
|||
|
||||
|
||||
class SampleProfiler:
|
||||
def __init__(self, pid, sample_interval_usec, all_threads):
|
||||
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL):
|
||||
self.pid = pid
|
||||
self.sample_interval_usec = sample_interval_usec
|
||||
self.all_threads = all_threads
|
||||
if _FREE_THREADED_BUILD:
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, all_threads=self.all_threads
|
||||
self.pid, all_threads=self.all_threads, mode=mode
|
||||
)
|
||||
else:
|
||||
only_active_threads = bool(self.all_threads)
|
||||
self.unwinder = _remote_debugging.RemoteUnwinder(
|
||||
self.pid, only_active_thread=only_active_threads
|
||||
self.pid, only_active_thread=only_active_threads, mode=mode
|
||||
)
|
||||
# Track sample intervals and total sample count
|
||||
self.sample_intervals = deque(maxlen=100)
|
||||
|
@ -596,21 +611,25 @@ def sample(
|
|||
show_summary=True,
|
||||
output_format="pstats",
|
||||
realtime_stats=False,
|
||||
mode=PROFILING_MODE_WALL,
|
||||
):
|
||||
profiler = SampleProfiler(
|
||||
pid, sample_interval_usec, all_threads=all_threads
|
||||
pid, sample_interval_usec, all_threads=all_threads, mode=mode
|
||||
)
|
||||
profiler.realtime_stats = realtime_stats
|
||||
|
||||
# Determine skip_idle for collector compatibility
|
||||
skip_idle = mode != PROFILING_MODE_WALL
|
||||
|
||||
collector = None
|
||||
match output_format:
|
||||
case "pstats":
|
||||
collector = PstatsCollector(sample_interval_usec)
|
||||
collector = PstatsCollector(sample_interval_usec, skip_idle=skip_idle)
|
||||
case "collapsed":
|
||||
collector = CollapsedStackCollector()
|
||||
collector = CollapsedStackCollector(skip_idle=skip_idle)
|
||||
filename = filename or f"collapsed.{pid}.txt"
|
||||
case "flamegraph":
|
||||
collector = FlamegraphCollector()
|
||||
collector = FlamegraphCollector(skip_idle=skip_idle)
|
||||
filename = filename or f"flamegraph.{pid}.html"
|
||||
case _:
|
||||
raise ValueError(f"Invalid output format: {output_format}")
|
||||
|
@ -661,6 +680,8 @@ def wait_for_process_and_sample(pid, sort_value, args):
|
|||
if not filename and args.format == "collapsed":
|
||||
filename = f"collapsed.{pid}.txt"
|
||||
|
||||
mode = _parse_mode(args.mode)
|
||||
|
||||
sample(
|
||||
pid,
|
||||
sort=sort_value,
|
||||
|
@ -672,6 +693,7 @@ def wait_for_process_and_sample(pid, sort_value, args):
|
|||
show_summary=not args.no_summary,
|
||||
output_format=args.format,
|
||||
realtime_stats=args.realtime_stats,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
|
||||
|
@ -726,6 +748,15 @@ def main():
|
|||
help="Print real-time sampling statistics (Hz, mean, min, max, stdev) during profiling",
|
||||
)
|
||||
|
||||
# Mode options
|
||||
mode_group = parser.add_argument_group("Mode options")
|
||||
mode_group.add_argument(
|
||||
"--mode",
|
||||
choices=["wall", "cpu", "gil"],
|
||||
default="wall",
|
||||
help="Sampling mode: wall (all threads), cpu (only CPU-running threads), gil (only GIL-holding threads)",
|
||||
)
|
||||
|
||||
# Output format selection
|
||||
output_group = parser.add_argument_group("Output options")
|
||||
output_format = output_group.add_mutually_exclusive_group()
|
||||
|
@ -850,6 +881,8 @@ def main():
|
|||
elif target_count > 1:
|
||||
parser.error("only one target type can be specified: -p/--pid, -m/--module, or script")
|
||||
|
||||
mode = _parse_mode(args.mode)
|
||||
|
||||
if args.pid:
|
||||
sample(
|
||||
args.pid,
|
||||
|
@ -862,6 +895,7 @@ def main():
|
|||
show_summary=not args.no_summary,
|
||||
output_format=args.format,
|
||||
realtime_stats=args.realtime_stats,
|
||||
mode=mode,
|
||||
)
|
||||
elif args.module or args.args:
|
||||
if args.module:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue