-
-
Notifications
You must be signed in to change notification settings - Fork 429
asyncio extension #930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
bdunahu
wants to merge
14
commits into
plasma-umass:master
Choose a base branch
from
bdunahu:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
asyncio extension #930
Changes from 6 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ca2ab3a
Redirect python shebang to use `env'
ee06f3a
Initial methods to collect idle asyncio task frames
1e18c76
Initial logic to incorporate profiling idle tasks
4af02e4
Correctly discard frames from tasks which call other tasks
4ee2486
Remove some leftover duplicated logic from _get_idle_task_frames
8791e75
Exhaustively search async generators, fix asyncgen double assignment
c0b97dc
Address request for type checks in search_awaitable (now trace_down)
b5bf0da
(Fix?) Ensure task is not assigned time when waiting on current task
5ed76cc
Do not factor in thread information when adding time to idle tasks
ce8be5a
Add safety for when new_frames are empty, idle_async_frames are not
d10fed4
Do not profile frames if they belong to an event loop without a task
630a2dc
Fix typing inconsistency on ScaleneAsyncio.loops
0a6e836
New metric to output percentages: total samples, not total time
c49a691
Readd samples belonging to the event loop.
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import asyncio | ||
| import sys | ||
| import threading | ||
| import gc | ||
|
|
||
| from types import ( | ||
| AsyncGeneratorType, | ||
| FrameType | ||
| ) | ||
| from typing import ( | ||
| List, | ||
| Tuple, | ||
| cast, | ||
| ) | ||
|
|
||
|
|
||
| class ScaleneAsyncio: | ||
| """Provides a set of methods to collect idle task frames.""" | ||
|
|
||
| should_trace = None | ||
| loops: List[Tuple[asyncio.AbstractEventLoop, int]] = [] | ||
| current_task = None | ||
|
|
||
| @staticmethod | ||
| def current_task_exists(tident) -> bool: | ||
| """Given TIDENT, returns true if a current task exists. Returns | ||
| true if no event loop is running on TIDENT.""" | ||
| current = True | ||
| for loop, t in ScaleneAsyncio.loops: | ||
| if t == tident: | ||
| current = asyncio.current_task(loop) | ||
| break | ||
| return bool(current) | ||
|
|
||
| @staticmethod | ||
| def compute_suspended_frames_to_record(should_trace) -> \ | ||
| List[Tuple[FrameType, int, FrameType]]: | ||
| """Collect all frames which belong to suspended tasks.""" | ||
| # TODO this is an ugly way to access the function | ||
| ScaleneAsyncio.should_trace = should_trace | ||
| ScaleneAsyncio.loops = ScaleneAsyncio._get_event_loops() | ||
|
|
||
| return ScaleneAsyncio._get_frames_from_loops(ScaleneAsyncio.loops) | ||
|
|
||
| @staticmethod | ||
| def _get_event_loops() -> List[Tuple[asyncio.AbstractEventLoop, int]]: | ||
| """Returns each thread's event loop. If there are none, returns | ||
| the empty array.""" | ||
| loops = [] | ||
| for t in threading.enumerate(): | ||
| frame = sys._current_frames().get(t.ident) | ||
| if frame: | ||
| loop = ScaleneAsyncio._walk_back_until_loop(frame) | ||
| # duplicates shouldn't be possible, but just in case... | ||
| if loop and loop not in loops: | ||
| loops.append((loop, cast(int, t.ident))) | ||
| return loops | ||
|
|
||
| @staticmethod | ||
| def _walk_back_until_loop(frame) -> asyncio.AbstractEventLoop: | ||
| """Helper for get_event_loops. | ||
| Walks back the callstack until we are in a method named '_run_once'. | ||
| If this becomes true and the 'self' variable is an instance of | ||
| AbstractEventLoop, then we return that variable. | ||
| This works because _run_once is one of the main methods asyncio uses | ||
| to facilitate its event loop, and is always on the stack while the | ||
| loop runs.""" | ||
| while frame: | ||
| if frame.f_code.co_name == '_run_once' and \ | ||
| 'self' in frame.f_locals: | ||
| loop = frame.f_locals['self'] | ||
| if isinstance(loop, asyncio.AbstractEventLoop): | ||
| return loop | ||
| else: | ||
| frame = frame.f_back | ||
| return None | ||
|
|
||
| @staticmethod | ||
| def _get_frames_from_loops(loops) -> \ | ||
| List[Tuple[FrameType, int, FrameType]]: | ||
| """Given LOOPS, returns a flat list of frames corresponding to idle | ||
| tasks.""" | ||
| return [ | ||
| (frame, tident, None) for loop, tident in loops | ||
| for frame in ScaleneAsyncio._get_idle_task_frames(loop) | ||
| ] | ||
|
|
||
| @staticmethod | ||
| def _get_idle_task_frames(loop) -> List[FrameType]: | ||
| """Given an asyncio event loop, returns the list of idle task frames. | ||
| We only care about idle task frames, as running tasks are already | ||
| included elsewhere. | ||
| A task is considered 'idle' if it is pending and not the current | ||
| task.""" | ||
| idle = [] | ||
|
|
||
| # set this when we start processing a loop. | ||
| # it is required later, but I only want to set it once. | ||
| ScaleneAsyncio.current_task = asyncio.current_task(loop) | ||
|
|
||
| for task in asyncio.all_tasks(loop): | ||
| if not ScaleneAsyncio._should_trace_task(task): | ||
| continue | ||
|
|
||
| coro = task.get_coro() | ||
|
|
||
| frame = ScaleneAsyncio._get_deepest_traceable_frame(coro) | ||
| if frame: | ||
| idle.append(cast(FrameType, frame)) | ||
|
|
||
| return idle | ||
|
|
||
| @staticmethod | ||
| def _get_deepest_traceable_frame(coro) -> FrameType: | ||
| """Get the deepest frame of coro we care to trace. | ||
| This is possible because each corooutine keeps a reference to the | ||
| coroutine it is waiting on. | ||
| Note that it cannot be the case that a task is suspended in a frame | ||
| that does not belong to a coroutine, asyncio is very particular about | ||
| that! This is also why we only track idle tasks this way.""" | ||
| curr = coro | ||
| deepest_frame = None | ||
| while curr: | ||
| frame = getattr(curr, 'cr_frame', None) | ||
|
|
||
| if not frame: | ||
| curr = ScaleneAsyncio._search_awaitable(curr) | ||
| if isinstance(curr, AsyncGeneratorType): | ||
| frame = getattr(curr, 'ag_frame', None) | ||
| else: | ||
| break | ||
|
|
||
| if ScaleneAsyncio.should_trace(frame.f_code.co_filename, | ||
| frame.f_code.co_name): | ||
| deepest_frame = frame | ||
|
|
||
| if isinstance(curr, AsyncGeneratorType): | ||
| curr = getattr(curr, 'ag_await', None) | ||
| else: | ||
| curr = getattr(curr, 'cr_await', None) | ||
|
|
||
| # if this task is found to point to another task we're profiling, | ||
| # then we will get the deepest frame later and should return nothing. | ||
| # this is specific to gathering futures, i.e., gather statement. | ||
| if isinstance(curr, asyncio.Future): | ||
| tasks = getattr(curr, '_children', []) | ||
| if any( | ||
| ScaleneAsyncio._should_trace_task(task) | ||
| for task in tasks | ||
| ): | ||
| return None | ||
|
|
||
| return deepest_frame | ||
|
|
||
| @staticmethod | ||
| def _search_awaitable(awaitable): | ||
| """Given an awaitable which is not a coroutine, assume it is a future | ||
| and attempt to find references to further futures or async generators. | ||
| """ | ||
| future = None | ||
| if not isinstance(awaitable, asyncio.Future): | ||
| # TODO some wrappers like _asyncio.FutureIter, | ||
| # async_generator_asend get caught here, I am not sure if a more | ||
| # robust approach is necessary | ||
|
|
||
| # can gc be avoided here? | ||
| refs = gc.get_referents(awaitable) | ||
| if refs: | ||
| future = refs[0] | ||
bdunahu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return future | ||
|
|
||
| @staticmethod | ||
| def _should_trace_task(task) -> bool: | ||
| """Returns FALSE if TASK is uninteresting to the user. | ||
| A task is interesting if it is not the current task, if it has actually | ||
| started executing, and if a child task did not originate from it. | ||
| """ | ||
| if not isinstance(task, asyncio.Task): | ||
| return False | ||
|
|
||
| # the task is not idle | ||
| if task == ScaleneAsyncio.current_task: | ||
| return False | ||
|
|
||
| coro = task.get_coro() | ||
|
|
||
| # the task hasn't even run yet | ||
| # assumes that all started tasks are sitting at an await | ||
| # statement. | ||
| # if this isn't the case, the associated coroutine will | ||
| # be 'waiting' on the coroutine declaration. No! Bad! | ||
| if getattr(coro, 'cr_frame', None) is None or \ | ||
| getattr(coro, 'cr_await', None) is None: | ||
| return False | ||
|
|
||
| frame = getattr(coro, 'cr_frame', None) | ||
|
|
||
| return ScaleneAsyncio.should_trace(frame.f_code.co_filename, | ||
| frame.f_code.co_name) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.