Skip to content

feat: add support for weak links#199

Closed
KowalskiThomas wants to merge 1 commit intoP403n1x87:mainfrom
KowalskiThomas:kowalski/feat-add-support-for-weak-links
Closed

feat: add support for weak links#199
KowalskiThomas wants to merge 1 commit intoP403n1x87:mainfrom
KowalskiThomas:kowalski/feat-add-support-for-weak-links

Conversation

@KowalskiThomas
Copy link
Collaborator

@KowalskiThomas KowalskiThomas commented Dec 1, 2025

https://datadoghq.atlassian.net/browse/PROF-13106

⚠️ This is a WIP that cannot be merged until #198 has been.

@KowalskiThomas KowalskiThomas force-pushed the kowalski/feat-add-support-for-weak-links branch 13 times, most recently from b7462e8 to 8b39b0c Compare December 3, 2025 15:01
@KowalskiThomas KowalskiThomas force-pushed the kowalski/feat-add-support-for-weak-links branch from 8b39b0c to cdcad7c Compare December 26, 2025 11:44
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant