Skip to content

Commit 26dfd6d

Browse files
committed
Improve asyncio tools to handle enhanced coroutine stack information
This commit updates the asyncio debugging tools to work with the enhanced structured data from the remote debugging module. The tools now process and display both internal coroutine stacks and external awaiter chains, providing much more comprehensive debugging information. The key improvements include: 1. Enhanced table display: Now shows both "coroutine stack" and "awaiter chain" columns, clearly separating what a task is doing internally vs what it's waiting for externally. 2. Improved tree rendering: Displays complete coroutine call stacks for leaf tasks, making it easier to understand the actual execution state of suspended coroutines. 3. Better cycle detection: Optimized DFS algorithm for detecting await cycles in the task dependency graph. 4. Structured data handling: Updated to work with the new FrameInfo, CoroInfo, TaskInfo, and AwaitedInfo structured types instead of raw tuples. The enhanced output transforms debugging from showing only file paths to revealing function names and complete call stacks, making it much easier to understand complex async execution patterns and diagnose issues in production asyncio applications.
1 parent 3041032 commit 26dfd6d

File tree

1 file changed

+96
-68
lines changed

1 file changed

+96
-68
lines changed

Lib/asyncio/tools.py

Lines changed: 96 additions & 68 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,36 +177,41 @@ 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-
)
186-
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])
214+
187215
return table
188216

189217
def _print_cycle_exception(exception: CycleFoundException):
@@ -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)