diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index c83d2cb2abeac81..f86a9d4d01bc32b 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -458,6 +458,21 @@ async def main(): self.assertIn(main_name, names) self.assertEqual([len(n) for n in names if n.startswith("x")], [255]) + @skip_if_not_supported + def test_recursive_coroutine_stack_is_not_truncated(self): + # gh-158522 + async def rec(n): + if n: + return await rec(n - 1) + return [ + frame.funcname.rpartition(".")[2] + for task in RemoteUnwinder(os.getpid()).get_async_stack_trace()[0].awaited_by + for coro in task.coroutine_stack + for frame in coro.call_stack + ] + + self.assertEqual(asyncio.run(rec(3)), ["rec"] * 4) + @skip_if_not_supported @unittest.skipIf( sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, diff --git a/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst new file mode 100644 index 000000000000000..da315f594080836 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-30-20-09-46.gh-issue-158522.L6j7tU.rst @@ -0,0 +1,2 @@ +Fix the sampling profiler showing a single frame for a task whose coroutine +recurses. Patch by Timofei Ivankov. diff --git a/Modules/_remote_debugging/_remote_debugging.h b/Modules/_remote_debugging/_remote_debugging.h index fa37fb7b2167ecf..6fc468a84f512f9 100644 --- a/Modules/_remote_debugging/_remote_debugging.h +++ b/Modules/_remote_debugging/_remote_debugging.h @@ -739,7 +739,7 @@ extern int parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_code_obj + uintptr_t running_task_frame ); /* Set iteration */ @@ -777,10 +777,10 @@ extern int find_running_task_in_thread( uintptr_t *running_task_addr ); -extern int get_task_code_object( +extern int get_task_frame( RemoteUnwinderObject *unwinder, uintptr_t task_addr, - uintptr_t *code_obj_addr + uintptr_t *frame_addr ); extern int append_awaited_by( diff --git a/Modules/_remote_debugging/asyncio.c b/Modules/_remote_debugging/asyncio.c index 44a9a3cbce0061a..71870ff6e457332 100644 --- a/Modules/_remote_debugging/asyncio.c +++ b/Modules/_remote_debugging/asyncio.c @@ -724,7 +724,7 @@ find_running_task_in_thread( } int -get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *code_obj_addr) { +get_task_frame(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintptr_t *frame_addr) { uintptr_t running_coro_addr = 0; if(read_py_ptr( @@ -741,21 +741,7 @@ get_task_code_object(RemoteUnwinderObject *unwinder, uintptr_t task_addr, uintpt return -1; } - // note: genobject's gi_iframe is an embedded struct so the address to - // the offset leads directly to its first field: f_executable - if (read_py_ptr( - unwinder, - running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe, code_obj_addr) < 0) { - set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read running task code object"); - return -1; - } - - if (*code_obj_addr == 0) { - PyErr_SetString(PyExc_RuntimeError, "Running task code object is NULL"); - set_exception_cause(unwinder, PyExc_RuntimeError, "Running task code object address is NULL"); - return -1; - } - + *frame_addr = running_coro_addr + (uintptr_t)unwinder->debug_offsets.gen_object.gi_iframe; return 0; } @@ -768,7 +754,7 @@ parse_async_frame_chain( RemoteUnwinderObject *unwinder, PyObject *calls, uintptr_t address_of_thread, - uintptr_t running_task_code_obj + uintptr_t running_task_frame ) { uintptr_t address_of_current_frame; if (find_running_frame(unwinder, address_of_thread, &address_of_current_frame) < 0) { @@ -779,6 +765,8 @@ parse_async_frame_chain( while ((void*)address_of_current_frame != NULL) { PyObject* frame_info = NULL; uintptr_t address_of_code_object; + + uintptr_t this_frame = address_of_current_frame; int res = parse_frame_object( unwinder, &frame_info, @@ -804,7 +792,8 @@ parse_async_frame_chain( Py_DECREF(frame_info); - if (address_of_code_object == running_task_code_obj) { + // gh-158522: compare frame instead of code-object + if (this_frame == running_task_frame) { break; } } @@ -940,8 +929,8 @@ process_running_task_chain( uintptr_t thread_state_addr, PyObject *result ) { - uintptr_t running_task_code_obj = 0; - if(get_task_code_object(unwinder, running_task_addr, &running_task_code_obj) < 0) { + uintptr_t running_task_frame = 0; + if(get_task_frame(unwinder, running_task_addr, &running_task_frame) < 0) { return -1; } @@ -973,7 +962,7 @@ process_running_task_chain( } // Add the chain from the current frame to this task - if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_code_obj) < 0) { + if (parse_async_frame_chain(unwinder, frame_chain, thread_state_addr, running_task_frame) < 0) { return -1; }