33import threading
44import gc
55
6- from types import FrameType
6+ from types import (
7+ AsyncGeneratorType ,
8+ FrameType
9+ )
710from typing import (
811 List ,
912 Tuple ,
@@ -108,17 +111,6 @@ def _get_idle_task_frames(loop) -> List[FrameType]:
108111 if frame :
109112 idle .append (cast (FrameType , frame ))
110113
111- # TODO
112- # handle async generators
113- # ideally, we would access these from _get_deepest_traceable_frame.
114- # doing it this way causes us to also assign the generator's time to
115- # the coroutine that called this generator in
116- # _get_deepest_traceable_frame
117- for ag in loop ._asyncgens :
118- f = getattr (ag , 'ag_frame' , None )
119- if f and \
120- ScaleneAsyncio .should_trace (f .f_code .co_filename , f .f_code .co_name ):
121- idle .append (cast (FrameType , f ))
122114 return idle
123115
124116 @staticmethod
@@ -134,41 +126,53 @@ def _get_deepest_traceable_frame(coro) -> FrameType:
134126 deepest_frame = None
135127 while curr :
136128 frame = getattr (curr , 'cr_frame' , None )
129+
137130 if not frame :
138- break
131+ curr = ScaleneAsyncio ._search_awaitable (curr )
132+ if isinstance (curr , AsyncGeneratorType ):
133+ frame = getattr (curr , 'ag_frame' , None )
134+ else :
135+ break
136+
139137 if ScaleneAsyncio .should_trace (frame .f_code .co_filename ,
140138 frame .f_code .co_name ):
141139 deepest_frame = frame
142- curr = getattr (curr , 'cr_await' , None )
140+
141+ if isinstance (curr , AsyncGeneratorType ):
142+ curr = getattr (curr , 'ag_await' , None )
143+ else :
144+ curr = getattr (curr , 'cr_await' , None )
143145
144146 # if this task is found to point to another task we're profiling,
145147 # then we will get the deepest frame later and should return nothing.
146- if curr and any (
147- ScaleneAsyncio ._should_trace_task (task )
148- for task in ScaleneAsyncio ._try_link_tasks (curr )
149- ):
150- return None
148+ # this is specific to gathering futures, i.e., gather statement.
149+ if isinstance (curr , asyncio .Future ):
150+ tasks = getattr (curr , '_children' , [])
151+ if any (
152+ ScaleneAsyncio ._should_trace_task (task )
153+ for task in tasks
154+ ):
155+ return None
151156
152157 return deepest_frame
153158
154159 @staticmethod
155- def _try_link_tasks (awaitable ) -> List [asyncio .Task ]:
156- """Given an AWAITABLE which is not a coroutine, assume it is a future
157- and attempt to find references to which tasks it is waiting for."""
158-
160+ def _search_awaitable (awaitable ):
161+ """Given an awaitable which is not a coroutine, assume it is a future
162+ and attempt to find references to further futures or async generators.
163+ """
164+ future = None
159165 if not isinstance (awaitable , asyncio .Future ):
160- # TODO some wrappers like _asyncio.FutureIter get caught here,
161- # I am not sure if a more robust approach is necessary
166+ # TODO some wrappers like _asyncio.FutureIter,
167+ # async_generator_asend get caught here, I am not sure if a more
168+ # robust approach is necessary
162169
163170 # can gc be avoided here?
164171 refs = gc .get_referents (awaitable )
165172 if refs :
166- awaitable = refs [0 ]
167-
168- if not isinstance (awaitable , asyncio .Future ):
169- return []
173+ future = refs [0 ]
170174
171- return getattr ( awaitable , '_children' , [])
175+ return future
172176
173177 @staticmethod
174178 def _should_trace_task (task ) -> bool :
@@ -177,6 +181,9 @@ def _should_trace_task(task) -> bool:
177181 A task is interesting if it is not the current task, if it has actually
178182 started executing, and if a child task did not originate from it.
179183 """
184+ if not isinstance (task , asyncio .Task ):
185+ return False
186+
180187 # the task is not idle
181188 if task == ScaleneAsyncio .current_task :
182189 return False
0 commit comments