From a13a4aa30faf0d934ff141c87a0a1fe3a2b68f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Kiss=20Koll=C3=A1r?= Date: Mon, 22 Dec 2025 14:14:02 +0000 Subject: [PATCH 1/2] Standardise time units --- Lib/profiling/sampling/_child_monitor.py | 4 +-- Lib/profiling/sampling/_sync_coordinator.py | 8 +++--- Lib/profiling/sampling/cli.py | 12 ++++----- Lib/profiling/sampling/constants.py | 4 +++ .../sampling/live_collector/__init__.py | 4 +-- .../sampling/live_collector/collector.py | 14 +++++----- .../sampling/live_collector/constants.py | 2 +- .../sampling/live_collector/widgets.py | 8 +++--- Lib/profiling/sampling/pstats_collector.py | 5 ++-- .../test_live_collector_interaction.py | 26 +++++++++---------- 10 files changed, 46 insertions(+), 41 deletions(-) diff --git a/Lib/profiling/sampling/_child_monitor.py b/Lib/profiling/sampling/_child_monitor.py index e06c550d938b13..ec56f75719f9d1 100644 --- a/Lib/profiling/sampling/_child_monitor.py +++ b/Lib/profiling/sampling/_child_monitor.py @@ -16,7 +16,7 @@ _CHILD_POLL_INTERVAL_SEC = 0.1 # Default timeout for waiting on child profilers -_DEFAULT_WAIT_TIMEOUT = 30.0 +_DEFAULT_WAIT_TIMEOUT_SEC = 30.0 # Maximum number of child profilers to spawn (prevents resource exhaustion) _MAX_CHILD_PROFILERS = 100 @@ -138,7 +138,7 @@ def spawned_profilers(self): with self._lock: return list(self._spawned_profilers) - def wait_for_profilers(self, timeout=_DEFAULT_WAIT_TIMEOUT): + def wait_for_profilers(self, timeout=_DEFAULT_WAIT_TIMEOUT_SEC): """ Wait for all spawned child profilers to complete. diff --git a/Lib/profiling/sampling/_sync_coordinator.py b/Lib/profiling/sampling/_sync_coordinator.py index 1a4af42588a3f5..63d057043f0416 100644 --- a/Lib/profiling/sampling/_sync_coordinator.py +++ b/Lib/profiling/sampling/_sync_coordinator.py @@ -73,8 +73,8 @@ def _validate_arguments(args: List[str]) -> tuple[int, str, List[str]]: # Constants for socket communication _MAX_RETRIES = 3 -_INITIAL_RETRY_DELAY = 0.1 -_SOCKET_TIMEOUT = 2.0 +_INITIAL_RETRY_DELAY_SEC = 0.1 +_SOCKET_TIMEOUT_SEC = 2.0 _READY_MESSAGE = b"ready" @@ -93,14 +93,14 @@ def _signal_readiness(sync_port: int) -> None: for attempt in range(_MAX_RETRIES): try: # Use context manager for automatic cleanup - with socket.create_connection(("127.0.0.1", sync_port), timeout=_SOCKET_TIMEOUT) as sock: + with socket.create_connection(("127.0.0.1", sync_port), timeout=_SOCKET_TIMEOUT_SEC) as sock: sock.send(_READY_MESSAGE) return except (socket.error, OSError) as e: last_error = e if attempt < _MAX_RETRIES - 1: # Exponential backoff before retry - time.sleep(_INITIAL_RETRY_DELAY * (2 ** attempt)) + time.sleep(_INITIAL_RETRY_DELAY_SEC * (2 ** attempt)) # If we get here, all retries failed raise SyncError(f"Failed to signal readiness after {_MAX_RETRIES} attempts: {last_error}") from last_error diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index ccd6e954d79698..c43e4452f9b8d9 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -66,8 +66,8 @@ class CustomFormatter( # Constants for socket synchronization -_SYNC_TIMEOUT = 5.0 -_PROCESS_KILL_TIMEOUT = 2.0 +_SYNC_TIMEOUT_SEC = 5.0 +_PROCESS_KILL_TIMEOUT_SEC = 2.0 _READY_MESSAGE = b"ready" _RECV_BUFFER_SIZE = 1024 @@ -239,7 +239,7 @@ def _run_with_sync(original_cmd, suppress_output=False): sync_sock.bind(("127.0.0.1", 0)) # Let OS choose a free port sync_port = sync_sock.getsockname()[1] sync_sock.listen(1) - sync_sock.settimeout(_SYNC_TIMEOUT) + sync_sock.settimeout(_SYNC_TIMEOUT_SEC) # Get current working directory to preserve it cwd = os.getcwd() @@ -268,7 +268,7 @@ def _run_with_sync(original_cmd, suppress_output=False): process = subprocess.Popen(cmd, **popen_kwargs) try: - _wait_for_ready_signal(sync_sock, process, _SYNC_TIMEOUT) + _wait_for_ready_signal(sync_sock, process, _SYNC_TIMEOUT_SEC) # Close stderr pipe if we were capturing it if process.stderr: @@ -279,7 +279,7 @@ def _run_with_sync(original_cmd, suppress_output=False): 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() @@ -965,7 +965,7 @@ def _handle_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/constants.py b/Lib/profiling/sampling/constants.py index 34b85ba4b3c61d..366cbb38365c9f 100644 --- a/Lib/profiling/sampling/constants.py +++ b/Lib/profiling/sampling/constants.py @@ -1,5 +1,9 @@ """Constants for the sampling profiler.""" +# Time unit conversion constants +MICROSECONDS_PER_SECOND = 1_000_000 +MILLISECONDS_PER_SECOND = 1_000 + # Profiling mode constants PROFILING_MODE_WALL = 0 PROFILING_MODE_CPU = 1 diff --git a/Lib/profiling/sampling/live_collector/__init__.py b/Lib/profiling/sampling/live_collector/__init__.py index 175e4610d232c5..59d50955e52959 100644 --- a/Lib/profiling/sampling/live_collector/__init__.py +++ b/Lib/profiling/sampling/live_collector/__init__.py @@ -114,7 +114,7 @@ from .constants import ( MICROSECONDS_PER_SECOND, DISPLAY_UPDATE_HZ, - DISPLAY_UPDATE_INTERVAL, + DISPLAY_UPDATE_INTERVAL_SEC, MIN_TERMINAL_WIDTH, MIN_TERMINAL_HEIGHT, WIDTH_THRESHOLD_SAMPLE_PCT, @@ -165,7 +165,7 @@ # Constants "MICROSECONDS_PER_SECOND", "DISPLAY_UPDATE_HZ", - "DISPLAY_UPDATE_INTERVAL", + "DISPLAY_UPDATE_INTERVAL_SEC", "MIN_TERMINAL_WIDTH", "MIN_TERMINAL_HEIGHT", "WIDTH_THRESHOLD_SAMPLE_PCT", diff --git a/Lib/profiling/sampling/live_collector/collector.py b/Lib/profiling/sampling/live_collector/collector.py index dcb9fcabe32779..cdf95a77eeccd8 100644 --- a/Lib/profiling/sampling/live_collector/collector.py +++ b/Lib/profiling/sampling/live_collector/collector.py @@ -24,7 +24,7 @@ ) from .constants import ( MICROSECONDS_PER_SECOND, - DISPLAY_UPDATE_INTERVAL, + DISPLAY_UPDATE_INTERVAL_SEC, MIN_TERMINAL_WIDTH, MIN_TERMINAL_HEIGHT, HEADER_LINES, @@ -157,7 +157,7 @@ def __init__( self.max_sample_rate = 0 # Track maximum sample rate seen self.successful_samples = 0 # Track samples that captured frames self.failed_samples = 0 # Track samples that failed to capture frames - self.display_update_interval = DISPLAY_UPDATE_INTERVAL # Instance variable for display refresh rate + self.display_update_interval_sec = DISPLAY_UPDATE_INTERVAL_SEC # Instance variable for display refresh rate # Thread status statistics (bit flags) self.thread_status_counts = { @@ -410,7 +410,7 @@ def collect(self, stack_frames, timestamp_us=None): if ( self._last_display_update is None or (current_time - self._last_display_update) - >= self.display_update_interval + >= self.display_update_interval_sec ): self._update_display() self._last_display_update = current_time @@ -987,14 +987,14 @@ def _handle_input(self): elif ch == ord("+") or ch == ord("="): # Decrease update interval (faster refresh) - self.display_update_interval = max( - 0.05, self.display_update_interval - 0.05 + self.display_update_interval_sec = max( + 0.05, self.display_update_interval_sec - 0.05 ) # Min 20Hz elif ch == ord("-") or ch == ord("_"): # Increase update interval (slower refresh) - self.display_update_interval = min( - 1.0, self.display_update_interval + 0.05 + self.display_update_interval_sec = min( + 1.0, self.display_update_interval_sec + 0.05 ) # Max 1Hz elif ch == ord("c") or ch == ord("C"): diff --git a/Lib/profiling/sampling/live_collector/constants.py b/Lib/profiling/sampling/live_collector/constants.py index 8462c0de3fd680..4f4575f7b7aae2 100644 --- a/Lib/profiling/sampling/live_collector/constants.py +++ b/Lib/profiling/sampling/live_collector/constants.py @@ -5,7 +5,7 @@ # Display update constants DISPLAY_UPDATE_HZ = 10 -DISPLAY_UPDATE_INTERVAL = 1.0 / DISPLAY_UPDATE_HZ # 0.1 seconds +DISPLAY_UPDATE_INTERVAL_SEC = 1.0 / DISPLAY_UPDATE_HZ # 0.1 seconds # Terminal size constraints MIN_TERMINAL_WIDTH = 60 diff --git a/Lib/profiling/sampling/live_collector/widgets.py b/Lib/profiling/sampling/live_collector/widgets.py index 314f3796a093ad..cf04f3aa3254ef 100644 --- a/Lib/profiling/sampling/live_collector/widgets.py +++ b/Lib/profiling/sampling/live_collector/widgets.py @@ -13,7 +13,7 @@ WIDTH_THRESHOLD_CUMUL_PCT, WIDTH_THRESHOLD_CUMTIME, MICROSECONDS_PER_SECOND, - DISPLAY_UPDATE_INTERVAL, + DISPLAY_UPDATE_INTERVAL_SEC, MIN_BAR_WIDTH, MAX_SAMPLE_RATE_BAR_WIDTH, MAX_EFFICIENCY_BAR_WIDTH, @@ -181,7 +181,7 @@ def draw_header_info(self, line, width, elapsed): # Calculate display refresh rate refresh_hz = ( - 1.0 / self.collector.display_update_interval if self.collector.display_update_interval > 0 else 0 + 1.0 / self.collector.display_update_interval_sec if self.collector.display_update_interval_sec > 0 else 0 ) # Get current view mode and thread display @@ -235,8 +235,8 @@ def draw_header_info(self, line, width, elapsed): def format_rate_with_units(self, rate_hz): """Format a rate in Hz with appropriate units (Hz, KHz, MHz).""" - if rate_hz >= 1_000_000: - return f"{rate_hz / 1_000_000:.1f}MHz" + if rate_hz >= MICROSECONDS_PER_SECOND: + return f"{rate_hz / MICROSECONDS_PER_SECOND:.1f}MHz" elif rate_hz >= 1_000: return f"{rate_hz / 1_000:.1f}KHz" else: diff --git a/Lib/profiling/sampling/pstats_collector.py b/Lib/profiling/sampling/pstats_collector.py index 1b2fe6a77278ee..e0dc9ab6bb7edb 100644 --- a/Lib/profiling/sampling/pstats_collector.py +++ b/Lib/profiling/sampling/pstats_collector.py @@ -3,6 +3,7 @@ from _colorize import ANSIColors from .collector import Collector, extract_lineno +from .constants import MICROSECONDS_PER_SECOND class PstatsCollector(Collector): @@ -68,7 +69,7 @@ def _dump_stats(self, file): # Needed for compatibility with pstats.Stats def create_stats(self): - sample_interval_sec = self.sample_interval_usec / 1_000_000 + sample_interval_sec = self.sample_interval_usec / MICROSECONDS_PER_SECOND callers = {} for fname, call_counts in self.result.items(): total = call_counts["direct_calls"] * sample_interval_sec @@ -263,7 +264,7 @@ def _determine_best_unit(max_value): elif max_value >= 0.001: return "ms", 1000.0 else: - return "μs", 1000000.0 + return "μs", float(MICROSECONDS_PER_SECOND) def _print_summary(self, stats_list, total_samples): """Print summary of interesting functions.""" diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py index a5870366552854..38f1d03e4939f1 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_live_collector_interaction.py @@ -35,7 +35,7 @@ def setUp(self): ) self.collector.start_time = time.perf_counter() # Set a consistent display update interval for tests - self.collector.display_update_interval = 0.1 + self.collector.display_update_interval_sec = 0.1 def tearDown(self): """Clean up after test.""" @@ -110,45 +110,45 @@ def test_reset_stats(self): def test_increase_refresh_rate(self): """Test increasing refresh rate (faster updates).""" - initial_interval = self.collector.display_update_interval + initial_interval = self.collector.display_update_interval_sec # Simulate '+' key press (faster = smaller interval) self.display.simulate_input(ord("+")) self.collector._handle_input() - self.assertLess(self.collector.display_update_interval, initial_interval) + self.assertLess(self.collector.display_update_interval_sec, initial_interval) def test_decrease_refresh_rate(self): """Test decreasing refresh rate (slower updates).""" - initial_interval = self.collector.display_update_interval + initial_interval = self.collector.display_update_interval_sec # Simulate '-' key press (slower = larger interval) self.display.simulate_input(ord("-")) self.collector._handle_input() - self.assertGreater(self.collector.display_update_interval, initial_interval) + self.assertGreater(self.collector.display_update_interval_sec, initial_interval) def test_refresh_rate_minimum(self): """Test that refresh rate has a minimum (max speed).""" - self.collector.display_update_interval = 0.05 # Set to minimum + self.collector.display_update_interval_sec = 0.05 # Set to minimum # Try to go faster self.display.simulate_input(ord("+")) self.collector._handle_input() # Should stay at minimum - self.assertEqual(self.collector.display_update_interval, 0.05) + self.assertEqual(self.collector.display_update_interval_sec, 0.05) def test_refresh_rate_maximum(self): """Test that refresh rate has a maximum (min speed).""" - self.collector.display_update_interval = 1.0 # Set to maximum + self.collector.display_update_interval_sec = 1.0 # Set to maximum # Try to go slower self.display.simulate_input(ord("-")) self.collector._handle_input() # Should stay at maximum - self.assertEqual(self.collector.display_update_interval, 1.0) + self.assertEqual(self.collector.display_update_interval_sec, 1.0) def test_help_toggle(self): """Test help screen toggle.""" @@ -289,23 +289,23 @@ def test_filter_clear_uppercase(self): def test_increase_refresh_rate_with_equals(self): """Test increasing refresh rate with '=' key.""" - initial_interval = self.collector.display_update_interval + initial_interval = self.collector.display_update_interval_sec # Simulate '=' key press (alternative to '+') self.display.simulate_input(ord("=")) self.collector._handle_input() - self.assertLess(self.collector.display_update_interval, initial_interval) + self.assertLess(self.collector.display_update_interval_sec, initial_interval) def test_decrease_refresh_rate_with_underscore(self): """Test decreasing refresh rate with '_' key.""" - initial_interval = self.collector.display_update_interval + initial_interval = self.collector.display_update_interval_sec # Simulate '_' key press (alternative to '-') self.display.simulate_input(ord("_")) self.collector._handle_input() - self.assertGreater(self.collector.display_update_interval, initial_interval) + self.assertGreater(self.collector.display_update_interval_sec, initial_interval) def test_finished_state_displays_banner(self): """Test that finished state shows prominent banner.""" From f392e828a637bd3b28bf489a284977790a2382ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Kiss=20Koll=C3=A1r?= Date: Wed, 24 Dec 2025 13:16:53 +0000 Subject: [PATCH 2/2] Replace --interval with --sampling-rate Sampling rate is more intuitive to the number of samples per second taken, rather than the intervals between samples. --- Doc/library/profiling.sampling.rst | 37 ++++---- Lib/profiling/sampling/cli.py | 86 +++++++++++++++---- .../test_sampling_profiler/test_advanced.py | 4 +- .../test_sampling_profiler/test_children.py | 26 +++--- .../test_sampling_profiler/test_cli.py | 6 +- .../test_sampling_profiler/test_modes.py | 8 +- 6 files changed, 108 insertions(+), 59 deletions(-) diff --git a/Doc/library/profiling.sampling.rst b/Doc/library/profiling.sampling.rst index b5e6a2c7a0ed8e..370bbcd3242526 100644 --- a/Doc/library/profiling.sampling.rst +++ b/Doc/library/profiling.sampling.rst @@ -53,7 +53,7 @@ counts**, not direct measurements. Tachyon counts how many times each function appears in the collected samples, then multiplies by the sampling interval to estimate time. -For example, with a 100 microsecond sampling interval over a 10-second profile, +For example, with a 10 kHz sampling rate over a 10-second profile, Tachyon collects approximately 100,000 samples. If a function appears in 5,000 samples (5% of total), Tachyon estimates it consumed 5% of the 10-second duration, or about 500 milliseconds. This is a statistical estimate, not a @@ -142,7 +142,7 @@ Use live mode for real-time monitoring (press ``q`` to quit):: Profile for 60 seconds with a faster sampling rate:: - python -m profiling.sampling run -d 60 -i 50 script.py + python -m profiling.sampling run -d 60 -r 20khz script.py Generate a line-by-line heatmap:: @@ -326,8 +326,8 @@ The default configuration works well for most use cases: * - Option - Default - * - Default for ``--interval`` / ``-i`` - - 100 µs between samples (~10,000 samples/sec) + * - Default for ``--sampling-rate`` / ``-r`` + - 1 kHz * - Default for ``--duration`` / ``-d`` - 10 seconds * - Default for ``--all-threads`` / ``-a`` @@ -346,23 +346,22 @@ The default configuration works well for most use cases: - Disabled (non-blocking sampling) -Sampling interval and duration ------------------------------- +Sampling rate and duration +-------------------------- -The two most fundamental parameters are the sampling interval and duration. +The two most fundamental parameters are the sampling rate and duration. Together, these determine how many samples will be collected during a profiling session. -The :option:`--interval` option (:option:`-i`) sets the time between samples in -microseconds. The default is 100 microseconds, which produces approximately -10,000 samples per second:: +The :option:`--sampling-rate` option (:option:`-r`) sets how frequently samples +are collected. The default is 1 kHz (10,000 samples per second):: - python -m profiling.sampling run -i 50 script.py + python -m profiling.sampling run -r 20khz script.py -Lower intervals capture more samples and provide finer-grained data at the -cost of slightly higher profiler CPU usage. Higher intervals reduce profiler +Higher rates capture more samples and provide finer-grained data at the +cost of slightly higher profiler CPU usage. Lower rates reduce profiler overhead but may miss short-lived functions. For most applications, the -default interval provides a good balance between accuracy and overhead. +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:: @@ -573,9 +572,9 @@ appended: - For pstats format (which defaults to stdout), subprocesses produce files like ``profile_12345.pstats`` -The subprocess profilers inherit most sampling options from the parent (interval, -duration, thread selection, native frames, GC frames, async-aware mode, and -output format). All Python descendant processes are profiled recursively, +The subprocess profilers inherit most sampling options from the parent (sampling +rate, duration, thread selection, native frames, GC frames, async-aware mode, +and output format). All Python descendant processes are profiled recursively, including grandchildren and further descendants. Subprocess detection works by periodically scanning for new descendants of @@ -1389,9 +1388,9 @@ Global options Sampling options ---------------- -.. option:: -i , --interval +.. option:: -r , --sampling-rate - Sampling interval in microseconds. Default: 100. + Sampling rate (for example, ``10000``, ``10khz``, ``10k``). Default: ``1khz``. .. option:: -d , --duration diff --git a/Lib/profiling/sampling/cli.py b/Lib/profiling/sampling/cli.py index c43e4452f9b8d9..9e60961943a8d0 100644 --- a/Lib/profiling/sampling/cli.py +++ b/Lib/profiling/sampling/cli.py @@ -4,6 +4,7 @@ import importlib.util import locale import os +import re import selectors import socket import subprocess @@ -20,6 +21,7 @@ from .binary_collector import BinaryCollector from .binary_reader import BinaryReader from .constants import ( + MICROSECONDS_PER_SECOND, PROFILING_MODE_ALL, PROFILING_MODE_WALL, PROFILING_MODE_CPU, @@ -116,7 +118,8 @@ def _build_child_profiler_args(args): child_args = [] # Sampling options - child_args.extend(["-i", str(args.interval)]) + hz = MICROSECONDS_PER_SECOND // args.sample_interval_usec + child_args.extend(["-r", str(hz)]) child_args.extend(["-d", str(args.duration)]) if args.all_threads: @@ -290,16 +293,64 @@ def _run_with_sync(original_cmd, suppress_output=False): return process +_RATE_PATTERN = re.compile(r''' + ^ # Start of string + ( # Group 1: The numeric value + \d+ # One or more digits (integer part) + (?:\.\d+)? # Optional: decimal point followed by digits + ) # Examples: "10", "0.5", "100.25" + ( # Group 2: Optional unit suffix + hz # "hz" - hertz + | khz # "khz" - kilohertz + | k # "k" - shorthand for kilohertz + )? # Suffix is optional (bare number = Hz) + $ # End of string + ''', re.VERBOSE | re.IGNORECASE) + + +def _parse_sampling_rate(rate_str: str) -> int: + """Parse sampling rate string to microseconds.""" + rate_str = rate_str.strip().lower() + + match = _RATE_PATTERN.match(rate_str) + if not match: + raise argparse.ArgumentTypeError( + f"Invalid sampling rate format: {rate_str}. " + "Expected: number followed by optional suffix (hz, khz, k) with no spaces (e.g., 10khz)" + ) + + number_part = match.group(1) + suffix = match.group(2) or '' + + # Determine multiplier based on suffix + suffix_map = { + 'hz': 1, + 'khz': 1000, + 'k': 1000, + } + multiplier = suffix_map.get(suffix, 1) + hz = float(number_part) * multiplier + if hz <= 0: + raise argparse.ArgumentTypeError(f"Sampling rate must be positive: {rate_str}") + + interval_usec = int(MICROSECONDS_PER_SECOND / hz) + if interval_usec < 1: + raise argparse.ArgumentTypeError(f"Sampling rate too high: {rate_str}") + + return interval_usec + + def _add_sampling_options(parser): """Add sampling configuration options to a parser.""" sampling_group = parser.add_argument_group("Sampling configuration") sampling_group.add_argument( - "-i", - "--interval", - type=int, - default=100, - metavar="MICROSECONDS", - help="sampling interval", + "-r", + "--sampling-rate", + type=_parse_sampling_rate, + default="1khz", + metavar="RATE", + dest="sample_interval_usec", + help="sampling rate (e.g., 10000, 10khz, 10k)", ) sampling_group.add_argument( "-d", @@ -487,14 +538,13 @@ def _sort_to_mode(sort_choice): } return sort_map.get(sort_choice, SORT_MODE_NSAMPLES) - -def _create_collector(format_type, interval, skip_idle, opcodes=False, +def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=False, output_file=None, compression='auto'): """Create the appropriate collector based on format type. Args: format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap', 'binary') - interval: Sampling interval in microseconds + sample_interval_usec: Sampling interval in microseconds skip_idle: Whether to skip idle samples opcodes: Whether to collect opcode information (only used by gecko format for creating interval markers in Firefox Profiler) @@ -519,9 +569,9 @@ def _create_collector(format_type, interval, skip_idle, opcodes=False, # and is the only format that uses opcodes for interval markers if format_type == "gecko": skip_idle = False - return collector_class(interval, skip_idle=skip_idle, opcodes=opcodes) + return collector_class(sample_interval_usec, skip_idle=skip_idle, opcodes=opcodes) - return collector_class(interval, skip_idle=skip_idle) + return collector_class(sample_interval_usec, skip_idle=skip_idle) def _generate_output_filename(format_type, pid): @@ -725,8 +775,8 @@ def _main(): # Generate flamegraph from a script `python -m profiling.sampling run --flamegraph -o output.html script.py` - # Profile with custom interval and duration - `python -m profiling.sampling run -i 50 -d 30 script.py` + # Profile with custom rate and duration + `python -m profiling.sampling run -r 5khz -d 30 script.py` # Save collapsed stacks to file `python -m profiling.sampling run --collapsed -o stacks.txt script.py` @@ -860,7 +910,7 @@ def _handle_attach(args): # Create the appropriate collector collector = _create_collector( - args.format, args.interval, skip_idle, args.opcodes, + args.format, args.sample_interval_usec, skip_idle, args.opcodes, output_file=output_file, compression=getattr(args, 'compression', 'auto') ) @@ -938,7 +988,7 @@ def _handle_run(args): # Create the appropriate collector collector = _create_collector( - args.format, args.interval, skip_idle, args.opcodes, + args.format, args.sample_interval_usec, skip_idle, args.opcodes, output_file=output_file, compression=getattr(args, 'compression', 'auto') ) @@ -980,7 +1030,7 @@ def _handle_live_attach(args, pid): # Create live collector with default settings collector = LiveStatsCollector( - args.interval, + args.sample_interval_usec, skip_idle=skip_idle, sort_by="tottime", # Default initial sort limit=20, # Default limit @@ -1027,7 +1077,7 @@ def _handle_live_run(args): # Create live collector with default settings collector = LiveStatsCollector( - args.interval, + args.sample_interval_usec, skip_idle=skip_idle, sort_by="tottime", # Default initial sort limit=20, # Default limit diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py b/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py index ef9ea64b67af61..bcd4de7f5d7ebe 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_advanced.py @@ -219,8 +219,8 @@ def worker(x): "run", "-d", "5", - "-i", - "100000", + "-r", + "10", script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_children.py b/Lib/test/test_profiling/test_sampling_profiler/test_children.py index 4007b3e8d7a41f..b7dc878a238f8d 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_children.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_children.py @@ -279,11 +279,11 @@ def test_monitor_creation(self): monitor = ChildProcessMonitor( pid=os.getpid(), - cli_args=["-i", "100", "-d", "5"], + cli_args=["-r", "10khz", "-d", "5"], output_pattern="test_{pid}.pstats", ) self.assertEqual(monitor.parent_pid, os.getpid()) - self.assertEqual(monitor.cli_args, ["-i", "100", "-d", "5"]) + self.assertEqual(monitor.cli_args, ["-r", "10khz", "-d", "5"]) self.assertEqual(monitor.output_pattern, "test_{pid}.pstats") def test_monitor_lifecycle(self): @@ -386,7 +386,7 @@ def test_build_child_profiler_args(self): from profiling.sampling.cli import _build_child_profiler_args args = argparse.Namespace( - interval=200, + sample_interval_usec=200, duration=15, all_threads=True, realtime_stats=False, @@ -420,7 +420,7 @@ def assert_flag_value_pair(flag, value): f"'{child_args[flag_index + 1]}' in args: {child_args}", ) - assert_flag_value_pair("-i", 200) + assert_flag_value_pair("-r", 5000) assert_flag_value_pair("-d", 15) assert_flag_value_pair("--mode", "cpu") @@ -444,7 +444,7 @@ def test_build_child_profiler_args_no_gc(self): from profiling.sampling.cli import _build_child_profiler_args args = argparse.Namespace( - interval=100, + sample_interval_usec=100, duration=5, all_threads=False, realtime_stats=False, @@ -510,7 +510,7 @@ def test_setup_child_monitor(self): from profiling.sampling.cli import _setup_child_monitor args = argparse.Namespace( - interval=100, + sample_interval_usec=100, duration=5, all_threads=False, realtime_stats=False, @@ -690,7 +690,7 @@ def test_monitor_respects_max_limit(self): # Create a monitor monitor = ChildProcessMonitor( pid=os.getpid(), - cli_args=["-i", "100", "-d", "5"], + cli_args=["-r", "10khz", "-d", "5"], output_pattern="test_{pid}.pstats", ) @@ -927,8 +927,8 @@ def test_subprocesses_flag_spawns_child_and_creates_output(self): "--subprocesses", "-d", "3", - "-i", - "10000", + "-r", + "100", "-o", output_file, script_file, @@ -989,8 +989,8 @@ def test_subprocesses_flag_with_flamegraph_output(self): "--subprocesses", "-d", "2", - "-i", - "10000", + "-r", + "100", "--flamegraph", "-o", output_file, @@ -1043,8 +1043,8 @@ def test_subprocesses_flag_no_crash_on_quick_child(self): "--subprocesses", "-d", "2", - "-i", - "10000", + "-r", + "100", "-o", output_file, script_file, diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_cli.py b/Lib/test/test_profiling/test_sampling_profiler/test_cli.py index 9b2b16d6e1965b..fb4816a0b6085a 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_cli.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_cli.py @@ -232,7 +232,7 @@ def test_cli_module_with_profiler_options(self): test_args = [ "profiling.sampling.cli", "run", - "-i", + "-r", "1000", "-d", "30", @@ -265,8 +265,8 @@ def test_cli_script_with_profiler_options(self): test_args = [ "profiling.sampling.cli", "run", - "-i", - "2000", + "-r", + "500", "-d", "60", "--collapsed", diff --git a/Lib/test/test_profiling/test_sampling_profiler/test_modes.py b/Lib/test/test_profiling/test_sampling_profiler/test_modes.py index 247416389daa07..877237866b1e65 100644 --- a/Lib/test/test_profiling/test_sampling_profiler/test_modes.py +++ b/Lib/test/test_profiling/test_sampling_profiler/test_modes.py @@ -306,8 +306,8 @@ def test_gil_mode_cli_argument_parsing(self): "12345", "--mode", "gil", - "-i", - "500", + "-r", + "2000", "-d", "5", ] @@ -488,8 +488,8 @@ def test_exception_mode_cli_argument_parsing(self): "12345", "--mode", "exception", - "-i", - "500", + "-r", + "2000", "-d", "5", ]