You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(profiling): track running asyncio loop if it exists (#15120)
## Description
https://datadoghq.atlassian.net/browse/PROF-12842
This PR updates the wrapping and thread-registering logic for the
Profiler in order to track the running loop when it exists.
This is needed because otherwise, importing/starting the Profiler after
starting a Task (or a loop more generally) will make us blind to the
existing running loop.
Currently, we `wrap` the `asyncio.set_event_loop` function to capture
when the Event Loop is first set (or is swapped). However, if the
`_asyncio` module that sets up wrapping is imported/executed _after_ the
loop has been set, we will miss that first call to `set_event_loop` and
be blind to `asyncio` Tasks until the Event Loop is changed (which in
many cases never happens).
Note that we also need to execute the "find loop and track it" logic
when we start the Profiler generally speaking, as in this case we may
have tried (earlier) to call `track_event_loop` but that would have
failed as no thread was registered in the Profiler.
I added four tests that account for various edge cases. Unfortunately,
currently, two of them fail (marked them as `xfail`) and there is no way
to correctly fix them. The issue is that we can only get _the current
running loop_ and not _the current (non-running) event loop_.
In other words, if an event loop is created and set in `asyncio`, and
immediately after the Profiler is started without a Task having first
been started, we will not be able to see that loop from the
initialisation code and we will thus not be able to observe it from the
Profiler thread.
In short, what works is the most common case:
* ✅ Import Profiler, start Profiler, import asyncio, start Tasks
* ✅ Import asyncio, Import Profiler, start Profiler, start Tasks
* ✅ Import asyncio, Import Profiler, start Tasks (from within the Tasks)
* 🚫 Import asyncio, Import Profiler, create (non running) event loop,
start Profiler, start Task
* 🚫 Import asyncio, Import Profiler, create (non running) event loop,
create Task, start Profiler
It is OK to start with that as I really consider the latter two to be
edge cases.
**Example: today we miss all `asyncio` data with the following code**
```py
# 0. Profiler is NOT imported here, no watching is set up
import os
import asyncio
async def my_coroutine(n):
await asyncio.sleep(n)
# 0. Function is defined, not run, Profiler is still not imported
async def main():
# 3. We get here, import the Profiler module (and _asyncio as well)
# We also start watching for set_event_loop_calls – we don't see the existing loop
from ddtrace.profiling import Profiler
prof = Profiler()
prof.start() # Should be as early as possible, eg before other imports, to ensure everything is profiled
EXECUTION_TIME_SEC = int(os.environ.get("EXECUTION_TIME_SEC", "2"))
t = asyncio.create_task(my_coroutine(EXECUTION_TIME_SEC / 2))
await asyncio.gather(t, my_coroutine(EXECUTION_TIME_SEC))
# 4. Interestingly, we detect a set_event_loop call here, but it's
# being set to None before exiting
# 1. This is executed first
if __name__ == "__main__":
# 2. This implicitly creates and set the Event Loop
asyncio.run(main())
```
## Testing
I have tested this in `prof-correctness` (initially just replicated that
it _did not_ work) and it now works as expected. I will be adding more
correctness tests, one with a "top of file" import and Profiler start,
one with a "top of file import" and "in-code Profiler start", and one
with both an "in-code file import" and "in-code Profiler start".
I also added four new tests to make sure we catch different edge cases
with order of imports and order of task/profiler starts. Currently, two
of them are marked as `XFAILED` because there is no way to reliably make
them pass.
0 commit comments