Skip to content

Commit f392e82

Browse files
committed
Replace --interval with --sampling-rate
Sampling rate is more intuitive to the number of samples per second taken, rather than the intervals between samples.
1 parent a13a4aa commit f392e82

File tree

6 files changed

+108
-59
lines changed

6 files changed

+108
-59
lines changed

Doc/library/profiling.sampling.rst

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ counts**, not direct measurements. Tachyon counts how many times each function
5353
appears in the collected samples, then multiplies by the sampling interval to
5454
estimate time.
5555

56-
For example, with a 100 microsecond sampling interval over a 10-second profile,
56+
For example, with a 10 kHz sampling rate over a 10-second profile,
5757
Tachyon collects approximately 100,000 samples. If a function appears in 5,000
5858
samples (5% of total), Tachyon estimates it consumed 5% of the 10-second
5959
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)::
142142

143143
Profile for 60 seconds with a faster sampling rate::
144144

145-
python -m profiling.sampling run -d 60 -i 50 script.py
145+
python -m profiling.sampling run -d 60 -r 20khz script.py
146146

147147
Generate a line-by-line heatmap::
148148

@@ -326,8 +326,8 @@ The default configuration works well for most use cases:
326326

327327
* - Option
328328
- Default
329-
* - Default for ``--interval`` / ``-i``
330-
- 100 µs between samples (~10,000 samples/sec)
329+
* - Default for ``--sampling-rate`` / ``-r``
330+
- 1 kHz
331331
* - Default for ``--duration`` / ``-d``
332332
- 10 seconds
333333
* - Default for ``--all-threads`` / ``-a``
@@ -346,23 +346,22 @@ The default configuration works well for most use cases:
346346
- Disabled (non-blocking sampling)
347347

348348

349-
Sampling interval and duration
350-
------------------------------
349+
Sampling rate and duration
350+
--------------------------
351351

352-
The two most fundamental parameters are the sampling interval and duration.
352+
The two most fundamental parameters are the sampling rate and duration.
353353
Together, these determine how many samples will be collected during a profiling
354354
session.
355355

356-
The :option:`--interval` option (:option:`-i`) sets the time between samples in
357-
microseconds. The default is 100 microseconds, which produces approximately
358-
10,000 samples per second::
356+
The :option:`--sampling-rate` option (:option:`-r`) sets how frequently samples
357+
are collected. The default is 1 kHz (10,000 samples per second)::
359358

360-
python -m profiling.sampling run -i 50 script.py
359+
python -m profiling.sampling run -r 20khz script.py
361360

362-
Lower intervals capture more samples and provide finer-grained data at the
363-
cost of slightly higher profiler CPU usage. Higher intervals reduce profiler
361+
Higher rates capture more samples and provide finer-grained data at the
362+
cost of slightly higher profiler CPU usage. Lower rates reduce profiler
364363
overhead but may miss short-lived functions. For most applications, the
365-
default interval provides a good balance between accuracy and overhead.
364+
default rate provides a good balance between accuracy and overhead.
366365

367366
The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
368367
default is 10 seconds::
@@ -573,9 +572,9 @@ appended:
573572
- For pstats format (which defaults to stdout), subprocesses produce files like
574573
``profile_12345.pstats``
575574

576-
The subprocess profilers inherit most sampling options from the parent (interval,
577-
duration, thread selection, native frames, GC frames, async-aware mode, and
578-
output format). All Python descendant processes are profiled recursively,
575+
The subprocess profilers inherit most sampling options from the parent (sampling
576+
rate, duration, thread selection, native frames, GC frames, async-aware mode,
577+
and output format). All Python descendant processes are profiled recursively,
579578
including grandchildren and further descendants.
580579

581580
Subprocess detection works by periodically scanning for new descendants of
@@ -1389,9 +1388,9 @@ Global options
13891388
Sampling options
13901389
----------------
13911390

1392-
.. option:: -i <microseconds>, --interval <microseconds>
1391+
.. option:: -r <rate>, --sampling-rate <rate>
13931392

