Skip to content

Commit 6a3b06f

Browse files
committed
Add a test to exercise asyncio stack traces in out-of-process profilers
Signed-off-by: Pablo Galindo <[email protected]>
1 parent cfa97d3 commit 6a3b06f

File tree

5 files changed

+1112
-158
lines changed

5 files changed

+1112
-158
lines changed

Include/internal/pycore_runtime.h

+16
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ typedef struct _Py_DebugOffsets {
108108
uint64_t instr_ptr;
109109
uint64_t localsplus;
110110
uint64_t owner;
111+
uint64_t stackpointer;
111112
} interpreter_frame;
112113

113114
// Code object offset;
@@ -152,6 +153,13 @@ typedef struct _Py_DebugOffsets {
152153
uint64_t ob_size;
153154
} list_object;
154155

156+
// PySet object offset;
157+
struct _set_object {
158+
uint64_t size;
159+
uint64_t used;
160+
uint64_t table;
161+
} set_object;
162+
155163
// PyDict object offset;
156164
struct _dict_object {
157165
uint64_t size;
@@ -192,6 +200,14 @@ typedef struct _Py_DebugOffsets {
192200
uint64_t size;
193201
uint64_t collecting;
194202
} gc;
203+
204+
struct _gen_object {
205+
uint64_t size;
206+
uint64_t gi_name;
207+
uint64_t gi_iframe;
208+
uint64_t gi_task;
209+
uint64_t gi_frame_state;
210+
} gen_object;
195211
} _Py_DebugOffsets;
196212

197213
/* Reference tracer state */

Include/internal/pycore_runtime_init.h

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern "C" {
2121
#include "pycore_runtime_init_generated.h" // _Py_bytes_characters_INIT
2222
#include "pycore_signal.h" // _signals_RUNTIME_INIT
2323
#include "pycore_tracemalloc.h" // _tracemalloc_runtime_state_INIT
24+
#include "pycore_genobject.h"
2425

2526

2627
extern PyTypeObject _PyExc_MemoryError;
@@ -73,6 +74,7 @@ extern PyTypeObject _PyExc_MemoryError;
7374
.instr_ptr = offsetof(_PyInterpreterFrame, instr_ptr), \
7475
.localsplus = offsetof(_PyInterpreterFrame, localsplus), \
7576
.owner = offsetof(_PyInterpreterFrame, owner), \
77+
.stackpointer = offsetof(_PyInterpreterFrame, stackpointer), \
7678
}, \
7779
.code_object = { \
7880
.size = sizeof(PyCodeObject), \
@@ -106,6 +108,11 @@ extern PyTypeObject _PyExc_MemoryError;
106108
.ob_item = offsetof(PyListObject, ob_item), \
107109
.ob_size = offsetof(PyListObject, ob_base.ob_size), \
108110
}, \
111+
.set_object = { \
112+
.size = sizeof(PySetObject), \
113+
.used = offsetof(PySetObject, used), \
114+
.table = offsetof(PySetObject, table), \
115+
}, \
109116
.dict_object = { \
110117
.size = sizeof(PyDictObject), \
111118
.ma_keys = offsetof(PyDictObject, ma_keys), \
@@ -135,6 +142,13 @@ extern PyTypeObject _PyExc_MemoryError;
135142
.size = sizeof(struct _gc_runtime_state), \
136143
.collecting = offsetof(struct _gc_runtime_state, collecting), \
137144
}, \
145+
.gen_object = { \
146+
.size = sizeof(PyGenObject), \
147+
.gi_name = offsetof(PyGenObject, gi_name), \
148+
.gi_iframe = offsetof(PyGenObject, gi_iframe), \
149+
.gi_task = offsetof(PyGenObject, gi_task), \
150+
.gi_frame_state = offsetof(PyGenObject, gi_frame_state), \
151+
}, \
138152
}, \
139153
.allocators = { \
140154
.standard = _pymem_allocators_standard_INIT(runtime), \

Lib/test/test_external_inspection.py

+74
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
try:
1414
from _testexternalinspection import PROCESS_VM_READV_SUPPORTED
1515
from _testexternalinspection import get_stack_trace
16+
from _testexternalinspection import get_async_stack_trace
1617
except ImportError:
1718
raise unittest.SkipTest("Test only runs when _testexternalinspection is available")
1819

@@ -74,6 +75,79 @@ def foo():
7475
]
7576
self.assertEqual(stack_trace, expected_stack_trace)
7677

