@@ -36,119 +36,45 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
3636 yield frames , thread_info .thread_id
3737
3838 def _iter_async_frames (self , awaited_info_list ):
39- """
40- Iterate over all async frame stacks from awaited info.
41- """
42- # First, index all tasks by their IDs so we can look up parents easily
4339 all_tasks = {}
44- tasks_by_name = {}
40+
4541 for awaited_info in awaited_info_list :
42+ thread_id = awaited_info .thread_id
4643 for task_info in awaited_info .awaited_by :
47- all_tasks [task_info .task_id ] = (task_info , awaited_info .thread_id )
48- display_name = task_info .task_name or f"Task-{ task_info .task_id } "
49- tasks_by_name .setdefault (display_name , []).append (
50- (task_info , awaited_info .thread_id )
51- )
52- fallback_name = f"Task-{ task_info .task_id } "
53- if fallback_name != display_name :
54- tasks_by_name .setdefault (fallback_name , []).append (
55- (task_info , awaited_info .thread_id )
56- )
57-
58- # Use a cache for memoizing parent chains so we don't recompute them repeatedly
59- cache = {}
60- root_frame = FrameInfo (("<root>" , 0 , "<all tasks>" ))
61-
62- def build_parent_chain (task_id , parent_name , thread_id , await_frames ):
63- """
64- Recursively build the parent chain for a given task by:
65- - Finding the parent's await-site frames
66- - Recursing up the parent chain until reaching Program Root
67- - Add Program Root at the top of the chain
68- - Cache results along the way to avoid redundant work
69- """
70- def frame_signature (frame ):
71- func = getattr (frame , "function" , None )
72- if func is None :
73- func = getattr (frame , "funcname" , None )
74- return (
75- getattr (frame , "filename" , None ),
76- getattr (frame , "lineno" , None ),
77- func ,
78- )
79-
80- frames_signature = tuple (
81- frame_signature (frame ) for frame in await_frames or []
82- )
83- cache_key = (task_id , parent_name , thread_id , frames_signature )
84- if cache_key in cache :
85- return cache [cache_key ]
86-
87- if not parent_name :
88- chain = list (await_frames or []) + [root_frame ]
89- cache [cache_key ] = chain
90- return chain
91-
92- parent_entry = None
93- for candidate_info , candidate_tid in tasks_by_name .get (parent_name , []):
94- if candidate_tid == thread_id :
95- parent_entry = (candidate_info , candidate_tid )
96- break
97-
98- if parent_entry is None :
99- chain = list (await_frames or []) + [root_frame ]
100- cache [cache_key ] = chain
101- return chain
102-
103- parent_info , parent_thread = parent_entry
104-
105- # Recursively build grandparent chain, or terminate with Program Root
106- grandparent_chain = resolve_parent_chain (
107- parent_info .task_id , parent_thread , parent_info .awaited_by
108- )
109- chain = list (await_frames or []) + grandparent_chain
110-
111- cache [cache_key ] = chain
112- return chain
113-
114- def resolve_parent_chain (task_id , thread_id , awaited_by_list ):
115- """Find the best available parent chain for the given task.
116- Best means the longest chain (most frames) among all possible parents."""
117- best_chain = [root_frame ]
118- for coro_info in awaited_by_list or []:
119- parent_name = coro_info .task_name
120- await_frames = list (coro_info .call_stack or [])
121- candidate = build_parent_chain (
122- task_id ,
123- parent_name ,
124- thread_id ,
125- await_frames ,
126- )
127- if len (candidate ) > len (best_chain ):
128- best_chain = candidate
129- if len (best_chain ) > 1 :
130- break
131- return best_chain
132-
133- # Yield one complete stack per task in LEAF→ROOT order
44+ all_tasks [task_info .task_id ] = (task_info , thread_id )
45+
46+ # For each task, reconstruct the full call stack by following coroutine chains
13447 for task_id , (task_info , thread_id ) in all_tasks .items ():
135- # Start with the task's own body frames (deepest frames first)
136- body_frames = [
48+ frames = [
13749 frame
138- for coro in ( task_info .coroutine_stack or [])
139- for frame in ( coro .call_stack or [])
50+ for coro in task_info .coroutine_stack
51+ for frame in coro .call_stack
14052 ]
14153
142- if task_info .awaited_by :
143- # Add synthetic frame for the task itself
144- task_name = task_info .task_name or f"Task-{ task_id } "
145- synthetic = FrameInfo (("<task>" , 0 , f"running { task_name } " ))
146-
147- # Append parent chain (await-site frames + parents recursively)
148- parent_chain = resolve_parent_chain (
149- task_id , thread_id , task_info .awaited_by
150- )
151- yield body_frames + [synthetic ] + parent_chain , task_id
152- else :
153- # Root task: no synthetic marker needed, just add root marker
154- yield body_frames + [root_frame ], task_id
54+ task_name = task_info .task_name or f"Task-{ task_id } "
55+ synthetic_frame = FrameInfo (("<task>" , 0 , task_name ))
56+ frames .append (synthetic_frame )
57+
58+ current_parents = task_info .awaited_by
59+ visited = set ()
60+
61+ while current_parents :
62+ next_parents = []
63+ for parent_coro in current_parents :
64+ frames .extend (parent_coro .call_stack )
65+ parent_task_id = parent_coro .task_name
66+
67+ if parent_task_id in visited or parent_task_id not in all_tasks :
68+ continue
69+ visited .add (parent_task_id )
70+
71+ parent_task_info , _ = all_tasks [parent_task_id ]
72+
73+ parent_name = parent_task_info .task_name or f"Task-{ parent_task_id } "
74+ synthetic_parent_frame = FrameInfo (("<task>" , 0 , parent_name ))
75+ frames .append (synthetic_parent_frame )
76+
77+ if parent_task_info .awaited_by :
78+ next_parents .extend (parent_task_info .awaited_by )
79+ current_parents = next_parents
80+ yield frames , thread_id , task_id
0 commit comments