feat: add support for weak links#199
Closed
KowalskiThomas wants to merge 1 commit intoP403n1x87:mainfrom
Closed
Conversation
b7462e8 to
8b39b0c
Compare
8b39b0c to
cdcad7c
Compare
KowalskiThomas
added a commit
to DataDog/dd-trace-py
that referenced
this pull request
Jan 16, 2026
## Description Related PRs: - Echion PR: P403n1x87/echion#199 - Depends on: #15789 - https://datadoghq.atlassian.net/browse/PROF-13106 This PR adds support for _weak links_ between `asyncio` Tasks in the Python Profiler. Weak Links (as opposed to _Strong Links_) are links between the Task that creates another Task and the created Task itself. We need Weak Links because without them, creating a Task without awaiting it – or creating a Task without awaiting it _immediately_ – will result in the created Task appearing as "independent" of anything else (because nothing is awaiting it), which will make us show a separate Stack (or really, whole separate Flame Graph) for it. That isn't great in terms of user experience, as we usually make Task relationships appear in the Flame Graph (Stack for Task A awaiting Task B is appended on top of the Stack for Task B). Note that Weak Links are named _Weak Links_ (as opposed to _Strong Links_) because they're only used as a fallback. If a certain Task is awaited by another Task than the one that created it, the Weak Link will not be used (in favour of the "real `await` link). --- Here are screenshots of two Flame Graphs – one before and one after – for the following script ```py import asyncio async def func_not_awaited() -> None: await asyncio.sleep(0.5) async def func_awaited() -> None: await asyncio.sleep(1) async def parent() -> asyncio.Task: t_not_awaited = asyncio.create_task(func_not_awaited(), name="Task-not_awaited") t_awaited = asyncio.create_task(func_awaited(), name="Task-awaited") await t_awaited # At this point, we have not awaited t_not_awaited but it should have finished # before t_awaited as the delay is much shorter. # Returning it to avoid the warning on unused variable. return t_not_awaited def main(): while True: asyncio.run(parent()) if __name__ == "__main__": main() ``` Before the change: `func_not_awaited` gets its own Flame Graph, outside `parent`. <img width="1391" height="127" alt="image" src="https://github.com/user-attachments/assets/db9c804d-eb78-43ad-81f3-650f1b11ed72" /> After the change: even though `func_not_awaited` is run in Task that isn't being awaited by the Task running `parent`, it appears under it because it was created by that coroutine. <img width="1393" height="128" alt="image" src="https://github.com/user-attachments/assets/580ef206-662a-4d00-8ac4-034b6ca8affb" /> ## Testing I added a unit test and tested in staging. ## Performance This change should come at very little (or zero) performance cost. We now do more work than we used to in the Python patches (every `create_task` call is instrumented) but that isn't in the _real_ hot path. On the C++ side of things, the processing is slightly more complex (because we need to keep track of Weak Links on top of the ones we already kept track of before) but the complexity is unchanged and those parts of the code aren't what we spend the better part of our time in today.
tillwf
pushed a commit
to tillwf/dd-trace-py
that referenced
this pull request
Jan 22, 2026
## Description Related PRs: - Echion PR: P403n1x87/echion#199 - Depends on: DataDog#15789 - https://datadoghq.atlassian.net/browse/PROF-13106 This PR adds support for _weak links_ between `asyncio` Tasks in the Python Profiler. Weak Links (as opposed to _Strong Links_) are links between the Task that creates another Task and the created Task itself. We need Weak Links because without them, creating a Task without awaiting it – or creating a Task without awaiting it _immediately_ – will result in the created Task appearing as "independent" of anything else (because nothing is awaiting it), which will make us show a separate Stack (or really, whole separate Flame Graph) for it. That isn't great in terms of user experience, as we usually make Task relationships appear in the Flame Graph (Stack for Task A awaiting Task B is appended on top of the Stack for Task B). Note that Weak Links are named _Weak Links_ (as opposed to _Strong Links_) because they're only used as a fallback. If a certain Task is awaited by another Task than the one that created it, the Weak Link will not be used (in favour of the "real `await` link). --- Here are screenshots of two Flame Graphs – one before and one after – for the following script ```py import asyncio async def func_not_awaited() -> None: await asyncio.sleep(0.5) async def func_awaited() -> None: await asyncio.sleep(1) async def parent() -> asyncio.Task: t_not_awaited = asyncio.create_task(func_not_awaited(), name="Task-not_awaited") t_awaited = asyncio.create_task(func_awaited(), name="Task-awaited") await t_awaited # At this point, we have not awaited t_not_awaited but it should have finished # before t_awaited as the delay is much shorter. # Returning it to avoid the warning on unused variable. return t_not_awaited def main(): while True: asyncio.run(parent()) if __name__ == "__main__": main() ``` Before the change: `func_not_awaited` gets its own Flame Graph, outside `parent`. <img width="1391" height="127" alt="image" src="https://github.com/user-attachments/assets/db9c804d-eb78-43ad-81f3-650f1b11ed72" /> After the change: even though `func_not_awaited` is run in Task that isn't being awaited by the Task running `parent`, it appears under it because it was created by that coroutine. <img width="1393" height="128" alt="image" src="https://github.com/user-attachments/assets/580ef206-662a-4d00-8ac4-034b6ca8affb" /> ## Testing I added a unit test and tested in staging. ## Performance This change should come at very little (or zero) performance cost. We now do more work than we used to in the Python patches (every `create_task` call is instrumented) but that isn't in the _real_ hot path. On the C++ side of things, the processing is slightly more complex (because we need to keep track of Weak Links on top of the ones we already kept track of before) but the complexity is unchanged and those parts of the code aren't what we spend the better part of our time in today.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
https://datadoghq.atlassian.net/browse/PROF-13106