Skip to content

Commit ff983d8

Browse files
committed
Fix tests
1 parent 56661dc commit ff983d8

5 files changed

Lines changed: 292 additions & 91 deletions

File tree

Lib/profiling/sampling/cli.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,19 @@ def _validate_args(args, parser):
390390
"Live mode requires the curses module, which is not available."
391391
)
392392

393+
# Async-aware mode is incompatible with --native and --gc/--no-gc
394+
if args.async_aware is not None:
395+
issues = []
396+
if args.native:
397+
issues.append("--native")
398+
if not args.gc:
399+
issues.append("--no-gc")
400+
if issues:
401+
parser.error(
402+
f"Options {', '.join(issues)} are incompatible with --async-aware. "
403+
"Async-aware profiling uses task-based stack reconstruction."
404+
)
405+
393406
# Live mode is incompatible with format options
394407
if hasattr(args, 'live') and args.live:
395408
if args.format != "pstats":

Lib/profiling/sampling/live_collector/collector.py

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -308,26 +308,6 @@ def _get_async_frame_iterator(self, stack_frames):
308308
for frames, thread_id, task_id in self._iter_async_frames(stack_frames):
309309
yield frames, thread_id
310310

311-
def _collect_sync_thread_stats(self, stack_frames, has_gc_frame):
312-
"""Collect thread status stats for sync mode."""
313-
status_counts, sample_has_gc, per_thread_stats = self._collect_thread_status_stats(stack_frames)
314-
for key, count in status_counts.items():
315-
self.thread_status_counts[key] += count
316-
if sample_has_gc:
317-
has_gc_frame = True
318-
319-
for thread_id, stats in per_thread_stats.items():
320-
thread_data = self._get_or_create_thread_data(thread_id)
321-
thread_data.has_gil += stats.get("has_gil", 0)
322-
thread_data.on_cpu += stats.get("on_cpu", 0)
323-
thread_data.gil_requested += stats.get("gil_requested", 0)
324-
thread_data.unknown += stats.get("unknown", 0)
325-
thread_data.total += stats.get("total", 0)
326-
if stats.get("gc_samples", 0):
327-
thread_data.gc_frame_samples += stats["gc_samples"]
328-
329-
return has_gc_frame
330-
331311
def collect_failed_sample(self):
332312
self.failed_samples += 1
333313
self.total_samples += 1
@@ -342,7 +322,21 @@ def collect(self, stack_frames):
342322

343323
# Collect thread status stats (only available in sync mode)
344324
if not self.async_aware:
345-
has_gc_frame = self._collect_sync_thread_stats(stack_frames, has_gc_frame)
325+
status_counts, sample_has_gc, per_thread_stats = self._collect_thread_status_stats(stack_frames)
326+
for key, count in status_counts.items():
327+
self.thread_status_counts[key] += count
328+
if sample_has_gc:
329+
has_gc_frame = True
330+
331+
for thread_id, stats in per_thread_stats.items():
332+
thread_data = self._get_or_create_thread_data(thread_id)
333+
thread_data.has_gil += stats.get("has_gil", 0)
334+
thread_data.on_cpu += stats.get("on_cpu", 0)
335+
thread_data.gil_requested += stats.get("gil_requested", 0)
336+
thread_data.unknown += stats.get("unknown", 0)
337+
thread_data.total += stats.get("total", 0)
338+
if stats.get("gc_samples", 0):
339+
thread_data.gc_frame_samples += stats["gc_samples"]
346340

347341
# Process frames using pre-selected iterator
348342
for frames, thread_id in self._get_frame_iterator(stack_frames):
@@ -355,20 +349,9 @@ def collect(self, stack_frames):
355349
if thread_id is not None and thread_id not in self.thread_ids:
356350
self.thread_ids.append(thread_id)
357351

358-
# Check for GC frames and update per-thread sample count
359-
thread_has_gc_frame = False
360-
for frame in frames:
361-
funcname = getattr(frame, "funcname", "")
362-
if "<GC>" in funcname or "gc_collect" in funcname:
363-
has_gc_frame = True
364-
thread_has_gc_frame = True
365-
break
366-
367352
if thread_id is not None:
368353
thread_data = self._get_or_create_thread_data(thread_id)
369354
thread_data.sample_count += 1
370-
if thread_has_gc_frame:
371-
thread_data.gc_frame_samples += 1
372355

373356
if has_gc_frame:
374357
self.gc_frame_samples += 1

Lib/test/test_profiling/test_sampling_profiler/test_async.py

