https://github.com/python/cpython/commit/888d101445c72c7cf23923e99ed567732f42fb79
commit: 888d101445c72c7cf23923e99ed567732f42fb79
branch: main
author: László Kiss Kollár <[email protected]>
committer: pablogsal <[email protected]>
date: 2025-12-25T19:21:16Z
summary:

gh-138122: Remove default duration for statistical profiling (#143174)

Co-authored-by: Pablo Galindo Salgado <[email protected]>

files:
M Doc/library/profiling.sampling.rst
M Lib/profiling/sampling/cli.py
M Lib/profiling/sampling/sample.py

diff --git a/Doc/library/profiling.sampling.rst 
b/Doc/library/profiling.sampling.rst
index 370bbcd3242526..dae67cca66d9b4 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 <seconds>, --duration <seconds>
 
-   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 10341c1570ceca..dd6431a0322bc7 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 2fe022c85b0b31..5525bffdf5747d 100644
--- a/Lib/profiling/sampling/sample.py
+++ b/Lib/profiling/sampling/sample.py
@@ -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
@@ -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()
@@ -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)}")
 
@@ -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(
@@ -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,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to