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
30 changes: 30 additions & 0 deletions Lib/test/test_external_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,36 @@ async def main():
self.assertIn(main_name, names)
self.assertEqual([len(n) for n in names if n.startswith("x")], [255])

@skip_if_not_supported
def test_deep_awaiter_chain_is_walked_depth_first(self):
# gh-158688
depth = 20000

async def root():
await asyncio.sleep(0)
return [
task.task_name
for info in RemoteUnwinder(os.getpid()).get_async_stack_trace()
for task in info.awaited_by
]

async def link(awaited):
return await awaited

async def main():
first = asyncio.create_task(root(), name="root")
top = first
for i in range(depth):
top = asyncio.create_task(link(top), name=f"a{i}")
asyncio.create_task(link(first), name="b")
return (await top), asyncio.current_task().get_name()

names, main_name = asyncio.run(main())
self.assertEqual(len(names), depth + 3)
head = names.index("a0")
self.assertEqual(names[head:head + 2], ["a0", "a1"])
self.assertEqual(names[head + depth], main_name)

@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 a crash in the sampling profiler when dumping a program with a deep
chain of awaiting tasks. Patch by Timofei Ivankov.
6 changes: 0 additions & 6 deletions Modules/_remote_debugging/_remote_debugging.h
Original file line number Diff line number Diff line change
Expand Up @@ -765,12 +765,6 @@ extern int process_single_task_node(
PyObject *result
);

extern int process_task_and_waiters(
RemoteUnwinderObject *unwinder,
uintptr_t task_addr,
PyObject *result
);

extern int find_running_task_in_thread(
RemoteUnwinderObject *unwinder,
uintptr_t thread_state_addr,
Expand Down
95 changes: 76 additions & 19 deletions Modules/_remote_debugging/asyncio.c
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,6 @@ parse_task(
* TASK AWAITED_BY PROCESSING
* ============================================================================ */

// Forward declaration for mutual recursion
static int process_waiter_task(RemoteUnwinderObject *unwinder, uintptr_t key_addr, void *context);

// Processor function for parsing tasks in sets
static int
process_task_parser(
Expand Down Expand Up @@ -658,30 +655,90 @@ process_single_task_node(
return -1;
}

int
process_task_and_waiters(
// Processor function for task waiters
static int
queue_waiter_task(
RemoteUnwinderObject *unwinder,
uintptr_t task_addr,
PyObject *result
uintptr_t key_addr,
void *context
) {
// First, add this task to the result
if (process_single_task_node(unwinder, task_addr, NULL, result) < 0) {
PyObject *queue = (PyObject *)context;
PyObject *addr = PyLong_FromUnsignedLongLong(key_addr);
if (addr == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task address");
return -1;
}

// Now find all tasks that are waiting for this task and process them
return process_task_awaited_by(unwinder, task_addr, process_waiter_task, result);
int res = PyList_Append(queue, addr);
Py_DECREF(addr);
if (res < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to queue waiter task");
}
return res;
}

// Processor function for task waiters
// gh-158688: Add every task that transitively waits for task_addr, already in the result
static int
process_waiter_task(
process_task_waiters(
RemoteUnwinderObject *unwinder,
uintptr_t key_addr,
void *context
uintptr_t task_addr,
PyObject *result
) {
PyObject *result = (PyObject *)context;
return process_task_and_waiters(unwinder, key_addr, result);
PyObject *queue = PyList_New(0);
PyObject *seen = PySet_New(NULL);
PyObject *waiters = PyList_New(0);
int res = -1;

if (queue == NULL || seen == NULL || waiters == NULL) {
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task queue");
goto done;
}

int seeded = queue_waiter_task(unwinder, task_addr, queue);
if (seeded < 0) {
goto done;
}

for (Py_ssize_t i = 0; i < PyList_GET_SIZE(queue); i++) {
PyObject *addr = PyList_GET_ITEM(queue, i);
Py_ssize_t seen_count = PySet_GET_SIZE(seen);

int marked = PySet_Add(seen, addr);
if (marked < 0) {
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to mark task as seen");
goto done;
}
if (PySet_GET_SIZE(seen) == seen_count) {
// already visited
continue;
}

uintptr_t waiter_addr = (uintptr_t)PyLong_AsUnsignedLongLong(addr);
if (i > 0) {
int added = process_single_task_node(unwinder, waiter_addr, NULL, result);
if (added < 0) {
goto done;
}
}

int queued = process_task_awaited_by(unwinder, waiter_addr, queue_waiter_task, waiters);
if (queued < 0) {
goto done;
}

// Visit the waiters before the rest of the queue, depth first
int spliced = PyList_SetSlice(queue, i + 1, i + 1, waiters);
int cleared = PyList_SetSlice(waiters, 0, PyList_GET_SIZE(waiters), NULL);
if (spliced < 0 || cleared < 0) {
goto done;
}
}
res = 0;

done:
Py_XDECREF(queue);
Py_XDECREF(seen);
Py_XDECREF(waiters);
return res;
}

/* ============================================================================
Expand Down Expand Up @@ -978,7 +1035,7 @@ process_running_task_chain(
}

// Now find all tasks that are waiting for this task and process them
if (process_task_awaited_by(unwinder, running_task_addr, process_waiter_task, result) < 0) {
if (process_task_waiters(unwinder, running_task_addr, result) < 0) {
return -1;
}

Expand Down
Loading