78+
@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
79+
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
80+
def test_async_remote_stack_trace(self):
81+
# Spawn a process with some realistic Python code
82+
script = textwrap.dedent("""\
83+
import asyncio
84+
import time
85+
import os
86+
import sys
87+
import test.test_asyncio.test_stack as ts
88+
89+
def c5():
90+
fifo = sys.argv[1]
91+
with open(sys.argv[1], "w") as fifo:
92+
fifo.write("ready")
93+
time.sleep(10000)
94+
95+
async def c4():
96+
await asyncio.sleep(0)
97+
c5()
98+
99+
async def c3():
100+
await c4()
101+
102+
async def c2():
103+
await c3()
104+
105+
async def c1(task):
106+
await task
107+
108+
async def main():
109+
async with asyncio.TaskGroup() as tg:
110+
task = tg.create_task(c2(), name="c2_root")
111+
tg.create_task(c1(task), name="sub_main_1")
112+
tg.create_task(c1(task), name="sub_main_2")
113+
114+
asyncio.run(main())
115+
""")
116+
stack_trace = None
117+
with os_helper.temp_dir() as work_dir:
118+
script_dir = os.path.join(work_dir, "script_pkg")
119+
os.mkdir(script_dir)
120+
fifo = f"{work_dir}/the_fifo"
121+
os.mkfifo(fifo)
122+
script_name = _make_test_script(script_dir, 'script', script)
123+
try:
124+
p = subprocess.Popen([sys.executable, script_name, str(fifo)])
125+
with open(fifo, "r") as fifo_file:
126+
response = fifo_file.read()
127+
self.assertEqual(response, "ready")
128+
stack_trace = get_async_stack_trace(p.pid)
129+
except PermissionError:
130+
self.skipTest("Insufficient permissions to read the stack trace")
131+
finally:
132+
os.remove(fifo)
133+
p.kill()
134+
p.terminate()
135+
p.wait(timeout=SHORT_TIMEOUT)
136+
137+
138+
expected_stack_trace = [
139+
["c5", "c4", "c3", "c2"],
140+
"c2_root",
141+
[
142+
[["main"], "Task-1", []],
143+
[["c1"], "sub_main_2", [[["main"], "Task-1", []]]],
144+
[["c1"], "sub_main_1", [[["main"], "Task-1", []]]],
145+
],
146+
]
147+
self.assertEqual(stack_trace, expected_stack_trace)
148+
149+
150+
77151
@unittest.skipIf(sys.platform != "darwin" and sys.platform != "linux", "Test only runs on Linux and MacOS")
78152
@unittest.skipIf(sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, "Test only runs on Linux with process_vm_readv support")
79153
def test_self_trace(self):

Modules/_asynciomodule.c

+43-6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
#include <stddef.h> // offsetof()
1818

19+
#if defined(__APPLE__)
20+
# include <mach-o/loader.h>
21+
#endif
1922

2023
/*[clinic input]
2124
module _asyncio
@@ -42,15 +45,15 @@ typedef enum {
4245
PyObject *prefix##_cancelled_exc; \
4346
PyObject *prefix##_awaited_by; \
4447
fut_state prefix##_state; \
45-
/* These bitfields need to be at the end of the struct
46-
so that these and bitfields from TaskObj are contiguous.
48+
/* Used by profilers to make traversing the stack from an external \
49+
process faster. */ \
50+
char prefix##_is_task; \
51+
char prefix##_awaited_by_is_set; \
52+
/* These bitfields need to be at the end of the struct \
53+
so that these and bitfields from TaskObj are contiguous. \
4754
*/ \
4855
unsigned prefix##_log_tb: 1; \
4956
unsigned prefix##_blocking: 1; \
50-
/* Used by profilers to make traversing the stack from an external \
51-
process faster. */ \
52-
unsigned prefix##_is_task: 1; \
53-
unsigned prefix##_awaited_by_is_set: 1;
5457

5558
typedef struct {
5659
FutureObj_HEAD(fut)
@@ -102,6 +105,40 @@ typedef struct {
102105
#endif
103106

104107
typedef struct futureiterobject futureiterobject;
108+
typedef struct _Py_AsyncioModuleDebugOffsets {
109+
struct _asyncio_task_object {
110+
uint64_t size;
111+
uint64_t task_name;
112+
uint64_t task_awaited_by;
113+
uint64_t task_is_task;
114+
uint64_t task_awaited_by_is_set;
115+
uint64_t task_coro;
116+
} asyncio_task_object;
117+
} Py_AsyncioModuleDebugOffsets;
118+
119+
#if defined(MS_WINDOWS)
120+
121+
#pragma section("AsyncioDebug", read, write)
122+
__declspec(allocate("AsyncioDebug"))
123+
124+
#elif defined(__APPLE__)
125+
126+
__attribute__((section(SEG_DATA ",AsyncioDebug")))
127+
128+
#endif
129+
130+
Py_AsyncioModuleDebugOffsets AsyncioDebug
131+
#if defined(__linux__) && (defined(__GNUC__) || defined(__clang__))
132+
__attribute__((section(".AsyncioDebug")))
133+
#endif
134+
= {.asyncio_task_object = {
135+
.size = sizeof(TaskObj),
136+
.task_name = offsetof(TaskObj, task_name),
137+
.task_awaited_by = offsetof(TaskObj, task_awaited_by),
138+
.task_is_task = offsetof(TaskObj, task_is_task),
139+
.task_awaited_by_is_set = offsetof(TaskObj, task_awaited_by_is_set),
140+
.task_coro = offsetof(TaskObj, task_coro),
141+
}};
105142

106143
/* State of the _asyncio module */
107144
typedef struct {

0 commit comments

Comments
 (0)