Skip to content

Commit a59e178

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 fb0b8fd commit a59e178

File tree

3 files changed

+76
-39
lines changed

3 files changed

+76
-39
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

@@ -296,8 +296,8 @@ The default configuration works well for most use cases:
296296

297297
* - Option
298298
- Default
299-
* - Default for ``--interval`` / ``-i``
300-
- 100 µs between samples (~10,000 samples/sec)
299+
* - Default for ``--sampling-rate`` / ``-r``
300+
- 10 kHz
301301
* - Default for ``--duration`` / ``-d``
302302
- 10 seconds
303303
* - Default for ``--all-threads`` / ``-a``
@@ -314,23 +314,22 @@ The default configuration works well for most use cases:
314314
- Disabled
315315

316316

317-
Sampling interval and duration
318-
------------------------------
317+
Sampling rate and duration
318+
--------------------------
319319

320-
The two most fundamental parameters are the sampling interval and duration.
320+
The two most fundamental parameters are the sampling rate and duration.
321321
Together, these determine how many samples will be collected during a profiling
322322
session.
323323

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

328-
python -m profiling.sampling run -i 50 script.py
327+
python -m profiling.sampling run -r 20khz script.py
329328

330-
Lower intervals capture more samples and provide finer-grained data at the
331-
cost of slightly higher profiler CPU usage. Higher intervals reduce profiler
329+
Higher rates capture more samples and provide finer-grained data at the
330+
cost of slightly higher profiler CPU usage. Lower rates reduce profiler
332331
overhead but may miss short-lived functions. For most applications, the
333-
default interval provides a good balance between accuracy and overhead.
332+
default rate provides a good balance between accuracy and overhead.
334333

335334
The :option:`--duration` option (:option:`-d`) sets how long to profile in seconds. The
336335
default is 10 seconds::
@@ -497,9 +496,9 @@ appended:
497496
- For pstats format (which defaults to stdout), subprocesses produce files like
498497
``profile_12345.pstats``
499498

500-
The subprocess profilers inherit most sampling options from the parent (interval,
501-
duration, thread selection, native frames, GC frames, async-aware mode, and
502-
output format). All Python descendant processes are profiled recursively,
499+
The subprocess profilers inherit most sampling options from the parent (sampling
500+
rate, duration, thread selection, native frames, GC frames, async-aware mode,
501+
and output format). All Python descendant processes are profiled recursively,
503502
including grandchildren and further descendants.
504503

505504
Subprocess detection works by periodically scanning for new descendants of
@@ -1256,9 +1255,9 @@ Global options
12561255
Sampling options
12571256
----------------
12581257

1259-
.. option:: -i <microseconds>, --interval <microseconds>
1258+
.. option:: -r <rate>, --sampling-rate <rate>
12601259

1261-
Sampling interval in microseconds. Default: 100.
1260+
Sampling rate (for example, ``10000``, ``10khz``, ``10k``). Default: ``10khz``.
12621261

12631262
.. option:: -d <seconds>, --duration <seconds>
12641263

Lib/profiling/sampling/cli.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import importlib.util
55
import os
6+
import re
67
import selectors
78
import socket
89
import subprocess
@@ -17,6 +18,7 @@
1718
from .heatmap_collector import HeatmapCollector
1819
from .gecko_collector import GeckoCollector
1920
from .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+
288326
def _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

489527
def _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

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)