mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +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:
|
if output_format == "pstats" and not filename:
|
||||||
stats = pstats.SampledStats(collector).strip_dirs()
|
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(
|
print_sampled_stats(
|
||||||
stats, sort, limit, show_summary, sample_interval_usec
|
stats, sort, limit, show_summary, sample_interval_usec
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,7 @@ def load_stats(self, arg):
|
||||||
arg.create_stats()
|
arg.create_stats()
|
||||||
self.stats = arg.stats
|
self.stats = arg.stats
|
||||||
arg.stats = {}
|
arg.stats = {}
|
||||||
|
return
|
||||||
if not self.stats:
|
if not self.stats:
|
||||||
raise TypeError("Cannot create or construct a %r object from %r"
|
raise TypeError("Cannot create or construct a %r object from %r"
|
||||||
% (self.__class__, arg))
|
% (self.__class__, arg))
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from collections import namedtuple
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from profiling.sampling.pstats_collector import PstatsCollector
|
from profiling.sampling.pstats_collector import PstatsCollector
|
||||||
|
|
@ -84,6 +85,8 @@ def __repr__(self):
|
||||||
"Test only runs on Linux, Windows and MacOS",
|
"Test only runs on Linux, Windows and MacOS",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SubprocessInfo = namedtuple('SubprocessInfo', ['process', 'socket'])
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def test_subprocess(script):
|
def test_subprocess(script):
|
||||||
|
|
@ -123,7 +126,7 @@ def test_subprocess(script):
|
||||||
if response != b"ready":
|
if response != b"ready":
|
||||||
raise RuntimeError(f"Unexpected response from subprocess: {response}")
|
raise RuntimeError(f"Unexpected response from subprocess: {response}")
|
||||||
|
|
||||||
yield proc
|
yield SubprocessInfo(proc, client_socket)
|
||||||
finally:
|
finally:
|
||||||
if client_socket is not None:
|
if client_socket is not None:
|
||||||
client_socket.close()
|
client_socket.close()
|
||||||
|
|
@ -1752,13 +1755,13 @@ def main_loop():
|
||||||
|
|
||||||
def test_sampling_basic_functionality(self):
|
def test_sampling_basic_functionality(self):
|
||||||
with (
|
with (
|
||||||
test_subprocess(self.test_script) as proc,
|
test_subprocess(self.test_script) as subproc,
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
mock.patch("sys.stdout", captured_output),
|
mock.patch("sys.stdout", captured_output),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=2,
|
duration_sec=2,
|
||||||
sample_interval_usec=1000, # 1ms
|
sample_interval_usec=1000, # 1ms
|
||||||
show_summary=False,
|
show_summary=False,
|
||||||
|
|
@ -1782,7 +1785,7 @@ def test_sampling_with_pstats_export(self):
|
||||||
)
|
)
|
||||||
self.addCleanup(close_and_unlink, pstats_out)
|
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
|
# Suppress profiler output when testing file export
|
||||||
with (
|
with (
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
|
|
@ -1790,7 +1793,7 @@ def test_sampling_with_pstats_export(self):
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=1,
|
duration_sec=1,
|
||||||
filename=pstats_out.name,
|
filename=pstats_out.name,
|
||||||
sample_interval_usec=10000,
|
sample_interval_usec=10000,
|
||||||
|
|
@ -1826,7 +1829,7 @@ def test_sampling_with_collapsed_export(self):
|
||||||
self.addCleanup(close_and_unlink, collapsed_file)
|
self.addCleanup(close_and_unlink, collapsed_file)
|
||||||
|
|
||||||
with (
|
with (
|
||||||
test_subprocess(self.test_script) as proc,
|
test_subprocess(self.test_script) as subproc,
|
||||||
):
|
):
|
||||||
# Suppress profiler output when testing file export
|
# Suppress profiler output when testing file export
|
||||||
with (
|
with (
|
||||||
|
|
@ -1835,7 +1838,7 @@ def test_sampling_with_collapsed_export(self):
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=1,
|
duration_sec=1,
|
||||||
filename=collapsed_file.name,
|
filename=collapsed_file.name,
|
||||||
output_format="collapsed",
|
output_format="collapsed",
|
||||||
|
|
@ -1876,14 +1879,14 @@ def test_sampling_with_collapsed_export(self):
|
||||||
|
|
||||||
def test_sampling_all_threads(self):
|
def test_sampling_all_threads(self):
|
||||||
with (
|
with (
|
||||||
test_subprocess(self.test_script) as proc,
|
test_subprocess(self.test_script) as subproc,
|
||||||
# Suppress profiler output
|
# Suppress profiler output
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
mock.patch("sys.stdout", captured_output),
|
mock.patch("sys.stdout", captured_output),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=1,
|
duration_sec=1,
|
||||||
all_threads=True,
|
all_threads=True,
|
||||||
sample_interval_usec=10000,
|
sample_interval_usec=10000,
|
||||||
|
|
@ -1969,14 +1972,14 @@ def test_invalid_pid(self):
|
||||||
profiling.sampling.sample.sample(-1, duration_sec=1)
|
profiling.sampling.sample.sample(-1, duration_sec=1)
|
||||||
|
|
||||||
def test_process_dies_during_sampling(self):
|
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 (
|
with (
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
mock.patch("sys.stdout", captured_output),
|
mock.patch("sys.stdout", captured_output),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=2, # Longer than process lifetime
|
duration_sec=2, # Longer than process lifetime
|
||||||
sample_interval_usec=50000,
|
sample_interval_usec=50000,
|
||||||
)
|
)
|
||||||
|
|
@ -2018,17 +2021,17 @@ def test_invalid_output_format_with_mocked_profiler(self):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_is_process_running(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:
|
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:
|
except PermissionError:
|
||||||
self.skipTest(
|
self.skipTest(
|
||||||
"Insufficient permissions to read the stack trace"
|
"Insufficient permissions to read the stack trace"
|
||||||
)
|
)
|
||||||
self.assertTrue(profiler._is_process_running())
|
self.assertTrue(profiler._is_process_running())
|
||||||
self.assertIsNotNone(profiler.unwinder.get_stack_trace())
|
self.assertIsNotNone(profiler.unwinder.get_stack_trace())
|
||||||
proc.kill()
|
subproc.process.kill()
|
||||||
proc.wait()
|
subproc.process.wait()
|
||||||
self.assertRaises(ProcessLookupError, profiler.unwinder.get_stack_trace)
|
self.assertRaises(ProcessLookupError, profiler.unwinder.get_stack_trace)
|
||||||
|
|
||||||
# Exit the context manager to ensure the process is terminated
|
# 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")
|
@unittest.skipUnless(sys.platform == "linux", "Only valid on Linux")
|
||||||
def test_esrch_signal_handling(self):
|
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:
|
try:
|
||||||
unwinder = _remote_debugging.RemoteUnwinder(proc.pid)
|
unwinder = _remote_debugging.RemoteUnwinder(subproc.process.pid)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.skipTest(
|
self.skipTest(
|
||||||
"Insufficient permissions to read the stack trace"
|
"Insufficient permissions to read the stack trace"
|
||||||
|
|
@ -2047,10 +2050,10 @@ def test_esrch_signal_handling(self):
|
||||||
initial_trace = unwinder.get_stack_trace()
|
initial_trace = unwinder.get_stack_trace()
|
||||||
self.assertIsNotNone(initial_trace)
|
self.assertIsNotNone(initial_trace)
|
||||||
|
|
||||||
proc.kill()
|
subproc.process.kill()
|
||||||
|
|
||||||
# Wait for the process to die and try to get another trace
|
# Wait for the process to die and try to get another trace
|
||||||
proc.wait()
|
subproc.process.wait()
|
||||||
|
|
||||||
with self.assertRaises(ProcessLookupError):
|
with self.assertRaises(ProcessLookupError):
|
||||||
unwinder.get_stack_trace()
|
unwinder.get_stack_trace()
|
||||||
|
|
@ -2644,10 +2647,13 @@ def test_cpu_mode_integration_filtering(self):
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
cpu_ready = threading.Event()
|
||||||
|
|
||||||
def idle_worker():
|
def idle_worker():
|
||||||
time.sleep(999999)
|
time.sleep(999999)
|
||||||
|
|
||||||
def cpu_active_worker():
|
def cpu_active_worker():
|
||||||
|
cpu_ready.set()
|
||||||
x = 1
|
x = 1
|
||||||
while True:
|
while True:
|
||||||
x += 1
|
x += 1
|
||||||
|
|
@ -2658,21 +2664,30 @@ def main():
|
||||||
cpu_thread = threading.Thread(target=cpu_active_worker)
|
cpu_thread = threading.Thread(target=cpu_active_worker)
|
||||||
idle_thread.start()
|
idle_thread.start()
|
||||||
cpu_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()
|
idle_thread.join()
|
||||||
cpu_thread.join()
|
cpu_thread.join()
|
||||||
|
|
||||||
main()
|
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 (
|
with (
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
mock.patch("sys.stdout", captured_output),
|
mock.patch("sys.stdout", captured_output),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=0.5,
|
duration_sec=2.0,
|
||||||
sample_interval_usec=5000,
|
sample_interval_usec=5000,
|
||||||
mode=1, # CPU mode
|
mode=1, # CPU mode
|
||||||
show_summary=False,
|
show_summary=False,
|
||||||
|
|
@ -2690,8 +2705,8 @@ def main():
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=0.5,
|
duration_sec=2.0,
|
||||||
sample_interval_usec=5000,
|
sample_interval_usec=5000,
|
||||||
mode=0, # Wall-clock mode
|
mode=0, # Wall-clock mode
|
||||||
show_summary=False,
|
show_summary=False,
|
||||||
|
|
@ -2716,6 +2731,37 @@ def main():
|
||||||
self.assertIn("cpu_active_worker", wall_mode_output)
|
self.assertIn("cpu_active_worker", wall_mode_output)
|
||||||
self.assertIn("idle_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):
|
class TestGilModeFiltering(unittest.TestCase):
|
||||||
"""Test GIL mode filtering functionality (--mode=gil)."""
|
"""Test GIL mode filtering functionality (--mode=gil)."""
|
||||||
|
|
@ -2852,10 +2898,13 @@ def test_gil_mode_integration_behavior(self):
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
gil_ready = threading.Event()
|
||||||
|
|
||||||
def gil_releasing_work():
|
def gil_releasing_work():
|
||||||
time.sleep(999999)
|
time.sleep(999999)
|
||||||
|
|
||||||
def gil_holding_work():
|
def gil_holding_work():
|
||||||
|
gil_ready.set()
|
||||||
x = 1
|
x = 1
|
||||||
while True:
|
while True:
|
||||||
x += 1
|
x += 1
|
||||||
|
|
@ -2866,20 +2915,29 @@ def main():
|
||||||
cpu_thread = threading.Thread(target=gil_holding_work)
|
cpu_thread = threading.Thread(target=gil_holding_work)
|
||||||
idle_thread.start()
|
idle_thread.start()
|
||||||
cpu_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()
|
idle_thread.join()
|
||||||
cpu_thread.join()
|
cpu_thread.join()
|
||||||
|
|
||||||
main()
|
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 (
|
with (
|
||||||
io.StringIO() as captured_output,
|
io.StringIO() as captured_output,
|
||||||
mock.patch("sys.stdout", captured_output),
|
mock.patch("sys.stdout", captured_output),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=0.5,
|
duration_sec=2.0,
|
||||||
sample_interval_usec=5000,
|
sample_interval_usec=5000,
|
||||||
mode=2, # GIL mode
|
mode=2, # GIL mode
|
||||||
show_summary=False,
|
show_summary=False,
|
||||||
|
|
@ -2897,7 +2955,7 @@ def main():
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
profiling.sampling.sample.sample(
|
profiling.sampling.sample.sample(
|
||||||
proc.pid,
|
subproc.process.pid,
|
||||||
duration_sec=0.5,
|
duration_sec=0.5,
|
||||||
sample_interval_usec=5000,
|
sample_interval_usec=5000,
|
||||||
mode=0, # Wall-clock mode
|
mode=0, # Wall-clock mode
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue