GeezerVison is showing github.com — GH-91048: Add utils for capturing async call stack for asyncio programs and enable profiling by 1st1 · Pull Request #124640 · python/cpython
Conversation
1st1
commented
Sep 26, 2024
•
edited by ambv
Loading
Uh oh!
There was an error while loading. Please reload this page.
This PR introduces low overhead utils to enable async stack reconstruction for running and suspended tasks and futures. The runtime overhead this PR adds is making tasks set & reset a reference to future/tasks they await on. Control flow primitives like asyncio.TaskGroup, asyncio.gather(), asyncio.shield() are also updated to perform this simple bookkeeping.
Remaining to-do for @pablogsal, @ambv, and myself
-
swap_current_task in asynciomodule.c needs to be updated to take care of the new ts->asyncio_running_task
- Add a few tests for eager tasks and how they would interact with this (e.g. running only eager tasks, running eager task within a task, running a task from an eager task from a task, etc.)
-
test_running_loop_within_a_loop seg faults if the test is ran twice (I think), a repro is running ./python.exe -m test test_asyncio -R3:3
-
-R3:3 reports refleaks , might be nothing, but needs to be checked (OK, definitely looks like there's a leak "test_asyncio.test_unix_events leaked [492, 492, 492] references, sum=1476", repro ./python.exe -m test test_asyncio.test_subprocess test_asyncio.test_taskgroups -R3:3)
- Run full perf test again, we might have other regressions besides the already fixed
gather()
- Cover both Python and C implementations (right now only C is being tested)
New APIs:
asyncio.capture_call_graph(*, future=None, depth=1) to capture the call stack for the current task or for the specified task or future
asyncio.print_call_graph(*, future=None, file=None, depth=1) to print the call stack for the current task or for the specified task or future
asyncio.future_add_to_awaited_by() and asyncio.future_discard_from_awaited_by() to enable "stitching" tasks and futures that are awaiting other tasks and futures.
frame.f_generator a getter to return the generator object associated with the frame or None. The implementation is maximally straightforward and does not require any new fields in any of the internal interpreter structs.
New C APIs:
Coroutine and generator structs gain a new pointer cr_task. It is a borrowed pointer to the task that runs the coroutine. This is meant for external profilers to quickly find the associated task (an alternative to this would be a rather costly traversal of interpreter state to find the module state of asyncio, requiring to read many Python dict structs).
A "private" C API function void _PyCoro_SetTask(PyObject *coro, PyObject *task) to set the cr_task field from _asynciomodule.c.
We have a complete functional test for out of process introspection in test_external_inspection.py (sampling profilers and debuggers will follow the same approach).
Example
This example program:
async def deep():
await asyncio.sleep(0)
import pprint
pprint.pp(asyncio.capture_call_graph())
async def c1():
await asyncio.sleep(0)
await deep()
async def c2():
await asyncio.sleep(0)
async def main():
await asyncio.gather(c1(), c2())
asyncio.run(main())
will print:
FrameCallGraphEntry(
future=<Task pending name='Task-2' ...>,
call_stack=[
FrameCallGraphEntry(
frame=...,
FrameCallGraphEntry(
frame=...)
],
awaited_by=[
FrameCallGraphEntry(
future=<Task pending name='Task-1' ...>,
call_stack=[
FrameCallGraphEntry(
frame=...)
],
awaited_by=[]
)
]
)
📚 Documentation preview 📚: https://cpython-previews--124640.org.readthedocs.build/
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
hauntsaninja, soltanoff, and johnslavik reacted with heart emoji
|
I've implemented proof of concept out of process async stack reconstruction here: 1st1@7185f8d
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
mpage
left a comment
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this! Do you know how much overhead the awaiter tracking adds?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
1st1
commented
Oct 2, 2024
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@mpage
Thanks for doing this! Do you know how much overhead the awaiter tracking adds?
I haven't measured, but I don't expect the overhead to be detectable at all. In the most common scenario the tracking is just assigning / resetting a pointer in Task/Future C structs.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
@mpage
Thanks for doing this! Do you know how much overhead the awaiter tracking adds?
I haven't measured, but I don't expect the overhead to be detectable at all. In the most common scenario the tracking is just assigning / resetting a pointer in Task/Future C structs.
I ran some async benchmarks from pyperformance and a small echo tcp server and the Perf impact is below the noise.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
`f_generator` returns the generator / coroutine / async generator
object that owns the frame. For all other kinds of frames it will
return `None`.
This is useful to reconstruct call stack for async/await code.
pablogsal
commented
Jan 22, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@1st1 @ambv I fixed the crashes, refleaks and I did one of the most painful rebases ever, so please let get this merged as soon as possible to avoid collisions with the other work going on :)
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
There have been some changes in asyncio which are relevant for this:
- The C implementation is thread safe so new code should ideally be thread safe as well, thread safety is superficial here as tasks aren't thread safe really, it just shouldn't crash, mostly adding
@critical_section to getter and methods would make it work.
- Add object locked assertion to any internal method if necessary
- There have been discussions about moving current task to per loop for free-threading, that might break the external introspection in future.
I haven't followed on this PR so not sure how many of those points matter here, but still I wrote it incase anyone else missed them.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
- There have been discussions about moving current task to per loop for free-threading, that might break the external introspection in future.
Then we should find some solution that doesn't break the external introspection in the future ;)
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
- The C implementation is thread safe so new code should ideally be thread safe as well, thread safety is superficial here as tasks aren't thread safe really, it just shouldn't crash, mostly adding
@critical_section to getter and methods would make it work.
- Add object locked assertion to any internal method if necessary
This is slightly tricky and if you recall we removed the locks we added at your request. This PR is already unwieldy complicated so I would prefer if we work together in a separate PR to ensure the lock safety is addressed as you would like it
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This is slightly tricky and if you recall we removed the locks we added at your request. This PR is already unwieldy complicated so I would prefer if we work together in a separate PR to ensure the lock safety is addressed as you would like it
Yes, I do remember but it was long time ago 2-3 months, I have made significant changes related to free-threading since that time. It's fine by me to work on free-threading later and merge this first "as-is".
|
ambv and pablogsal reacted with thumbs up emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
Thank you, Kumar. I landed this feature. We've received a ton of review here, definitely more than most changes. We addressed all feedback on design, correctness, and performance. The long-lived branch is hard to keep alive for longer. Thank you to everyone involved, let's evolve this forward from the main branch.
We'll help with #128869 and future free-threading compatibility to make sure external introspection keeps working. As part of the free-threading compatibility of this particular feature, we'll address this in a subsequent PR soon, where we will also add support for eager task external introspection. The new tests added by Kumar in January show some interesting behavior of eager tasks, I'm looking into that. Will provide more information on the subsequent PR.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ambv
added a commit
to ambv/cpython
that referenced
this pull request
Jan 22, 2025
…r tasks
This was missing from pythongh-124640. It's already covered by the new
test_asyncio/test_free_threading.py in combination with the runtime
assertion in set_ts_asyncio_running_task.
|
Not sure if this is directly related, but we're seeing build bot failures now: https://buildbot.python.org/#/builders/125/builds/6894
test_async_gather_remote_stack_trace (test.test_external_inspection.TestGetStackTrace.test_async_gather_remote_stack_trace) ... Fatal Python error: _Py_CheckFunctionResult: a function returned NULL without setting an exception
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Not sure if this is directly related, but we're seeing build bot failures now: https://buildbot.python.org/#/builders/125/builds/6894
test_async_gather_remote_stack_trace (test.test_external_inspection.TestGetStackTrace.test_async_gather_remote_stack_trace) ... Fatal Python error: _Py_CheckFunctionResult: a function returned NULL without setting an exception
On it
|
picnixz and encukou reacted with heart emoji
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
ambv
added a commit
that referenced
this pull request
Jan 23, 2025
#129197)
This was missing from gh-124640. It's already covered by the new
test_asyncio/test_free_threading.py in combination with the runtime
assertion in set_ts_asyncio_running_task.
Co-authored-by: Kumar Aditya <[email protected]>
encukou
commented
Jan 24, 2025
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|
@pablogsal @ambv, this broke several tier-1 buildbots for more than a day so, by the book, it should be reverted. But, I also know there are issues with a buildbot update currently, and reverting & reapplying would be a lot churn in this case.
I'll leave it to you to handle this one.
FWIW, each working day I check there are no other buildbot failures, but I'll be offline during the weekend.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
|
I wrote a PR to fix the test: #129262
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|