gh-135371: Fix asyncio introspection output to include internal coroutine chains (#135436)

This commit is contained in:
Pablo Galindo Salgado 2025-06-14 13:48:25 +01:00 committed by GitHub
parent 7b15873ed0
commit 028309fb47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1838 additions and 663 deletions

View file

@ -816,43 +816,58 @@ Executing the new tool on the running process will yield a table like this:
python -m asyncio ps 12345
tid task id task name coroutine chain awaiter name awaiter id
---------------------------------------------------------------------------------------------------------------------------------------
8138752 0x564bd3d0210 Task-1 0x0
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
tid task id task name coroutine stack awaiter chain awaiter name awaiter id
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
1935500 0x7fc930c18050 Task-1 TaskGroup._aexit -> TaskGroup.__aexit__ -> main 0x0
1935500 0x7fc930c18230 Sundowning TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
1935500 0x7fc93173fa50 TMBTE TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
1935500 0x7fc93173fdf0 TNDNBTG sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
1935500 0x7fc930d32510 Levitate sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
1935500 0x7fc930d32890 DYWTYLM sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
1935500 0x7fc93161ec30 Aqua Regia sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
or:
or a tree like this:
.. code-block:: bash
python -m asyncio pstree 12345
└── (T) Task-1
└── main
└── __aexit__
└── _aexit
└── main example.py:13
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
├── (T) Sundowning
│ └── album
│ └── __aexit__
│ └── _aexit
│ └── album example.py:8
│ └── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
│ └── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
│ ├── (T) TNDNBTG
│ │ └── play example.py:4
│ │ └── sleep Lib/asyncio/tasks.py:702
│ └── (T) Levitate
│ └── play example.py:4
│ └── sleep Lib/asyncio/tasks.py:702
└── (T) TMBTE
└── album
└── __aexit__
└── _aexit
└── album example.py:8
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
├── (T) DYWTYLM
│ └── play example.py:4
│ └── sleep Lib/asyncio/tasks.py:702
└── (T) Aqua Regia
└── play example.py:4
└── sleep Lib/asyncio/tasks.py:702
If a cycle is detected in the async await graph (which could indicate a
programming issue), the tool raises an error and lists the cycle paths that
prevent tree construction.
prevent tree construction:
.. code-block:: bash
python -m asyncio pstree 12345
ERROR: await-graph contains cycles - cannot print a tree!
cycle: Task-2 → Task-3 → Task-2
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
Gomez Macias in :gh:`91048`.)

View file

