Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import collections.abc
import unittest
import os
import textwrap
Expand All @@ -14,6 +15,7 @@
from unittest.mock import ANY
from test.support import (
os_helper,
threading_helper,
SHORT_TIMEOUT,
busy_retry,
requires_gil_enabled,
Expand Down Expand Up @@ -663,6 +665,84 @@ 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([["__sleep0", "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
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`profiling.sampling` ``--async-aware`` profiles showing event loop
frames under tasks. Patch by Timofei Ivankov.
21 changes: 13 additions & 8 deletions Modules/_remote_debugging/asyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,10 @@ 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;
}

*frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe;
return 0;
Expand All @@ -781,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");
Expand Down Expand Up @@ -826,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);
}

/* ============================================================================
Expand Down Expand Up @@ -972,6 +978,11 @@ process_running_task_chain(
return -1;
}

// gh-159645: non-native coroutines have no frame to look for.
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);
Expand All @@ -987,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;
Expand Down
Loading