Skip to content

Commit 47ebc11

Browse files
committed
Small fixes
1 parent e6eaa2c commit 47ebc11

File tree

4 files changed

+73
-61
lines changed

4 files changed

+73
-61
lines changed

Lib/test/test_profiling/test_sampling_profiler/test_integration.py

Lines changed: 48 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,36 @@ async def main():
827827
asyncio.run(main())
828828
'''
829829

830+
def _collect_async_samples(self, async_aware_mode):
831+
"""Helper to collect samples and count function occurrences.
832+
833+
Returns a dict mapping function names to their sample counts.
834+
"""
835+
with test_subprocess(self.async_script) as subproc:
836+
try:
837+
collector = CollapsedStackCollector(1000, skip_idle=False)
838+
profiling.sampling.sample.sample(
839+
subproc.process.pid,
840+
collector,
841+
duration_sec=SHORT_TIMEOUT,
842+
async_aware=async_aware_mode,
843+
)
844+
except PermissionError:
845+
self.skipTest("Insufficient permissions for remote profiling")
846+
847+
# Count samples per function from collapsed stacks
848+
# stack_counter keys are (call_tree, thread_id) where call_tree
849+
# is a tuple of (file, line, func) tuples
850+
func_samples = {}
851+
total = 0
852+
for (call_tree, _thread_id), count in collector.stack_counter.items():
853+
total += count
854+
for _file, _line, func in call_tree:
855+
func_samples[func] = func_samples.get(func, 0) + count
856+
857+
func_samples["_total"] = total
858+
return func_samples
859+
830860
def test_async_aware_all_sees_sleeping_and_running_tasks(self):
831861
"""Test that async_aware='all' captures both sleeping and CPU-running tasks.
832862
@@ -840,35 +870,12 @@ def test_async_aware_all_sees_sleeping_and_running_tasks(self):
840870
841871
async_aware='all' should see ALL 4 leaf tasks in the output.
842872
"""
843-
with (
844-
test_subprocess(self.async_script) as subproc,
845-
io.StringIO() as captured_output,
846-
mock.patch("sys.stdout", captured_output),
847-
):
848-
try:
849-
collector = PstatsCollector(sample_interval_usec=1000, skip_idle=False)
850-
profiling.sampling.sample.sample(
851-
subproc.process.pid,
852-
collector,
853-
duration_sec=SHORT_TIMEOUT,
854-
async_aware="all",
855-
)
856-
collector.print_stats(show_summary=False)
857-
except PermissionError:
858-
self.skipTest("Insufficient permissions for remote profiling")
859-
860-
output = captured_output.getvalue()
873+
samples = self._collect_async_samples("all")
861874

862-
# async_aware="all" should see ALL leaf tasks
863-
self.assertIn("sleeping_leaf", output)
864-
self.assertIn("cpu_leaf", output)
865-
# Should see the tree structure via task markers
866-
self.assertIn("<task>", output)
867-
# Should see task names in output (leaf tasks)
868-
self.assertIn("Sleeper", output)
869-
self.assertIn("Worker", output)
870-
# Should see the parent task in the tree (supervisor function)
871-
self.assertIn("supervisor", output)
875+
self.assertGreater(samples["_total"], 0, "Should have collected samples")
876+
self.assertIn("sleeping_leaf", samples)
877+
self.assertIn("cpu_leaf", samples)
878+
self.assertIn("supervisor", samples)
872879

873880
def test_async_aware_running_sees_only_cpu_task(self):
874881
"""Test that async_aware='running' only captures the actively running task.
@@ -883,32 +890,18 @@ def test_async_aware_running_sees_only_cpu_task(self):
883890
884891
async_aware='running' should only see the Worker task doing CPU work.
885892
"""
886-
with (
887-
test_subprocess(self.async_script) as subproc,
888-
io.StringIO() as captured_output,
889-
mock.patch("sys.stdout", captured_output),
890-
):
891-
try:
892-
collector = PstatsCollector(sample_interval_usec=1000, skip_idle=False)
893-
profiling.sampling.sample.sample(
894-
subproc.process.pid,
895-
collector,
896-
duration_sec=SHORT_TIMEOUT,
897-
async_aware="running",
898-
)
899-
collector.print_stats(show_summary=False)
900-
except PermissionError:
901-
self.skipTest("Insufficient permissions for remote profiling")
893+
samples = self._collect_async_samples("running")
902894

903-
output = captured_output.getvalue()
895+
total = samples["_total"]
896+
cpu_leaf_samples = samples.get("cpu_leaf", 0)
897+
898+
self.assertGreater(total, 0, "Should have collected some samples")
899+
self.assertGreater(cpu_leaf_samples, 0, "cpu_leaf should appear in samples")
904900

905-
# async_aware="running" should see the CPU task prominently
906-
self.assertIn("cpu_leaf", output)
907-
# Should see Worker task marker
908-
self.assertIn("Worker", output)
909-
# Should see the tree structure (supervisor in the parent chain)
910-
self.assertIn("supervisor", output)
911-
# Should see task boundary markers
912-
self.assertIn("<task>", output)
913-
# async_aware="running" should NOT see sleeping tasks
914-
self.assertNotIn("sleeping_leaf", output)
901+
# cpu_leaf should have at least 90% of samples (typically 99%+)
902+
# sleeping_leaf may occasionally appear with very few samples (< 1%)
903+
# when tasks briefly wake up to check sleep timers
904+
cpu_percentage = (cpu_leaf_samples / total) * 100
905+
self.assertGreater(cpu_percentage, 90.0,
906+
f"cpu_leaf should dominate samples in 'running' mode, "
907+
f"got {cpu_percentage:.1f}% ({cpu_leaf_samples}/{total})")

Modules/_remote_debugging/_remote_debugging.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ extern PyObject* unwind_stack_for_thread(
405405

406406
extern uintptr_t _Py_RemoteDebug_GetAsyncioDebugAddress(proc_handle_t* handle);
407407
extern int read_async_debug(RemoteUnwinderObject *unwinder);
408+
extern int ensure_async_debug_offsets(RemoteUnwinderObject *unwinder);
408409

409410
/* Task parsing */
410411
extern PyObject *parse_task_name(RemoteUnwinderObject *unwinder, uintptr_t task_address);

Modules/_remote_debugging/asyncio.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ read_async_debug(RemoteUnwinderObject *unwinder)
7171
return result;
7272
}
7373

74+
int
75+
ensure_async_debug_offsets(RemoteUnwinderObject *unwinder)
76+
{
77+
// If already available, nothing to do
78+
if (unwinder->async_debug_offsets_available) {
79+
return 0;
80+
}
81+
82+
// Try to load async debug offsets (the target process may have
83+
// loaded asyncio since we last checked)
84+
if (read_async_debug(unwinder) < 0) {
85+
PyErr_Clear();
86+
PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available");
87+
set_exception_cause(unwinder, PyExc_RuntimeError,
88+
"AsyncioDebug section unavailable - asyncio module may not be loaded in target process");
89+
return -1;
90+
}
91+
92+
unwinder->async_debug_offsets_available = 1;
93+
return 0;
94+
}
95+
7496
/* ============================================================================
7597
* SET ITERATION FUNCTIONS
7698
* ============================================================================ */

Modules/_remote_debugging/module.c

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -645,9 +645,7 @@ static PyObject *
645645
_remote_debugging_RemoteUnwinder_get_all_awaited_by_impl(RemoteUnwinderObject *self)
646646
/*[clinic end generated code: output=6a49cd345e8aec53 input=307f754cbe38250c]*/
647647
{
648-
if (!self->async_debug_offsets_available) {
649-
PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available");
650-
set_exception_cause(self, PyExc_RuntimeError, "AsyncioDebug section unavailable in get_all_awaited_by");
648+
if (ensure_async_debug_offsets(self) < 0) {
651649
return NULL;
652650
}
653651

@@ -736,9 +734,7 @@ static PyObject *
736734
_remote_debugging_RemoteUnwinder_get_async_stack_trace_impl(RemoteUnwinderObject *self)
737735
/*[clinic end generated code: output=6433d52b55e87bbe input=6129b7d509a887c9]*/
738736
{
739-
if (!self->async_debug_offsets_available) {
740-
PyErr_SetString(PyExc_RuntimeError, "AsyncioDebug section not available");
741-
set_exception_cause(self, PyExc_RuntimeError, "AsyncioDebug section unavailable in get_async_stack_trace");
737+
if (ensure_async_debug_offsets(self) < 0) {
742738
return NULL;
743739
}
744740

0 commit comments

Comments
 (0)