Skip to content

GH-91048: Add utils for printing the call stack for asyncio tasks #133284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
May 4, 2025
Merged
Show file tree
Hide file tree
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
Next Next commit
GH-91048: Add utils for printing the call stack for asyncio tasks
  • Loading branch information
pablogsal committed May 2, 2025
commit b11469a4d20877e07f7e11b2ee96d596692eb5d4
145 changes: 145 additions & 0 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import argparse
from collections import defaultdict
from itertools import count
from enum import Enum
import sys
from _remotedebugging import get_all_awaited_by


class NodeType(Enum):
COROUTINE = 1
TASK = 2


# ─── indexing helpers ───────────────────────────────────────────
def _index(result):
id2name, awaits = {}, []
for _thr_id, tasks in result:
for tid, tname, awaited in tasks:
id2name[tid] = tname
for stack, parent_id in awaited:
awaits.append((parent_id, stack, tid))
return id2name, awaits


def _build_tree(id2name, awaits):
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
children = defaultdict(list)
cor_names = defaultdict(dict) # (parent) -> {frame: node}
cor_id_seq = count(1)

def _cor_node(parent_key, frame_name):
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
bucket = cor_names[parent_key]
if frame_name in bucket:
return bucket[frame_name]
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
id2label[node_key] = frame_name
children[parent_key].append(node_key)
bucket[frame_name] = node_key
return node_key

# touch every task so it’s present even if it awaits nobody
for tid in id2name:
children[(NodeType.TASK, tid)]

# lay down parent ➜ …frames… ➜ child paths
for parent_id, stack, child_id in awaits:
cur = (NodeType.TASK, parent_id)
for frame in reversed(stack): # outer-most → inner-most
cur = _cor_node(cur, frame)
child_key = (NodeType.TASK, child_id)
if child_key not in children[cur]:
children[cur].append(child_key)

return id2label, children


def _roots(id2label, children):
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]


# ─── 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()`,
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


def build_task_table(result):
id2name, awaits = _index(result)
table = []
for tid, tasks in result:
for task_id, task_name, awaited in tasks:
for stack, awaiter_id in awaited:
coroutine_chain = " -> ".join(stack)
awaiter_name = id2name.get(awaiter_id, "Unknown")
table.append(
[
tid,
hex(task_id),
task_name,
coroutine_chain,
awaiter_name,
hex(awaiter_id),
]
)

return table


if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Show Python async tasks in a process")
parser.add_argument("pid", type=int, help="Process ID(s) to inspect.")
parser.add_argument(
"--tree", "-t", action="store_true", help="Display tasks in a tree format"
)
args = parser.parse_args()

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

if args.tree:
# Print the async call tree
result = print_async_tree(tasks)
for tree in result:
print("\n".join(tree))
else:
# Build and print the task table
table = build_task_table(tasks)
# Print the table in a simple tabular format
print(
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
)
print("-" * 135)
for row in table:
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
10 changes: 5 additions & 5 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
PROCESS_VM_READV_SUPPORTED = False

try:
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
from _testexternalinspection import get_stack_trace
from _testexternalinspection import get_async_stack_trace
from _testexternalinspection import get_all_awaited_by
from _remotedebuggingg import PROCESS_VM_READV_SUPPORTED
from _remotedebuggingg import get_stack_trace
from _remotedebuggingg import get_async_stack_trace
from _remotedebuggingg import get_all_awaited_by
except ImportError:
raise unittest.SkipTest(
"Test only runs when _testexternalinspection is available")
"Test only runs when _remotedebuggingmodule is available")

def _make_test_script(script_dir, script_basename, source):
to_return = make_script(script_dir, script_basename, source)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1960,7 +1960,7 @@ def _supports_remote_attaching():
PROCESS_VM_READV_SUPPORTED = False

try:
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
from _remotedebuggingmodule import PROCESS_VM_READV_SUPPORTED
except ImportError:
pass

Expand Down
2 changes: 1 addition & 1 deletion Modules/Setup
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ PYTHONPATH=$(COREPYTHONPATH)
#_testcapi _testcapimodule.c
#_testimportmultiple _testimportmultiple.c
#_testmultiphase _testmultiphase.c
#_testexternalinspection _testexternalinspection.c
#_remotedebuggingmodule _remotedebuggingmodule.c
#_testsinglephase _testsinglephase.c

# ---
Expand Down
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
# Modules that should always be present (POSIX and Windows):
@MODULE_ARRAY_TRUE@array arraymodule.c
@MODULE__ASYNCIO_TRUE@_asyncio _asynciomodule.c
@MODULE__REMOTEDEBUGGING_TRUE@_remotedebugging _remotedebuggingmodule.c
@MODULE__BISECT_TRUE@_bisect _bisectmodule.c
@MODULE__CSV_TRUE@_csv _csv.c
@MODULE__HEAPQ_TRUE@_heapq _heapqmodule.c
Expand Down Expand Up @@ -186,7 +187,6 @@
@MODULE__TESTIMPORTMULTIPLE_TRUE@_testimportmultiple _testimportmultiple.c
@MODULE__TESTMULTIPHASE_TRUE@_testmultiphase _testmultiphase.c
@MODULE__TESTSINGLEPHASE_TRUE@_testsinglephase _testsinglephase.c
@MODULE__TESTEXTERNALINSPECTION_TRUE@_testexternalinspection _testexternalinspection.c
@MODULE__CTYPES_TEST_TRUE@_ctypes_test _ctypes/_ctypes_test.c

# Limited API template modules; must be built as shared modules.
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy