Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b11469a
GH-91048: Add utils for printing the call stack for asyncio tasks
pablogsal May 1, 2025
7f800e8
Maybe
pablogsal May 2, 2025
c5e4efe
Maybe
pablogsal May 2, 2025
1c982b1
Maybe
pablogsal May 2, 2025
2d94cde
fix configure
pablogsal May 2, 2025
0a9a496
fix configure
pablogsal May 2, 2025
db47ff3
fix configure
mgmacias95 May 2, 2025
6f8bd4c
some tests + fixes
mgmacias95 May 2, 2025
152b3d7
improve tests
mgmacias95 May 2, 2025
955ef27
dsf
pablogsal May 2, 2025
65aee3c
dsf
pablogsal May 2, 2025
51e689e
test fixes
pablogsal May 3, 2025
1d27348
test fixes
pablogsal May 3, 2025
1d1b0e9
test fixes
pablogsal May 3, 2025
edad4d1
test fixes
pablogsal May 3, 2025
199589c
Fix free threading offsets
pablogsal May 3, 2025
9e87032
Fix free threading offsets AGAIN
pablogsal May 3, 2025
69e9221
Debugging
pablogsal May 3, 2025
b6cb609
More tests
pablogsal May 3, 2025
2dd3452
Add news entry
pablogsal May 3, 2025
a84a171
Doc fixes
pablogsal May 3, 2025
0f75edc
Fix doc build
ambv May 3, 2025
c3a6bcb
Add Yury
ambv May 3, 2025
5e1cb87
fix: Show independent tasks in the table
mgmacias95 May 3, 2025
d92b520
Merge pull request #101 from mgmacias95/GH-91048-tasks
pablogsal May 3, 2025
af6a8bf
Temporarily skip test_async_global_awaited_by on free-threading
ambv May 3, 2025
8db5dbe
Drop the `tools`. It's cleaner.
ambv May 3, 2025
6f8aa6b
Satisfy the linting gods
ambv May 3, 2025
8d566c6
chore: Refactor
mgmacias95 May 4, 2025
977c15a
Merge pull request #103 from mgmacias95/GH-91048-tasks
pablogsal May 4, 2025
9dbe00d
Doc fixes
pablogsal May 4, 2025
c56782b
Type fixes
pablogsal May 4, 2025
293337f
Type fixes
pablogsal May 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Maybe
  • Loading branch information
pablogsal committed May 2, 2025
commit c5e4efe5d3bdb38d749e8886c80cc628712dc81e
148 changes: 75 additions & 73 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,89 +56,80 @@ def _cor_node(parent_key, frame_name):


def _roots(id2label, children):
"""Return every task that is *not awaited by anybody*."""
roots = [n for n, lbl in id2label.items() if lbl == "Task-1"]
if roots:
return roots
all_children = {c for kids in children.values() for c in kids}
return [n for n in id2label if n not in all_children]

# ─── helpers for _roots() ─────────────────────────────────────────
from collections import defaultdict
# ─── detect cycles in the task-to-task graph ───────────────────────
def _task_graph(awaits):
"""Return {parent_task_id: {child_task_id, …}, …}."""
from collections import defaultdict
g = defaultdict(set)
for parent_id, _stack, child_id in awaits:
g[parent_id].add(child_id)
return g

def _roots(id2label, children):

def _find_cycles(graph):
"""
Return one root per *source* strongly-connected component (SCC).
Depth-first search for back-edges.

• Build the graph that contains **only tasks** as nodes and edges
parent-task ─▶ child-task (ignore coroutine frames).
Returns a list of cycles (each cycle is a list of task-ids) or an
empty list if the graph is acyclic.
"""
WHITE, GREY, BLACK = 0, 1, 2
color = {n: WHITE for n in graph}
path, cycles = [], []

