-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
GH-91048: Add utils for capturing async call stack for asyncio programs and enable profiling #124640
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
GH-91048: Add utils for capturing async call stack for asyncio programs and enable profiling #124640
Changes from 48 commits
1b01a91
0fc5511
1d20a51
c8be18e
abf2cb9
20ceab7
72d9321
c9475f6
e1099e9
817f88b
54386ac
98434f0
485c166
8802be7
1ddc9cf
bc9beb8
fd141d4
2d72f24
391defa
d6357fd
bb3b6df
54c99ec
c1a4f09
027d522
c2d5ec6
08d09eb
fe3113b
18ec26d
e4cc462
d5cdc36
83606f2
5edac41
8dc6d34
30884ea
1317658
81b0a31
258ce3d
b9ecefb
b77dcb0
8867946
87d2524
b47bef1
230b7ec
b1d6158
ac51364
c7e59eb
9eba5e1
59121f6
f8f48f0
74c5ad1
067c043
9f04911
0774805
3048493
1f42873
7799391
03ed5c1
21f9ea9
8a43dfa
b3fae68
d0aedf0
df0032a
0ce241b
8f126f6
966d84e
f56468a
404b88a
911fed8
ab511a4
c3c685a
785adeb
a577328
064129a
ce332d9
d6d943f
703ff46
e867863
9cb5b29
61b2b7b
9533ab9
596191d
ad9152e
066bf21
4caeec4
38f061d
a8dd667
cf8f5e5
eda9c7c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
.. currentmodule:: asyncio | ||
|
||
|
||
.. _asyncio-stack: | ||
|
||
=================== | ||
Stack Introspection | ||
=================== | ||
|
||
**Source code:** :source:`Lib/asyncio/stack.py` | ||
|
||
------------------------------------- | ||
|
||
asyncio has powerful runtime call stack introspection utilities | ||
to trace the entire call graph of a running coroutine or task, or | ||
a suspended *future*. These utilities and the underlying machinery | ||
can be used by users in their Python code or by external profilers | ||
and debuggers. | ||
|
||
.. versionadded:: 3.14 | ||
|
||
|
||
.. function:: print_call_graph(*, future=None, file=None, depth=1) | ||
|
||
Print the async call graph for the current task or the provided | ||
:class:`Task` or :class:`Future`. | ||
|
||
The function receives an optional keyword-only *future* argument. | ||
If not passed, the current running task will be used. If there's no | ||
current task, the function returns ``None``. | ||
|
||
If the function is called on *the current task*, the optional | ||
keyword-only *depth* argument can be used to skip the specified | ||
number of frames from top of the stack. | ||
|
||
If *file* is not specified the function will print to :data:`sys.stdout`. | ||
|
||
**Example:** | ||
|
||
The following Python code: | ||
|
||
.. code-block:: python | ||
|
||
import asyncio | ||
|
||
async def test(): | ||
asyncio.print_call_graph() | ||
|
||
async def main(): | ||
async with asyncio.TaskGroup() as g: | ||
g.create_task(test()) | ||
|
||
asyncio.run(main()) | ||
|
||
will print:: | ||
|
||
* Task(name='Task-2', id=0x1039f0fe0) | ||
+ Call stack: | ||
| File 't2.py', line 4, in async test() | ||
+ Awaited by: | ||
* Task(name='Task-1', id=0x103a5e060) | ||
+ Call stack: | ||
| File 'taskgroups.py', line 107, in async TaskGroup.__aexit__() | ||
| File 't2.py', line 7, in async main() | ||
|
||
For rendering the call stack to a string the following pattern | ||
should be used: | ||
|
||
.. code-block:: python | ||
|
||
import io | ||
|
||
... | ||
|
||
buf = io.StringIO() | ||
asyncio.print_call_graph(file=buf) | ||
output = buf.getvalue() | ||
|
||
|
||
.. function:: capture_call_graph(*, future=None) | ||
|
||
Capture the async call graph for the current task or the provided | ||
:class:`Task` or :class:`Future`. | ||
|
||
The function receives an optional keyword-only *future* argument. | ||
If not passed, the current running task will be used. If there's no | ||
current task, the function returns ``None``. | ||
|
||
If the function is called on *the current task*, the optional | ||
keyword-only ``depth`` argument can be used to skip the specified | ||
number of frames from top of the stack. | ||
|
||
Returns a ``FutureCallGraph`` data class object: | ||
|
||
* ``FutureCallGraph(future, call_stack, awaited_by)`` | ||
|
||
Where 'future' is a reference to a *Future* or a *Task* | ||
(or their subclasses.) | ||
|
||
``call_stack`` is a list of ``FrameCallGraphEntry`` objects. | ||
|
||
``awaited_by`` is a list of ``FutureCallGraph`` objects. | ||
|
||
* ``FrameCallGraphEntry(frame)`` | ||
|
||
Where ``frame`` is a frame object of a regular Python function | ||
in the call stack. | ||
|
||
|
||
Low level utility functions | ||
=========================== | ||
|
||
To introspect an async call graph asyncio requires cooperation from | ||
control flow structures, such as :func:`shield` or :class:`TaskGroup`. | ||
Any time an intermediate ``Future`` object with low-level APIs like | ||
:meth:`Future.add_done_callback() <asyncio.Future.add_done_callback>` is | ||
involved, the following two functions should be used to inform *asyncio* | ||
about how exactly such intermediate future objects are connected with | ||
the tasks they wrap or control. | ||
|
||
|
||
.. function:: future_add_to_awaited_by(future, waiter, /) | ||
|
||
Record that *future* is awaited on by *waiter*. | ||
|
||
Both *future* and *waiter* must be instances of | ||
:class:`asyncio.Future <Future>` or :class:`asyncio.Task <Task>` or | ||
their subclasses, otherwise the call would have no effect. | ||
1st1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
A call to ``future_add_to_awaited_by()`` must be followed by an | ||
eventual call to the ``future_discard_from_awaited_by()`` function | ||
with the same arguments. | ||
|
||
|
||
.. function:: future_discard_from_awaited_by(future, waiter, /) | ||
|
||
Record that *future* is no longer awaited on by *waiter*. | ||
|
||
Both *future* and *waiter* must be instances of | ||
:class:`asyncio.Future <Future>` or :class:`asyncio.Task <Task>` or | ||
their subclasses, otherwise the call would have no effect. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,13 @@ extern "C" { | |
PyObject *prefix##_qualname; \ | ||
_PyErr_StackItem prefix##_exc_state; \ | ||
PyObject *prefix##_origin_or_finalizer; \ | ||
/* A *borrowed* reference to a task that drives the coroutine. \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is adding yet another field generators for the use of async. Is there no way to add this tasks, not generators? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I don't see how it is safe for this to be a borrowed reference. What guarantee is there that the object passed to If the answer is "_PyCoro_SetTask is an internal function", then why is it used by asyncio which is a module? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
AFAICT there is too much entanglement between generators and coroutines to split their implementation entirely to avoid this. How can we measure what this unwanted expense actually is? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Splitting their implementations is desirable, but I appreciate that it won't happen in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That means that it not only does it takes space and needs initializing, it will also need to be cleared when the generator is destroyed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @markshannon we agree with your feedback. I'll refactor the PR to remove the need for |
||
The field is meant to be used by profilers and debuggers only. \ | ||
The main invariant is that a task can't get GC'ed while \ | ||
the coroutine it drives is alive and vice versa. \ | ||
Profilers can use this field to reconstruct the full async \ | ||
call stack of program. */ \ | ||
PyObject *prefix##_task; \ | ||
char prefix##_hooks_inited; \ | ||
char prefix##_closed; \ | ||
char prefix##_running_async; \ | ||
|
Uh oh!
There was an error while loading. Please reload this page.