Skip to content

Commit 028309f

Browse files
authored
gh-135371: Fix asyncio introspection output to include internal coroutine chains (#135436)
1 parent 7b15873 commit 028309f

File tree

6 files changed

+1847
-672
lines changed

6 files changed

+1847
-672
lines changed

Doc/whatsnew/3.14.rst

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -816,43 +816,58 @@ Executing the new tool on the running process will yield a table like this:
816816
817817
python -m asyncio ps 12345
818818
819-
tid task id task name coroutine chain awaiter name awaiter id
820-
---------------------------------------------------------------------------------------------------------------------------------------
821-
8138752 0x564bd3d0210 Task-1 0x0
822-
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
823-
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
824-
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
825-
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
826-
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
827-
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
828-
829-
830-
or:
819+
tid task id task name coroutine stack awaiter chain awaiter name awaiter id
820+
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
821+
1935500 0x7fc930c18050 Task-1 TaskGroup._aexit -> TaskGroup.__aexit__ -> main 0x0
822+
1935500 0x7fc930c18230 Sundowning TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
823+
1935500 0x7fc93173fa50 TMBTE TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
824+
1935500 0x7fc93173fdf0 TNDNBTG sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
825+
1935500 0x7fc930d32510 Levitate sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
826+
1935500 0x7fc930d32890 DYWTYLM sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
827+
1935500 0x7fc93161ec30 Aqua Regia sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
828+
829+
or a tree like this:
831830

832831
.. code-block:: bash
833832
834833
python -m asyncio pstree 12345
835834
836835
└── (T) Task-1
837-
└── main
838-
└── __aexit__
839-
└── _aexit
836+
└── main example.py:13
837+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
838+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
840839
├── (T) Sundowning
841-
│ └── album
842-
│ └── __aexit__
843-
│ └── _aexit
840+
│ └── album example.py:8
841+
│ └── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
842+
│ └── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
844843
│ ├── (T) TNDNBTG
844+
│ │ └── play example.py:4
845+
│ │ └── sleep Lib/asyncio/tasks.py:702
845846
│ └── (T) Levitate
847+
│ └── play example.py:4
848+
│ └── sleep Lib/asyncio/tasks.py:702
846849
└── (T) TMBTE
847-
└── album
848-
└── __aexit__
849-
└── _aexit
850+
└── album example.py:8
851+
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
852+
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
850853
├── (T) DYWTYLM
854+
│ └── play example.py:4
855+
│ └── sleep Lib/asyncio/tasks.py:702
851856
└── (T) Aqua Regia
857+
└── play example.py:4
858+
└── sleep Lib/asyncio/tasks.py:702
852859
853860
If a cycle is detected in the async await graph (which could indicate a
854861
programming issue), the tool raises an error and lists the cycle paths that
855-
prevent tree construction.
862+
prevent tree construction:
863+
864+
.. code-block:: bash
865+
866+
python -m asyncio pstree 12345
867+
868+
ERROR: await-graph contains cycles - cannot print a tree!
869+
870+
cycle: Task-2 → Task-3 → Task-2
856871
857872
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
858873
Gomez Macias in :gh:`91048`.)

Lib/asyncio/tools.py

Lines changed: 95 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
"""Tools to analyze tasks running in asyncio programs."""
22

3-
from collections import defaultdict
3+
from collections import defaultdict, namedtuple
44
from itertools import count
55
from enum import Enum
66
import sys
7-
from _remote_debugging import RemoteUnwinder
8-
7+
from _remote_debugging import RemoteUnwinder, FrameInfo
98