def dfs(v):
color[v] = GREY
path.append(v)
for w in graph.get(v, ()):
if color[w] == WHITE:
dfs(w)
elif color[w] == GREY: # back-edge → cycle!
i = path.index(w)
cycles.append(path[i:] + [w]) # make a copy
color[v] = BLACK
path.pop()

for v in list(graph):
if color[v] == WHITE:
dfs(v)
return cycles

• Collapse it into SCCs with Tarjan (linear time).

• For every component whose condensation-DAG in-degree is 0, choose a
stable representative (lexicographically-smallest label, fallback to
smallest object-id) and return that list.
# ─── PRINT TREE FUNCTION ───────────────────────────────────────
def print_async_tree(result, task_emoji="(T)", cor_emoji="", printer=print):
"""
TASK = NodeType.TASK
task_nodes = [n for n in id2label if n[0] == TASK]

# ------------ adjacency list among *tasks only* -----------------
adj = defaultdict(list)
for p in task_nodes:
adj[p] = [c for c in children.get(p, []) if c[0] == TASK]

# ------------ Tarjan’s algorithm --------------------------------
index = 0
stack, on_stack = [], set()
node_index, low = {}, {}
comp_of = {} # node -> comp-id
comps = defaultdict(list) # comp-id -> [members]

def strong(v):
nonlocal index
node_index[v] = low[v] = index
index += 1
stack.append(v)
on_stack.add(v)

for w in adj[v]:
if w not in node_index:
strong(w)
low[v] = min(low[v], low[w])
elif w in on_stack:
low[v] = min(low[v], node_index[w])

if low[v] == node_index[v]: # root of an SCC
while True:
w = stack.pop()
on_stack.remove(w)
comp_of[w] = v # use root-node as comp-id
comps[v].append(w)
if w == v:
break

for v in task_nodes:
if v not in node_index:
strong(v)

# ------------ condensation DAG in-degrees -----------------------
indeg = defaultdict(int)
for p in task_nodes:
cp = comp_of[p]
for q in adj[p]:
cq = comp_of[q]
if cp != cq:
indeg[cq] += 1

# ------------ choose one representative per source-SCC ----------
roots = []
for cid, members in comps.items():
if indeg[cid] == 0: # source component
roots.append(min(
members,
key=lambda n: (id2label[n], n[1]) # stable pick
))
return roots
Pretty-print the async call tree produced by `get_all_async_stacks()`,
prefixing tasks with *task_emoji* and coroutine frames with *cor_emoji*.
"""
id2name, awaits = _index(result)
labels, children = _build_tree(id2name, awaits)

def pretty(node):
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
return f"{flag} {labels[node]}"

def render(node, prefix="", last=True, buf=None):
if buf is None:
buf = []
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
new_pref = prefix + (" " if last else "│ ")
kids = children.get(node, [])
for i, kid in enumerate(kids):
render(kid, new_pref, i == len(kids) - 1, buf)
return buf

result = []
for r, root in enumerate(_roots(labels, children)):
result.append(render(root))
return result


# ─── PRINT TREE FUNCTION ───────────────────────────────────────
def print_async_tree(result, task_emoji="(T)", cor_emoji="", printer=print):
"""
Pretty-print the async call tree produced by `get_all_async_stacks()`,
Expand Down Expand Up @@ -208,13 +199,24 @@ def build_task_table(result):

try:
tasks = get_all_awaited_by(args.pid)
print(tasks)
except RuntimeError as e:
print(f"Error retrieving tasks: {e}")
sys.exit(1)

if args.tree:
# Print the async call tree
id2name, awaits = _index(tasks)
g = _task_graph(awaits)
cycles = _find_cycles(g)

if cycles:
print("ERROR: await-graph contains cycles – cannot print a tree!\n")
for c in cycles:
# pretty-print task names instead of bare ids
names = " → ".join(id2name.get(tid, hex(tid)) for tid in c)
print(f" cycle: {names}")
sys.exit(1)

result = print_async_tree(tasks)
for tree in result:
print("\n".join(tree))
Expand Down