33import argparse
44import importlib .util
55import os
6+ import re
67import selectors
78import socket
89import subprocess
1718from .heatmap_collector import HeatmapCollector
1819from .gecko_collector import GeckoCollector
1920from .constants import (
21+ MICROSECONDS_PER_SECOND ,
2022 PROFILING_MODE_ALL ,
2123 PROFILING_MODE_WALL ,
2224 PROFILING_MODE_CPU ,
@@ -111,7 +113,8 @@ def _build_child_profiler_args(args):
111113 child_args = []
112114
113115 # Sampling options
114- child_args .extend (["-i" , str (args .interval )])
116+ hz = MICROSECONDS_PER_SECOND // args .sampling_rate
117+ child_args .extend (["-r" , str (hz )])
115118 child_args .extend (["-d" , str (args .duration )])
116119
117120 if args .all_threads :
@@ -285,16 +288,51 @@ def _run_with_sync(original_cmd, suppress_output=False):
285288 return process
286289
287290
291+ _RATE_PATTERN = re .compile (r'^(\d+(?:\.\d+)?)(hz|khz|k)?$' , re .IGNORECASE )
292+
293+
294+ def _parse_sampling_rate (rate_str : str ) -> int :
295+ """Parse sampling rate string to microseconds."""
296+ rate_str = rate_str .strip ().lower ()
297+
298+ match = _RATE_PATTERN .match (rate_str )
299+ if not match :
300+ raise argparse .ArgumentTypeError (
301+ f"Invalid sampling rate format: { rate_str } . "
302+ "Expected: number followed by optional suffix (hz, khz, k)"
303+ )
304+
305+ number_part = match .group (1 )
306+ suffix = match .group (2 ) or ''
307+
308+ # Determine multiplier based on suffix
309+ suffix_map = {
310+ 'hz' : 1 ,
311+ 'khz' : 1000 ,
312+ 'k' : 1000 ,
313+ }
314+ multiplier = suffix_map .get (suffix , 1 )
315+ hz = float (number_part ) * multiplier
316+ if hz <= 0 :
317+ raise argparse .ArgumentTypeError (f"Sampling rate must be positive: { rate_str } " )
318+
319+ interval_usec = int (MICROSECONDS_PER_SECOND / hz )
320+ if interval_usec < 1 :
321+ raise argparse .ArgumentTypeError (f"Sampling rate too high: { rate_str } " )
322+
323+ return interval_usec
324+
325+
288326def _add_sampling_options (parser ):
289327 """Add sampling configuration options to a parser."""
290328 sampling_group = parser .add_argument_group ("Sampling configuration" )
291329 sampling_group .add_argument (
292- "-i " ,
293- "--interval " ,
294- type = int ,
295- default = 100 ,
296- metavar = "MICROSECONDS " ,
297- help = "sampling interval " ,
330+ "-r " ,
331+ "--sampling-rate " ,
332+ type = _parse_sampling_rate ,
333+ default = "10khz" ,
334+ metavar = "RATE " ,
335+ help = "sampling rate (e.g., 10000, 10khz, 10k) " ,
298336 )
299337 sampling_group .add_argument (
300338 "-d" ,
@@ -460,12 +498,12 @@ def _sort_to_mode(sort_choice):
460498 return sort_map .get (sort_choice , SORT_MODE_NSAMPLES )
461499
462500
463- def _create_collector (format_type , interval , skip_idle , opcodes = False ):
501+ def _create_collector (format_type , sample_interval_usec , skip_idle , opcodes = False ):
464502 """Create the appropriate collector based on format type.
465503
466504 Args:
467505 format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap')
468- interval : Sampling interval in microseconds
506+ sample_interval_usec : Sampling interval in microseconds
469507 skip_idle: Whether to skip idle samples
470508 opcodes: Whether to collect opcode information (only used by gecko format
471509 for creating interval markers in Firefox Profiler)
@@ -481,9 +519,9 @@ def _create_collector(format_type, interval, skip_idle, opcodes=False):
481519 # and is the only format that uses opcodes for interval markers
482520 if format_type == "gecko" :
483521 skip_idle = False
484- return collector_class (interval , skip_idle = skip_idle , opcodes = opcodes )
522+ return collector_class (sample_interval_usec , skip_idle = skip_idle , opcodes = opcodes )
485523
486- return collector_class (interval , skip_idle = skip_idle )
524+ return collector_class (sample_interval_usec , skip_idle = skip_idle )
487525
488526
489527def _generate_output_filename (format_type , pid ):
@@ -659,8 +697,8 @@ def main():
659697 # Generate flamegraph from a script
660698 `python -m profiling.sampling run --flamegraph -o output.html script.py`
661699
662- # Profile with custom interval and duration
663- `python -m profiling.sampling run -i 50 -d 30 script.py`
700+ # Profile with custom rate and duration
701+ `python -m profiling.sampling run -r 5khz -d 30 script.py`
664702
665703 # Save collapsed stacks to file
666704 `python -m profiling.sampling run --collapsed -o stacks.txt script.py`
@@ -764,7 +802,7 @@ def _handle_attach(args):
764802 )
765803
766804 # Create the appropriate collector
767- collector = _create_collector (args .format , args .interval , skip_idle , args .opcodes )
805+ collector = _create_collector (args .format , args .sampling_rate , skip_idle , args .opcodes )
768806
769807 with _get_child_monitor_context (args , args .pid ):
770808 collector = sample (
@@ -833,7 +871,7 @@ def _handle_run(args):
833871 )
834872
835873 # Create the appropriate collector
836- collector = _create_collector (args .format , args .interval , skip_idle , args .opcodes )
874+ collector = _create_collector (args .format , args .sampling_rate , skip_idle , args .opcodes )
837875
838876 with _get_child_monitor_context (args , process .pid ):
839877 try :
@@ -871,7 +909,7 @@ def _handle_live_attach(args, pid):
871909
872910 # Create live collector with default settings
873911 collector = LiveStatsCollector (
874- args .interval ,
912+ args .sampling_rate ,
875913 skip_idle = skip_idle ,
876914 sort_by = "tottime" , # Default initial sort
877915 limit = 20 , # Default limit
@@ -917,7 +955,7 @@ def _handle_live_run(args):
917955
918956 # Create live collector with default settings
919957 collector = LiveStatsCollector (
920- args .interval ,
958+ args .sampling_rate ,
921959 skip_idle = skip_idle ,
922960 sort_by = "tottime" , # Default initial sort
923961 limit = 20 , # Default limit
0 commit comments