gh-141999: Handle KeyboardInterrupt when sampling in the new tachyon profiler (#142000)

This commit is contained in:
yihong 2025-11-30 10:49:13 +08:00 committed by GitHub
parent ea51e745c7
commit 056d6c5ed9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 37 deletions

View file

@ -57,7 +57,9 @@ def sample(self, collector, duration_sec=10):
last_sample_time = start_time last_sample_time = start_time
realtime_update_interval = 1.0 # Update every second realtime_update_interval = 1.0 # Update every second
last_realtime_update = start_time last_realtime_update = start_time
interrupted = False
try:
while running_time < duration_sec: while running_time < duration_sec:
# Check if live collector wants to stop # Check if live collector wants to stop
if hasattr(collector, 'running') and not collector.running: if hasattr(collector, 'running') and not collector.running:
@ -101,6 +103,10 @@ def sample(self, collector, duration_sec=10):
next_time += sample_interval_sec next_time += sample_interval_sec
running_time = time.perf_counter() - start_time running_time = time.perf_counter() - start_time
except KeyboardInterrupt:
interrupted = True
running_time = time.perf_counter() - start_time
print("Interrupted by user.")
# Clear real-time stats line if it was being displayed # Clear real-time stats line if it was being displayed
if self.realtime_stats and len(self.sample_intervals) > 0: if self.realtime_stats and len(self.sample_intervals) > 0:
@ -121,7 +127,7 @@ def sample(self, collector, duration_sec=10):
collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, mode=self.mode) collector.set_stats(self.sample_interval_usec, running_time, sample_rate, error_rate, mode=self.mode)
expected_samples = int(duration_sec / sample_interval_sec) expected_samples = int(duration_sec / sample_interval_sec)
if num_samples < expected_samples and not is_live_mode: if num_samples < expected_samples and not is_live_mode and not interrupted:
print( print(
f"Warning: missed {expected_samples - num_samples} samples " f"Warning: missed {expected_samples - num_samples} samples "
f"from the expected total of {expected_samples} " f"from the expected total of {expected_samples} "

View file

@ -224,6 +224,46 @@ def test_sample_profiler_missed_samples_warning(self):
self.assertIn("Warning: missed", result) self.assertIn("Warning: missed", result)
self.assertIn("samples from the expected total", result) self.assertIn("samples from the expected total", result)
def test_sample_profiler_keyboard_interrupt(self):
mock_unwinder = mock.MagicMock()
mock_unwinder.get_stack_trace.side_effect = [
[
(
1,
[
mock.MagicMock(
filename="test.py", lineno=10, funcname="test_func"
)
],
)
],
KeyboardInterrupt(),
]
with mock.patch(
"_remote_debugging.RemoteUnwinder"
) as mock_unwinder_class:
mock_unwinder_class.return_value = mock_unwinder
profiler = SampleProfiler(
pid=12345, sample_interval_usec=10000, all_threads=False
)
mock_collector = mock.MagicMock()
times = [0.0, 0.01, 0.02, 0.03, 0.04]
with mock.patch("time.perf_counter", side_effect=times):
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
try:
profiler.sample(mock_collector, duration_sec=1.0)
except KeyboardInterrupt:
self.fail(
"KeyboardInterrupt was not handled by the profiler"
)
result = output.getvalue()
self.assertIn("Interrupted by user.", result)
self.assertIn("Captured", result)
self.assertIn("samples", result)
self.assertNotIn("Warning: missed", result)
@force_not_colorized_test_class @force_not_colorized_test_class
class TestPrintSampledStats(unittest.TestCase): class TestPrintSampledStats(unittest.TestCase):

View file

@ -0,0 +1,2 @@
Correctly allow :exc:`KeyboardInterrupt` to stop the process when using
:mod:`!profiling.sampling`.