Skip to content

Commit 18fcb50

Browse files
committed
gh-140137: Handle empty collections in profiling.sampling
1 parent 46f11b3 commit 18fcb50

3 files changed

Lines changed: 60 additions & 5 deletions

File tree

Lib/profiling/sampling/sample.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -642,9 +642,14 @@ def sample(
642642

643643
if output_format == "pstats" and not filename:
644644
stats = pstats.SampledStats(collector).strip_dirs()
645-
print_sampled_stats(
646-
stats, sort, limit, show_summary, sample_interval_usec
647-
)
645+
if not stats.stats:
646+
print("No samples were collected.")
647+
if mode == PROFILING_MODE_CPU:
648+
print("This can happen in CPU mode when all threads are idle.")
649+
else:
650+
print_sampled_stats(
651+
stats, sort, limit, show_summary, sample_interval_usec
652+
)
648653
else:
649654
collector.export(filename)
650655

Lib/pstats.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ def load_stats(self, arg):
154154
arg.create_stats()
155155
self.stats = arg.stats
156156
arg.stats = {}
157+
return
157158
if not self.stats:
158159
raise TypeError("Cannot create or construct a %r object from %r"
159160
% (self.__class__, arg))

Lib/test/test_profiling/test_sampling_profiler.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,20 +2644,29 @@ def test_cpu_mode_integration_filtering(self):
26442644
import time
26452645
import threading
26462646
2647+
# Event to signal when CPU thread is running
2648+
cpu_ready = threading.Event()
2649+
26472650
def idle_worker():
26482651
time.sleep(999999)
26492652
26502653
def cpu_active_worker():
2654+
cpu_ready.set() # Signal that we're running
26512655
x = 1
26522656
while True:
26532657
x += 1
26542658
26552659
def main():
2656-
# Start both threads
2660+
from test.support import SHORT_TIMEOUT
2661+
# Start both threads
26572662
idle_thread = threading.Thread(target=idle_worker)
26582663
cpu_thread = threading.Thread(target=cpu_active_worker)
26592664
idle_thread.start()
26602665
cpu_thread.start()
2666+
2667+
# Wait for CPU thread to be running before continuing
2668+
cpu_ready.wait(timeout=SHORT_TIMEOUT)
2669+
26612670
idle_thread.join()
26622671
cpu_thread.join()
26632672
@@ -2716,6 +2725,37 @@ def main():
27162725
self.assertIn("cpu_active_worker", wall_mode_output)
27172726
self.assertIn("idle_worker", wall_mode_output)
27182727

2728+
def test_cpu_mode_with_no_samples(self):
2729+
"""Test that CPU mode handles no samples gracefully when no samples are collected."""
2730+
# Mock a collector that returns empty stats
2731+
mock_collector = mock.MagicMock()
2732+
mock_collector.stats = {}
2733+
mock_collector.create_stats = mock.MagicMock()
2734+
2735+
with (
2736+
io.StringIO() as captured_output,
2737+
mock.patch("sys.stdout", captured_output),
2738+
mock.patch("profiling.sampling.sample.PstatsCollector", return_value=mock_collector),
2739+
mock.patch("profiling.sampling.sample.SampleProfiler") as mock_profiler_class,
2740+
):
2741+
mock_profiler = mock.MagicMock()
2742+
mock_profiler_class.return_value = mock_profiler
2743+
2744+
profiling.sampling.sample.sample(
2745+
12345, # dummy PID
2746+
duration_sec=0.5,
2747+
sample_interval_usec=5000,
2748+
mode=1, # CPU mode
2749+
show_summary=False,
2750+
all_threads=True,
2751+
)
2752+
2753+
output = captured_output.getvalue()
2754+
2755+
# Should see the "No samples were collected" message
2756+
self.assertIn("No samples were collected", output)
2757+
self.assertIn("CPU mode", output)
2758+
27192759

27202760
class TestGilModeFiltering(unittest.TestCase):
27212761
"""Test GIL mode filtering functionality (--mode=gil)."""
@@ -2852,20 +2892,29 @@ def test_gil_mode_integration_behavior(self):
28522892
import time
28532893
import threading
28542894
2895+
# Event to signal when thread is running
2896+
gil_ready = threading.Event()
2897+
28552898
def gil_releasing_work():
28562899
time.sleep(999999)
28572900
28582901
def gil_holding_work():
2902+
gil_ready.set() # Signal that we're running
28592903
x = 1
28602904
while True:
28612905
x += 1
28622906
28632907
def main():
2864-
# Start both threads
2908+
from test.support import SHORT_TIMEOUT
2909+
# Start both threads
28652910
idle_thread = threading.Thread(target=gil_releasing_work)
28662911
cpu_thread = threading.Thread(target=gil_holding_work)
28672912
idle_thread.start()
28682913
cpu_thread.start()
2914+
2915+
# Wait for GIL-holding thread to be running before continuing
2916+
gil_ready.wait(timeout=SHORT_TIMEOUT)
2917+
28692918
idle_thread.join()
28702919
cpu_thread.join()
28712920

0 commit comments

Comments
 (0)