From: László Kiss Kollár Date: Thu, 25 Dec 2025 19:21:16 +0000 (+0000) Subject: gh-138122: Remove default duration for statistical profiling (#143174) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=888d101445c72c7cf23923e99ed567732f42fb79;p=thirdparty%2FPython%2Fcpython.git gh-138122: Remove default duration for statistical profiling (#143174) Co-authored-by: Pablo Galindo Salgado --- diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst index 370bbcd32425..dae67cca66d9 100644 --- a/Doc/library/profiling.sampling.rst +++ b/Doc/library/profiling.sampling.rst @@ -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 @@ -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`` @@ -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 @@ -1394,7 +1393,7 @@ Sampling options .. option:: -d , --duration - Profiling duration in seconds. Default: 10. + Profiling duration in seconds. Default: run to completion. .. option:: -a, --all-threads diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index 10341c1570ce..dd6431a0322b 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -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: @@ -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", @@ -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) @@ -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 ) @@ -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() diff --git a/Lib/profiling/sampling/sample.py b/Lib/profiling/sampling/sample.py index 2fe022c85b0b..5525bffdf574 100644 --- a/Lib/profiling/sampling/sample.py +++ b/Lib/profiling/sampling/sample.py @@ -76,18 +76,18 @@ class SampleProfiler: ) 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 @@ -104,7 +104,7 @@ class SampleProfiler: 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() @@ -135,25 +135,25 @@ class SampleProfiler: 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)}") @@ -166,7 +166,7 @@ class SampleProfiler: # 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( @@ -363,7 +363,7 @@ def sample( pid, collector, *, - duration_sec=10, + duration_sec=None, all_threads=False, realtime_stats=False, mode=PROFILING_MODE_WALL, @@ -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), @@ -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,