@@ -56,18 +56,93 @@ def _cor_node(parent_key, frame_name):
56
56
57
57
58
58
def _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*."""
62
60
all_children = {c for kids in children .values () for c in kids }
63
61
return [n for n in id2label if n not in all_children ]
64
62
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
+
65
140
66
141
# ─── PRINT TREE FUNCTION ───────────────────────────────────────
67
142
def print_async_tree (result , task_emoji = "(T)" , cor_emoji = "" , printer = print ):
68
143
"""
69
144
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 .
71
146
"""
72
147
id2name , awaits = _index (result )
73
148
labels , children = _build_tree (id2name , awaits )
@@ -76,20 +151,29 @@ def pretty(node):
76
151
flag = task_emoji if node [0 ] == NodeType .TASK else cor_emoji
77
152
return f"{ flag } { labels [node ]} "
78
153
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
+ """
80
159
if buf is None :
81
160
buf = []
161
+
162
+ if node in ancestry :
163
+ buf .append (f"{ prefix } { '└── ' if last else '├── ' } ↺ { pretty (node )} (cycle)" )
164
+ return buf
165
+
82
166
buf .append (f"{ prefix } { '└── ' if last else '├── ' } { pretty (node )} " )
83
167
new_pref = prefix + (" " if last else "│ " )
84
168
kids = children .get (node , [])
85
169
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 } )
87
171
return buf
88
172
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
93
177
94
178
95
179
def build_task_table (result ):
@@ -124,6 +208,7 @@ def build_task_table(result):
124
208
125
209
try :
126
210
tasks = get_all_awaited_by (args .pid )
211
+ print (tasks )
127
212
except RuntimeError as e :
128
213
print (f"Error retrieving tasks: { e } " )
129
214
sys .exit (1 )
0 commit comments