@ -1,11 +1,10 @@
"""Tools to analyze tasks running in asyncio programs."""
from collections import defaultdict
from collections import defaultdict, namedtuple
from itertools import count
from enum import Enum
import sys
from _remote_debugging import RemoteUnwinder
from _remote_debugging import RemoteUnwinder, FrameInfo
class NodeType(Enum):
COROUTINE = 1
@ -26,51 +25,75 @@ class CycleFoundException(Exception):
# ─── indexing helpers ───────────────────────────────────────────
def _format_stack_entry(elem: tuple[str, str, int] | str) -> str:
if isinstance(elem, tuple):
fqname, path, line_no = elem
return f"{fqname} {path}:{line_no}"
def _format_stack_entry(elem: str|FrameInfo) -> str:
if not isinstance(elem, str):
if elem.lineno == 0 and elem.filename == "":
return f"{elem.funcname}"
else:
return f"{elem.funcname} {elem.filename}:{elem.lineno}"
return elem
def _index(result):
id2name, awaits = {}, []
for _thr_id, tasks in result:
for tid, tname, awaited in tasks:
id2name[tid] = tname
for stack, parent_id in awaited:
stack = [_format_stack_entry(elem) for elem in stack]
awaits.append((parent_id, stack, tid))
return id2name, awaits
id2name, awaits, task_stacks = {}, [], {}
for awaited_info in result:
for task_info in awaited_info.awaited_by:
task_id = task_info.task_id
task_name = task_info.task_name
id2name[task_id] = task_name
# Store the internal coroutine stack for this task
if task_info.coroutine_stack:
for coro_info in task_info.coroutine_stack:
call_stack = coro_info.call_stack
internal_stack = [_format_stack_entry(frame) for frame in call_stack]
task_stacks[task_id] = internal_stack
# Add the awaited_by relationships (external dependencies)
if task_info.awaited_by:
for coro_info in task_info.awaited_by:
call_stack = coro_info.call_stack
parent_task_id = coro_info.task_name
stack = [_format_stack_entry(frame) for frame in call_stack]
awaits.append((parent_task_id, stack, task_id))
return id2name, awaits, task_stacks
def _build_tree(id2name, awaits):
def _build_tree(id2name, awaits, task_stacks):
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
children = defaultdict(list)
cor_names = defaultdict(dict) # (parent) -> {frame: node}
cor_id_seq = count(1)
cor_nodes = defaultdict(dict) # Maps parent -> {frame_name: node_key}
next_cor_id = count(1)
def _cor_node(parent_key, frame_name):
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
bucket = cor_names[parent_key]
if frame_name in bucket:
return bucket[frame_name]
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
id2label[node_key] = frame_name
children[parent_key].append(node_key)
bucket[frame_name] = node_key
def get_or_create_cor_node(parent, frame):
"""Get existing coroutine node or create new one under parent"""
if frame in cor_nodes[parent]:
return cor_nodes[parent][frame]
node_key = (NodeType.COROUTINE, f"c{next(next_cor_id)}")
id2label[node_key] = frame
children[parent].append(node_key)
cor_nodes[parent][frame] = node_key
return node_key
# lay down parent ➜ …frames… ➜ child paths
# Build task dependency tree with coroutine frames
for parent_id, stack, child_id in awaits:
cur = (NodeType.TASK, parent_id)
for frame in reversed(stack): # outer-most → inner-most
cur = _cor_node(cur, frame)
for frame in reversed(stack):
cur = get_or_create_cor_node(cur, frame)
child_key = (NodeType.TASK, child_id)
if child_key not in children[cur]:
children[cur].append(child_key)
# Add coroutine stacks for leaf tasks
awaiting_tasks = {parent_id for parent_id, _, _ in awaits}
for task_id in id2name:
if task_id not in awaiting_tasks and task_id in task_stacks:
cur = (NodeType.TASK, task_id)
for frame in reversed(task_stacks[task_id]):
cur = get_or_create_cor_node(cur, frame)
return id2label, children
@ -129,12 +152,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
with `task_emoji` and coroutine frames with `cor_emoji`.
"""
id2name, awaits = _index(result)
id2name, awaits, task_stacks = _index(result)
g = _task_graph(awaits)
cycles = _find_cycles(g)
if cycles:
raise CycleFoundException(cycles, id2name)
labels, children = _build_tree(id2name, awaits)
labels, children = _build_tree(id2name, awaits, task_stacks)
def pretty(node):
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
@ -154,35 +177,40 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
def build_task_table(result):
id2name, awaits = _index(result)
id2name, _, _ = _index(result)
table = []
for tid, tasks in result:
for task_id, task_name, awaited in tasks:
if not awaited:
table.append(
[
tid,
hex(task_id),
task_name,
"",
"",
"0x0"
]
)
for stack, awaiter_id in awaited:
stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack]
coroutine_chain = " -> ".join(stack)
awaiter_name = id2name.get(awaiter_id, "Unknown")
table.append(
[
tid,
hex(task_id),
task_name,
coroutine_chain,
awaiter_name,
hex(awaiter_id),
]
)
for awaited_info in result:
thread_id = awaited_info.thread_id
for task_info in awaited_info.awaited_by:
# Get task info
task_id = task_info.task_id
task_name = task_info.task_name
# Build coroutine stack string
frames = [frame for coro in task_info.coroutine_stack
for frame in coro.call_stack]
coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0]
for x in frames)
# Handle tasks with no awaiters
if not task_info.awaited_by:
table.append([thread_id, hex(task_id), task_name, coro_stack,
"", "", "0x0"])
continue
# Handle tasks with awaiters
for coro_info in task_info.awaited_by:
parent_id = coro_info.task_name
awaiter_frames = [_format_stack_entry(x).split(" ")[0]
for x in coro_info.call_stack]
awaiter_chain = " -> ".join(awaiter_frames)
awaiter_name = id2name.get(parent_id, "Unknown")
parent_id_str = (hex(parent_id) if isinstance(parent_id, int)
else str(parent_id))
table.append([thread_id, hex(task_id), task_name, coro_stack,
awaiter_chain, awaiter_name, parent_id_str])
return table
@ -211,11 +239,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
table = build_task_table(tasks)
# Print the table in a simple tabular format
print(
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine stack':<50} {'awaiter chain':<50} {'awaiter name':<15} {'awaiter id':<15}"
)
print("-" * 135)
print("-" * 180)
for row in table:
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
def display_awaited_by_tasks_tree(pid: int) -> None:

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ import importlib
import sys
import socket
import threading
from asyncio import staggered, taskgroups
from asyncio import staggered, taskgroups, base_events, tasks
from unittest.mock import ANY
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
from test.support.script_helper import make_script
@ -18,8 +18,12 @@ PROCESS_VM_READV_SUPPORTED = False
try:
from _remote_debugging import PROCESS_VM_READV_SUPPORTED
from _remote_debugging import RemoteUnwinder
from _remote_debugging import FrameInfo, CoroInfo, TaskInfo
except ImportError:
raise unittest.SkipTest("Test only runs when _remote_debugging is available")
raise unittest.SkipTest(
"Test only runs when _remote_debugging is available"
)
def _make_test_script(script_dir, script_basename, source):
to_return = make_script(script_dir, script_basename, source)
@ -28,7 +32,11 @@ def _make_test_script(script_dir, script_basename, source):
skip_if_not_supported = unittest.skipIf(
(sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32"),
(
sys.platform != "darwin"
and sys.platform != "linux"
and sys.platform != "win32"
),
"Test only runs on Linux, Windows and MacOS",
)
@ -101,11 +109,16 @@ class TestGetStackTrace(unittest.TestCase):
client_socket, _ = server_socket.accept()
server_socket.close()
response = b""
while b"ready:main" not in response or b"ready:thread" not in response:
while (
b"ready:main" not in response
or b"ready:thread" not in response
):
response += client_socket.recv(1024)
stack_trace = get_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -114,17 +127,17 @@ class TestGetStackTrace(unittest.TestCase):
p.wait(timeout=SHORT_TIMEOUT)
thread_expected_stack_trace = [
(script_name, 15, "foo"),
(script_name, 12, "baz"),
(script_name, 9, "bar"),
(threading.__file__, ANY, 'Thread.run')
FrameInfo([script_name, 15, "foo"]),
FrameInfo([script_name, 12, "baz"]),
FrameInfo([script_name, 9, "bar"]),
FrameInfo([threading.__file__, ANY, "Thread.run"]),
]
# Is possible that there are more threads, so we check that the
# expected stack traces are in the result (looking at you Windows!)
self.assertIn((ANY, thread_expected_stack_trace), stack_trace)
# Check that the main thread stack trace is in the result
frame = (script_name, 19, "<module>")
frame = FrameInfo([script_name, 19, "<module>"])
for _, stack in stack_trace:
if frame in stack:
break
@ -189,8 +202,12 @@ class TestGetStackTrace(unittest.TestCase):
):
script_dir = os.path.join(work_dir, "script_pkg")
os.mkdir(script_dir)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket = socket.socket(
socket.AF_INET, socket.SOCK_STREAM
)
server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
)
server_socket.bind(("localhost", port))
server_socket.settimeout(SHORT_TIMEOUT)
server_socket.listen(1)
@ -208,7 +225,9 @@ class TestGetStackTrace(unittest.TestCase):
self.assertEqual(response, b"ready")
stack_trace = get_async_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -219,79 +238,49 @@ class TestGetStackTrace(unittest.TestCase):
# sets are unordered, so we want to sort "awaited_by"s
stack_trace[2].sort(key=lambda x: x[1])
root_task = "Task-1"
expected_stack_trace = [
[
(script_name, 10, "c5"),
(script_name, 14, "c4"),
(script_name, 17, "c3"),
(script_name, 20, "c2"),
FrameInfo([script_name, 10, "c5"]),
FrameInfo([script_name, 14, "c4"]),
FrameInfo([script_name, 17, "c3"]),
FrameInfo([script_name, 20, "c2"]),
],
"c2_root",
[
[
[
(
taskgroups.__file__,
ANY,
"TaskGroup._aexit"
),
(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__"
),
(script_name, 26, "main"),
],
"Task-1",
[],
],
[
[(script_name, 23, "c1")],
"sub_main_1",
CoroInfo(
[
[
[
(
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit"
),
(
"TaskGroup._aexit",
]
),
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__"
),
(script_name, 26, "main"),
],
"Task-1",
[],
]
],
],
[
[(script_name, 23, "c1")],
"sub_main_2",
"TaskGroup.__aexit__",
]
),
FrameInfo([script_name, 26, "main"]),
],
"Task-1",
]
),
CoroInfo(
[
[
[
(
taskgroups.__file__,
ANY,
"TaskGroup._aexit"
),
(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__"
),
(script_name, 26, "main"),
],
"Task-1",
[],
]
],
],
[FrameInfo([script_name, 23, "c1"])],
"sub_main_1",
]
),
CoroInfo(
[
[FrameInfo([script_name, 23, "c1"])],
"sub_main_2",
]
),
],
]
self.assertEqual(stack_trace, expected_stack_trace)
@ -350,7 +339,9 @@ class TestGetStackTrace(unittest.TestCase):
self.assertEqual(response, b"ready")
stack_trace = get_async_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -363,9 +354,9 @@ class TestGetStackTrace(unittest.TestCase):
expected_stack_trace = [
[
(script_name, 10, "gen_nested_call"),
(script_name, 16, "gen"),
(script_name, 19, "main"),
FrameInfo([script_name, 10, "gen_nested_call"]),
FrameInfo([script_name, 16, "gen"]),
FrameInfo([script_name, 19, "main"]),
],
"Task-1",
[],
@ -427,7 +418,9 @@ class TestGetStackTrace(unittest.TestCase):
self.assertEqual(response, b"ready")
stack_trace = get_async_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -439,9 +432,12 @@ class TestGetStackTrace(unittest.TestCase):
stack_trace[2].sort(key=lambda x: x[1])
expected_stack_trace = [
[(script_name, 11, "deep"), (script_name, 15, "c1")],
[
FrameInfo([script_name, 11, "deep"]),
FrameInfo([script_name, 15, "c1"]),
],
"Task-2",
[[[(script_name, 21, "main")], "Task-1", []]],
[CoroInfo([[FrameInfo([script_name, 21, "main"])], "Task-1"])],
]
self.assertEqual(stack_trace, expected_stack_trace)
@ -503,7 +499,9 @@ class TestGetStackTrace(unittest.TestCase):
self.assertEqual(response, b"ready")
stack_trace = get_async_stack_trace(p.pid)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -515,20 +513,29 @@ class TestGetStackTrace(unittest.TestCase):
stack_trace[2].sort(key=lambda x: x[1])
expected_stack_trace = [
[
(script_name, 11, "deep"),
(script_name, 15, "c1"),
(staggered.__file__, ANY, "staggered_race.<locals>.run_one_coro"),
FrameInfo([script_name, 11, "deep"]),
FrameInfo([script_name, 15, "c1"]),
FrameInfo(
[
staggered.__file__,
ANY,
"staggered_race.<locals>.run_one_coro",
]
),
],
"Task-2",
[
[
CoroInfo(
[
(staggered.__file__, ANY, "staggered_race"),
(script_name, 21, "main"),
],
"Task-1",
[],
]
[
FrameInfo(
[staggered.__file__, ANY, "staggered_race"]
),
FrameInfo([script_name, 21, "main"]),
],
"Task-1",
]
)
],
]
self.assertEqual(stack_trace, expected_stack_trace)
@ -659,62 +666,174 @@ class TestGetStackTrace(unittest.TestCase):
# expected: at least 1000 pending tasks
self.assertGreaterEqual(len(entries), 1000)
# the first three tasks stem from the code structure
self.assertIn((ANY, "Task-1", []), entries)
main_stack = [
(
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
FrameInfo([taskgroups.__file__, ANY, "TaskGroup._aexit"]),
FrameInfo(
[taskgroups.__file__, ANY, "TaskGroup.__aexit__"]
),
(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
),
(script_name, 60, "main"),
FrameInfo([script_name, 60, "main"]),
]
self.assertIn(
(ANY, "server task", [[main_stack, ANY]]),
TaskInfo(
[ANY, "Task-1", [CoroInfo([main_stack, ANY])], []]
),
entries,
)
self.assertIn(
(ANY, "echo client spam", [[main_stack, ANY]]),
TaskInfo(
[
ANY,
"server task",
[
CoroInfo(
[
[
FrameInfo(
[
base_events.__file__,
ANY,
"Server.serve_forever",
]
)
],
ANY,
]
)
],
[
CoroInfo(
[
[
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
FrameInfo(
[script_name, ANY, "main"]
),
],
ANY,
]
)
],
]
),
entries,
)
self.assertIn(
TaskInfo(
[
ANY,
"Task-4",
[
CoroInfo(
[
[
FrameInfo(
[tasks.__file__, ANY, "sleep"]
),
FrameInfo(
[
script_name,
38,
"echo_client",
]
),
],
ANY,
]
)
],
[
CoroInfo(
[
[
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
FrameInfo(
[
script_name,
41,
"echo_client_spam",
]
),
],
ANY,
]
)
],
]
),
entries,
)
expected_stack = [
[
expected_awaited_by = [
CoroInfo(
[
(
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
),
(
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
),
(script_name, 41, "echo_client_spam"),
],
ANY,
]
[
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup._aexit",
]
),
FrameInfo(
[
taskgroups.__file__,
ANY,
"TaskGroup.__aexit__",
]
),
FrameInfo(
[script_name, 41, "echo_client_spam"]
),
],
ANY,
]
)
]
tasks_with_stack = [
task for task in entries if task[2] == expected_stack
tasks_with_awaited = [
task
for task in entries
if task.awaited_by == expected_awaited_by
]
self.assertGreaterEqual(len(tasks_with_stack), 1000)
self.assertGreaterEqual(len(tasks_with_awaited), 1000)
# the final task will have some random number, but it should for
# sure be one of the echo client spam horde (In windows this is not true
# for some reason)
if sys.platform != "win32":
self.assertEqual(
expected_stack,
entries[-1][2],
tasks_with_awaited[-1].awaited_by,
entries[-1].awaited_by,
)
except PermissionError:
self.skipTest("Insufficient permissions to read the stack trace")
self.skipTest(
"Insufficient permissions to read the stack trace"
)
finally:
if client_socket is not None:
client_socket.close()
@ -740,17 +859,21 @@ class TestGetStackTrace(unittest.TestCase):
self.assertEqual(
stack[:2],
[
(
__file__,
get_stack_trace.__code__.co_firstlineno + 2,
"get_stack_trace",
FrameInfo(
[
__file__,
get_stack_trace.__code__.co_firstlineno + 2,
"get_stack_trace",
]
),
(
__file__,
self.test_self_trace.__code__.co_firstlineno + 6,
"TestGetStackTrace.test_self_trace",
FrameInfo(
[
__file__,
self.test_self_trace.__code__.co_firstlineno + 6,
"TestGetStackTrace.test_self_trace",
]
),
]
],
)

View file

@ -0,0 +1,4 @@
Fixed :mod:`asyncio` debugging tools to properly display internal coroutine
call stacks alongside external task dependencies. The ``python -m asyncio
ps`` and ``python -m asyncio pstree`` commands now show complete execution
context. Patch by Pablo Galindo.

View file

@ -97,24 +97,82 @@ struct _Py_AsyncioModuleDebugOffsets {
} asyncio_thread_state;
};
typedef struct {
PyObject_HEAD
proc_handle_t handle;
uintptr_t runtime_start_address;
struct _Py_DebugOffsets debug_offsets;
int async_debug_offsets_available;
struct _Py_AsyncioModuleDebugOffsets async_debug_offsets;
uintptr_t interpreter_addr;
uintptr_t tstate_addr;
uint64_t code_object_generation;
_Py_hashtable_t *code_object_cache;
int debug;
#ifdef Py_GIL_DISABLED
// TLBC cache invalidation tracking
uint32_t tlbc_generation; // Track TLBC index pool changes
_Py_hashtable_t *tlbc_cache; // Cache of TLBC arrays by code object address
#endif
} RemoteUnwinderObject;
/* ============================================================================
* STRUCTSEQ TYPE DEFINITIONS
* ============================================================================ */
// TaskInfo structseq type - replaces 4-tuple (task_id, task_name, coroutine_stack, awaited_by)
static PyStructSequence_Field TaskInfo_fields[] = {
{"task_id", "Task ID (memory address)"},
{"task_name", "Task name"},
{"coroutine_stack", "Coroutine call stack"},
{"awaited_by", "Tasks awaiting this task"},
{NULL}
};
static PyStructSequence_Desc TaskInfo_desc = {
"_remote_debugging.TaskInfo",
"Information about an asyncio task",
TaskInfo_fields,
4
};
// FrameInfo structseq type - replaces 3-tuple (filename, lineno, funcname)
static PyStructSequence_Field FrameInfo_fields[] = {
{"filename", "Source code filename"},
{"lineno", "Line number"},
{"funcname", "Function name"},
{NULL}
};
static PyStructSequence_Desc FrameInfo_desc = {
"_remote_debugging.FrameInfo",
"Information about a frame",
FrameInfo_fields,
3
};
// CoroInfo structseq type - replaces 2-tuple (call_stack, task_name)
static PyStructSequence_Field CoroInfo_fields[] = {
{"call_stack", "Coroutine call stack"},
{"task_name", "Task name"},
{NULL}
};
static PyStructSequence_Desc CoroInfo_desc = {
"_remote_debugging.CoroInfo",
"Information about a coroutine",
CoroInfo_fields,
2
};
// ThreadInfo structseq type - replaces 2-tuple (thread_id, frame_info)
static PyStructSequence_Field ThreadInfo_fields[] = {
{"thread_id", "Thread ID"},
{"frame_info", "Frame information"},
{NULL}
};
static PyStructSequence_Desc ThreadInfo_desc = {
"_remote_debugging.ThreadInfo",
"Information about a thread",
ThreadInfo_fields,
2
};
// AwaitedInfo structseq type - replaces 2-tuple (tid, awaited_by_list)
static PyStructSequence_Field AwaitedInfo_fields[] = {
{"thread_id", "Thread ID"},
{"awaited_by", "List of tasks awaited by this thread"},
{NULL}
};
static PyStructSequence_Desc AwaitedInfo_desc = {
"_remote_debugging.AwaitedInfo",
"Information about what a thread is awaiting",
AwaitedInfo_fields,
2
};
typedef struct {
PyObject *func_name;
@ -127,8 +185,33 @@ typedef struct {
typedef struct {
/* Types */
PyTypeObject *RemoteDebugging_Type;
PyTypeObject *TaskInfo_Type;
PyTypeObject *FrameInfo_Type;
PyTypeObject *CoroInfo_Type;
PyTypeObject *ThreadInfo_Type;
PyTypeObject *AwaitedInfo_Type;
} RemoteDebuggingState;
typedef struct {
PyObject_HEAD
proc_handle_t handle;
uintptr_t runtime_start_address;
struct _Py_DebugOffsets debug_offsets;
int async_debug_offsets_available;
struct _Py_AsyncioModuleDebugOffsets async_debug_offsets;
uintptr_t interpreter_addr;
uintptr_t tstate_addr;
uint64_t code_object_generation;
_Py_hashtable_t *code_object_cache;
int debug;
RemoteDebuggingState *cached_state; // Cached module state
#ifdef Py_GIL_DISABLED
// TLBC cache invalidation tracking
uint32_t tlbc_generation; // Track TLBC index pool changes
_Py_hashtable_t *tlbc_cache; // Cache of TLBC arrays by code object address
#endif
} RemoteUnwinderObject;
typedef struct
{
int lineno;
@ -218,6 +301,24 @@ RemoteDebugging_GetState(PyObject *module)
return (RemoteDebuggingState *)state;
}
static inline RemoteDebuggingState *
RemoteDebugging_GetStateFromType(PyTypeObject *type)
{
PyObject *module = PyType_GetModule(type);
assert(module != NULL);
return RemoteDebugging_GetState(module);
}
static inline RemoteDebuggingState *
RemoteDebugging_GetStateFromObject(PyObject *obj)
{
RemoteUnwinderObject *unwinder = (RemoteUnwinderObject *)obj;
if (unwinder->cached_state == NULL) {
unwinder->cached_state = RemoteDebugging_GetStateFromType(Py_TYPE(obj));
}
return unwinder->cached_state;
}
static inline int
RemoteDebugging_InitState(RemoteDebuggingState *st)
{
@ -854,24 +955,14 @@ create_task_result(
char task_obj[SIZEOF_TASK_OBJ];
uintptr_t coro_addr;
result = PyList_New(0);
if (result == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task result list");
goto error;
}
// Create call_stack first since it's the first tuple element
call_stack = PyList_New(0);
if (call_stack == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create call stack list");
goto error;
}
if (PyList_Append(result, call_stack)) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append call stack to task result");
goto error;
}
Py_CLEAR(call_stack);
// Create task name/address for second tuple element
if (recurse_task) {
tn = parse_task_name(unwinder, task_address);
} else {
@ -882,12 +973,6 @@ create_task_result(
goto error;
}
if (PyList_Append(result, tn)) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task name to result");
goto error;
}
Py_CLEAR(tn);
// Parse coroutine chain
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, task_address,
unwinder->async_debug_offsets.asyncio_task_object.size,
@ -900,31 +985,29 @@ create_task_result(
coro_addr &= ~Py_TAG_BITS;
if ((void*)coro_addr != NULL) {
call_stack = PyList_New(0);
if (call_stack == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create coro call stack list");
goto error;
}
if (parse_coro_chain(unwinder, coro_addr, call_stack) < 0) {
Py_DECREF(call_stack);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain");
goto error;
}
if (PyList_Reverse(call_stack)) {
Py_DECREF(call_stack);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to reverse call stack");
goto error;
}
if (PyList_SetItem(result, 0, call_stack) < 0) {
Py_DECREF(call_stack);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to set call stack in result");
goto error;
}
}
// Create final CoroInfo result
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
result = PyStructSequence_New(state->CoroInfo_Type);
if (result == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create CoroInfo");
goto error;
}
// PyStructSequence_SetItem steals references, so we don't need to DECREF on success
PyStructSequence_SetItem(result, 0, call_stack); // This steals the reference
PyStructSequence_SetItem(result, 1, tn); // This steals the reference
return result;
error:
@ -943,7 +1026,6 @@ parse_task(
) {
char is_task;
PyObject* result = NULL;
PyObject* awaited_by = NULL;
int err;
err = read_char(
@ -962,48 +1044,37 @@ parse_task(
goto error;
}
} else {
result = PyList_New(0);
// Create an empty CoroInfo for non-task objects
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
result = PyStructSequence_New(state->CoroInfo_Type);
if (result == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty task result");
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty CoroInfo");
goto error;
}
PyObject *empty_list = PyList_New(0);
if (empty_list == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty list");
goto error;
}
PyObject *task_name = PyLong_FromUnsignedLongLong(task_address);
if (task_name == NULL) {
Py_DECREF(empty_list);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task name");
goto error;
}
PyStructSequence_SetItem(result, 0, empty_list); // This steals the reference
PyStructSequence_SetItem(result, 1, task_name); // This steals the reference
}
if (PyList_Append(render_to, result)) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task result to render list");
goto error;
}
if (recurse_task) {
awaited_by = PyList_New(0);
if (awaited_by == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by list");
goto error;
}
if (PyList_Append(result, awaited_by)) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by to result");
goto error;
}
Py_DECREF(awaited_by);
/* awaited_by is borrowed from 'result' to simplify cleanup */
if (parse_task_awaited_by(unwinder, task_address, awaited_by, 1) < 0) {
// Clear the pointer so the cleanup doesn't try to decref it since
// it's borrowed from 'result' and will be decrefed when result is
// deleted.
awaited_by = NULL;
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task awaited_by relationships");
goto error;
}
}
Py_DECREF(result);
return 0;
error:
Py_XDECREF(result);
Py_XDECREF(awaited_by);
return -1;
}
@ -1161,6 +1232,7 @@ process_single_task_node(
PyObject *current_awaited_by = NULL;
PyObject *task_id = NULL;
PyObject *result_item = NULL;
PyObject *coroutine_stack = NULL;
tn = parse_task_name(unwinder, task_addr);
if (tn == NULL) {
@ -1174,25 +1246,40 @@ process_single_task_node(
goto error;
}
// Extract the coroutine stack for this task
coroutine_stack = PyList_New(0);
if (coroutine_stack == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create coroutine stack list in single task node");
goto error;
}
if (parse_task(unwinder, task_addr, coroutine_stack, 0) < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task coroutine stack in single task node");
goto error;
}
task_id = PyLong_FromUnsignedLongLong(task_addr);
if (task_id == NULL) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task ID in single task node");
goto error;
}
result_item = PyTuple_New(3);
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
result_item = PyStructSequence_New(state->TaskInfo_Type);
if (result_item == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create result tuple in single task node");
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create TaskInfo in single task node");
goto error;
}
PyTuple_SET_ITEM(result_item, 0, task_id); // steals ref
PyTuple_SET_ITEM(result_item, 1, tn); // steals ref
PyTuple_SET_ITEM(result_item, 2, current_awaited_by); // steals ref
PyStructSequence_SetItem(result_item, 0, task_id); // steals ref
PyStructSequence_SetItem(result_item, 1, tn); // steals ref
PyStructSequence_SetItem(result_item, 2, coroutine_stack); // steals ref
PyStructSequence_SetItem(result_item, 3, current_awaited_by); // steals ref
// References transferred to tuple
task_id = NULL;
tn = NULL;
coroutine_stack = NULL;
current_awaited_by = NULL;
if (PyList_Append(result, result_item)) {
@ -1203,9 +1290,11 @@ process_single_task_node(
Py_DECREF(result_item);
// Get back current_awaited_by reference for parse_task_awaited_by
current_awaited_by = PyTuple_GET_ITEM(result_item, 2);
current_awaited_by = PyStructSequence_GetItem(result_item, 3);
if (parse_task_awaited_by(unwinder, task_addr, current_awaited_by, 0) < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse awaited_by in single task node");
// No cleanup needed here since all references were transferred to result_item
// and result_item was already added to result list and decreffed
return -1;
}
@ -1216,6 +1305,7 @@ error:
Py_XDECREF(current_awaited_by);
Py_XDECREF(task_id);
Py_XDECREF(result_item);
Py_XDECREF(coroutine_stack);
return -1;
}
@ -1554,17 +1644,18 @@ done_tlbc:
goto error;
}
tuple = PyTuple_New(3);
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
tuple = PyStructSequence_New(state->FrameInfo_Type);
if (!tuple) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create result tuple for code object");
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create FrameInfo for code object");
goto error;
}
Py_INCREF(meta->func_name);
Py_INCREF(meta->file_name);
PyTuple_SET_ITEM(tuple, 0, meta->file_name);
PyTuple_SET_ITEM(tuple, 1, lineno);
PyTuple_SET_ITEM(tuple, 2, meta->func_name);
PyStructSequence_SetItem(tuple, 0, meta->file_name);
PyStructSequence_SetItem(tuple, 1, lineno);
PyStructSequence_SetItem(tuple, 2, meta->func_name);
*result = tuple;
return 0;
@ -2212,23 +2303,24 @@ append_awaited_by(
return -1;
}
PyObject *result_item = PyTuple_New(2);
if (result_item == NULL) {
Py_DECREF(tid_py);
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by result tuple");
return -1;
}
PyObject* awaited_by_for_thread = PyList_New(0);
if (awaited_by_for_thread == NULL) {
Py_DECREF(tid_py);
Py_DECREF(result_item);
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by thread list");
return -1;
}
PyTuple_SET_ITEM(result_item, 0, tid_py); // steals ref
PyTuple_SET_ITEM(result_item, 1, awaited_by_for_thread); // steals ref
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
PyObject *result_item = PyStructSequence_New(state->AwaitedInfo_Type);
if (result_item == NULL) {
Py_DECREF(tid_py);
Py_DECREF(awaited_by_for_thread);
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create AwaitedInfo");
return -1;
}
PyStructSequence_SetItem(result_item, 0, tid_py); // steals ref
PyStructSequence_SetItem(result_item, 1, awaited_by_for_thread); // steals ref
if (PyList_Append(result, result_item)) {
Py_DECREF(result_item);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by result item");
@ -2352,14 +2444,15 @@ unwind_stack_for_thread(
goto error;
}
result = PyTuple_New(2);
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
result = PyStructSequence_New(state->ThreadInfo_Type);
if (result == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create thread unwind result tuple");
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create ThreadInfo");
goto error;
}
PyTuple_SET_ITEM(result, 0, thread_id); // Steals reference
PyTuple_SET_ITEM(result, 1, frame_info); // Steals reference
PyStructSequence_SetItem(result, 0, thread_id); // Steals reference
PyStructSequence_SetItem(result, 1, frame_info); // Steals reference
cleanup_stack_chunks(&chunks);
return result;
@ -2414,6 +2507,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
/*[clinic end generated code: output=3982f2a7eba49334 input=48a762566b828e91]*/
{
self->debug = debug;
self->cached_state = NULL;
if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) {
set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle");
return -1;
@ -2860,6 +2954,47 @@ _remote_debugging_exec(PyObject *m)
if (PyModule_AddType(m, st->RemoteDebugging_Type) < 0) {
return -1;
}
// Initialize structseq types
st->TaskInfo_Type = PyStructSequence_NewType(&TaskInfo_desc);
if (st->TaskInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->TaskInfo_Type) < 0) {
return -1;
}
st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc);
if (st->FrameInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->FrameInfo_Type) < 0) {
return -1;
}
st->CoroInfo_Type = PyStructSequence_NewType(&CoroInfo_desc);
if (st->CoroInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->CoroInfo_Type) < 0) {
return -1;
}
st->ThreadInfo_Type = PyStructSequence_NewType(&ThreadInfo_desc);
if (st->ThreadInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->ThreadInfo_Type) < 0) {
return -1;
}
st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc);
if (st->AwaitedInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->AwaitedInfo_Type) < 0) {
return -1;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
@ -2878,6 +3013,11 @@ remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg)
{
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_VISIT(state->RemoteDebugging_Type);
Py_VISIT(state->TaskInfo_Type);
Py_VISIT(state->FrameInfo_Type);
Py_VISIT(state->CoroInfo_Type);
Py_VISIT(state->ThreadInfo_Type);
Py_VISIT(state->AwaitedInfo_Type);
return 0;
}
@ -2886,6 +3026,11 @@ remote_debugging_clear(PyObject *mod)
{
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_CLEAR(state->RemoteDebugging_Type);
Py_CLEAR(state->TaskInfo_Type);
Py_CLEAR(state->FrameInfo_Type);
Py_CLEAR(state->CoroInfo_Type);
Py_CLEAR(state->ThreadInfo_Type);
Py_CLEAR(state->AwaitedInfo_Type);
return 0;
}