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,51 +25,75 @@ 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
- 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
63
77
return node_key
64
78
65
- # lay down parent ➜ …frames… ➜ child paths
79
+ # Build task dependency tree with coroutine frames
66
80
for parent_id , stack , child_id in awaits :
67
81
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
+
70
85
child_key = (NodeType .TASK , child_id )
71
86
if child_key not in children [cur ]:
72
87
children [cur ].append (child_key )
73
88
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
+
74
97
return id2label , children
75
98
76
99
@@ -129,12 +152,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
129
152
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
130
153
with `task_emoji` and coroutine frames with `cor_emoji`.
131
154
"""
132
- id2name , awaits = _index (result )
155
+ id2name , awaits , task_stacks = _index (result )
133
156
g = _task_graph (awaits )
134
157
cycles = _find_cycles (g )
135
158
if cycles :
136
159
raise CycleFoundException (cycles , id2name )
137
- labels , children = _build_tree (id2name , awaits )
160
+ labels , children = _build_tree (id2name , awaits , task_stacks )
138
161
139
162
def pretty (node ):
140
163
flag = task_emoji if node [0 ] == NodeType .TASK else cor_emoji
@@ -154,35 +177,40 @@ def render(node, prefix="", last=True, buf=None):
154
177
155
178
156
179
def build_task_table (result ):
157
- id2name , awaits = _index (result )
180
+ id2name , _ , _ = _index (result )
158
181
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 ])
186
214
187
215
return table
188
216
@@ -211,11 +239,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
211
239
table = build_task_table (tasks )
212
240
# Print the table in a simple tabular format
213
241
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} "
215
243
)
216
- print ("-" * 135 )
244
+ print ("-" * 180 )
217
245
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} " )
219
247
220
248
221
249
def display_awaited_by_tasks_tree (pid : int ) -> None :
0 commit comments