Skip to content

Commit 8791e75

Browse files
committed
Exhaustively search async generators, fix asyncgen double assignment
1 parent 4ee2486 commit 8791e75

1 file changed

Lines changed: 37 additions & 30 deletions

File tree

scalene/scalene_asyncio.py

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import threading
44
import gc
55

6-
from types import FrameType
6+
from types import (
7+
AsyncGeneratorType,
8+
FrameType
9+
)
710
from 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

Comments
 (0)