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,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
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 )
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
156183def 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
221278def display_awaited_by_tasks_tree (pid : int ) -> None :
0 commit comments