Skip to content

Commit 757ce0a

Browse files
committed
use sampling rate in progress bar
1 parent bac7bfa commit 757ce0a

2 files changed

Lines changed: 102 additions & 8 deletions

File tree

Lib/profiling/sampling/live_collector.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@ def draw_header_info(self, line, width, elapsed):
347347
col += len(text)
348348
return line + 1
349349

350+
def format_rate_with_units(self, rate_hz):
351+
"""Format a rate in Hz with appropriate units (Hz, KHz, MHz)."""
352+
if rate_hz >= 1_000_000:
353+
return f"{rate_hz / 1_000_000:.1f}MHz"
354+
elif rate_hz >= 1_000:
355+
return f"{rate_hz / 1_000:.1f}KHz"
356+
else:
357+
return f"{rate_hz:.1f}Hz"
358+
350359
def draw_sample_stats(self, line, width, elapsed):
351360
"""Draw sample statistics with visual progress bar."""
352361
sample_rate = (
@@ -373,15 +382,29 @@ def draw_sample_stats(self, line, width, elapsed):
373382
col += 23
374383

375384
# Draw sample rate bar
376-
max_label = f" max: {self.collector._max_sample_rate:>7.1f}/s"
377-
available_width = width - col - len(max_label) - 3
385+
target_rate = MICROSECONDS_PER_SECOND / self.collector.sample_interval_usec
386+
387+
# Show current/target ratio with percentage
388+
if sample_rate > 0 and target_rate > 0:
389+
percentage = min((sample_rate / target_rate) * 100, 100)
390+
current_formatted = self.format_rate_with_units(sample_rate)
391+
target_formatted = self.format_rate_with_units(target_rate)
392+
393+
if percentage >= 99.5: # Show 100% when very close
394+
rate_label = f" {current_formatted}/{target_formatted} (100%)"
395+
else:
396+
rate_label = f" {current_formatted}/{target_formatted} ({percentage:>4.1f}%)"
397+
else:
398+
target_formatted = self.format_rate_with_units(target_rate)
399+
rate_label = f" target: {target_formatted}"
400+
401+
available_width = width - col - len(rate_label) - 3
378402

379403
if available_width >= MIN_BAR_WIDTH:
380404
bar_width = min(MAX_SAMPLE_RATE_BAR_WIDTH, available_width)
381-
max_rate = max(
382-
self.collector._max_sample_rate, MIN_SAMPLE_RATE_FOR_SCALING
383-
)
384-
normalized_rate = min(sample_rate / max_rate, 1.0)
405+
# Use target rate as the reference, with a minimum for scaling
406+
reference_rate = max(target_rate, MIN_SAMPLE_RATE_FOR_SCALING)
407+
normalized_rate = min(sample_rate / reference_rate, 1.0)
385408
bar_fill = int(normalized_rate * bar_width)
386409

387410
bar = "["
@@ -391,8 +414,8 @@ def draw_sample_stats(self, line, width, elapsed):
391414
self.add_str(line, col, bar, self.colors["green"])
392415
col += len(bar)
393416

394-
if col + len(max_label) < width - 1:
395-
self.add_str(line, col + 1, max_label, curses.A_DIM)
417+
if col + len(rate_label) < width - 1:
418+
self.add_str(line, col + 1, rate_label, curses.A_DIM)
396419
return line + 1
397420

398421
def draw_efficiency_bar(self, line, width):

Lib/test/test_profiling/test_live_collector.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,77 @@ def test_draw_sample_stats(self):
839839
self.assertEqual(line, 1)
840840
self.assertGreater(self.collector._max_sample_rate, 0)
841841

842+
def test_progress_bar_uses_target_rate(self):
843+
"""Test that progress bar uses target rate instead of max rate."""
844+
# Set up collector with specific sampling interval
845+
collector = LiveStatsCollector(10000, pid=12345, display=self.mock_display) # 10ms = 100Hz target
846+
collector.start_time = time.perf_counter()
847+
collector.total_samples = 500
848+
collector._max_sample_rate = 150 # Higher than target to test we don't use this
849+
850+
colors = {"cyan": curses.A_BOLD, "green": curses.A_BOLD}
851+
collector._initialize_widgets(colors)
852+
853+
# Clear the display buffer to capture only our progress bar content
854+
self.mock_display.buffer.clear()
855+
856+
# Draw sample stats with a known elapsed time that gives us a specific sample rate
857+
elapsed = 10.0 # 500 samples in 10 seconds = 50 samples/second
858+
line = collector._header_widget.draw_sample_stats(0, 160, elapsed)
859+
860+
# Verify display was updated
861+
self.assertEqual(line, 1)
862+
self.assertGreater(len(self.mock_display.buffer), 0)
863+
864+
# Verify the label shows current/target format with units instead of "max"
865+
found_current_target_label = False
866+
found_max_label = False
867+
for (line_num, col), (text, attr) in self.mock_display.buffer.items():
868+
# Should show "50.0Hz/100.0Hz (50.0%)" since we're at 50% of target (50/100)
869+
if "50.0Hz/100.0Hz" in text and "50.0%" in text:
870+
found_current_target_label = True
871+
if "max:" in text:
872+
found_max_label = True
873+
874+
self.assertTrue(found_current_target_label, "Should display current/target rate with percentage")
875+
self.assertFalse(found_max_label, "Should not display max rate label")
876+
877+
def test_progress_bar_different_intervals(self):
878+
"""Test that progress bar adapts to different sampling intervals."""
879+
test_cases = [
880+
(1000, "1.0KHz", "100.0Hz"), # 1ms interval -> 1000Hz target (1.0KHz), 100Hz current
881+
(5000, "200.0Hz", "100.0Hz"), # 5ms interval -> 200Hz target, 100Hz current
882+
(20000, "50.0Hz", "100.0Hz"), # 20ms interval -> 50Hz target, 100Hz current
883+
(100000, "10.0Hz", "100.0Hz"), # 100ms interval -> 10Hz target, 100Hz current
884+
]
885+
886+
for interval_usec, expected_target_formatted, expected_current_formatted in test_cases:
887+
with self.subTest(interval=interval_usec):
888+
collector = LiveStatsCollector(interval_usec, display=MockDisplay())
889+
collector.start_time = time.perf_counter()
890+
collector.total_samples = 100
891+
892+
colors = {"cyan": curses.A_BOLD, "green": curses.A_BOLD}
893+
collector._initialize_widgets(colors)
894+
895+
# Clear buffer
896+
collector.display.buffer.clear()
897+
898+
# Draw with 1 second elapsed time (gives us current rate of 100Hz)
899+
collector._header_widget.draw_sample_stats(0, 160, 1.0)
900+
901+
# Check that the current/target format appears in the display with proper units
902+
found_current_target_format = False
903+
for (line_num, col), (text, attr) in collector.display.buffer.items():
904+
# Looking for format like "100.0Hz/1.0KHz" or "100.0Hz/200.0Hz"
905+
expected_format = f"{expected_current_formatted}/{expected_target_formatted}"
906+
if expected_format in text and "%" in text:
907+
found_current_target_format = True
908+
break
909+
910+
self.assertTrue(found_current_target_format,
911+
f"Should display current/target rate format with units for {interval_usec}µs interval")
912+
842913
def test_draw_efficiency_bar(self):
843914
"""Test drawing efficiency bar."""
844915
self.collector._successful_samples = 900

0 commit comments

Comments
 (0)