mirror of
https://github.com/python/cpython.git
synced 2025-11-01 22:21:35 +00:00
gh-140137: Handle empty collections in profiling.sampling (#140154)
This commit is contained in:
parent
a05aece543
commit
0c66da8de4
3 changed files with 97 additions and 33 deletions
|
|
@ -642,6 +642,11 @@ def sample(
|
|||
|
||||
if output_format == "pstats" and not filename:
|
||||
stats = pstats.SampledStats(collector).strip_dirs()
|
||||
if not stats.stats:
|
||||
print("No samples were collected.")
|
||||
if mode == PROFILING_MODE_CPU:
|
||||
print("This can happen in CPU mode when all threads are idle.")
|
||||
else:
|
||||
print_sampled_stats(
|
||||
stats, sort, limit, show_summary, sample_interval_usec
|
||||
)
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ def load_stats(self, arg):
|
|||
arg.create_stats()
|
||||
self.stats = arg.stats
|
||||
arg.stats = {}
|
||||
return
|
||||
if not self.stats:
|
||||
raise TypeError("Cannot create or construct a %r object from %r"
|
||||
% (self.__class__, arg))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import namedtuple
|
||||
from unittest import mock
|
||||
|
||||
from profiling.sampling.pstats_collector import PstatsCollector
|
||||
|
|
@ -84,6 +85,8 @@ def __repr__(self):
|
|||
"Test only runs on Linux, Windows and MacOS",
|
||||
)
|
||||
|
||||
SubprocessInfo = namedtuple('SubprocessInfo', ['process', 'socket'])
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def test_subprocess(script):
|
||||
|
|
@ -123,7 +126,7 @@ def test_subprocess(script):
|
|||
if response != b"ready":
|
||||
raise RuntimeError(f"Unexpected response from subprocess: {response}")
|
||||
|
||||
yield proc
|
||||
yield SubprocessInfo(proc, client_socket)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
|
@ -1752,13 +1755,13 @@ def main_loop():
|
|||
|
||||
def test_sampling_basic_functionality(self):
|
||||
with (
|
||||
test_subprocess(self.test_script) as proc,
|
||||
test_subprocess(self.test_script) as subproc,
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=2,
|
||||
sample_interval_usec=1000, # 1ms
|
||||
show_summary=False,
|
||||
|
|
@ -1782,7 +1785,7 @@ def test_sampling_with_pstats_export(self):
|
|||
)
|
||||
self.addCleanup(close_and_unlink, pstats_out)
|
||||
|
||||
with test_subprocess(self.test_script) as proc:
|
||||
with test_subprocess(self.test_script) as subproc:
|
||||
# Suppress profiler output when testing file export
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
|
|
@ -1790,7 +1793,7 @@ def test_sampling_with_pstats_export(self):
|
|||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=1,
|
||||
filename=pstats_out.name,
|
||||
sample_interval_usec=10000,
|
||||
|
|
@ -1826,7 +1829,7 @@ def test_sampling_with_collapsed_export(self):
|
|||
self.addCleanup(close_and_unlink, collapsed_file)
|
||||
|
||||
with (
|
||||
test_subprocess(self.test_script) as proc,
|
||||
test_subprocess(self.test_script) as subproc,
|
||||
):
|
||||
# Suppress profiler output when testing file export
|
||||
with (
|
||||
|
|
@ -1835,7 +1838,7 @@ def test_sampling_with_collapsed_export(self):
|
|||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=1,
|
||||
filename=collapsed_file.name,
|
||||
output_format="collapsed",
|
||||
|
|
@ -1876,14 +1879,14 @@ def test_sampling_with_collapsed_export(self):
|
|||
|
||||
def test_sampling_all_threads(self):
|
||||
with (
|
||||
test_subprocess(self.test_script) as proc,
|
||||
test_subprocess(self.test_script) as subproc,
|
||||
# Suppress profiler output
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=1,
|
||||
all_threads=True,
|
||||
sample_interval_usec=10000,
|
||||
|
|
@ -1969,14 +1972,14 @@ def test_invalid_pid(self):
|
|||
profiling.sampling.sample.sample(-1, duration_sec=1)
|
||||
|
||||
def test_process_dies_during_sampling(self):
|
||||
with test_subprocess("import time; time.sleep(0.5); exit()") as proc:
|
||||
with test_subprocess("import time; time.sleep(0.5); exit()") as subproc:
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=2, # Longer than process lifetime
|
||||
sample_interval_usec=50000,
|
||||
)
|
||||
|
|
@ -2018,17 +2021,17 @@ def test_invalid_output_format_with_mocked_profiler(self):
|
|||
)
|
||||
|
||||
def test_is_process_running(self):
|
||||
with test_subprocess("import time; time.sleep(1000)") as proc:
|
||||
with test_subprocess("import time; time.sleep(1000)") as subproc:
|
||||
try:
|
||||
profiler = SampleProfiler(pid=proc.pid, sample_interval_usec=1000, all_threads=False)
|
||||
profiler = SampleProfiler(pid=subproc.process.pid, sample_interval_usec=1000, all_threads=False)
|
||||
except PermissionError:
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
self.assertTrue(profiler._is_process_running())
|
||||
self.assertIsNotNone(profiler.unwinder.get_stack_trace())
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
subproc.process.kill()
|
||||
subproc.process.wait()
|
||||
self.assertRaises(ProcessLookupError, profiler.unwinder.get_stack_trace)
|
||||
|
||||
# Exit the context manager to ensure the process is terminated
|
||||
|
|
@ -2037,9 +2040,9 @@ def test_is_process_running(self):
|
|||
|
||||
@unittest.skipUnless(sys.platform == "linux", "Only valid on Linux")
|
||||
def test_esrch_signal_handling(self):
|
||||
with test_subprocess("import time; time.sleep(1000)") as proc:
|
||||
with test_subprocess("import time; time.sleep(1000)") as subproc:
|
||||
try:
|
||||
unwinder = _remote_debugging.RemoteUnwinder(proc.pid)
|
||||
unwinder = _remote_debugging.RemoteUnwinder(subproc.process.pid)
|
||||
except PermissionError:
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
|
|
@ -2047,10 +2050,10 @@ def test_esrch_signal_handling(self):
|
|||
initial_trace = unwinder.get_stack_trace()
|
||||
self.assertIsNotNone(initial_trace)
|
||||
|
||||
proc.kill()
|
||||
subproc.process.kill()
|
||||
|
||||
# Wait for the process to die and try to get another trace
|
||||
proc.wait()
|
||||
subproc.process.wait()
|
||||
|
||||
with self.assertRaises(ProcessLookupError):
|
||||
unwinder.get_stack_trace()
|
||||
|
|
@ -2644,10 +2647,13 @@ def test_cpu_mode_integration_filtering(self):
|
|||
import time
|
||||
import threading
|
||||
|
||||
cpu_ready = threading.Event()
|
||||
|
||||
def idle_worker():
|
||||
time.sleep(999999)
|
||||
|
||||
def cpu_active_worker():
|
||||
cpu_ready.set()
|
||||
x = 1
|
||||
while True:
|
||||
x += 1
|
||||
|
|
@ -2658,21 +2664,30 @@ def main():
|
|||
cpu_thread = threading.Thread(target=cpu_active_worker)
|
||||
idle_thread.start()
|
||||
cpu_thread.start()
|
||||
|
||||
# Wait for CPU thread to be running, then signal test
|
||||
cpu_ready.wait()
|
||||
_test_sock.sendall(b"threads_ready")
|
||||
|
||||
idle_thread.join()
|
||||
cpu_thread.join()
|
||||
|
||||
main()
|
||||
|
||||
'''
|
||||
with test_subprocess(cpu_vs_idle_script) as proc:
|
||||
with test_subprocess(cpu_vs_idle_script) as subproc:
|
||||
# Wait for signal that threads are running
|
||||
response = subproc.socket.recv(1024)
|
||||
self.assertEqual(response, b"threads_ready")
|
||||
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
duration_sec=0.5,
|
||||
subproc.process.pid,
|
||||
duration_sec=2.0,
|
||||
sample_interval_usec=5000,
|
||||
mode=1, # CPU mode
|
||||
show_summary=False,
|
||||
|
|
@ -2690,8 +2705,8 @@ def main():
|
|||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
duration_sec=0.5,
|
||||
subproc.process.pid,
|
||||
duration_sec=2.0,
|
||||
sample_interval_usec=5000,
|
||||
mode=0, # Wall-clock mode
|
||||
show_summary=False,
|
||||
|
|
@ -2716,6 +2731,37 @@ def main():
|
|||
self.assertIn("cpu_active_worker", wall_mode_output)
|
||||
self.assertIn("idle_worker", wall_mode_output)
|
||||
|
||||
def test_cpu_mode_with_no_samples(self):
|
||||
"""Test that CPU mode handles no samples gracefully when no samples are collected."""
|
||||
# Mock a collector that returns empty stats
|
||||
mock_collector = mock.MagicMock()
|
||||
mock_collector.stats = {}
|
||||
mock_collector.create_stats = mock.MagicMock()
|
||||
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
mock.patch("profiling.sampling.sample.PstatsCollector", return_value=mock_collector),
|
||||
mock.patch("profiling.sampling.sample.SampleProfiler") as mock_profiler_class,
|
||||
):
|
||||
mock_profiler = mock.MagicMock()
|
||||
mock_profiler_class.return_value = mock_profiler
|
||||
|
||||
profiling.sampling.sample.sample(
|
||||
12345, # dummy PID
|
||||
duration_sec=0.5,
|
||||
sample_interval_usec=5000,
|
||||
mode=1, # CPU mode
|
||||
show_summary=False,
|
||||
all_threads=True,
|
||||
)
|
||||
|
||||
output = captured_output.getvalue()
|
||||
|
||||
# Should see the "No samples were collected" message
|
||||
self.assertIn("No samples were collected", output)
|
||||
self.assertIn("CPU mode", output)
|
||||
|
||||
|
||||
class TestGilModeFiltering(unittest.TestCase):
|
||||
"""Test GIL mode filtering functionality (--mode=gil)."""
|
||||
|
|
@ -2852,10 +2898,13 @@ def test_gil_mode_integration_behavior(self):
|
|||
import time
|
||||
import threading
|
||||
|
||||
gil_ready = threading.Event()
|
||||
|
||||
def gil_releasing_work():
|
||||
time.sleep(999999)
|
||||
|
||||
def gil_holding_work():
|
||||
gil_ready.set()
|
||||
x = 1
|
||||
while True:
|
||||
x += 1
|
||||
|
|
@ -2866,20 +2915,29 @@ def main():
|
|||
cpu_thread = threading.Thread(target=gil_holding_work)
|
||||
idle_thread.start()
|
||||
cpu_thread.start()
|
||||
|
||||
# Wait for GIL-holding thread to be running, then signal test
|
||||
gil_ready.wait()
|
||||
_test_sock.sendall(b"threads_ready")
|
||||
|
||||
idle_thread.join()
|
||||
cpu_thread.join()
|
||||
|
||||
main()
|
||||
'''
|
||||
with test_subprocess(gil_test_script) as proc:
|
||||
with test_subprocess(gil_test_script) as subproc:
|
||||
# Wait for signal that threads are running
|
||||
response = subproc.socket.recv(1024)
|
||||
self.assertEqual(response, b"threads_ready")
|
||||
|
||||
with (
|
||||
io.StringIO() as captured_output,
|
||||
mock.patch("sys.stdout", captured_output),
|
||||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
duration_sec=0.5,
|
||||
subproc.process.pid,
|
||||
duration_sec=2.0,
|
||||
sample_interval_usec=5000,
|
||||
mode=2, # GIL mode
|
||||
show_summary=False,
|
||||
|
|
@ -2897,7 +2955,7 @@ def main():
|
|||
):
|
||||
try:
|
||||
profiling.sampling.sample.sample(
|
||||
proc.pid,
|
||||
subproc.process.pid,
|
||||
duration_sec=0.5,
|
||||
sample_interval_usec=5000,
|
||||
mode=0, # Wall-clock mode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue