Skip to content
Open
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
88 changes: 88 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3044,6 +3044,94 @@ def _sample_frames(
client_socket.sendall(send_ack)
return frames

@skip_if_not_supported
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
@unittest.skipIf(
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Requires process_vm_readv",
)
def test_tlbc_cache_refresh_after_growth(self):
# All threads must exist before the first sample: allocating a new
# thread index would invalidate the cache and hide the growth bug.
script_body = """\
import threading
import sys

if sys._is_gil_enabled() or sys._xoptions.get("tlbc") == "0":
sock.sendall(b"skip-ready")
sys.exit()

stop = threading.Event()
gates = [threading.Event() for _ in range(18)]
ready = [threading.Event() for _ in range(18)]

def leaf():
sock.sendall(b"leaf")
stop.wait()

def worker(index):
ready[index].set()
gates[index].wait()
leaf()

for index in range(18):
threading.Thread(target=worker, args=(index,), daemon=True).start()
ready[index].wait()
sock.sendall(f"{leaf.__code__.co_firstlineno + 2}:ready".encode())
sock.recv(1)
gates[0].set()
sock.recv(1)
gates[-1].set()
sock.recv(1)
"""

for cache_frames in (False, True):
with self.subTest(cache_frames=cache_frames):
with self._target_process(script_body) as (
process, client_socket, make_unwinder
):
signal = _wait_for_signal(client_socket, b"ready")
if b"skip" in signal:
self.skipTest("Target requires free-threading and TLBC")
expected_lineno = int(signal.split(b":", 1)[0])
unwinder = make_unwinder(cache_frames=cache_frames)
client_socket.sendall(b"1")
_wait_for_signal(client_socket, b"leaf")
# The signal can arrive before leaf reaches stop.wait().
# Wait for that known line in both sampling phases.
initial_leaf = None
for _ in range(MAX_TRIES):
frames = self._get_frames_with_retry(unwinder, {"leaf"})
if frames:
initial_leaf = next(f for f in frames if f.funcname == "leaf")
if initial_leaf.location.lineno == expected_lineno:
break
time.sleep(RETRY_DELAY)
self.assertIsNotNone(initial_leaf, "Failed to cache initial TLBC array")
self.assertEqual(initial_leaf.location.lineno, expected_lineno)

client_socket.sendall(b"2")
_wait_for_signal(client_socket, b"leaf")
leaves = []
for _ in range(MAX_TRIES):
with contextlib.suppress(*TRANSIENT_ERRORS):
traces = unwinder.get_stack_trace()
leaves = [
frame
for interp in traces
for thread in interp.threads
for frame in thread.frame_info
if frame.funcname == "leaf"
]
if (len(leaves) == 2 and
all(f.location.lineno == expected_lineno for f in leaves)):
break
time.sleep(RETRY_DELAY)
self.assertEqual(len(leaves), 2,
"Original unwinder must sample both workers after growth")
self.assertEqual([f.location.lineno for f in leaves],
[expected_lineno] * 2)

@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 persistent sampling errors in :mod:`!_remote_debugging` when a code
object's thread-local bytecode array grows in a free-threaded build.
4 changes: 3 additions & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,9 @@ typedef struct {

extern void tlbc_cache_entry_destroy(void *ptr);
extern TLBCCacheEntry *get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t current_generation);
extern int cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t tlbc_array_addr, uint32_t generation);
extern int cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr,
uintptr_t tlbc_array_addr, uint32_t generation,
bool force_refresh);
#endif

/* ============================================================================
Expand Down
26 changes: 21 additions & 5 deletions Modules/_remote_debugging/code_objects.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,18 @@ get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t c
}

int
cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t tlbc_array_addr, uint32_t generation)
cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr,
uintptr_t tlbc_array_addr, uint32_t generation, bool force_refresh)
{
int (*read_memory)(proc_handle_t *, uintptr_t, size_t, void *) =
force_refresh ? _Py_RemoteDebug_ReadRemoteMemory
: _Py_RemoteDebug_PagedReadRemoteMemory;
uintptr_t tlbc_array_ptr;
void *tlbc_array = NULL;
TLBCCacheEntry *entry = NULL;

// Read the TLBC array pointer
if (read_ptr(unwinder, tlbc_array_addr, &tlbc_array_ptr) != 0) {
if (read_memory(&unwinder->handle, tlbc_array_addr, sizeof(tlbc_array_ptr), &tlbc_array_ptr) != 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array pointer");
return 0; // Read error
}
Expand All @@ -61,7 +65,7 @@ cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t

// Read the TLBC array size
Py_ssize_t tlbc_size;
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, tlbc_array_ptr, sizeof(tlbc_size), &tlbc_size) != 0) {
if (read_memory(&unwinder->handle, tlbc_array_ptr, sizeof(tlbc_size), &tlbc_size) != 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array size");
return 0; // Read error
}
Expand All @@ -87,7 +91,7 @@ cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t
return 0; // Memory error
}

if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, tlbc_array_ptr, sizeof(Py_ssize_t) + array_data_size, tlbc_array) != 0) {
if (read_memory(&unwinder->handle, tlbc_array_ptr, sizeof(Py_ssize_t) + array_data_size, tlbc_array) != 0) {
PyMem_RawFree(tlbc_array);
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read TLBC array data");
return 0; // Read error
Expand Down Expand Up @@ -442,13 +446,25 @@ parse_code_object(RemoteUnwinderObject *unwinder,

if (!tlbc_entry) {
// Cache miss - try to read and cache TLBC array
if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc, unwinder->tlbc_generation)) {
if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc, unwinder->tlbc_generation, false)) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to cache TLBC array");
goto error;
}
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
}

if (tlbc_entry && ctx->tlbc_index >= tlbc_entry->tlbc_array_size) {
TLBCCacheEntry *old = _Py_hashtable_steal(unwinder->tlbc_cache, (void *)real_address);
if (old != NULL) {
tlbc_cache_entry_destroy(old);
}
if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc,
unwinder->tlbc_generation, true)) {
goto error;
}
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
}

// Validate tlbc_index and check TLBC cache
if (tlbc_entry) {
// Validate index bounds (also catches negative values since tlbc_index is signed)
Expand Down
Loading