Skip to content

Commit d8f78f5

Browse files
committed
gh-157605: Accept empty native-thread stacks when sampling
1 parent 6b5d414 commit d8f78f5

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,6 +1666,54 @@ def test_self_trace(self):
16661666
self.assertEqual(this_thread_stack[1].funcname, "TestGetStackTrace.test_self_trace")
16671667
self.assertTrue(this_thread_stack[1].filename.endswith("test_external_inspection.py"))
16681668

1669+
@skip_if_not_supported
1670+
@unittest.skipIf(
1671+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1672+
"Test only runs on Linux with process_vm_readv support",
1673+
)
1674+
def test_empty_native_thread_stack(self):
1675+
import_module("_testcapi")
1676+
script = textwrap.dedent("""\
1677+
import _testcapi
1678+
import socket
1679+
import threading
1680+
import time
1681+
1682+
lock = threading.Lock()
1683+
lock.acquire()
1684+
# A built-in callback leaves the C thread's Python stack empty.
1685+
_testcapi.call_in_temporary_c_thread(lock.acquire, False)
1686+
1687+
def main_work():
1688+
with socket.create_connection(('localhost', {port})) as sock:
1689+
sock.sendall(b'ready')
1690+
time.sleep(10_000)
1691+
1692+
main_work()
1693+
""")
1694+
1695+
def check_trace(pid):
1696+
for cache_frames in (False, True):
1697+
with self.subTest(cache_frames=cache_frames):
1698+
unwinder = RemoteUnwinder(
1699+
pid, all_threads=True, cache_frames=cache_frames,
1700+
)
1701+
# Wait until the C thread has registered its thread state.
1702+
trace = _get_stack_trace_with_retry(
1703+
unwinder, condition=lambda t: len(t[0].threads) == 2,
1704+
)
1705+
for _ in range(3):
1706+
self.assertEqual(len(trace[0].threads), 2)
1707+
self.assertEqual(
1708+
sum(not t.frame_info for t in trace[0].threads), 1,
1709+
)
1710+
self.assertIsNotNone(self._find_frame_in_trace(
1711+
trace, lambda f: f.funcname == "main_work",
1712+
))
1713+
trace = unwinder.get_stack_trace()
1714+
1715+
self._run_script_and_get_trace(script, check_trace, [b"ready"])
1716+
16691717
@skip_if_not_supported
16701718
@unittest.skipIf(
16711719
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: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,6 @@ 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>");

0 commit comments

Comments
 (0)