Skip to content

Commit b11469a

Browse files
committed
pythonGH-91048: Add utils for printing the call stack for asyncio tasks
1 parent bd2ed7c commit b11469a

13 files changed

+244
-71
lines changed

Lib/asyncio/tools.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import argparse
2+
from collections import defaultdict
3+
from itertools import count
4+
from enum import Enum
5+
import sys
6+
from _remotedebugging import get_all_awaited_by
7+
8+
9+
class NodeType(Enum):
10+
COROUTINE = 1
11+
TASK = 2
12+
13+
14+
# ─── indexing helpers ───────────────────────────────────────────
15+
def _index(result):
16+
id2name, awaits = {}, []
17+
for _thr_id, tasks in result:
18+
for tid, tname, awaited in tasks:
19+
id2name[tid] = tname
20+
for stack, parent_id in awaited:
21+
awaits.append((parent_id, stack, tid))
22+
return id2name, awaits
23+
24+
25+
def _build_tree(id2name, awaits):
26+
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
27+
children = defaultdict(list)
28+
cor_names = defaultdict(dict) # (parent) -> {frame: node}
29+
cor_id_seq = count(1)
30+
31+
def _cor_node(parent_key, frame_name):
32+
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
33+
bucket = cor_names[parent_key]
34+
if frame_name in bucket:
35+
return bucket[frame_name]
36+
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
37+
id2label[node_key] = frame_name
38+
children[parent_key].append(node_key)
39+
bucket[frame_name] = node_key
40+
return node_key
41+
42+
# touch every task so it’s present even if it awaits nobody
43+
for tid in id2name:
44+
children[(NodeType.TASK, tid)]
45+
46+
# lay down parent ➜ …frames… ➜ child paths
47+
for parent_id, stack, child_id in awaits:
48+
cur = (NodeType.TASK, parent_id)
49+
for frame in reversed(stack): # outer-most → inner-most
50+
cur = _cor_node(cur, frame)
51+
child_key = (NodeType.TASK, child_id)
52+
if child_key not in children[cur]:
53+
children[cur].append(child_key)
54+
55+
return id2label, children
56+
57+
58+
def _roots(id2label, children):
59+
roots = [n for n, lbl in id2label.items() if lbl == "Task-1"]
60+
if roots:
61+
return roots
62+
all_children = {c for kids in children.values() for c in kids}
63+
return [n for n in id2label if n not in all_children]
64+
65+
66+
# ─── PRINT TREE FUNCTION ───────────────────────────────────────
67+
def print_async_tree(result, task_emoji="(T)", cor_emoji="", printer=print):
68+
"""
69+
Pretty-print the async call tree produced by `get_all_async_stacks()`,
70+
prefixing tasks with *task_emoji* and coroutine frames with *cor_emoji*.
71+
"""
72+
id2name, awaits = _index(result)
73+
labels, children = _build_tree(id2name, awaits)
74+
75+
def pretty(node):
76+
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
77+
return f"{flag} {labels[node]}"
78+
79+
def render(node, prefix="", last=True, buf=None):
80+
if buf is None:
81+
buf = []
82+
buf.append(f"{prefix}{'└── ' if last else '├── '}{pretty(node)}")
83+
new_pref = prefix + (" " if last else "│ ")
84+
kids = children.get(node, [])
85+
for i, kid in enumerate(kids):
86+
render(kid, new_pref, i == len(kids) - 1, buf)
87+
return buf
88+
89+
result = []
90+
for r, root in enumerate(_roots(labels, children)):
91+
result.append(render(root))
92+
return result
93+
94+
95+
def build_task_table(result):
96+
id2name, awaits = _index(result)
97+
table = []
98+
for tid, tasks in result:
99+
for task_id, task_name, awaited in tasks:
100+
for stack, awaiter_id in awaited:
101+
coroutine_chain = " -> ".join(stack)
102+
awaiter_name = id2name.get(awaiter_id, "Unknown")
103+
table.append(
104+
[
105+
tid,
106+
hex(task_id),
107+
task_name,
108+
coroutine_chain,
109+
awaiter_name,
110+
hex(awaiter_id),
111+
]
112+
)
113+
114+
return table
115+
116+
117+
if __name__ == "__main__":
118+
parser = argparse.ArgumentParser(description="Show Python async tasks in a process")
119+
parser.add_argument("pid", type=int, help="Process ID(s) to inspect.")
120+
parser.add_argument(
121+
"--tree", "-t", action="store_true", help="Display tasks in a tree format"
122+
)
123+
args = parser.parse_args()
124+
125+
try:
126+
tasks = get_all_awaited_by(args.pid)
127+
except RuntimeError as e:
128+
print(f"Error retrieving tasks: {e}")
129+
sys.exit(1)
130+
131+
if args.tree:
132+
# Print the async call tree
133+
result = print_async_tree(tasks)
134+
for tree in result:
135+
print("\n".join(tree))
136+
else:
137+
# Build and print the task table
138+
table = build_task_table(tasks)
139+
# Print the table in a simple tabular format
140+
print(
141+
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
142+
)
143+
print("-" * 135)
144+
for row in table:
145+
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")

Lib/test/test_external_inspection.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
PROCESS_VM_READV_SUPPORTED = False
1414

1515
try:
16-
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
17-
from _testexternalinspection import get_stack_trace
18-
from _testexternalinspection import get_async_stack_trace
19-
from _testexternalinspection import get_all_awaited_by
16+
from _remotedebuggingg import PROCESS_VM_READV_SUPPORTED
17+
from _remotedebuggingg import get_stack_trace
18+
from _remotedebuggingg import get_async_stack_trace
19+
from _remotedebuggingg import get_all_awaited_by
2020
except ImportError:
2121
raise unittest.SkipTest(
22-
"Test only runs when _testexternalinspection is available")
22+
"Test only runs when _remotedebuggingmodule is available")
2323

2424
def _make_test_script(script_dir, script_basename, source):
2525
to_return = make_script(script_dir, script_basename, source)

Lib/test/test_sys.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1960,7 +1960,7 @@ def _supports_remote_attaching():
19601960
PROCESS_VM_READV_SUPPORTED = False
19611961

19621962
try:
1963-
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
1963+
from _remotedebuggingmodule import PROCESS_VM_READV_SUPPORTED
19641964
except ImportError:
19651965
pass
19661966

Modules/Setup

+1-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
286286
#_testcapi _testcapimodule.c
287287
#_testimportmultiple _testimportmultiple.c
288288
#_testmultiphase _testmultiphase.c
289-
#_testexternalinspection _testexternalinspection.c
289+
#_remotedebuggingmodule _remotedebuggingmodule.c
290290
#_testsinglephase _testsinglephase.c
291291

292292
# ---

Modules/Setup.stdlib.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# Modules that should always be present (POSIX and Windows):
3434
@MODULE_ARRAY_TRUE@array arraymodule.c
3535
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
36+
@MODULE__REMOTEDEBUGGING_TRUE@_remotedebugging _remotedebuggingmodule.c
3637
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
3738
@MODULE__CSV_TRUE@_csv _csv.c
3839
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
@@ -186,7 +187,6 @@
186187
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
187188
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
188189
@MODULE__TESTSINGLEPHASE_TRUE@_testsinglephase _testsinglephase.c
189-
@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
190190
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c
191191

192192
# Limited API template modules; must be built as shared modules.

0 commit comments

Comments
 (0)