]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138122: Remove default duration for statistical profiling (#143174) main
authorLászló Kiss Kollár <kiss.kollar.laszlo@gmail.com>
Thu, 25 Dec 2025 19:21:16 +0000 (19:21 +0000)
committerGitHub <noreply@github.com>
Thu, 25 Dec 2025 19:21:16 +0000 (19:21 +0000)
Co-authored-by: Pablo Galindo Salgado <pablogsal@gmail.com>
Doc/library/profiling.sampling.rst
Lib/profiling/sampling/cli.py
Lib/profiling/sampling/sample.py

index 370bbcd32425264087792affdcd60123ab6fc822..dae67cca66d9b44b3659e87612e9885cd1ce5f21 100644 (file)
@@ -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 <seconds>, --duration <seconds>
 
-   Profiling duration in seconds. Default: 10.
+   Profiling duration in seconds. Default: run to completion.
 
 .. option:: -a, --all-threads
 
index 10341c1570cecafe4ca8a707933abf03000a834e..dd6431a0322bc7c8bfd2b448cfdbc06af8355e92 100644 (file)
@@ -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()
index 2fe022c85b0b317892c41a0503b3d8698331188b..5525bffdf5747d67a60d186f3302c651804e4ab6 100644 (file)
@@ -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,