Skip to content

Commit 1885988

Browse files
1st1pablogsalkumaraditya303ambvsavannahostrowski
authored
GH-91048: Add utils for capturing async call stack for asyncio programs and enable profiling (#124640)
Signed-off-by: Pablo Galindo <[email protected]> Co-authored-by: Pablo Galindo <[email protected]> Co-authored-by: Kumar Aditya <[email protected]> Co-authored-by: Łukasz Langa <[email protected]> Co-authored-by: Savannah Ostrowski <[email protected]> Co-authored-by: Jacob Coffee <[email protected]> Co-authored-by: Irit Katriel <[email protected]>
1 parent 60a3a0d commit 1885988

23 files changed

+2915
-233
lines changed

Doc/library/asyncio-graph.rst

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
.. currentmodule:: asyncio
2+
3+
4+
.. _asyncio-graph:
5+
6+
========================
7+
Call Graph Introspection
8+
========================
9+
10+
**Source code:** :source:`Lib/asyncio/graph.py`
11+
12+
-------------------------------------
13+
14+
asyncio has powerful runtime call graph introspection utilities
15+
to trace the entire call graph of a running *coroutine* or *task*, or
16+
a suspended *future*. These utilities and the underlying machinery
17+
can be used from within a Python program or by external profilers
18+
and debuggers.
19+
20+
.. versionadded:: next
21+
22+
23+
.. function:: print_call_graph(future=None, /, *, file=None, depth=1, limit=None)
24+
25+
Print the async call graph for the current task or the provided
26+
:class:`Task` or :class:`Future`.
27+
28+
This function prints entries starting from the top frame and going
29+
down towards the invocation point.
30+
31+
The function receives an optional *future* argument.
32+
If not passed, the current running task will be used.
33+
34+
If the function is called on *the current task*, the optional
35+
keyword-only *depth* argument can be used to skip the specified
36+
number of frames from top of the stack.
37+
38+
If the optional keyword-only *limit* argument is provided, each call stack
39+
in the resulting graph is truncated to include at most ``abs(limit)``
40+
entries. If *limit* is positive, the entries left are the closest to
41+
the invocation point. If *limit* is negative, the topmost entries are
42+
left. If *limit* is omitted or ``None``, all entries are present.
43+
If *limit* is ``0``, the call stack is not printed at all, only
44+
"awaited by" information is printed.
45+
46+
If *file* is omitted or ``None``, the function will print
47+
to :data:`sys.stdout`.
48+
49+
**Example:**
50+
51+
The following Python code:
52+
53+
.. code-block:: python
54+
55+
import asyncio
56+
57+
async def test():
58+
asyncio.print_call_graph()
59+
60+
async def main():
61+
async with asyncio.TaskGroup() as g:
62+
g.create_task(test())
63+
64+
asyncio.run(main())
65+
66+
will print::
67+
68+
* Task(name='Task-2', id=0x1039f0fe0)
69+
+ Call stack:
70+
| File 't2.py', line 4, in async test()
71+
+ Awaited by:
72+
* Task(name='Task-1', id=0x103a5e060)
73+
+ Call stack:
74+
| File 'taskgroups.py', line 107, in async TaskGroup.__aexit__()
75+
| File 't2.py', line 7, in async main()
76+
77+
.. function:: format_call_graph(future=None, /, *, depth=1, limit=None)
78+
79+
Like :func:`print_call_graph`, but returns a string.
80+
If *future* is ``None`` and there's no current task,
81+
the function returns an empty string.
82+
83+
84+
.. function:: capture_call_graph(future=None, /, *, depth=1, limit=None)
85+
86+
Capture the async call graph for the current task or the provided
87+
:class:`Task` or :class:`Future`.
88+
89+
The function receives an optional *future* argument.
90+
If not passed, the current running task will be used. If there's no
91+
current task, the function returns ``None``.
92+
93+
If the function is called on *the current task*, the optional
94+
keyword-only *depth* argument can be used to skip the specified
95+
number of frames from top of the stack.
96+
97+
Returns a ``FutureCallGraph`` data class object:
98+
99+
* ``FutureCallGraph(future, call_stack, awaited_by)``
100+
101+
Where *future* is a reference to a :class:`Future` or
102+
a :class:`Task` (or their subclasses.)
103+
104+
``call_stack`` is a tuple of ``FrameCallGraphEntry`` objects.
105+
106+
``awaited_by`` is a tuple of ``FutureCallGraph`` objects.
107+
108+
* ``FrameCallGraphEntry(frame)``
109+
110+
Where *frame* is a frame object of a regular Python function
111+
in the call stack.
112+
113+
114+
Low level utility functions
115+
===========================
116+
117+
To introspect an async call graph asyncio requires cooperation from
118+
control flow structures, such as :func:`shield` or :class:`TaskGroup`.
119+
Any time an intermediate :class:`Future` object with low-level APIs like
120+
:meth:`Future.add_done_callback() <asyncio.Future.add_done_callback>` is
121+
involved, the following two functions should be used to inform asyncio
122+
about how exactly such intermediate future objects are connected with
123+
the tasks they wrap or control.
124+
125+
126+
.. function:: future_add_to_awaited_by(future, waiter, /)
127+
128+
Record that *future* is awaited on by *waiter*.
129+
130+
Both *future* and *waiter* must be instances of
131+
:class:`Future` or :class:`Task` or their subclasses,
132+
otherwise the call would have no effect.
133+
134+
A call to ``future_add_to_awaited_by()`` must be followed by an
135+
eventual call to the :func:`future_discard_from_awaited_by` function
136+
with the same arguments.
137+
138+
139+
.. function:: future_discard_from_awaited_by(future, waiter, /)
140+
141+
Record that *future* is no longer awaited on by *waiter*.
142+
143+
Both *future* and *waiter* must be instances of
144+
:class:`Future` or :class:`Task` or their subclasses, otherwise
145+
the call would have no effect.

Doc/library/asyncio.rst

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ You can experiment with an ``asyncio`` concurrent context in the :term:`REPL`:
9999
asyncio-subprocess.rst
100100
asyncio-queue.rst
101101
asyncio-exceptions.rst
102+
asyncio-graph.rst
102103

103104
.. toctree::
104105
:caption: Low-level APIs

Doc/library/inspect.rst

+10
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
150150
| | f_locals | local namespace seen by |
151151
| | | this frame |
152152
+-----------------+-------------------+---------------------------+
153+
| | f_generator | returns the generator or |
154+
| | | coroutine object that |
155+
| | | owns this frame, or |
156+
| | | ``None`` if the frame is |
157+
| | | of a regular function |
158+
+-----------------+-------------------+---------------------------+
153159
| | f_trace | tracing function for this |
154160
| | | frame, or ``None`` |
155161
+-----------------+-------------------+---------------------------+
@@ -310,6 +316,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
310316

311317
Add ``__builtins__`` attribute to functions.
312318

319+
.. versionchanged:: next
320+
321+
Add ``f_generator`` attribute to frames.
322+
313323
.. function:: getmembers(object[, predicate])
314324

315325
Return all the members of an object in a list of ``(name, value)``

Doc/whatsnew/3.14.rst

+5
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,11 @@ asyncio
755755
reduces memory usage.
756756
(Contributed by Kumar Aditya in :gh:`107803`.)
757757

758+
* :mod:`asyncio` has new utility functions for introspecting and printing
759+
the program's call graph: :func:`asyncio.capture_call_graph` and
760+
:func:`asyncio.print_call_graph`.
761+
(Contributed by Yury Selivanov, Pablo Galindo Salgado, and Łukasz Langa
762+
in :gh:`91048`.)
758763

759764
base64
760765
------

Include/internal/pycore_debug_offsets.h

+65
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ extern "C" {
1111

1212
#define _Py_Debug_Cookie "xdebugpy"
1313

14+
#if defined(__APPLE__)
15+
# include <mach-o/loader.h>
16+
#endif
17+
18+
// Macros to burn global values in custom sections so out-of-process
19+
// profilers can locate them easily.
20+
21+
#define GENERATE_DEBUG_SECTION(name, declaration) \
22+
_GENERATE_DEBUG_SECTION_WINDOWS(name) \
23+
_GENERATE_DEBUG_SECTION_APPLE(name) \
24+
declaration \
25+
_GENERATE_DEBUG_SECTION_LINUX(name)
26+
27+
#if defined(MS_WINDOWS)
28+
#define _GENERATE_DEBUG_SECTION_WINDOWS(name) \
29+
_Pragma(Py_STRINGIFY(section(Py_STRINGIFY(name), read, write))) \
30+
__declspec(allocate(Py_STRINGIFY(name)))
31+
#else
32+
#define _GENERATE_DEBUG_SECTION_WINDOWS(name)
33+
#endif
34+
35+
#if defined(__APPLE__)
36+
#define _GENERATE_DEBUG_SECTION_APPLE(name) \
37+
__attribute__((section(SEG_DATA "," Py_STRINGIFY(name))))
38+
#else
39+
#define _GENERATE_DEBUG_SECTION_APPLE(name)
40+
#endif
41+
42+
#if defined(__linux__) && (defined(__GNUC__) || defined(__clang__))
43+
#define _GENERATE_DEBUG_SECTION_LINUX(name) \
44+
__attribute__((section("." Py_STRINGIFY(name))))
45+
#else
46+
#define _GENERATE_DEBUG_SECTION_LINUX(name)
47+
#endif
48+
1449
#ifdef Py_GIL_DISABLED
1550
# define _Py_Debug_gilruntimestate_enabled offsetof(struct _gil_runtime_state, enabled)
1651
# define _Py_Debug_Free_Threaded 1
@@ -69,6 +104,7 @@ typedef struct _Py_DebugOffsets {
69104
uint64_t instr_ptr;
70105
uint64_t localsplus;
71106
uint64_t owner;
107+
uint64_t stackpointer;
72108
} interpreter_frame;
73109

74110
// Code object offset;
@@ -113,6 +149,14 @@ typedef struct _Py_DebugOffsets {
113149
uint64_t ob_size;
114150
} list_object;
115151

152+
// PySet object offset;
153+
struct _set_object {
154+
uint64_t size;
155+
uint64_t used;
156+
uint64_t table;
157+
uint64_t mask;
158+
} set_object;
159+
116160
// PyDict object offset;
117161
struct _dict_object {
118162
uint64_t size;
@@ -153,6 +197,14 @@ typedef struct _Py_DebugOffsets {
153197
uint64_t size;
154198
uint64_t collecting;
155199
} gc;
200+
201+
// Generator object offset;
202+
struct _gen_object {
203+
uint64_t size;
204+
uint64_t gi_name;
205+
uint64_t gi_iframe;
206+
uint64_t gi_frame_state;
207+
} gen_object;
156208
} _Py_DebugOffsets;
157209

158210

@@ -198,6 +250,7 @@ typedef struct _Py_DebugOffsets {
198250
.instr_ptr = offsetof(_PyInterpreterFrame, instr_ptr), \
199251
.localsplus = offsetof(_PyInterpreterFrame, localsplus), \
200252
.owner = offsetof(_PyInterpreterFrame, owner), \
253+
.stackpointer = offsetof(_PyInterpreterFrame, stackpointer), \
201254
}, \
202255
.code_object = { \
203256
.size = sizeof(PyCodeObject), \
@@ -231,6 +284,12 @@ typedef struct _Py_DebugOffsets {
231284
.ob_item = offsetof(PyListObject, ob_item), \
232285
.ob_size = offsetof(PyListObject, ob_base.ob_size), \
233286
}, \
287+
.set_object = { \
288+
.size = sizeof(PySetObject), \
289+
.used = offsetof(PySetObject, used), \
290+
.table = offsetof(PySetObject, table), \
291+
.mask = offsetof(PySetObject, mask), \
292+
}, \
234293
.dict_object = { \
235294
.size = sizeof(PyDictObject), \
236295
.ma_keys = offsetof(PyDictObject, ma_keys), \
@@ -260,6 +319,12 @@ typedef struct _Py_DebugOffsets {
260319
.size = sizeof(struct _gc_runtime_state), \
261320
.collecting = offsetof(struct _gc_runtime_state, collecting), \
262321
}, \
322+
.gen_object = { \
323+
.size = sizeof(PyGenObject), \
324+
.gi_name = offsetof(PyGenObject, gi_name), \
325+
.gi_iframe = offsetof(PyGenObject, gi_iframe), \
326+
.gi_frame_state = offsetof(PyGenObject, gi_frame_state), \
327+
}, \
263328
}
264329

265330

Include/internal/pycore_tstate.h

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ typedef struct _PyThreadStateImpl {
2222
PyThreadState base;
2323

2424
PyObject *asyncio_running_loop; // Strong reference
25+
PyObject *asyncio_running_task; // Strong reference
2526

2627
struct _qsbr_thread_state *qsbr; // only used by free-threaded build
2728
struct llist_node mem_free_queue; // delayed free queue

Lib/asyncio/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .events import *
1111
from .exceptions import *
1212
from .futures import *
13+
from .graph import *
1314
from .locks import *
1415
from .protocols import *
1516
from .runners import *
@@ -27,6 +28,7 @@
2728
events.__all__ +
2829
exceptions.__all__ +
2930
futures.__all__ +
31+
graph.__all__ +
3032
locks.__all__ +
3133
protocols.__all__ +
3234
runners.__all__ +

0 commit comments

Comments
 (0)