Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions Doc/library/profiling.sampling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,8 @@ is unaware it is being profiled.
When profiling production systems, keep these guidelines in mind:

Start with shorter durations (10-30 seconds) to get quick results, then extend
if you need more statistical accuracy. The default 10-second duration is usually
sufficient to identify major hotspots.
if you need more statistical accuracy. By default, profiling runs until the
target process completes, which is usually sufficient to identify major hotspots.

If possible, profile during representative load rather than peak traffic.
Profiles collected during normal operation are easier to interpret than those
Expand Down Expand Up @@ -329,7 +329,7 @@ The default configuration works well for most use cases:
* - Default for ``--sampling-rate`` / ``-r``
- 1 kHz
* - Default for ``--duration`` / ``-d``
- 10 seconds
- Run to completion
* - Default for ``--all-threads`` / ``-a``
- Main thread only
* - Default for ``--native``
Expand Down Expand Up @@ -363,15 +363,14 @@ cost of slightly higher profiler CPU usage. Lower rates reduce profiler
overhead but may miss short-lived functions. For most applications, the
default rate provides a good balance between accuracy and overhead.

The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
default is 10 seconds::
The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. By
default, profiling continues until the target process exits or is interrupted::

python -m profiling.sampling run -d 60 script.py

Longer durations collect more samples and produce more statistically reliable
results, especially for code paths that execute infrequently. When profiling
a program that runs for a fixed time, you may want to set the duration to
match or exceed the expected runtime.
Specifying a duration is useful when attaching to long-running processes or when
you want to limit profiling to a specific time window. When profiling a script,
the default behavior of running to completion is usually what you want.


Thread selection
Expand Down Expand Up @@ -1394,7 +1393,7 @@ Sampling options

.. option:: -d <seconds>, --duration <seconds>

Profiling duration in seconds. Default: 10.
Profiling duration in seconds. Default: run to completion.

.. option:: -a, --all-threads

Expand Down
20 changes: 10 additions & 10 deletions Lib/profiling/sampling/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ def _build_child_profiler_args(args):
# Sampling options
hz = MICROSECONDS_PER_SECOND // args.sample_interval_usec
child_args.extend(["-r", str(hz)])
child_args.extend(["-d", str(args.duration)])

if args.duration is not None:
child_args.extend(["-d", str(args.duration)])
if args.all_threads:
child_args.append("-a")
if args.realtime_stats:
Expand Down Expand Up @@ -356,9 +356,9 @@ def _add_sampling_options(parser):
"-d",
"--duration",
type=int,
default=10,
default=None,
metavar="SECONDS",
help="Sampling duration",
help="Sampling duration (default: run to completion)",
)
sampling_group.add_argument(
"-a",
Expand Down Expand Up @@ -562,7 +562,7 @@ def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=Fals
if format_type == "binary":
if output_file is None:
raise ValueError("Binary format requires an output file")
return collector_class(output_file, interval, skip_idle=skip_idle,
return collector_class(output_file, sample_interval_usec, skip_idle=skip_idle,
compression=compression)

# Gecko format never skips idle (it needs both GIL and CPU data)
Expand Down Expand Up @@ -643,11 +643,11 @@ def _validate_args(args, parser):
return

# Warn about blocking mode with aggressive sampling intervals
if args.blocking and args.interval < 100:
if args.blocking and args.sample_interval_usec < 100:
print(
f"Warning: --blocking with a {args.interval} µs interval will stop all threads "
f"{1_000_000 // args.interval} times per second. "
"Consider using --interval 1000 or higher to reduce overhead.",
f"Warning: --blocking with a {args.sample_interval_usec} µs interval will stop all threads "
f"{1_000_000 // args.sample_interval_usec} times per second. "
"Consider using --sampling-rate 1khz or lower to reduce overhead.",
file=sys.stderr
)

Expand Down Expand Up @@ -1107,7 +1107,7 @@ def _handle_live_run(args):
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=_PROCESS_KILL_TIMEOUT)
process.wait(timeout=_PROCESS_KILL_TIMEOUT_SEC)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
Expand Down
27 changes: 14 additions & 13 deletions Lib/profiling/sampling/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,18 @@ def _new_unwinder(self, native, gc, opcodes, skip_non_matching_threads):
)
return unwinder

