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
* - 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``
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
.. option:: -d <seconds>, --duration <seconds>
- Profiling duration in seconds. Default: 10.
+ Profiling duration in seconds. Default: run to completion.
.. option:: -a, --all-threads
# 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:
"-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",
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)
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
)
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()
)
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
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()
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)}")
# 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(
pid,
collector,
*,
- duration_sec=10,
+ duration_sec=None,
all_threads=False,
realtime_stats=False,
mode=PROFILING_MODE_WALL,
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),
pid,
collector,
*,
- duration_sec=10,
+ duration_sec=None,
all_threads=False,
realtime_stats=False,
mode=PROFILING_MODE_WALL,