Skip to content

Commit 6c73136

Browse files
committed
gh-159645: Fix --async-aware profiles showing event loop frames under tasks
1 parent 97a7135 commit 6c73136

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import collections.abc
23
import unittest
34
import os
45
import textwrap
@@ -614,6 +615,54 @@ async def main():
614615
stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
615616
self.assertEqual(stacks["consumer"], ["consumer"])
616617

618+
@skip_if_not_supported
619+
def test_running_task_stack_between_steps(self):
620+
# gh-159645
621+
traces = []
622+
623+
class Loop(asyncio.SelectorEventLoop):
624+
def call_soon(self, *args, **kwargs):
625+
if asyncio.current_task(self) is not None:
626+
unwinder = RemoteUnwinder(os.getpid())
627+
traces.append(unwinder.get_async_stack_trace())
628+
return super().call_soon(*args, **kwargs)
629+
630+
async def child():
631+
await asyncio.sleep(0)
632+
633+
async def main():
634+
await asyncio.create_task(child(), name="child")
635+
636+
asyncio.run(main(), loop_factory=Loop)
637+
stacks = [list(self._extract_coroutine_funcnames(trace).values())
638+
for trace in traces]
639+
self.assertIn([["sleep", "child"], ["main"]], stacks)
640+
self.assertIn([[], ["main"]], stacks)
641+
642+
@skip_if_not_supported
643+
def test_running_task_with_non_native_coroutine(self):
644+
# gh-159645
645+
class Wrapper(collections.abc.Coroutine):
646+
def __init__(self, coro):
647+
self._coro = coro
648+
def send(self, value):
649+
return self._coro.send(value)
650+
def throw(self, *args):
651+
return self._coro.throw(*args)
652+
def close(self):
653+
return self._coro.close()
654+
def __await__(self):
655+
return self._coro.__await__()
656+
657+
async def inner():
658+
return RemoteUnwinder(os.getpid()).get_async_stack_trace()
659+
660+
async def main():
661+
return await asyncio.create_task(Wrapper(inner()), name="wrapper")
662+
663+
stacks = self._extract_coroutine_funcnames(asyncio.run(main()))
664+
self.assertEqual(stacks["wrapper"], [])
665+
617666
@skip_if_not_supported
618667
@unittest.skipIf(
619668
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:profiling.sampling --async-aware profiles showing event loop frames
2+
under tasks. Patch by Timofei Ivankov.

‎Modules/_remote_debugging/asyncio.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,24 @@ get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *f
765765
if (get_coro_like_address(unwinder, running_coro_addr, &running_coro_addr) < 0) {
766766
return -1;
767767
}
768+
if (running_coro_addr == 0) {
769+
*frame_addr = 0;
770+
return 0;
771+
}
772+
773+
int8_t frame_state;
774+
if (_Py_RemoteDebug_PagedReadRemoteMemory(
775+
&unwinder->handle,
776+
running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_frame_state,
777+
sizeof(frame_state),
778+
&frame_state) < 0) {
779+
set_exception_cause(unwinder, PyExc_RuntimeError, "Running task coro frame state read failed");
780+
return -1;
781+
}
782+
if (frame_state != FRAME_EXECUTING) {
783+
*frame_addr = 0;
784+
return 0;
785+
}
768786

769787
*frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe;
770788
return 0;
@@ -972,6 +990,11 @@ process_running_task_chain(
972990
return -1;
973991
}
974992

993+
// gh-159645: the coroutine isn't running, so keep its await chain.
994+
if (running_task_frame == 0) {
995+
return process_task_waiters(unwinder, result);
996+
}
997+
975998
// Get the chain from the current frame to this task
976999
PyObject *coro_chain = PyStructSequence_GET_ITEM(task_info, 2);
9771000
assert(coro_chain != NULL);

0 commit comments

Comments
 (0)