def sample(self, collector, duration_sec=10, *, async_aware=False):
def sample(self, collector, duration_sec=None, *, async_aware=False):
sample_interval_sec = self.sample_interval_usec / 1_000_000
running_time = 0
num_samples = 0
errors = 0
interrupted = False
running_time_sec = 0
start_time = next_time = time.perf_counter()
last_sample_time = start_time
realtime_update_interval = 1.0 # Update every second
last_realtime_update = start_time
try:
while running_time < duration_sec:
while duration_sec is None or running_time_sec < duration_sec:
# Check if live collector wants to stop
if hasattr(collector, 'running') and not collector.running:
break
Expand All @@ -104,7 +104,7 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
stack_frames = self.unwinder.get_stack_trace()
collector.collect(stack_frames)
except ProcessLookupError as e:
duration_sec = current_time - start_time
running_time_sec = current_time - start_time
break
except (RuntimeError, UnicodeDecodeError, MemoryError, OSError):
collector.collect_failed_sample()
Expand Down Expand Up @@ -135,25 +135,25 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
num_samples += 1
next_time += sample_interval_sec

running_time = time.perf_counter() - start_time
running_time_sec = time.perf_counter() - start_time
except KeyboardInterrupt:
interrupted = True
running_time = time.perf_counter() - start_time
running_time_sec = time.perf_counter() - start_time
print("Interrupted by user.")

# Clear real-time stats line if it was being displayed
if self.realtime_stats and len(self.sample_intervals) > 0:
print() # Add newline after real-time stats

sample_rate = num_samples / running_time if running_time > 0 else 0
sample_rate = num_samples / running_time_sec if running_time_sec > 0 else 0
error_rate = (errors / num_samples) * 100 if num_samples > 0 else 0
expected_samples = int(duration_sec / sample_interval_sec)
expected_samples = int(running_time_sec / sample_interval_sec)
missed_samples = (expected_samples - num_samples) / expected_samples * 100 if expected_samples > 0 else 0

# Don't print stats for live mode (curses is handling display)
is_live_mode = LiveStatsCollector is not None and isinstance(collector, LiveStatsCollector)
if not is_live_mode:
print(f"Captured {num_samples:n} samples in {fmt(running_time, 2)} seconds")
print(f"Captured {num_samples:n} samples in {fmt(running_time_sec, 2)} seconds")
print(f"Sample rate: {fmt(sample_rate, 2)} samples/sec")
print(f"Error rate: {fmt(error_rate, 2)}")

Expand All @@ -166,7 +166,7 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):

# Pass stats to flamegraph collector if it's the right type
if hasattr(collector, 'set_stats'):
collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, missed_samples, mode=self.mode)
collector.set_stats(self.sample_interval_usec, running_time_sec, sample_rate, error_rate, missed_samples, mode=self.mode)

if num_samples < expected_samples and not is_live_mode and not interrupted:
print(
Expand Down Expand Up @@ -363,7 +363,7 @@ def sample(
pid,
collector,
*,
duration_sec=10,
duration_sec=None,
all_threads=False,
realtime_stats=False,
mode=PROFILING_MODE_WALL,
Expand All @@ -378,7 +378,8 @@ def sample(
Args:
pid: Process ID to sample
collector: Collector instance to use for gathering samples
duration_sec: How long to sample for (seconds)
duration_sec: How long to sample for (seconds), or None to run until
the process exits or interrupted
all_threads: Whether to sample all threads
realtime_stats: Whether to print real-time sampling statistics
mode: Profiling mode - WALL (all samples), CPU (only when on CPU),
Expand Down Expand Up @@ -427,7 +428,7 @@ def sample_live(
pid,
collector,
*,
duration_sec=10,
duration_sec=None,
all_threads=False,
realtime_stats=False,
mode=PROFILING_MODE_WALL,
Expand Down
Loading