@@ -56,18 +56,93 @@ def _cor_node(parent_key, frame_name):
5656
5757
5858def _roots (id2label , children ):
59- roots = [n for n , lbl in id2label .items () if lbl == "Task-1" ]
60- if roots :
61- return roots
59+ """Return every task that is *not awaited by anybody*."""
6260 all_children = {c for kids in children .values () for c in kids }
6361 return [n for n in id2label if n not in all_children ]
6462
63+ # ─── helpers for _roots() ─────────────────────────────────────────
64+ from collections import defaultdict
65+
66+ def _roots (id2label , children ):
67+ """
68+ Return one root per *source* strongly-connected component (SCC).
69+
70+ • Build the graph that contains **only tasks** as nodes and edges
71+ parent-task ─▶ child-task (ignore coroutine frames).
72+
73+ • Collapse it into SCCs with Tarjan (linear time).
74+
75+ • For every component whose condensation-DAG in-degree is 0, choose a
76+ stable representative (lexicographically-smallest label, fallback to
77+ smallest object-id) and return that list.
78+ """
79+ TASK = NodeType .TASK
80+ task_nodes = [n for n in id2label if n [0 ] == TASK ]
81+
82+ # ------------ adjacency list among *tasks only* -----------------
83+ adj = defaultdict (list )
84+ for p in task_nodes :
85+ adj [p ] = [c for c in children .get (p , []) if c [0 ] == TASK ]
86+
87+ # ------------ Tarjan’s algorithm --------------------------------
88+ index = 0
89+ stack , on_stack = [], set ()
90+ node_index , low = {}, {}
91+ comp_of = {} # node -> comp-id
92+ comps = defaultdict (list ) # comp-id -> [members]
93+
94+ def strong (v ):
95+ nonlocal index
96+ node_index [v ] = low [v ] = index
97+ index += 1
98+ stack .append (v )
99+ on_stack .add (v )
100+
101+ for w in adj [v ]:
102+ if w not in node_index :
103+ strong (w )
104+ low [v ] = min (low [v ], low [w ])
105+ elif w in on_stack :
106+ low [v ] = min (low [v ], node_index [w ])
107+
108+ if low [v ] == node_index [v ]: # root of an SCC
109+ while True :
110+ w = stack .pop ()
111+ on_stack .remove (w )
112+ comp_of [w ] = v # use root-node as comp-id
113+ comps [v ].append (w )
114+ if w == v :
115+ break
116+
117+ for v in task_nodes :
118+ if v not in node_index :
119+ strong (v )
120+
121+ # ------------ condensation DAG in-degrees -----------------------
122+ indeg = defaultdict (int )
123+ for p in task_nodes :
124+ cp = comp_of [p ]
125+ for q in adj [p ]:
126+ cq = comp_of [q ]
127+ if cp != cq :
128+ indeg [cq ] += 1
129+
130+ # ------------ choose one representative per source-SCC ----------
131+ roots = []
132+ for cid , members in comps .items ():
133+ if indeg [cid ] == 0 : # source component
134+ roots .append (min (
135+ members ,
136+ key = lambda n : (id2label [n ], n [1 ]) # stable pick
137+ ))
138+ return roots
139+
65140
66141# ─── PRINT TREE FUNCTION ───────────────────────────────────────
67142def print_async_tree (result , task_emoji = "(T)" , cor_emoji = "" , printer = print ):
68143 """
69144 Pretty-print the async call tree produced by `get_all_async_stacks()`,
70- prefixing tasks with *task_emoji* and coroutine frames with *cor_emoji* .
145+ coping safely with cycles .
71146 """
72147 id2name , awaits = _index (result )
73148 labels , children = _build_tree (id2name , awaits )
@@ -76,20 +151,29 @@ def pretty(node):
76151 flag = task_emoji if node [0 ] == NodeType .TASK else cor_emoji
77152 return f"{ flag } { labels [node ]} "
78153
79- def render (node , prefix = "" , last = True , buf = None ):
154+ def render (node , prefix = "" , last = True , buf = None , ancestry = frozenset ()):
155+ """
156+ DFS renderer that stops if *node* already occurs in *ancestry*
157+ (i.e. we just found a cycle).
158+ """
80159 if buf is None :
81160 buf = []
161+
162+ if node in ancestry :
163+ buf .append (f"{ prefix } { '└── ' if last else '├── ' } ↺ { pretty (node )} (cycle)" )
164+ return buf
165+
82166 buf .append (f"{ prefix } { '└── ' if last else '├── ' } { pretty (node )} " )
83167 new_pref = prefix + (" " if last else "│ " )
84168 kids = children .get (node , [])
85169 for i , kid in enumerate (kids ):
86- render (kid , new_pref , i == len (kids ) - 1 , buf )
170+ render (kid , new_pref , i == len (kids ) - 1 , buf , ancestry | { node } )
87171 return buf
88172
89- result = []
90- for r , root in enumerate ( _roots (labels , children ) ):
91- result .append (render (root ))
92- return result
173+ forest = []
174+ for root in _roots (labels , children ):
175+ forest .append (render (root ))
176+ return forest
93177
94178
95179def build_task_table (result ):
@@ -124,6 +208,7 @@ def build_task_table(result):
124208
125209 try :
126210 tasks = get_all_awaited_by (args .pid )
211+ print (tasks )
127212 except RuntimeError as e :
128213 print (f"Error retrieving tasks: { e } " )
129214 sys .exit (1 )
0 commit comments