11"""Tools to analyze tasks running in asyncio programs."""
22
3- from collections import defaultdict
3+ from collections import defaultdict , namedtuple
44from itertools import count
55from enum import Enum
66import sys
7- from _remote_debugging import RemoteUnwinder
8-
7+ from _remote_debugging import RemoteUnwinder , FrameInfo
98
109class 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
3737def _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
156179def 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
221249def display_awaited_by_tasks_tree (pid : int ) -> None :
0 commit comments