diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index d6243c3fc818ea..07fc0ac617f102 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -1,4 +1,5 @@ import asyncio +import collections.abc import unittest import os import textwrap @@ -614,6 +615,54 @@ async def main(): stacks = self._extract_coroutine_funcnames(asyncio.run(main())) self.assertEqual(stacks["consumer"], ["consumer"]) + @skip_if_not_supported + def test_running_task_stack_between_steps(self): + # gh-159645 + traces = [] + + class Loop(asyncio.SelectorEventLoop): + def call_soon(self, *args, **kwargs): + if asyncio.current_task(self) is not None: + unwinder = RemoteUnwinder(os.getpid()) + traces.append(unwinder.get_async_stack_trace()) + return super().call_soon(*args, **kwargs) + + async def child(): + await asyncio.sleep(0) + + async def main(): + await asyncio.create_task(child(), name="child") + + asyncio.run(main(), loop_factory=Loop) + stacks = [list(self._extract_coroutine_funcnames(trace).values()) + for trace in traces] + self.assertIn([["sleep", "child"], ["main"]], stacks) + self.assertIn([[], ["main"]], stacks) + + @skip_if_not_supported + def test_running_task_with_non_native_coroutine(self): + # gh-159645 + class Wrapper(collections.abc.Coroutine): + def __init__(self, coro): + self._coro = coro + def send(self, value): + return self._coro.send(value) + def throw(self, *args): + return self._coro.throw(*args) + def close(self): + return self._coro.close() + def __await__(self): + return self._coro.__await__() + + async def inner(): + return RemoteUnwinder(os.getpid()).get_async_stack_trace() + + async def main(): + return await asyncio.create_task(Wrapper(inner()), name="wrapper") + + stacks = self._extract_coroutine_funcnames(asyncio.run(main())) + self.assertEqual(stacks["wrapper"], []) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, diff --git a/Misc/NEWS.d/next/Library/2026-10-18-19-18-20.gh-issue-159645.IOXcpT.rst b/Misc/NEWS.d/next/Library/2026-10-18-19-18-20.gh-issue-159645.IOXcpT.rst new file mode 100644 index 00000000000000..fde97e6f131cb5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-18-19-18-20.gh-issue-159645.IOXcpT.rst @@ -0,0 +1,2 @@ +Fix :mod:`profiling.sampling` ``--async-aware`` profiles showing event loop +frames under tasks. Patch by Timofei Ivankov. diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 884065b6efdee7..16530ecdab9c84 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -765,6 +765,24 @@ get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *f if (get_coro_like_address(unwinder, running_coro_addr, &running_coro_addr) < 0) { return -1; } + if (running_coro_addr == 0) { + *frame_addr = 0; + return 0; + } + + int8_t frame_state; + if (_Py_RemoteDebug_PagedReadRemoteMemory( + &unwinder->handle, + running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_frame_state, + sizeof(frame_state), + &frame_state) < 0) { + set_exception_cause(unwinder, PyExc_RuntimeError, "Running task coro frame state read failed"); + return -1; + } + if (frame_state != FRAME_EXECUTING) { + *frame_addr = 0; + return 0; + } *frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; return 0; @@ -972,6 +990,11 @@ process_running_task_chain( return -1; } + // gh-159645: the coroutine isn't running, so keep its await chain. + if (running_task_frame == 0) { + return process_task_waiters(unwinder, result); + } + // Get the chain from the current frame to this task PyObject *coro_chain = PyStructSequence_GET_ITEM(task_info, 2); assert(coro_chain != NULL);