109
class NodeType(Enum):
1110
COROUTINE = 1
@@ -26,51 +25,75 @@ def __init__(
2625

2726

2827
# ─── indexing helpers ───────────────────────────────────────────
29-
def _format_stack_entry(elem: tuple[str, str, int] | str) -> str:
30-
if isinstance(elem, tuple):
31-
fqname, path, line_no = elem
32-
return f"{fqname} {path}:{line_no}"
33-
28+
def _format_stack_entry(elem: str|FrameInfo) -> str:
29+
if not isinstance(elem, str):
30+
if elem.lineno == 0 and elem.filename == "":
31+
return f"{elem.funcname}"
32+
else:
33+
return f"{elem.funcname} {elem.filename}:{elem.lineno}"
3434
return elem
3535

3636

3737
def _index(result):
38-
id2name, awaits = {}, []
39-
for _thr_id, tasks in result:
40-
for tid, tname, awaited in tasks:
41-
id2name[tid] = tname
42-
for stack, parent_id in awaited:
43-
stack = [_format_stack_entry(elem) for elem in stack]
44-
awaits.append((parent_id, stack, tid))
45-
return id2name, awaits
46-
47-
48-
def _build_tree(id2name, awaits):
38+
id2name, awaits, task_stacks = {}, [], {}
39+
for awaited_info in result:
40+
for task_info in awaited_info.awaited_by:
41+
task_id = task_info.task_id
42+
task_name = task_info.task_name
43+
id2name[task_id] = task_name
44+
45+
# Store the internal coroutine stack for this task
46+
if task_info.coroutine_stack:
47+
for coro_info in task_info.coroutine_stack:
48+
call_stack = coro_info.call_stack
49+
internal_stack = [_format_stack_entry(frame) for frame in call_stack]
50+
task_stacks[task_id] = internal_stack
51+
52+
# Add the awaited_by relationships (external dependencies)
53+
if task_info.awaited_by:
54+
for coro_info in task_info.awaited_by:
55+
call_stack = coro_info.call_stack
56+
parent_task_id = coro_info.task_name
57+
stack = [_format_stack_entry(frame) for frame in call_stack]
58+
awaits.append((parent_task_id, stack, task_id))
59+
return id2name, awaits, task_stacks
60+
61+
62+
def _build_tree(id2name, awaits, task_stacks):
4963
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
5064
children = defaultdict(list)
51-
cor_names = defaultdict(dict) # (parent) -> {frame: node}
52-
cor_id_seq = count(1)
53-
54-
def _cor_node(parent_key, frame_name):
55-
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
56-
bucket = cor_names[parent_key]
57-
if frame_name in bucket:
58-
return bucket[frame_name]
59-
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
60-
id2label[node_key] = frame_name
61-
children[parent_key].append(node_key)
62-
bucket[frame_name] = node_key
65+
cor_nodes = defaultdict(dict) # Maps parent -> {frame_name: node_key}
66+
next_cor_id = count(1)
67+
68+
def get_or_create_cor_node(parent, frame):
69+
"""Get existing coroutine node or create new one under parent"""
70+
if frame in cor_nodes[parent]:
71+
return cor_nodes[parent][frame]
72+
73+
node_key = (NodeType.COROUTINE, f"c{next(next_cor_id)}")
74+
id2label[node_key] = frame
75+
children[parent].append(node_key)
76+
cor_nodes[parent][frame] = node_key
6377
return node_key
6478

65-
# lay down parent ➜ …frames… ➜ child paths
79+
# Build task dependency tree with coroutine frames
6680
for parent_id, stack, child_id in awaits:
6781
cur = (NodeType.TASK, parent_id)
68-
for frame in reversed(stack): # outer-most → inner-most
69-
cur = _cor_node(cur, frame)
82+
for frame in reversed(stack):
83+
cur = get_or_create_cor_node(cur, frame)
84+
7085
child_key = (NodeType.TASK, child_id)
7186
if child_key not in children[cur]:
7287
children[cur].append(child_key)
7388

89+
# Add coroutine stacks for leaf tasks
90+
awaiting_tasks = {parent_id for parent_id, _, _ in awaits}
91+
for task_id in id2name:
92+
if task_id not in awaiting_tasks and task_id in task_stacks:
93+
cur = (NodeType.TASK, task_id)
94+
for frame in reversed(task_stacks[task_id]):
95+
cur = get_or_create_cor_node(cur, frame)
96+
7497
return id2label, children
7598

7699

@@ -129,12 +152,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
129152
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
130153
with `task_emoji` and coroutine frames with `cor_emoji`.
131154
"""
132-
id2name, awaits = _index(result)
155+
id2name, awaits, task_stacks = _index(result)
133156
g = _task_graph(awaits)
134157
cycles = _find_cycles(g)
135158
if cycles:
136159
raise CycleFoundException(cycles, id2name)
137-
labels, children = _build_tree(id2name, awaits)
160+
labels, children = _build_tree(id2name, awaits, task_stacks)
138161

139162
def pretty(node):
140163
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
@@ -154,35 +177,40 @@ def render(node, prefix="", last=True, buf=None):
154177

155178

156179
def build_task_table(result):
157-
id2name, awaits = _index(result)
180+
id2name, _, _ = _index(result)
158181
table = []
159-
for tid, tasks in result:
160-
for task_id, task_name, awaited in tasks:
161-
if not awaited:
162-
table.append(
163-
[
164-
tid,
165-
hex(task_id),
166-
task_name,
167-
"",
168-
"",
169-
"0x0"
170-
]
171-
)
172-
for stack, awaiter_id in awaited:
173-
stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack]
174-
coroutine_chain = " -> ".join(stack)
175-
awaiter_name = id2name.get(awaiter_id, "Unknown")
176-
table.append(
177-
[
178-
tid,
179-
hex(task_id),
180-
task_name,
181-
coroutine_chain,
182-
awaiter_name,
183-
hex(awaiter_id),
184-
]
185-
)
182+
183+
for awaited_info in result:
184+
thread_id = awaited_info.thread_id
185+
for task_info in awaited_info.awaited_by:
186+
# Get task info
187+
task_id = task_info.task_id
188+
task_name = task_info.task_name
189+
190+
# Build coroutine stack string
191+
frames = [frame for coro in task_info.coroutine_stack
192+
for frame in coro.call_stack]
193+
coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0]
194+
for x in frames)
195+
196+
# Handle tasks with no awaiters
197+
if not task_info.awaited_by:
198+
table.append([thread_id, hex(task_id), task_name, coro_stack,
199+
"", "", "0x0"])
200+
continue
201+
202+
# Handle tasks with awaiters
203+
for coro_info in task_info.awaited_by:
204+
parent_id = coro_info.task_name
205+
awaiter_frames = [_format_stack_entry(x).split(" ")[0]
206+
for x in coro_info.call_stack]
207+
awaiter_chain = " -> ".join(awaiter_frames)
208+
awaiter_name = id2name.get(parent_id, "Unknown")
209+
parent_id_str = (hex(parent_id) if isinstance(parent_id, int)
210+
else str(parent_id))
211+
212+
table.append([thread_id, hex(task_id), task_name, coro_stack,
213+
awaiter_chain, awaiter_name, parent_id_str])
186214

187215
return table
188216

@@ -211,11 +239,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
211239
table = build_task_table(tasks)
212240
# Print the table in a simple tabular format
213241
print(
214-
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
242+
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine stack':<50} {'awaiter chain':<50} {'awaiter name':<15} {'awaiter id':<15}"
215243
)
216-
print("-" * 135)
244+
print("-" * 180)
217245
for row in table:
218-
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
246+
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
219247

220248

221249
def display_awaited_by_tasks_tree(pid: int) -> None:

0 commit comments

Comments
 (0)