|
| 1 | +"""Tools to analyze tasks running in asyncio programs.""" |
| 2 | + |
| 3 | +fromdataclassesimportdataclass |
| 4 | +fromcollectionsimportdefaultdict |
| 5 | +fromitertoolsimportcount |
| 6 | +fromenumimportEnum |
| 7 | +importsys |
| 8 | +from_remotedebuggingimportget_all_awaited_by |
| 9 | + |
| 10 | + |
| 11 | +classNodeType(Enum): |
| 12 | +COROUTINE=1 |
| 13 | +TASK=2 |
| 14 | + |
| 15 | + |
| 16 | +@dataclass(frozen=True) |
| 17 | +classCycleFoundException(Exception): |
| 18 | +"""Raised when there is a cycle when drawing the call tree.""" |
| 19 | +cycles:list[list[int]] |
| 20 | +id2name:dict[int,str] |
| 21 | + |
| 22 | + |
| 23 | +# ─── indexing helpers ─────────────────────────────────────────── |
| 24 | +def_index(result): |
| 25 | +id2name,awaits= {}, [] |
| 26 | +for_thr_id,tasksinresult: |
| 27 | +fortid,tname,awaitedintasks: |
| 28 | +id2name[tid]=tname |
| 29 | +forstack,parent_idinawaited: |
| 30 | +awaits.append((parent_id,stack,tid)) |
| 31 | +returnid2name,awaits |
| 32 | + |
| 33 | + |
| 34 | +def_build_tree(id2name,awaits): |
| 35 | +id2label= {(NodeType.TASK,tid):namefortid,nameinid2name.items()} |
| 36 | +children=defaultdict(list) |
| 37 | +cor_names=defaultdict(dict)# (parent) -> {frame: node} |
| 38 | +cor_id_seq=count(1) |
| 39 | + |
| 40 | +def_cor_node(parent_key,frame_name): |
| 41 | +"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*.""" |
| 42 | +bucket=cor_names[parent_key] |
| 43 | +ifframe_nameinbucket: |
| 44 | +returnbucket[frame_name] |
| 45 | +node_key= (NodeType.COROUTINE,f"c{next(cor_id_seq)}") |
| 46 | +id2label[node_key]=frame_name |
| 47 | +children[parent_key].append(node_key) |
| 48 | +bucket[frame_name]=node_key |
| 49 | +returnnode_key |
| 50 | + |
| 51 | +# lay down parent ➜ …frames… ➜ child paths |
| 52 | +forparent_id,stack,child_idinawaits: |
| 53 | +cur= (NodeType.TASK,parent_id) |
| 54 | +forframeinreversed(stack):# outer-most → inner-most |
| 55 | +cur=_cor_node(cur,frame) |
| 56 | +child_key= (NodeType.TASK,child_id) |
| 57 | +ifchild_keynotinchildren[cur]: |
| 58 | +children[cur].append(child_key) |
| 59 | + |
| 60 | +returnid2label,children |
| 61 | + |
| 62 | + |
| 63 | +def_roots(id2label,children): |
| 64 | +all_children= {cforkidsinchildren.values()forcinkids} |
| 65 | +return [nforninid2labelifnnotinall_children] |
| 66 | + |
| 67 | +# ─── detect cycles in the task-to-task graph ─────────────────────── |
| 68 | +def_task_graph(awaits): |
| 69 | +"""Return {parent_task_id: {child_task_id, …}, …}.""" |
| 70 | +g=defaultdict(set) |
| 71 | +forparent_id,_stack,child_idinawaits: |
| 72 | +g[parent_id].add(child_id) |
| 73 | +returng |
| 74 | + |
| 75 | + |
| 76 | +def_find_cycles(graph): |
| 77 | +""" |
| 78 | + Depth-first search for back-edges. |
| 79 | +
|
| 80 | + Returns a list of cycles (each cycle is a list of task-ids) or an |
| 81 | + empty list if the graph is acyclic. |
| 82 | + """ |
| 83 | +WHITE,GREY,BLACK=0,1,2 |
| 84 | +color=defaultdict(lambda:WHITE) |
| 85 | +path,cycles= [], [] |
| 86 | + |
| 87 | +defdfs(v): |
| 88 | +color[v]=GREY |
| 89 | +path.append(v) |
| 90 | +forwingraph.get(v, ()): |
| 91 | +ifcolor[w]==WHITE: |
| 92 | +dfs(w) |
| 93 | +elifcolor[w]==GREY:# back-edge → cycle! |
| 94 | +i=path.index(w) |
| 95 | +cycles.append(path[i:]+ [w])# make a copy |
| 96 | +color[v]=BLACK |
| 97 | +path.pop() |
| 98 | + |
| 99 | +forvinlist(graph): |
| 100 | +ifcolor[v]==WHITE: |
| 101 | +dfs(v) |
| 102 | +returncycles |
| 103 | + |
| 104 | + |
| 105 | +# ─── PRINT TREE FUNCTION ─────────────────────────────────────── |
| 106 | +defbuild_async_tree(result,task_emoji="(T)",cor_emoji=""): |
| 107 | +""" |
| 108 | + Build a list of strings for pretty-print a async call tree. |
| 109 | +
|
| 110 | + The call tree is produced by `get_all_async_stacks()`, prefixing tasks |
| 111 | + with `task_emoji` and coroutine frames with `cor_emoji`. |
| 112 | + """ |
| 113 | +id2name,awaits=_index(result) |
| 114 | +g=_task_graph(awaits) |
| 115 | +cycles=_find_cycles(g) |
| 116 | +ifcycles: |
| 117 | +raiseCycleFoundException(cycles,id2name) |
| 118 | +labels,children=_build_tree(id2name,awaits) |
| 119 | + |
| 120 | +defpretty(node): |
| 121 | +flag=task_emojiifnode[0]==NodeType.TASKelsecor_emoji |
| 122 | +returnf"{flag}{labels[node]}" |
| 123 | + |
| 124 | +defrender(node,prefix="",last=True,buf=None): |
| 125 | +ifbufisNone: |
| 126 | +buf= [] |
| 127 | +buf.append(f"{prefix}{'└── 'iflastelse'├── '}{pretty(node)}") |
| 128 | +new_pref=prefix+ (" "iflastelse"│ ") |
| 129 | +kids=children.get(node, []) |
| 130 | +fori,kidinenumerate(kids): |
| 131 | +render(kid,new_pref,i==len(kids)-1,buf) |
| 132 | +returnbuf |
| 133 | + |
| 134 | +return [render(root)forrootin_roots(labels,children)] |
| 135 | + |
| 136 | + |
| 137 | +defbuild_task_table(result): |
| 138 | +id2name,awaits=_index(result) |
| 139 | +table= [] |
| 140 | +fortid,tasksinresult: |
| 141 | +fortask_id,task_name,awaitedintasks: |
| 142 | +ifnotawaited: |
| 143 | +table.append( |
| 144 | + [ |
| 145 | +tid, |
| 146 | +hex(task_id), |
| 147 | +task_name, |
| 148 | +"", |
| 149 | +"", |
| 150 | +"0x0" |
| 151 | + ] |
| 152 | + ) |
| 153 | +forstack,awaiter_idinawaited: |
| 154 | +coroutine_chain=" -> ".join(stack) |
| 155 | +awaiter_name=id2name.get(awaiter_id,"Unknown") |
| 156 | +table.append( |
| 157 | + [ |
| 158 | +tid, |
| 159 | +hex(task_id), |
| 160 | +task_name, |
| 161 | +coroutine_chain, |
| 162 | +awaiter_name, |
| 163 | +hex(awaiter_id), |
| 164 | + ] |
| 165 | + ) |
| 166 | + |
| 167 | +returntable |
| 168 | + |
| 169 | +def_print_cycle_exception(exception:CycleFoundException): |
| 170 | +print("ERROR: await-graph contains cycles – cannot print a tree!",file=sys.stderr) |
| 171 | +print("",file=sys.stderr) |
| 172 | +forcinexception.cycles: |
| 173 | +inames=" → ".join(exception.id2name.get(tid,hex(tid))fortidinc) |
| 174 | +print(f"cycle:{inames}",file=sys.stderr) |
| 175 | + |
| 176 | + |
| 177 | +def_get_awaited_by_tasks(pid:int)->list: |
| 178 | +try: |
| 179 | +returnget_all_awaited_by(pid) |
| 180 | +exceptRuntimeErrorase: |
| 181 | +whilee.__context__isnotNone: |
| 182 | +e=e.__context__ |
| 183 | +print(f"Error retrieving tasks:{e}") |
| 184 | +sys.exit(1) |
| 185 | + |
| 186 | + |
| 187 | +defdisplay_awaited_by_tasks_table(pid:int)->None: |
| 188 | +"""Build and print a table of all pending tasks under `pid`.""" |
| 189 | + |
| 190 | +tasks=_get_awaited_by_tasks(pid) |
| 191 | +table=build_task_table(tasks) |
| 192 | +# Print the table in a simple tabular format |
| 193 | +print( |
| 194 | +f"{'tid':<10}{'task id':<20}{'task name':<20}{'coroutine chain':<50}{'awaiter name':<20}{'awaiter id':<15}" |
| 195 | + ) |
| 196 | +print("-"*135) |
| 197 | +forrowintable: |
| 198 | +print(f"{row[0]:<10}{row[1]:<20}{row[2]:<20}{row[3]:<50}{row[4]:<20}{row[5]:<15}") |
| 199 | + |
| 200 | + |
| 201 | +defdisplay_awaited_by_tasks_tree(pid:int)->None: |
| 202 | +"""Build and print a tree of all pending tasks under `pid`.""" |
| 203 | + |
| 204 | +tasks=_get_awaited_by_tasks(pid) |
| 205 | +try: |
| 206 | +result=build_async_tree(tasks) |
| 207 | +exceptCycleFoundExceptionase: |
| 208 | +_print_cycle_exception(e) |
| 209 | +sys.exit(1) |
| 210 | + |
| 211 | +fortreeinresult: |
| 212 | +print("\n".join(tree)) |