1394-
Sampling interval in microseconds. Default: 100.
1393+
Sampling rate (for example, ``10000``, ``10khz``, ``10k``). Default: ``1khz``.
13951394

13961395
.. option:: -d <seconds>, --duration <seconds>
13971396

Lib/profiling/sampling/cli.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import importlib.util
55
import locale
66
import os
7+
import re
78
import selectors
89
import socket
910
import subprocess
@@ -20,6 +21,7 @@
2021
from .binary_collector import BinaryCollector
2122
from .binary_reader import BinaryReader
2223
from .constants import (
24+
MICROSECONDS_PER_SECOND,
2325
PROFILING_MODE_ALL,
2426
PROFILING_MODE_WALL,
2527
PROFILING_MODE_CPU,
@@ -116,7 +118,8 @@ def _build_child_profiler_args(args):
116118
child_args = []
117119

118120
# Sampling options
119-
child_args.extend(["-i", str(args.interval)])
121+
hz = MICROSECONDS_PER_SECOND // args.sample_interval_usec
122+
child_args.extend(["-r", str(hz)])
120123
child_args.extend(["-d", str(args.duration)])
121124

122125
if args.all_threads:
@@ -290,16 +293,64 @@ def _run_with_sync(original_cmd, suppress_output=False):
290293
return process
291294

292295

296+
_RATE_PATTERN = re.compile(r'''
297+
^ # Start of string
298+
( # Group 1: The numeric value
299+
\d+ # One or more digits (integer part)
300+
(?:\.\d+)? # Optional: decimal point followed by digits
301+
) # Examples: "10", "0.5", "100.25"
302+
( # Group 2: Optional unit suffix
303+
hz # "hz" - hertz
304+
| khz # "khz" - kilohertz
305+
| k # "k" - shorthand for kilohertz
306+
)? # Suffix is optional (bare number = Hz)
307+
$ # End of string
308+
''', re.VERBOSE | re.IGNORECASE)
309+
310+
311+
def _parse_sampling_rate(rate_str: str) -> int:
312+
"""Parse sampling rate string to microseconds."""
313+
rate_str = rate_str.strip().lower()
314+
315+
match = _RATE_PATTERN.match(rate_str)
316+
if not match:
317+
raise argparse.ArgumentTypeError(
318+
f"Invalid sampling rate format: {rate_str}. "
319+
"Expected: number followed by optional suffix (hz, khz, k) with no spaces (e.g., 10khz)"
320+
)
321+
322+
number_part = match.group(1)
323+
suffix = match.group(2) or ''
324+
325+
# Determine multiplier based on suffix
326+
suffix_map = {
327+
'hz': 1,
328+
'khz': 1000,
329+
'k': 1000,
330+
}
331+
multiplier = suffix_map.get(suffix, 1)
332+
hz = float(number_part) * multiplier
333+
if hz <= 0:
334+
raise argparse.ArgumentTypeError(f"Sampling rate must be positive: {rate_str}")
335+
336+
interval_usec = int(MICROSECONDS_PER_SECOND / hz)
337+
if interval_usec < 1:
338+
raise argparse.ArgumentTypeError(f"Sampling rate too high: {rate_str}")
339+
340+
return interval_usec
341+
342+
293343
def _add_sampling_options(parser):
294344
"""Add sampling configuration options to a parser."""
295345
sampling_group = parser.add_argument_group("Sampling configuration")
296346
sampling_group.add_argument(
297-
"-i",
298-
"--interval",
299-
type=int,
300-
default=100,
301-
metavar="MICROSECONDS",
302-
help="sampling interval",
347+
"-r",
348+
"--sampling-rate",
349+
type=_parse_sampling_rate,
350+
default="1khz",
351+
metavar="RATE",
352+
dest="sample_interval_usec",
353+
help="sampling rate (e.g., 10000, 10khz, 10k)",
303354
)
304355
sampling_group.add_argument(
305356
"-d",
@@ -487,14 +538,13 @@ def _sort_to_mode(sort_choice):
487538
}
488539
return sort_map.get(sort_choice, SORT_MODE_NSAMPLES)
489540

490-
491-
def _create_collector(format_type, interval, skip_idle, opcodes=False,
541+
def _create_collector(format_type, sample_interval_usec, skip_idle, opcodes=False,
492542
output_file=None, compression='auto'):
493543
"""Create the appropriate collector based on format type.
494544
495545
Args:
496546
format_type: The output format ('pstats', 'collapsed', 'flamegraph', 'gecko', 'heatmap', 'binary')
497-
interval: Sampling interval in microseconds
547+
sample_interval_usec: Sampling interval in microseconds
498548
skip_idle: Whether to skip idle samples
499549
opcodes: Whether to collect opcode information (only used by gecko format
500550
for creating interval markers in Firefox Profiler)
@@ -519,9 +569,9 @@ def _create_collector(format_type, interval, skip_idle, opcodes=False,
519569
# and is the only format that uses opcodes for interval markers
520570
if format_type == "gecko":
521571
skip_idle = False
522-
return collector_class(interval, skip_idle=skip_idle, opcodes=opcodes)
572+
return collector_class(sample_interval_usec, skip_idle=skip_idle, opcodes=opcodes)
523573

524-
return collector_class(interval, skip_idle=skip_idle)
574+
return collector_class(sample_interval_usec, skip_idle=skip_idle)
525575

526576

527577
def _generate_output_filename(format_type, pid):
@@ -725,8 +775,8 @@ def _main():
725775
# Generate flamegraph from a script
726776
`python -m profiling.sampling run --flamegraph -o output.html script.py`
727777
728-
# Profile with custom interval and duration
729-
`python -m profiling.sampling run -i 50 -d 30 script.py`
778+
# Profile with custom rate and duration
779+
`python -m profiling.sampling run -r 5khz -d 30 script.py`
730780
731781
# Save collapsed stacks to file
732782
`python -m profiling.sampling run --collapsed -o stacks.txt script.py`
@@ -860,7 +910,7 @@ def _handle_attach(args):
860910

861911
# Create the appropriate collector
862912
collector = _create_collector(
863-
args.format, args.interval, skip_idle, args.opcodes,
913+
args.format, args.sample_interval_usec, skip_idle, args.opcodes,
864914
output_file=output_file,
865915
compression=getattr(args, 'compression', 'auto')
866916
)
@@ -938,7 +988,7 @@ def _handle_run(args):
938988

939989
# Create the appropriate collector
940990
collector = _create_collector(
941-
args.format, args.interval, skip_idle, args.opcodes,
991+
args.format, args.sample_interval_usec, skip_idle, args.opcodes,
942992
output_file=output_file,
943993
compression=getattr(args, 'compression', 'auto')
944994
)
@@ -980,7 +1030,7 @@ def _handle_live_attach(args, pid):
9801030

9811031
# Create live collector with default settings
9821032
collector = LiveStatsCollector(
983-
args.interval,
1033+
args.sample_interval_usec,
9841034
skip_idle=skip_idle,
9851035
sort_by="tottime", # Default initial sort
9861036
limit=20, # Default limit
@@ -1027,7 +1077,7 @@ def _handle_live_run(args):
10271077

10281078
# Create live collector with default settings
10291079
collector = LiveStatsCollector(
1030-
args.interval,
1080+
args.sample_interval_usec,
10311081
skip_idle=skip_idle,
10321082
sort_by="tottime", # Default initial sort
10331083
limit=20, # Default limit

Lib/test/test_profiling/test_sampling_profiler/test_advanced.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ def worker(x):
219219
"run",
220220
"-d",
221221
"5",
222-
"-i",
223-
"100000",
222+
"-r",
223+
"10",
224224
script,
225225
stdout=subprocess.PIPE,
226226
stderr=subprocess.PIPE,

Lib/test/test_profiling/test_sampling_profiler/test_children.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,11 @@ def test_monitor_creation(self):
279279

280280
monitor = ChildProcessMonitor(
281281
pid=os.getpid(),
282-
cli_args=["-i", "100", "-d", "5"],
282+
cli_args=["-r", "10khz", "-d", "5"],
283283
output_pattern="test_{pid}.pstats",
284284
)
285285
self.assertEqual(monitor.parent_pid, os.getpid())
286-
self.assertEqual(monitor.cli_args, ["-i", "100", "-d", "5"])
286+
self.assertEqual(monitor.cli_args, ["-r", "10khz", "-d", "5"])
287287
self.assertEqual(monitor.output_pattern, "test_{pid}.pstats")
288288

289289
def test_monitor_lifecycle(self):
@@ -386,7 +386,7 @@ def test_build_child_profiler_args(self):
386386
from profiling.sampling.cli import _build_child_profiler_args
387387

388388
args = argparse.Namespace(
389-
interval=200,
389+
sample_interval_usec=200,
390390
duration=15,
391391
all_threads=True,
392392
realtime_stats=False,
@@ -420,7 +420,7 @@ def assert_flag_value_pair(flag, value):
420420
f"'{child_args[flag_index + 1]}' in args: {child_args}",
421421
)
422422

423-
assert_flag_value_pair("-i", 200)
423+
assert_flag_value_pair("-r", 5000)
424424
assert_flag_value_pair("-d", 15)
425425
assert_flag_value_pair("--mode", "cpu")
426426

@@ -444,7 +444,7 @@ def test_build_child_profiler_args_no_gc(self):
444444
from profiling.sampling.cli import _build_child_profiler_args
445445

446446
args = argparse.Namespace(
447-
interval=100,
447+
sample_interval_usec=100,
448448
duration=5,
449449
all_threads=False,
450450
realtime_stats=False,
@@ -510,7 +510,7 @@ def test_setup_child_monitor(self):
510510
from profiling.sampling.cli import _setup_child_monitor
511511

512512
args = argparse.Namespace(
513-
interval=100,
513+
sample_interval_usec=100,
514514
duration=5,
515515
all_threads=False,
516516
realtime_stats=False,
@@ -690,7 +690,7 @@ def test_monitor_respects_max_limit(self):
690690
# Create a monitor
691691
monitor = ChildProcessMonitor(
692692
pid=os.getpid(),
693-
cli_args=["-i", "100", "-d", "5"],
693+
cli_args=["-r", "10khz", "-d", "5"],
694694
output_pattern="test_{pid}.pstats",
695695
)
696696

@@ -927,8 +927,8 @@ def test_subprocesses_flag_spawns_child_and_creates_output(self):
927927
"--subprocesses",
928928
"-d",
929929
"3",
930-
"-i",
931-
"10000",
930+
"-r",
931+
"100",
932932
"-o",
933933
output_file,
934934
script_file,
@@ -989,8 +989,8 @@ def test_subprocesses_flag_with_flamegraph_output(self):
989989
"--subprocesses",
990990
"-d",
991991
"2",
992-
"-i",
993-
"10000",
992+
"-r",
993+
"100",
994994
"--flamegraph",
995995
"-o",
996996
output_file,
@@ -1043,8 +1043,8 @@ def test_subprocesses_flag_no_crash_on_quick_child(self):
10431043
"--subprocesses",
10441044
"-d",
10451045
"2",
1046-
"-i",
1047-
"10000",
1046+
"-r",
1047+
"100",
10481048
"-o",
10491049
output_file,
10501050
script_file,

Lib/test/test_profiling/test_sampling_profiler/test_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def test_cli_module_with_profiler_options(self):
232232
test_args = [
233233
"profiling.sampling.cli",
234234
"run",
235-
"-i",
235+
"-r",
236236
"1000",
237237
"-d",
238238
"30",
@@ -265,8 +265,8 @@ def test_cli_script_with_profiler_options(self):
265265
test_args = [
266266
"profiling.sampling.cli",
267267
"run",
268-
"-i",
269-
"2000",
268+
"-r",
269+
"500",
270270
"-d",
271271
"60",
272272
"--collapsed",

0 commit comments

Comments
 (0)