8000 Improve asyncio tools to handle enhanced coroutine stack information · python/cpython@698669c · GitHub
[go: up one dir, main page]

Skip to content

Commit 698669c

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 698669c

File tree

1 file changed

+105
-48
lines changed

1 file changed

+105
-48
lines changed

Lib/asyncio/tools.py

Lines changed: 105 additions & 48 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,26 +25,41 @@ 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)
5165
cor_names = defaultdict(dict) # (parent) -> {frame: node}
@@ -71,6 +85,16 @@ def _cor_node(parent_key, frame_name):
7185
if child_key not in children[cur]:
7286
children[cur].append(child_key)
7387

88+
# Add internal coroutine stacks for leaf tasks (tasks that don't await other tasks)
89+
leaf_tasks = set(id2name.keys()) - set(parent_id for parent_id, _, _ in awaits)
90+
for task_id in leaf_tasks:
91+
if task_id in task_stacks:
92+
task_key = (NodeType.TASK, task_id)
93+
cur = task_key
94+
# Add the internal stack frames in reverse order (outermost to innermost)
95+
for frame in reversed(task_stacks[task_id]):
96+
cur = _cor_node(cur, frame)
97+
7498
return id2label, children
7599

76100

@@ -99,14 +123,17 @@ def _find_cycles(graph):
99123
path, cycles = [], []
100124

101125
def dfs(v):
126+
if color[v] == GREY: # back-edge → cycle!
127+
i = path.index(v)
128+
cycles.append(path[i:] + [v]) # make a copy
129+
return
130+
if color[v] == BLACK:
131+
return
132+
102133
color[v] = GREY
103134
path.append(v)
104135
for w in graph.get(v, ()):
105-
if color[w] == WHITE:
106-
dfs(w)
107-
elif color[w] == GREY: # back-edge → cycle!
108-
i = path.index(w)
109-
cycles.append(path[i:] + [w]) # make a copy
136+
dfs(w)
110137
color[v] = BLACK
111138
path.pop()
112139

@@ -129,12 +156,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
129156
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
130157
with `task_emoji` and coroutine frames with `cor_emoji`.
131158
"""
132-
id2name, awaits = _index(result)
159+
id2name, awaits, task_stacks = _index(result)
133160
g = _task_graph(awaits)
134161
cycles = _find_cycles(g)
135162
if cycles:
136163
raise CycleFoundException(cycles, id2name)
137-
labels, children = _build_tree(id2name, awaits)
164+
labels, children = _build_tree(id2name, awaits, task_stacks)
138165

139166
def pretty(node):
140167
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
@@ -154,35 +181,65 @@ def render(node, prefix="", last=True, buf=None):
154181

155182

156183
def build_task_table(result):
157-
id2name, awaits = _index(result)
184+
id2name, _, _= _index(result)
158185
table = []
159-
for tid, tasks in result:
160-
for task_id, task_name, awaited in tasks:
161-
if not awaited:
186+
for awaited_info in result:
187+
thread_id = awaited_info.thread_id
188+
for task_info in awaited_info.awaited_by:
189+
task_id = task_info.task_id
190+
task_name = task_info.task_name
191+
192+
# Interpret the data structure correctly:
193+
# If 3 elements: (task_id, name, awaited_by)
194+
# If 4 elements: (task_id, name, coroutine_stack, awaited_by)
195+
if len(task_info.coroutine_stack) == 0:
196+
coroutine_stack = []
197+
awaited_by = task_info.awaited_by
198+
else:
199+
coroutine_stack = task_info.coroutine_stack
200+
awaited_by = task_info.awaited_by
201+
202+
# Add coroutine stack information for the current task
203+
if coroutine_stack:
204+
coro_stack = []
205+
for coro_info in coroutine_stack:
206+
call_stack = coro_info.call_stack
207+
frame_names = [frame for frame in call_stack]
208+
coro_stack.extend(frame_names)
209+
coroutine_stack_str = " -> ".join([_format_stack_entry(x).split(" ")[0] for x in coro_stack])
210+
else:
211+
coroutine_stack_str = ""
212+
213+
if not awaited_by:
162214
table.append(
163215
[
164-
tid,
216+
thread_id,
165217
hex(task_id),
166218
task_name,
219+
coroutine_stack_str,
167220
"",
168221
"",
169222
"0x0"
170223
]
171224
)
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-
)
225+
else:
226+
for coro_info in awaited_by:
227+
call_stack = coro_info.call_stack
228+
parent_task_id = coro_info.task_name
229+
awaiter_stack = [frame for frame in call_stack]
230+
awaiter_chain = " -> ".join([_format_stack_entry(x).split(" ")[0] for x in awaiter_stack])
231+
awaiter_name = id2name.get(parent_task_id, "Unknown")
232+
table.append(
233+
[
234+
thread_id,
235+
hex(task_id),
236+
task_name,
237+
coroutine_stack_str,
238+
awaiter_chain,
239+
awaiter_name,
240+
hex(parent_task_id) if isinstance(parent_task_id, int) else str(parent_task_id),
241+
]
242+
)
186243

187244
return table
188245

@@ -211,11 +268,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
211268
table = build_task_table(tasks)
212269
# Print the table in a simple tabular format
213270
print(
214-
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
271+
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine stack':<50} {'awaiter chain':<50} {'awaiter name':<15} {'awaiter id':<15}"
215272
)
216-
print("-" * 135)
273+
print("-" * 180)
217274
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}")
275+
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
219276

220277

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

0 commit comments

Comments
 (0)
0