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
121 changes: 121 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,127 @@ def get_trace_with_opcodes(pid):
location.col_offset, location.end_col_offset)
self.assertIn(actual, valid_locations)

@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):
# Reproducer from gh-157660.
script = textwrap.dedent("""\
import os, threading
from _remote_debugging import RemoteUnwinder
from test import support

go = threading.Event()
stop = threading.Event()

def leaf():
stop.wait()

def wait_for_leaf_frames(u, expected_count):
for _ in support.sleeping_retry(
support.SHORT_TIMEOUT,
f"Expected {expected_count} leaf frames",
):
try:
traces = u.get_stack_trace()
except RuntimeError as exc:
if str(exc) != "Failed to parse initial frame in chain":
raise
continue
count = sum(
f.funcname == "leaf"
for i in traces
for t in i.threads for f in t.frame_info
)
if count == expected_count:
return

threading.Thread(target=leaf, daemon=True).start()
for _ in range(16):
threading.Thread(target=stop.wait, daemon=True).start()
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()

u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
wait_for_leaf_frames(u, 1)
go.set()
wait_for_leaf_frames(u, 2)
""")
result = subprocess.run(
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
capture_output=True,
text=True,
timeout=SHORT_TIMEOUT,
)
self.assertEqual(
result.returncode, 0,
f"stdout: {result.stdout}\nstderr: {result.stderr}",
)

@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_slot_fill(self):
# Reproducer from gh-157660.
script = textwrap.dedent("""\
import os, threading
from _remote_debugging import RemoteUnwinder

go = threading.Event()
stop = threading.Event()

def leaf():
stop.wait()

from test import support

def lines(u, expected_count):
for _ in support.sleeping_retry(
support.SHORT_TIMEOUT,
f"Expected {expected_count} leaf frames",
):
try:
traces = u.get_stack_trace()
except RuntimeError as exc:
if str(exc) != "Failed to parse initial frame in chain":
raise
continue
result = sorted(
f.location.lineno
for i in traces
for t in i.threads for f in t.frame_info
if f.funcname == "leaf"
)
# A new frame can still point at the function definition.
if (len(result) == expected_count and
leaf.__code__.co_firstlineno not in result):
return result

threading.Thread(target=leaf, daemon=True).start()
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
before = lines(u, 1)
assert before == [8], before
go.set()
cached = lines(u, 2)
assert cached == [8, 8], cached
""")
result = subprocess.run(
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
capture_output=True,
text=True,
timeout=SHORT_TIMEOUT,
)
self.assertEqual(
result.returncode, 0,
f"stdout: {result.stdout}\nstderr: {result.stderr}",
)


class TestUnsupportedPlatformHandling(unittest.TestCase):
@unittest.skipIf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix :mod:`profiling.sampling` reporting errors or incorrect line numbers in free-threaded
builds when a thread-local bytecode array grows or gains entries after being cached.
25 changes: 22 additions & 3 deletions Modules/_remote_debugging/code_objects.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t c
TLBCCacheEntry *entry = _Py_hashtable_get(self->tlbc_cache, key);

if (entry && entry->generation != current_generation) {
// Entry is stale, remove it by setting to NULL
_Py_hashtable_set(self->tlbc_cache, key, NULL);
entry = NULL;
// Entry is stale, remove it from the cache and destroy it
TLBCCacheEntry *old = _Py_hashtable_steal(self->tlbc_cache, key);
if (old != NULL) {
tlbc_cache_entry_destroy(old);
}
return NULL;
}

return entry;
Expand Down Expand Up @@ -449,6 +452,22 @@ parse_code_object(RemoteUnwinderObject *unwinder,
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
}

if (tlbc_entry && ctx->tlbc_index >= 0) {
uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t));
if (ctx->tlbc_index >= tlbc_entry->tlbc_array_size ||
entries[ctx->tlbc_index] == 0) {
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)) {
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