Skip to content

Commit 8aec82f

Browse files
committed
gh-157605: Accept empty native-thread stacks when sampling
1 parent a5b03fa commit 8aec82f

3 files changed

Lines changed: 41 additions & 5 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,41 @@ def test_self_trace(self):
16841684
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
16851685
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))
16861686

1687+
@skip_if_not_supported
1688+
@unittest.skipIf(
1689+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1690+
"Test only runs on Linux with process_vm_readv support",
1691+
)
1692+
def test_empty_native_thread_stack(self):
1693+
_testcapi = import_module("_testcapi")
1694+
lock = threading.Lock()
1695+
lock.acquire()
1696+
# A built-in callback leaves the C thread's Python stack empty.
1697+
_testcapi.call_in_temporary_c_thread(lock.acquire, False)
1698+
try:
1699+
for cache_frames, native in ((False, False), (False, True),
1700+
(True, False), (True, True)):
1701+
with self.subTest(cache_frames=cache_frames, native=native):
1702+
unwinder = RemoteUnwinder(
1703+
os.getpid(), all_threads=True, cache_frames=cache_frames,
1704+
native=native,
1705+
)
1706+
_get_stack_trace_with_retry(
1707+
unwinder, condition=lambda trace: len(trace[0].threads) == 2,
1708+
)
1709+
threads = unwinder.get_stack_trace()[0].threads
1710+
native_stack, python_stack = sorted(
1711+
(thread.frame_info for thread in threads), key=len,
1712+
)
1713+
self.assertEqual(native_stack, [])
1714+
self.assertEqual(
1715+
python_stack[0].funcname,
1716+
"TestGetStackTrace.test_empty_native_thread_stack",
1717+
)
1718+
finally:
1719+
lock.release()
1720+
_testcapi.join_temporary_c_thread()
1721+
16871722
@skip_if_not_supported
16881723
@unittest.skipIf(
16891724
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`profiling.sampling` failing when a native thread has an empty Python
2+
stack.

‎Modules/_remote_debugging/frames.c‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,19 +358,18 @@ process_frame_chain(
358358
continue;
359359
}
360360

361-
if (frame == NULL && PyList_GET_SIZE(ctx->frame_info) == 0) {
362-
const char *e = "Failed to parse initial frame in chain";
363-
PyErr_SetString(PyExc_RuntimeError, e);
364-
return -1;
365-
}
366361
PyObject *extra_frame = NULL;
367362
if (unwinder->gc && frame_addr == ctx->gc_frame) {
368363
_Py_DECLARE_STR(gc, "<GC>");
369364
extra_frame = &_Py_STR(gc);
370365
}
366+
// A leading frame without Python code marks no transition between
367+
// Python frames: it is a frame being popped or C code the thread is
368+
// returning into.
371369
else if (unwinder->native &&
372370
frame == NULL &&
373371
next_frame_addr &&
372+
PyList_GET_SIZE(ctx->frame_info) > 0 &&
374373
!(unwinder->gc && next_frame_addr == ctx->gc_frame))
375374
{
376375
_Py_DECLARE_STR(native, "<native>");

0 commit comments

Comments
 (0)