1
1
"""Tools to analyze tasks running in asyncio programs."""
2
2
3
- from collections import defaultdict
3
+ from collections import defaultdict , namedtuple
4
4
from itertools import count
5
5
from enum import Enum
6
6
import sys
7
- from _remote_debugging import RemoteUnwinder
8
-
7
+ from _remote_debugging import RemoteUnwinder , FrameInfo
9
8
10
9
class NodeType (Enum ):
11
10
COROUTINE = 1
@@ -26,26 +25,41 @@ def __init__(
26
25
27
26
28
27
# ─── 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 } "
34
34
return elem
35
35
36
36
37
37
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 ):
49
63
id2label = {(NodeType .TASK , tid ): name for tid , name in id2name .items ()}
50
64
children = defaultdict (list )
51
65
cor_names = defaultdict (dict ) # (parent) -> {frame: node}
@@ -71,6 +85,16 @@ def _cor_node(parent_key, frame_name):
71
85
if child_key not in children [cur ]:
72
86
children [cur ].append (child_key )
73
87
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
+
74
98
return id2label , children
75
99
76
100
@@ -99,14 +123,17 @@ def _find_cycles(graph):
99
123
path , cycles = [], []
100
124
101
125
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
+
102
133
color [v ] = GREY
103
134
path .append (v )
104
135
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 )
110
137
color [v ] = BLACK
111
138
path .pop ()
112
139
@@ -129,12 +156,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
129
156
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
130
157
with `task_emoji` and coroutine frames with `cor_emoji`.
131
158
"""
132
- id2name , awaits = _index (result )
159
+ id2name , awaits , task_stacks = _index (result )
133
160
g = _task_graph (awaits )
134
161
cycles = _find_cycles (g )
135
162
if cycles :
136
163
raise CycleFoundException (cycles , id2name )
137
- labels , children = _build_tree (id2name , awaits )
164
+ labels , children = _build_tree (id2name , awaits , task_stacks )
138
165
139
166
def pretty (node ):
140
167
flag = task_emoji if node [0 ] == NodeType .TASK else cor_emoji
@@ -154,35 +181,65 @@ def render(node, prefix="", last=True, buf=None):
154
181
155
182
156
183
def build_task_table (result ):
157
- id2name , awaits = _index (result )
184
+ id2name , _ , _ = _index (result )
158
185
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 :
162
214
table .append (
163
215
[
164
- tid ,
216
+ thread_id ,
165
217
hex (task_id ),
166
218
task_name ,
219
+ coroutine_stack_str ,
167
220
"" ,
168
221
"" ,
169
222
"0x0"
170
223
]
171
224
)
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
+ )
186
243
187
244
return table
188
245
@@ -211,11 +268,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
211
268
table = build_task_table (tasks )
212
269
# Print the table in a simple tabular format
213
270
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} "
215
272
)
216
- print ("-" * 135 )
273
+ print ("-" * 180 )
217
274
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} " )
219
276
220
277
221
278
def display_awaited_by_tasks_tree (pid : int ) -> None :
0 commit comments