gh-138709: Implement CPU time profiling in profiling.sample (#138710)

This commit is contained in:
Pablo Galindo Salgado 2025-09-19 19:17:28 +01:00 committed by GitHub
parent d06113c7a7
commit 67636f72d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 971 additions and 38 deletions

View file

@ -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: