Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
Revert "[3.15] gh-154194: Degrade frames in Tachyon instead of failin…
…g the sample (GH-154195) (#158831)"

This reverts commit c27f494.
  • Loading branch information
pablogsal committed Oct 5, 2026
commit 28d469937f37d76e7589adf8161ee1ed05720a49
10 changes: 4 additions & 6 deletions Lib/asyncio/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ def __init__(
# ─── indexing helpers ───────────────────────────────────────────
def _format_stack_entry(elem: str|FrameInfo) -> str:
if not isinstance(elem, str):
if elem.location is None:
if elem.filename in ("", "~"):
return f"{elem.funcname}"
return f"{elem.funcname} {elem.filename}"
if elem.location.lineno == 0 and elem.filename == "":
return f"{elem.funcname}"
else:
Expand Down Expand Up @@ -194,7 +190,8 @@ def build_task_table(result):
# Build coroutine stack string
frames = [frame for coro in task_info.coroutine_stack
for frame in coro.call_stack]
coro_stack = " -> ".join(x.funcname for x in frames)
coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0]
for x in frames)

# Handle tasks with no awaiters
if not task_info.awaited_by:
Expand All @@ -205,7 +202,8 @@ def build_task_table(result):
# Handle tasks with awaiters
for coro_info in task_info.awaited_by:
parent_id = coro_info.task_name
awaiter_frames = [x.funcname for x in coro_info.call_stack]
awaiter_frames = [_format_stack_entry(x).split(" ")[0]
for x in coro_info.call_stack]
awaiter_chain = " -> ".join(awaiter_frames)
awaiter_name = id2name.get(parent_id, "Unknown")
parent_id_str = (hex(parent_id) if isinstance(parent_id, int)
Expand Down
76 changes: 0 additions & 76 deletions Lib/test/test_asyncio/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1558,82 +1558,6 @@ def test_table_output_format(self):

class TestAsyncioToolsEdgeCases(unittest.TestCase):

def test_frames_without_location_tree(self):
"""Frames the unwinder could not fully read - should not crash."""
input_ = [
AwaitedInfo(
thread_id=1,
awaited_by=[
TaskInfo(
task_id=1,
task_name="Task-A",
coroutine_stack=[
CoroInfo(
call_stack=[
FrameInfo("<unreadable frame>", "~", None),
FrameInfo("<unknown function>", "app.py", None),
FrameInfo("big", "big.py", None),
],
task_name=1
)
],
awaited_by=[]
)
]
)
]
self.assertEqual(
tools.build_async_tree(input_),
[[
"└── (T) Task-A",
" └── big big.py",
" └── <unknown function> app.py",
" └── <unreadable frame>",
]],
)

def test_frames_without_location_table(self):
"""Frame names are not truncated at the first space."""
input_ = [
AwaitedInfo(
thread_id=1,
awaited_by=[
TaskInfo(
task_id=1,
task_name="Task-A",
coroutine_stack=[
CoroInfo(
call_stack=[
FrameInfo("<unreadable frame>", "~", None)
],
task_name=1
)
],
awaited_by=[
CoroInfo(
call_stack=[
FrameInfo("<unknown function>", "app.py", None)
],
task_name=2
)
]
)
]
)
]
self.assertEqual(
tools.build_task_table(input_),
[[
1,
"0x1",
"Task-A",
"<unreadable frame>",
"<unknown function>",
"Unknown",
"0x2",
]],
)

def test_task_awaits_self(self):
"""A task directly awaits itself - should raise a cycle."""
input_ = [
Expand Down
87 changes: 0 additions & 87 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4070,93 +4070,6 @@ def test_get_stats_disabled_raises(self):
client_socket.sendall(b"done")


@requires_remote_subprocess_debugging()
@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",
)
class TestMetadataDegradation(RemoteInspectionTestBase):
"""Tests for graceful degradation of oversized code-object metadata."""

def test_long_qualname_truncated_not_dropped(self):
"""A qualname longer than 1024 chars is truncated instead of
failing the whole sample."""
name = "f" * 1100
src = f"def {name}(sample):\n return sample()\n"
ns = {}
exec(src, ns)

trace = ns[name](RemoteUnwinder(os.getpid()).get_stack_trace)
frame = self._find_frame_in_trace(
trace, lambda f: f.funcname.startswith("fff")
)
self.assertIsNotNone(frame)
self.assertEqual(frame.funcname, "f" * 1024)

def test_long_filename_truncated(self):
"""A filename longer than 1024 chars is truncated instead of
failing the whole sample."""
src = "def g(sample):\n return sample()\n"
ns = {}
exec(compile(src, "x" * 1500 + ".py", "exec"), ns)

trace = ns["g"](RemoteUnwinder(os.getpid()).get_stack_trace)
frame = self._find_frame_in_trace(trace, lambda f: f.funcname == "g")
self.assertIsNotNone(frame)
self.assertEqual(frame.filename, "x" * 1024)

def test_oversized_linetable_degrades_to_no_location(self):
"""A linetable over MAX_LINETABLE_SIZE degrades to a frame without
location instead of failing the whole sample."""
src = (
"def big(sample):\n"
+ " x = 1\n" * 20_000
+ " return sample()\n"
)
ns = {}
exec(compile(src, "big_linetable.py", "exec"), ns)
big = ns["big"]
self.assertGreater(len(big.__code__.co_linetable), 64 * 1024)

trace = big(RemoteUnwinder(os.getpid()).get_stack_trace)
frame = self._find_frame_in_trace(
trace, lambda f: f.funcname == "big"
)
self.assertIsNone(frame.location)
self.assertEqual(frame.filename, "big_linetable.py")

@unittest.skipIf(
sys.platform == "win32",
"Process death maps to ProcessLookupError only on POSIX platforms",
)
def test_dead_process_raises_not_degrades(self):
"""Death of the target raises ProcessLookupError instead of
degrading to synthetic frames."""
script_body = """\
import time
sock.sendall(b"ready")
time.sleep(10_000)
"""
with self._target_process(script_body) as (p, client_socket, make_unwinder):
_wait_for_signal(client_socket, b"ready")
unwinder = make_unwinder()
_get_stack_trace_with_retry(unwinder)

p.kill()
p.wait()

for _ in busy_retry(SHORT_TIMEOUT, error=False):
try:
unwinder.get_stack_trace()
except ProcessLookupError:
break
except RuntimeError:
continue
else:
self.fail("ProcessLookupError never raised for dead process")


@requires_remote_subprocess_debugging()
class TestFrameChainLimits(RemoteInspectionTestBase):
"""Frame chain walks abort instead of looping/overflowing on deep chains."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -667,26 +667,6 @@ def test_same_line_different_columns(self):
collector, count = self.roundtrip(samples)
self.assertEqual(count, 3)

def test_synthetic_frames_roundtrip(self):
"""Degraded/sentinel frames (location=None) survive the binary format."""
frames = [
FrameInfo(("~", None, name, None))
for name in (
"<GC>",
"<native>",
"<unknown function>",
"<unknown file>",
"<unreadable frame>",
)
]
frames.append(FrameInfo(("app.py", None, "<unknown function>", None)))
frames.append(FrameInfo(("<unknown file>", None, "real_func", None)))
samples = [[make_interpreter(0, [make_thread(1, frames)])]]

collector, count = self.roundtrip(samples)
self.assertEqual(count, 1)
self.assert_samples_equal(samples, collector)


class TestBinaryEdgeCases(BinaryFormatTestBase):
"""Tests for edge cases in binary format."""
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ typedef enum _WIN32_THREADSTATE {
#define set_exception_cause(unwinder, exc_type, message) \
do { \
assert(PyErr_Occurred() && "function returned -1 without setting exception"); \
if (unwinder->debug && !_Py_RemoteDebug_IsFatalReadError()) { \
if (unwinder->debug && !_Py_RemoteDebug_HasPermissionError()) { \
_set_debug_exception_cause(exc_type, message); \
} \
} while (0)
Expand Down
58 changes: 8 additions & 50 deletions Modules/_remote_debugging/code_objects.c
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ parse_code_object(RemoteUnwinderObject *unwinder,
PyObject *func = NULL;
PyObject *file = NULL;
PyObject *linetable = NULL;
int code_metadata_incomplete = 0;

#ifdef Py_GIL_DISABLED
// In free threading builds, code object addresses might have the low bit set
Expand All @@ -370,59 +369,30 @@ parse_code_object(RemoteUnwinderObject *unwinder,
if (_Py_RemoteDebug_PagedReadRemoteMemory(
&unwinder->handle, real_address, SIZEOF_CODE_OBJ, code_object) < 0)
{
if (_Py_RemoteDebug_IsFatalReadError()) {
goto error;
}
PyErr_Clear();
func = PyUnicode_FromString("<unreadable frame>");
if (!func) {
goto error;
}
file = Py_NewRef(_Py_LATIN1_CHR('~'));
goto degraded;
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read code object");
goto error;
}

func = read_py_str(unwinder,
GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.qualname), 1024);
if (!func) {
if (_Py_RemoteDebug_IsFatalReadError()) {
goto error;
}
PyErr_Clear();
func = PyUnicode_FromString("<unknown function>");
if (!func) {
goto error;
}
code_metadata_incomplete = 1;
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read function name from code object");
goto error;
}

file = read_py_str(unwinder,
GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.filename), 1024);
if (!file) {
if (_Py_RemoteDebug_IsFatalReadError()) {
goto error;
}
PyErr_Clear();
file = PyUnicode_FromString("<unknown file>");
if (!file) {
goto error;
}
code_metadata_incomplete = 1;
}

if (code_metadata_incomplete) {
goto degraded;
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read filename from code object");
goto error;
}

linetable = read_py_bytes(unwinder,
GET_MEMBER(uintptr_t, code_object, unwinder->debug_offsets.code_object.linetable),
MAX_LINETABLE_SIZE);
if (!linetable) {
if (_Py_RemoteDebug_IsFatalReadError()) {
goto error;
}
PyErr_Clear();
goto degraded;
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to read linetable from code object");
goto error;
}

meta = PyMem_RawMalloc(sizeof(CachedCodeMetadata));
Expand Down Expand Up @@ -591,18 +561,6 @@ parse_code_object(RemoteUnwinderObject *unwinder,
*result = tuple;
return 0;

degraded: {
PyObject *degraded_tuple = make_frame_info(unwinder, file, Py_None,
func, Py_None);
Py_CLEAR(func);
Py_CLEAR(file);
if (!degraded_tuple) {
return -1;
}
*result = degraded_tuple;
return 0;
}

error:
Py_XDECREF(func);
Py_XDECREF(file);
Expand Down
11 changes: 1 addition & 10 deletions Python/remote_debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,9 @@ _Py_RemoteDebug_HasPermissionError(void)
&& PyErr_ExceptionMatches(PyExc_PermissionError);
}

static inline int
_Py_RemoteDebug_IsFatalReadError(void)
{
return _Py_RemoteDebug_HasPermissionError()
|| PyErr_ExceptionMatches(PyExc_MemoryError)
|| PyErr_ExceptionMatches(PyExc_ProcessLookupError)
|| (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_Exception));
}

#define _set_debug_exception_cause(exception, format, ...) \
do { \
if (!_Py_RemoteDebug_IsFatalReadError()) { \
if (!_Py_RemoteDebug_HasPermissionError()) { \
PyThreadState *tstate = _PyThreadState_GET(); \
if (!_PyErr_Occurred(tstate)) { \
_PyErr_Format(tstate, exception, format, ##__VA_ARGS__); \
Expand Down