Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1684,6 +1684,80 @@ def test_self_trace(self):
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_empty_native_thread_stack(self):
_testcapi = import_module("_testcapi")
lock = threading.Lock()
lock.acquire()
# A built-in callback leaves the C thread's Python stack empty.
_testcapi.call_in_temporary_c_thread(lock.acquire, False)
try:
for cache_frames, native in ((False, False), (False, True),
(True, False), (True, True)):
with self.subTest(cache_frames=cache_frames, native=native):
unwinder = RemoteUnwinder(
os.getpid(), all_threads=True, cache_frames=cache_frames,
native=native,
)
_get_stack_trace_with_retry(
unwinder, condition=lambda trace: len(trace[0].threads) == 2,
)
threads = unwinder.get_stack_trace()[0].threads
native_stack, python_stack = sorted(
(thread.frame_info for thread in threads), key=len,
)
self.assertEqual(native_stack, [])
self.assertEqual(
python_stack[0].funcname,
"TestGetStackTrace.test_empty_native_thread_stack",
)
finally:
lock.release()
_testcapi.join_temporary_c_thread()

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Test only runs on Linux with process_vm_readv support",
)
def test_popping_python_frame_is_not_native(self):
script = """\
def leaf(depth):
if depth:
leaf(depth - 1)

while True:
leaf(300)
"""
with _managed_subprocess([sys.executable, "-c", script]) as process:
for _ in busy_retry(SHORT_TIMEOUT):
try:
unwinder = RemoteUnwinder(
process.pid, native=True, gc=False, cache_frames=False,
)
except RuntimeError:
continue
break
samples = 0
for _ in range(10_000):
try:
threads = unwinder.get_stack_trace()[0].threads
except TRANSIENT_ERRORS:
continue
if not threads:
continue
frames = threads[0].frame_info
names = [frame.funcname for frame in frames]
if "leaf" not in names:
continue
samples += 1
self.assertNotIn(("leaf", "<native>"), zip(names, names[1:]))
self.assertGreater(samples, 1000)

@skip_if_not_supported
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`profiling.sampling` failing when a native thread has an empty Python
stack.
22 changes: 13 additions & 9 deletions Modules/_remote_debugging/frames.c
Original file line number Diff line number Diff line change
Expand Up @@ -163,21 +163,23 @@ find_frame_in_chunks(StackChunkList *chunks, uintptr_t remote_ptr)
* FRAME PARSING FUNCTIONS
* ============================================================================ */

enum { FRAME_PARSE_INTERPRETER = 2 };

int
is_frame_valid(
RemoteUnwinderObject *unwinder,
uintptr_t frame_addr,
uintptr_t code_object_addr
) {
if ((void*)code_object_addr == NULL) {
return 0;
return 0; // Frame being cleared
}

void* frame = (void*)frame_addr;

char owner = GET_MEMBER(char, frame, unwinder->debug_offsets.interpreter_frame.owner);
if (owner == FRAME_OWNED_BY_INTERPRETER) {
return 0; // C frame or sentinel base frame
return FRAME_PARSE_INTERPRETER; // C frame or sentinel base frame
}

if (owner != FRAME_OWNED_BY_GENERATOR && owner != FRAME_OWNED_BY_THREAD) {
Expand Down Expand Up @@ -313,6 +315,7 @@ process_frame_chain(
ctx->last_frame_visited = 0;

while ((void*)frame_addr != NULL) {
int parse_result = 0;
PyObject *frame = NULL;
uintptr_t next_frame_addr = 0;
uintptr_t stackpointer = 0;
Expand All @@ -326,14 +329,15 @@ process_frame_chain(
assert(frame_count <= MAX_FRAMES);

if (ctx->chunks && ctx->chunks->count > 0) {
if (parse_frame_from_chunks(unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks) == 0) {
parse_result = parse_frame_from_chunks(
unwinder, &frame, frame_addr, &next_frame_addr, &stackpointer, ctx->chunks);
if (parse_result == 0) {
goto parsed_frame;
}
PyErr_Clear();
}
{
uintptr_t address_of_code_object = 0;
int parse_result;
if (ctx->prefetch.frame && ctx->prefetch.frame_addr == frame_addr) {
parse_result = parse_frame_buffer(
unwinder, &frame, ctx->prefetch.frame,
Expand All @@ -358,19 +362,19 @@ process_frame_chain(
continue;
}

if (frame == NULL && PyList_GET_SIZE(ctx->frame_info) == 0) {
Comment thread
pablogsal marked this conversation as resolved.
const char *e = "Failed to parse initial frame in chain";
PyErr_SetString(PyExc_RuntimeError, e);
return -1;
}
PyObject *extra_frame = NULL;
if (unwinder->gc && frame_addr == ctx->gc_frame) {
_Py_DECLARE_STR(gc, "<GC>");
extra_frame = &_Py_STR(gc);
}
// A leading frame without Python code marks no transition between
// Python frames: it is a frame being popped or C code the thread is
// returning into.
else if (unwinder->native &&
frame == NULL &&
parse_result == FRAME_PARSE_INTERPRETER &&
next_frame_addr &&
PyList_GET_SIZE(ctx->frame_info) > 0 &&
!(unwinder->gc && next_frame_addr == ctx->gc_frame))
{
_Py_DECLARE_STR(native, "<native>");
Expand Down
Loading