Lines changed: 82 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -697,79 +697,102 @@ def test_sample_profiler_sample_accepts_async_aware(self):
697697
sig = inspect.signature(SampleProfiler.sample)
698698
self.assertIn("async_aware", sig.parameters)
699699

700-
def test_async_aware_all_uses_get_all_awaited_by(self):
701-
"""Test that async_aware='all' calls get_all_awaited_by on unwinder."""
702-
from unittest.mock import Mock, patch
703-
from profiling.sampling.sample import SampleProfiler
704-
705-
with patch('profiling.sampling.sample._remote_debugging') as mock_rd:
706-
mock_unwinder = Mock()
707-
mock_unwinder.get_all_awaited_by.return_value = []
708-
mock_rd.RemoteUnwinder.return_value = mock_unwinder
709-
710-
profiler = SampleProfiler(
711-
pid=12345,
712-
sample_interval_usec=1000,
713-
all_threads=False
714-
)
715-
profiler.unwinder = mock_unwinder
716-
717-
mock_collector = Mock()
718-
mock_collector.running = False # Stop immediately
700+
def test_async_aware_all_sees_sleeping_and_running_tasks(self):
701+
"""Test async_aware='all' captures both sleeping and CPU-running tasks."""
702+
# Sleeping task (awaiting)
703+
sleeping_task = MockTaskInfo(
704+
task_id=1,
705+
task_name="SleepingTask",
706+
coroutine_stack=[
707+
MockCoroInfo(
708+
task_name="SleepingTask",
709+
call_stack=[MockFrameInfo("sleeper.py", 10, "sleep_work")]
710+
)
711+
],
712+
awaited_by=[]
713+
)
719714

720-
# Sample with async_aware="all"
721-
profiler.sample(mock_collector, duration_sec=0.001, async_aware="all")
715+
# CPU-running task (active)
716+
running_task = MockTaskInfo(
717+
task_id=2,
718+
task_name="RunningTask",
719+
coroutine_stack=[
720+
MockCoroInfo(
721+
task_name="RunningTask",
722+
call_stack=[MockFrameInfo("runner.py", 20, "cpu_work")]
723+
)
724+
],
725+
awaited_by=[]
726+
)
722727

723-
# Should have called get_all_awaited_by
724-
mock_unwinder.get_all_awaited_by.assert_called()
728+
# Both tasks returned by get_all_awaited_by
729+
awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[sleeping_task, running_task])]
725730

726-
def test_async_aware_running_uses_get_async_stack_trace(self):
727-
"""Test that async_aware='running' calls get_async_stack_trace on unwinder."""
728-
from unittest.mock import Mock, patch
729-
from profiling.sampling.sample import SampleProfiler
731+
collector = PstatsCollector(sample_interval_usec=1000)
732+
collector.collect(awaited_info_list)
733+
collector.create_stats()
730734

731-
with patch('profiling.sampling.sample._remote_debugging') as mock_rd:
732-
mock_unwinder = Mock()
733-
mock_unwinder.get_async_stack_trace.return_value = []
734-
mock_rd.RemoteUnwinder.return_value = mock_unwinder
735+
# Both tasks should be visible
736+
sleeping_key = ("sleeper.py", 10, "sleep_work")
737+
running_key = ("runner.py", 20, "cpu_work")
735738

736-
profiler = SampleProfiler(
737-
pid=12345,
738-
sample_interval_usec=1000,
739-
all_threads=False
740-
)
741-
profiler.unwinder = mock_unwinder
739+
self.assertIn(sleeping_key, collector.stats)
740+
self.assertIn(running_key, collector.stats)
742741

743-
mock_collector = Mock()
744-
mock_collector.running = False
742+
# Task markers should also be present
743+
task_keys = [k for k in collector.stats if k[0] == "<task>"]
744+
self.assertGreater(len(task_keys), 0, "Should have <task> markers in stats")
745745

746-
profiler.sample(mock_collector, duration_sec=0.001, async_aware="running")
746+
# Verify task names are in the markers
747+
task_names = [k[2] for k in task_keys]
748+
self.assertTrue(
749+
any("SleepingTask" in name for name in task_names),
750+
"SleepingTask should be in task markers"
751+
)
752+
self.assertTrue(
753+
any("RunningTask" in name for name in task_names),
754+
"RunningTask should be in task markers"
755+
)
747756

748-
mock_unwinder.get_async_stack_trace.assert_called()
757+
def test_async_aware_running_sees_only_running_task(self):
758+
"""Test async_aware='running' only shows the currently running task stack."""
759+
# Only the running task's stack is returned by get_async_stack_trace
760+
running_task = MockTaskInfo(
761+
task_id=2,
762+
task_name="RunningTask",
763+
coroutine_stack=[
764+
MockCoroInfo(
765+
task_name="RunningTask",
766+
call_stack=[MockFrameInfo("runner.py", 20, "cpu_work")]
767+
)
768+
],
769+
awaited_by=[]
770+
)
749771

