|
| 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}") |
0 commit comments