From 6c73136f30cf71a94e926a77822e64add90f3460 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 19:20:17 +0300 Subject: [PATCH 1/4] gh-159645: Fix --async-aware profiles showing event loop frames under tasks --- Lib/test/test_external_inspection.py | 49 +++++++++++++++++++ ...-10-18-19-18-20.gh-issue-159645.IOXcpT.rst | 2 + Modules/_remote_debugging/asyncio.c | 23 +++++++++ 3 files changed, 74 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-18-19-18-20.gh-issue-159645.IOXcpT.rst diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index d6243c3fc818eab..07fc0ac617f1027 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 000000000000000..8f5e6aa43cbf618 --- /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 884065b6efdee74..16530ecdab9c845 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); From 94e332dbb1ecbcc3f3c4f6b02d55dfea743da37b Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sun, 18 Oct 2026 19:26:48 +0300 Subject: [PATCH 2/4] fix lint --- .../Library/2026-10-18-19-18-20.gh-issue-159645.IOXcpT.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 8f5e6aa43cbf618..fde97e6f131cb54 100644 --- 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 @@ -1,2 +1,2 @@ -Fix :mod:profiling.sampling --async-aware profiles showing event loop frames -under tasks. Patch by Timofei Ivankov. +Fix :mod:`profiling.sampling` ``--async-aware`` profiles showing event loop +frames under tasks. Patch by Timofei Ivankov. From e45f74941dbb1df51d5dbfc715e53d9be64b1136 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 31 Oct 2026 14:18:43 +0300 Subject: [PATCH 3/4] review changes --- Lib/test/test_external_inspection.py | 31 ++++++++++++++++++++++++++++ Modules/_remote_debugging/asyncio.c | 28 +++++-------------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index 07fc0ac617f1027..ef6dd74c31a06ba 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -14,6 +14,7 @@ from unittest.mock import ANY from test.support import ( os_helper, + threading_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled, @@ -639,6 +640,36 @@ async def main(): self.assertIn([["sleep", "child"], ["main"]], stacks) self.assertIn([[], ["main"]], stacks) + @skip_if_not_supported + def test_running_task_frame_not_in_thread_stack(self): + # gh-159645 + ready = threading.Event() + release = threading.Event() + + async def child(): + ready.set() + release.wait() + await asyncio.sleep(0) + + async def main(): + loop = asyncio.get_running_loop() + coro = child() + task = asyncio.create_task(coro, name="child") + thread = threading.Thread(target=coro.send, args=(None,)) + with threading_helper.start_threads([thread], unlock=release.set): + self.assertTrue(ready.wait(SHORT_TIMEOUT)) + self.assertTrue(coro.cr_running) + previous = tasks._swap_current_task(loop, task) + try: + trace = RemoteUnwinder(os.getpid()).get_async_stack_trace() + finally: + tasks._swap_current_task(loop, previous) + await task + return trace + + stacks = self._extract_coroutine_funcnames(asyncio.run(main())) + self.assertEqual(stacks["child"], ["child"]) + @skip_if_not_supported def test_running_task_with_non_native_coroutine(self): # gh-159645 diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 16530ecdab9c845..fdb8d8df914b175 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -770,20 +770,6 @@ get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *f 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; } @@ -799,6 +785,7 @@ parse_async_frame_chain( uintptr_t address_of_thread, uintptr_t running_task_frame ) { + Py_ssize_t coro_chain_size = PyList_GET_SIZE(calls); uintptr_t address_of_current_frame; if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) { set_exception_cause(unwinder, PyExc_RuntimeError, "Running frame search failed in async chain"); @@ -844,11 +831,12 @@ parse_async_frame_chain( // Stop at the task's own frame. Code objects are shared by // recursive calls, so they cannot identify it. if (this_frame == running_task_frame) { - break; + return PyList_SetSlice(calls, 0, coro_chain_size, NULL); } } - return 0; + // gh-159645: keep the coroutine chain if the task's frame was not found. + return PyList_SetSlice(calls, coro_chain_size, PY_SSIZE_T_MAX, NULL); } /* ============================================================================ @@ -990,7 +978,7 @@ process_running_task_chain( return -1; } - // gh-159645: the coroutine isn't running, so keep its await chain. + // gh-159645: non-native coroutines have no frame to look for. if (running_task_frame == 0) { return process_task_waiters(unwinder, result); } @@ -1010,12 +998,6 @@ process_running_task_chain( PyObject *frame_chain = PyStructSequence_GET_ITEM(coro_info, 0); assert(frame_chain != NULL); - // Clear the coro_chain - if (PyList_Clear(frame_chain) < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to clear coroutine chain"); - return -1; - } - // Add the chain from the current frame to this task if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_frame) < 0) { return -1; From 2688d4ce6b63590c5bff85d1e35055d698eabfcc Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 31 Oct 2026 14:55:28 +0300 Subject: [PATCH 4/4] actualize the test --- Lib/test/test_external_inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index beefcb67f592fe2..465f6a7d80c9bf6 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -686,7 +686,7 @@ async def main(): 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([["__sleep0", "sleep", "child"], ["main"]], stacks) self.assertIn([[], ["main"]], stacks) @skip_if_not_supported