750-
def test_async_aware_none_uses_get_stack_trace(self):
751-
"""Test that async_aware=None uses regular get_stack_trace."""
752-
from unittest.mock import Mock, patch
753-
from profiling.sampling.sample import SampleProfiler
772+
# get_async_stack_trace only returns the running task
773+
awaited_info_list = [MockAwaitedInfo(thread_id=100, awaited_by=[running_task])]
754774

755-
with patch('profiling.sampling.sample._remote_debugging') as mock_rd:
756-
mock_unwinder = Mock()
757-
mock_unwinder.get_stack_trace.return_value = []
758-
mock_rd.RemoteUnwinder.return_value = mock_unwinder
775+
collector = PstatsCollector(sample_interval_usec=1000)
776+
collector.collect(awaited_info_list)
777+
collector.create_stats()
759778

760-
profiler = SampleProfiler(
761-
pid=12345,
762-
sample_interval_usec=1000,
763-
all_threads=False
764-
)
765-
profiler.unwinder = mock_unwinder
779+
# Only running task should be visible
780+
running_key = ("runner.py", 20, "cpu_work")
781+
self.assertIn(running_key, collector.stats)
766782

767-
mock_collector = Mock()
768-
mock_collector.running = False
783+
# Verify we don't see the sleeping task (it wasn't in the input)
784+
sleeping_key = ("sleeper.py", 10, "sleep_work")
785+
self.assertNotIn(sleeping_key, collector.stats)
769786

770-
profiler.sample(mock_collector, duration_sec=0.001, async_aware=None)
787+
# Task marker for running task should be present
788+
task_keys = [k for k in collector.stats if k[0] == "<task>"]
789+
self.assertGreater(len(task_keys), 0, "Should have <task> markers in stats")
771790

772-
mock_unwinder.get_stack_trace.assert_called()
791+
task_names = [k[2] for k in task_keys]
792+
self.assertTrue(
793+
any("RunningTask" in name for name in task_names),
794+
"RunningTask should be in task markers"
795+
)
773796

774797

775798
if __name__ == "__main__":

Lib/test/test_profiling/test_sampling_profiler/test_cli.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,55 @@ def test_async_aware_invalid_choice(self):
607607
main()
608608

609609
self.assertEqual(cm.exception.code, 2) # argparse error
610+
611+
def test_async_aware_incompatible_with_native(self):
612+
"""Test --async-aware is incompatible with --native."""
613+
test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "all", "--native"]
614+
615+
with (
616+
mock.patch("sys.argv", test_args),
617+
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
618+
self.assertRaises(SystemExit) as cm,
619+
):
620+
from profiling.sampling.cli import main
621+
main()
622+
623+
self.assertEqual(cm.exception.code, 2) # argparse error
624+
error_msg = mock_stderr.getvalue()
625+
self.assertIn("--native", error_msg)
626+
self.assertIn("incompatible with --async-aware", error_msg)
627+
628+
def test_async_aware_incompatible_with_no_gc(self):
629+
"""Test --async-aware is incompatible with --no-gc."""
630+
test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "running", "--no-gc"]
631+
632+
with (
633+
mock.patch("sys.argv", test_args),
634+
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
635+
self.assertRaises(SystemExit) as cm,
636+
):
637+
from profiling.sampling.cli import main
638+
main()
639+
640+
self.assertEqual(cm.exception.code, 2) # argparse error
641+
error_msg = mock_stderr.getvalue()
642+
self.assertIn("--no-gc", error_msg)
643+
self.assertIn("incompatible with --async-aware", error_msg)
644+
645+
def test_async_aware_incompatible_with_both_native_and_no_gc(self):
646+
"""Test --async-aware is incompatible with both --native and --no-gc."""
647+
test_args = ["profiling.sampling.cli", "attach", "12345", "--async-aware", "all", "--native", "--no-gc"]
648+
649+
with (
650+
mock.patch("sys.argv", test_args),
651+
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
652+
self.assertRaises(SystemExit) as cm,
653+
):
654+
from profiling.sampling.cli import main
655+
main()
656+
657+
self.assertEqual(cm.exception.code, 2) # argparse error
658+
error_msg = mock_stderr.getvalue()
659+
self.assertIn("--native", error_msg)
660+
self.assertIn("--no-gc", error_msg)
661+
self.assertIn("incompatible with --async-aware", error_msg)

0 commit comments

Comments
 (0)