@@ -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