mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
gh-135371: Fix asyncio introspection output to include internal coroutine chains (#135436)
This commit is contained in:
parent
7b15873ed0
commit
028309fb47
6 changed files with 1838 additions and 663 deletions
|
@ -816,43 +816,58 @@ Executing the new tool on the running process will yield a table like this:
|
|||
|
||||
python -m asyncio ps 12345
|
||||
|
||||
tid task id task name coroutine chain awaiter name awaiter id
|
||||
---------------------------------------------------------------------------------------------------------------------------------------
|
||||
8138752 0x564bd3d0210 Task-1 0x0
|
||||
8138752 0x564bd3d0410 Sundowning _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
|
||||
8138752 0x564bd3d0610 TMBTE _aexit -> __aexit__ -> main Task-1 0x564bd3d0210
|
||||
8138752 0x564bd3d0810 TNDNBTG _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
|
||||
8138752 0x564bd3d0a10 Levitate _aexit -> __aexit__ -> album Sundowning 0x564bd3d0410
|
||||
8138752 0x564bd3e0550 DYWTYLM _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
|
||||
8138752 0x564bd3e0710 Aqua Regia _aexit -> __aexit__ -> album TMBTE 0x564bd3d0610
|
||||
tid task id task name coroutine stack awaiter chain awaiter name awaiter id
|
||||
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
1935500 0x7fc930c18050 Task-1 TaskGroup._aexit -> TaskGroup.__aexit__ -> main 0x0
|
||||
1935500 0x7fc930c18230 Sundowning TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
|
||||
1935500 0x7fc93173fa50 TMBTE TaskGroup._aexit -> TaskGroup.__aexit__ -> album TaskGroup._aexit -> TaskGroup.__aexit__ -> main Task-1 0x7fc930c18050
|
||||
1935500 0x7fc93173fdf0 TNDNBTG sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
|
||||
1935500 0x7fc930d32510 Levitate sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album Sundowning 0x7fc930c18230
|
||||
1935500 0x7fc930d32890 DYWTYLM sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
|
||||
1935500 0x7fc93161ec30 Aqua Regia sleep -> play TaskGroup._aexit -> TaskGroup.__aexit__ -> album TMBTE 0x7fc93173fa50
|
||||
|
||||
|
||||
or:
|
||||
or a tree like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio pstree 12345
|
||||
|
||||
└── (T) Task-1
|
||||
└── main
|
||||
└── __aexit__
|
||||
└── _aexit
|
||||
└── main example.py:13
|
||||
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
|
||||
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
|
||||
├── (T) Sundowning
|
||||
│ └── album
|
||||
│ └── __aexit__
|
||||
│ └── _aexit
|
||||
│ └── album example.py:8
|
||||
│ └── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
|
||||
│ └── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
|
||||
│ ├── (T) TNDNBTG
|
||||
│ │ └── play example.py:4
|
||||
│ │ └── sleep Lib/asyncio/tasks.py:702
|
||||
│ └── (T) Levitate
|
||||
│ └── play example.py:4
|
||||
│ └── sleep Lib/asyncio/tasks.py:702
|
||||
└── (T) TMBTE
|
||||
└── album
|
||||
└── __aexit__
|
||||
└── _aexit
|
||||
└── album example.py:8
|
||||
└── TaskGroup.__aexit__ Lib/asyncio/taskgroups.py:72
|
||||
└── TaskGroup._aexit Lib/asyncio/taskgroups.py:121
|
||||
├── (T) DYWTYLM
|
||||
│ └── play example.py:4
|
||||
│ └── sleep Lib/asyncio/tasks.py:702
|
||||
└── (T) Aqua Regia
|
||||
└── play example.py:4
|
||||
└── sleep Lib/asyncio/tasks.py:702
|
||||
|
||||
If a cycle is detected in the async await graph (which could indicate a
|
||||
programming issue), the tool raises an error and lists the cycle paths that
|
||||
prevent tree construction.
|
||||
prevent tree construction:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m asyncio pstree 12345
|
||||
|
||||
ERROR: await-graph contains cycles - cannot print a tree!
|
||||
|
||||
cycle: Task-2 → Task-3 → Task-2
|
||||
|
||||
(Contributed by Pablo Galindo, Łukasz Langa, Yury Selivanov, and Marta
|
||||
Gomez Macias in :gh:`91048`.)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
"""Tools to analyze tasks running in asyncio programs."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections import defaultdict, namedtuple
|
||||
from itertools import count
|
||||
from enum import Enum
|
||||
import sys
|
||||
from _remote_debugging import RemoteUnwinder
|
||||
|
||||
from _remote_debugging import RemoteUnwinder, FrameInfo
|
||||
|
||||
class NodeType(Enum):
|
||||
COROUTINE = 1
|
||||
|
@ -26,51 +25,75 @@ class CycleFoundException(Exception):
|
|||
|
||||
|
||||
# ─── indexing helpers ───────────────────────────────────────────
|
||||
def _format_stack_entry(elem: tuple[str, str, int] | str) -> str:
|
||||
if isinstance(elem, tuple):
|
||||
fqname, path, line_no = elem
|
||||
return f"{fqname} {path}:{line_no}"
|
||||
|
||||
def _format_stack_entry(elem: str|FrameInfo) -> str:
|
||||
if not isinstance(elem, str):
|
||||
if elem.lineno == 0 and elem.filename == "":
|
||||
return f"{elem.funcname}"
|
||||
else:
|
||||
return f"{elem.funcname} {elem.filename}:{elem.lineno}"
|
||||
return elem
|
||||
|
||||
|
||||
def _index(result):
|
||||
id2name, awaits = {}, []
|
||||
for _thr_id, tasks in result:
|
||||
for tid, tname, awaited in tasks:
|
||||
id2name[tid] = tname
|
||||
for stack, parent_id in awaited:
|
||||
stack = [_format_stack_entry(elem) for elem in stack]
|
||||
awaits.append((parent_id, stack, tid))
|
||||
return id2name, awaits
|
||||
id2name, awaits, task_stacks = {}, [], {}
|
||||
for awaited_info in result:
|
||||
for task_info in awaited_info.awaited_by:
|
||||
task_id = task_info.task_id
|
||||
task_name = task_info.task_name
|
||||
id2name[task_id] = task_name
|
||||
|
||||
# Store the internal coroutine stack for this task
|
||||
if task_info.coroutine_stack:
|
||||
for coro_info in task_info.coroutine_stack:
|
||||
call_stack = coro_info.call_stack
|
||||
internal_stack = [_format_stack_entry(frame) for frame in call_stack]
|
||||
task_stacks[task_id] = internal_stack
|
||||
|
||||
# Add the awaited_by relationships (external dependencies)
|
||||
if task_info.awaited_by:
|
||||
for coro_info in task_info.awaited_by:
|
||||
call_stack = coro_info.call_stack
|
||||
parent_task_id = coro_info.task_name
|
||||
stack = [_format_stack_entry(frame) for frame in call_stack]
|
||||
awaits.append((parent_task_id, stack, task_id))
|
||||
return id2name, awaits, task_stacks
|
||||
|
||||
|
||||
def _build_tree(id2name, awaits):
|
||||
def _build_tree(id2name, awaits, task_stacks):
|
||||
id2label = {(NodeType.TASK, tid): name for tid, name in id2name.items()}
|
||||
children = defaultdict(list)
|
||||
cor_names = defaultdict(dict) # (parent) -> {frame: node}
|
||||
cor_id_seq = count(1)
|
||||
cor_nodes = defaultdict(dict) # Maps parent -> {frame_name: node_key}
|
||||
next_cor_id = count(1)
|
||||
|
||||
def _cor_node(parent_key, frame_name):
|
||||
"""Return an existing or new (NodeType.COROUTINE, …) node under *parent_key*."""
|
||||
bucket = cor_names[parent_key]
|
||||
if frame_name in bucket:
|
||||
return bucket[frame_name]
|
||||
node_key = (NodeType.COROUTINE, f"c{next(cor_id_seq)}")
|
||||
id2label[node_key] = frame_name
|
||||
children[parent_key].append(node_key)
|
||||
bucket[frame_name] = node_key
|
||||
def get_or_create_cor_node(parent, frame):
|
||||
"""Get existing coroutine node or create new one under parent"""
|
||||
if frame in cor_nodes[parent]:
|
||||
return cor_nodes[parent][frame]
|
||||
|
||||
node_key = (NodeType.COROUTINE, f"c{next(next_cor_id)}")
|
||||
id2label[node_key] = frame
|
||||
children[parent].append(node_key)
|
||||
cor_nodes[parent][frame] = node_key
|
||||
return node_key
|
||||
|
||||
# lay down parent ➜ …frames… ➜ child paths
|
||||
# Build task dependency tree with coroutine frames
|
||||
for parent_id, stack, child_id in awaits:
|
||||
cur = (NodeType.TASK, parent_id)
|
||||
for frame in reversed(stack): # outer-most → inner-most
|
||||
cur = _cor_node(cur, frame)
|
||||
for frame in reversed(stack):
|
||||
cur = get_or_create_cor_node(cur, frame)
|
||||
|
||||
child_key = (NodeType.TASK, child_id)
|
||||
if child_key not in children[cur]:
|
||||
children[cur].append(child_key)
|
||||
|
||||
# Add coroutine stacks for leaf tasks
|
||||
awaiting_tasks = {parent_id for parent_id, _, _ in awaits}
|
||||
for task_id in id2name:
|
||||
if task_id not in awaiting_tasks and task_id in task_stacks:
|
||||
cur = (NodeType.TASK, task_id)
|
||||
for frame in reversed(task_stacks[task_id]):
|
||||
cur = get_or_create_cor_node(cur, frame)
|
||||
|
||||
return id2label, children
|
||||
|
||||
|
||||
|
@ -129,12 +152,12 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
|
|||
The call tree is produced by `get_all_async_stacks()`, prefixing tasks
|
||||
with `task_emoji` and coroutine frames with `cor_emoji`.
|
||||
"""
|
||||
id2name, awaits = _index(result)
|
||||
id2name, awaits, task_stacks = _index(result)
|
||||
g = _task_graph(awaits)
|
||||
cycles = _find_cycles(g)
|
||||
if cycles:
|
||||
raise CycleFoundException(cycles, id2name)
|
||||
labels, children = _build_tree(id2name, awaits)
|
||||
labels, children = _build_tree(id2name, awaits, task_stacks)
|
||||
|
||||
def pretty(node):
|
||||
flag = task_emoji if node[0] == NodeType.TASK else cor_emoji
|
||||
|
@ -154,35 +177,40 @@ def build_async_tree(result, task_emoji="(T)", cor_emoji=""):
|
|||
|
||||
|
||||
def build_task_table(result):
|
||||
id2name, awaits = _index(result)
|
||||
id2name, _, _ = _index(result)
|
||||
table = []
|
||||
for tid, tasks in result:
|
||||
for task_id, task_name, awaited in tasks:
|
||||
if not awaited:
|
||||
table.append(
|
||||
[
|
||||
tid,
|
||||
hex(task_id),
|
||||
task_name,
|
||||
"",
|
||||
"",
|
||||
"0x0"
|
||||
]
|
||||
)
|
||||
for stack, awaiter_id in awaited:
|
||||
stack = [elem[0] if isinstance(elem, tuple) else elem for elem in stack]
|
||||
coroutine_chain = " -> ".join(stack)
|
||||
awaiter_name = id2name.get(awaiter_id, "Unknown")
|
||||
table.append(
|
||||
[
|
||||
tid,
|
||||
hex(task_id),
|
||||
task_name,
|
||||
coroutine_chain,
|
||||
awaiter_name,
|
||||
hex(awaiter_id),
|
||||
]
|
||||
)
|
||||
|
||||
for awaited_info in result:
|
||||
thread_id = awaited_info.thread_id
|
||||
for task_info in awaited_info.awaited_by:
|
||||
# Get task info
|
||||
task_id = task_info.task_id
|
||||
task_name = task_info.task_name
|
||||
|
||||
# Build coroutine stack string
|
||||
frames = [frame for coro in task_info.coroutine_stack
|
||||
for frame in coro.call_stack]
|
||||
coro_stack = " -> ".join(_format_stack_entry(x).split(" ")[0]
|
||||
for x in frames)
|
||||
|
||||
# Handle tasks with no awaiters
|
||||
if not task_info.awaited_by:
|
||||
table.append([thread_id, hex(task_id), task_name, coro_stack,
|
||||
"", "", "0x0"])
|
||||
continue
|
||||
|
||||
# Handle tasks with awaiters
|
||||
for coro_info in task_info.awaited_by:
|
||||
parent_id = coro_info.task_name
|
||||
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)
|
||||
else str(parent_id))
|
||||
|
||||
table.append([thread_id, hex(task_id), task_name, coro_stack,
|
||||
awaiter_chain, awaiter_name, parent_id_str])
|
||||
|
||||
return table
|
||||
|
||||
|
@ -211,11 +239,11 @@ def display_awaited_by_tasks_table(pid: int) -> None:
|
|||
table = build_task_table(tasks)
|
||||
# Print the table in a simple tabular format
|
||||
print(
|
||||
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine chain':<50} {'awaiter name':<20} {'awaiter id':<15}"
|
||||
f"{'tid':<10} {'task id':<20} {'task name':<20} {'coroutine stack':<50} {'awaiter chain':<50} {'awaiter name':<15} {'awaiter id':<15}"
|
||||
)
|
||||
print("-" * 135)
|
||||
print("-" * 180)
|
||||
for row in table:
|
||||
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<20} {row[5]:<15}")
|
||||
print(f"{row[0]:<10} {row[1]:<20} {row[2]:<20} {row[3]:<50} {row[4]:<50} {row[5]:<15} {row[6]:<15}")
|
||||
|
||||
|
||||
def display_awaited_by_tasks_tree(pid: int) -> None:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,7 @@ import importlib
|
|||
import sys
|
||||
import socket
|
||||
import threading
|
||||
from asyncio import staggered, taskgroups
|
||||
from asyncio import staggered, taskgroups, base_events, tasks
|
||||
from unittest.mock import ANY
|
||||
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
|
||||
from test.support.script_helper import make_script
|
||||
|
@ -18,8 +18,12 @@ PROCESS_VM_READV_SUPPORTED = False
|
|||
try:
|
||||
from _remote_debugging import PROCESS_VM_READV_SUPPORTED
|
||||
from _remote_debugging import RemoteUnwinder
|
||||
from _remote_debugging import FrameInfo, CoroInfo, TaskInfo
|
||||
except ImportError:
|
||||
raise unittest.SkipTest("Test only runs when _remote_debugging is available")
|
||||
raise unittest.SkipTest(
|
||||
"Test only runs when _remote_debugging is available"
|
||||
)
|
||||
|
||||
|
||||
def _make_test_script(script_dir, script_basename, source):
|
||||
to_return = make_script(script_dir, script_basename, source)
|
||||
|
@ -28,7 +32,11 @@ def _make_test_script(script_dir, script_basename, source):
|
|||
|
||||
|
||||
skip_if_not_supported = unittest.skipIf(
|
||||
(sys.platform != "darwin" and sys.platform != "linux" and sys.platform != "win32"),
|
||||
(
|
||||
sys.platform != "darwin"
|
||||
and sys.platform != "linux"
|
||||
and sys.platform != "win32"
|
||||
),
|
||||
"Test only runs on Linux, Windows and MacOS",
|
||||
)
|
||||
|
||||
|
@ -101,11 +109,16 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
client_socket, _ = server_socket.accept()
|
||||
server_socket.close()
|
||||
response = b""
|
||||
while b"ready:main" not in response or b"ready:thread" not in response:
|
||||
while (
|
||||
b"ready:main" not in response
|
||||
or b"ready:thread" not in response
|
||||
):
|
||||
response += client_socket.recv(1024)
|
||||
stack_trace = get_stack_trace(p.pid)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -114,17 +127,17 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
p.wait(timeout=SHORT_TIMEOUT)
|
||||
|
||||
thread_expected_stack_trace = [
|
||||
(script_name, 15, "foo"),
|
||||
(script_name, 12, "baz"),
|
||||
(script_name, 9, "bar"),
|
||||
(threading.__file__, ANY, 'Thread.run')
|
||||
FrameInfo([script_name, 15, "foo"]),
|
||||
FrameInfo([script_name, 12, "baz"]),
|
||||
FrameInfo([script_name, 9, "bar"]),
|
||||
FrameInfo([threading.__file__, ANY, "Thread.run"]),
|
||||
]
|
||||
# Is possible that there are more threads, so we check that the
|
||||
# expected stack traces are in the result (looking at you Windows!)
|
||||
self.assertIn((ANY, thread_expected_stack_trace), stack_trace)
|
||||
|
||||
# Check that the main thread stack trace is in the result
|
||||
frame = (script_name, 19, "<module>")
|
||||
frame = FrameInfo([script_name, 19, "<module>"])
|
||||
for _, stack in stack_trace:
|
||||
if frame in stack:
|
||||
break
|
||||
|
@ -189,8 +202,12 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
):
|
||||
script_dir = os.path.join(work_dir, "script_pkg")
|
||||
os.mkdir(script_dir)
|
||||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server_socket = socket.socket(
|
||||
socket.AF_INET, socket.SOCK_STREAM
|
||||
)
|
||||
server_socket.setsockopt(
|
||||
socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
|
||||
)
|
||||
server_socket.bind(("localhost", port))
|
||||
server_socket.settimeout(SHORT_TIMEOUT)
|
||||
server_socket.listen(1)
|
||||
|
@ -208,7 +225,9 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
self.assertEqual(response, b"ready")
|
||||
stack_trace = get_async_stack_trace(p.pid)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -219,79 +238,49 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
# sets are unordered, so we want to sort "awaited_by"s
|
||||
stack_trace[2].sort(key=lambda x: x[1])
|
||||
|
||||
root_task = "Task-1"
|
||||
expected_stack_trace = [
|
||||
[
|
||||
(script_name, 10, "c5"),
|
||||
(script_name, 14, "c4"),
|
||||
(script_name, 17, "c3"),
|
||||
(script_name, 20, "c2"),
|
||||
FrameInfo([script_name, 10, "c5"]),
|
||||
FrameInfo([script_name, 14, "c4"]),
|
||||
FrameInfo([script_name, 17, "c3"]),
|
||||
FrameInfo([script_name, 20, "c2"]),
|
||||
],
|
||||
"c2_root",
|
||||
[
|
||||
[
|
||||
[
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit"
|
||||
),
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__"
|
||||
),
|
||||
(script_name, 26, "main"),
|
||||
],
|
||||
"Task-1",
|
||||
[],
|
||||
],
|
||||
[
|
||||
[(script_name, 23, "c1")],
|
||||
"sub_main_1",
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
[
|
||||
(
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit"
|
||||
),
|
||||
(
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__"
|
||||
),
|
||||
(script_name, 26, "main"),
|
||||
],
|
||||
"Task-1",
|
||||
[],
|
||||
]
|
||||
],
|
||||
],
|
||||
[
|
||||
[(script_name, 23, "c1")],
|
||||
"sub_main_2",
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
FrameInfo([script_name, 26, "main"]),
|
||||
],
|
||||
"Task-1",
|
||||
]
|
||||
),
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
[
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit"
|
||||
),
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__"
|
||||
),
|
||||
(script_name, 26, "main"),
|
||||
],
|
||||
"Task-1",
|
||||
[],
|
||||
]
|
||||
],
|
||||
],
|
||||
[FrameInfo([script_name, 23, "c1"])],
|
||||
"sub_main_1",
|
||||
]
|
||||
),
|
||||
CoroInfo(
|
||||
[
|
||||
[FrameInfo([script_name, 23, "c1"])],
|
||||
"sub_main_2",
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
self.assertEqual(stack_trace, expected_stack_trace)
|
||||
|
@ -350,7 +339,9 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
self.assertEqual(response, b"ready")
|
||||
stack_trace = get_async_stack_trace(p.pid)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -363,9 +354,9 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
|
||||
expected_stack_trace = [
|
||||
[
|
||||
(script_name, 10, "gen_nested_call"),
|
||||
(script_name, 16, "gen"),
|
||||
(script_name, 19, "main"),
|
||||
FrameInfo([script_name, 10, "gen_nested_call"]),
|
||||
FrameInfo([script_name, 16, "gen"]),
|
||||
FrameInfo([script_name, 19, "main"]),
|
||||
],
|
||||
"Task-1",
|
||||
[],
|
||||
|
@ -427,7 +418,9 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
self.assertEqual(response, b"ready")
|
||||
stack_trace = get_async_stack_trace(p.pid)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -439,9 +432,12 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
stack_trace[2].sort(key=lambda x: x[1])
|
||||
|
||||
expected_stack_trace = [
|
||||
[(script_name, 11, "deep"), (script_name, 15, "c1")],
|
||||
[
|
||||
FrameInfo([script_name, 11, "deep"]),
|
||||
FrameInfo([script_name, 15, "c1"]),
|
||||
],
|
||||
"Task-2",
|
||||
[[[(script_name, 21, "main")], "Task-1", []]],
|
||||
[CoroInfo([[FrameInfo([script_name, 21, "main"])], "Task-1"])],
|
||||
]
|
||||
self.assertEqual(stack_trace, expected_stack_trace)
|
||||
|
||||
|
@ -503,7 +499,9 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
self.assertEqual(response, b"ready")
|
||||
stack_trace = get_async_stack_trace(p.pid)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -515,20 +513,29 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
stack_trace[2].sort(key=lambda x: x[1])
|
||||
expected_stack_trace = [
|
||||
[
|
||||
(script_name, 11, "deep"),
|
||||
(script_name, 15, "c1"),
|
||||
(staggered.__file__, ANY, "staggered_race.<locals>.run_one_coro"),
|
||||
FrameInfo([script_name, 11, "deep"]),
|
||||
FrameInfo([script_name, 15, "c1"]),
|
||||
FrameInfo(
|
||||
[
|
||||
staggered.__file__,
|
||||
ANY,
|
||||
"staggered_race.<locals>.run_one_coro",
|
||||
]
|
||||
),
|
||||
],
|
||||
"Task-2",
|
||||
[
|
||||
[
|
||||
CoroInfo(
|
||||
[
|
||||
(staggered.__file__, ANY, "staggered_race"),
|
||||
(script_name, 21, "main"),
|
||||
],
|
||||
"Task-1",
|
||||
[],
|
||||
]
|
||||
[
|
||||
FrameInfo(
|
||||
[staggered.__file__, ANY, "staggered_race"]
|
||||
),
|
||||
FrameInfo([script_name, 21, "main"]),
|
||||
],
|
||||
"Task-1",
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
self.assertEqual(stack_trace, expected_stack_trace)
|
||||
|
@ -659,62 +666,174 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
# expected: at least 1000 pending tasks
|
||||
self.assertGreaterEqual(len(entries), 1000)
|
||||
# the first three tasks stem from the code structure
|
||||
self.assertIn((ANY, "Task-1", []), entries)
|
||||
main_stack = [
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
FrameInfo([taskgroups.__file__, ANY, "TaskGroup._aexit"]),
|
||||
FrameInfo(
|
||||
[taskgroups.__file__, ANY, "TaskGroup.__aexit__"]
|
||||
),
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
),
|
||||
(script_name, 60, "main"),
|
||||
FrameInfo([script_name, 60, "main"]),
|
||||
]
|
||||
self.assertIn(
|
||||
(ANY, "server task", [[main_stack, ANY]]),
|
||||
TaskInfo(
|
||||
[ANY, "Task-1", [CoroInfo([main_stack, ANY])], []]
|
||||
),
|
||||
entries,
|
||||
)
|
||||
self.assertIn(
|
||||
(ANY, "echo client spam", [[main_stack, ANY]]),
|
||||
TaskInfo(
|
||||
[
|
||||
ANY,
|
||||
"server task",
|
||||
[
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
base_events.__file__,
|
||||
ANY,
|
||||
"Server.serve_forever",
|
||||
]
|
||||
)
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
],
|
||||
[
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[script_name, ANY, "main"]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
),
|
||||
entries,
|
||||
)
|
||||
self.assertIn(
|
||||
TaskInfo(
|
||||
[
|
||||
ANY,
|
||||
"Task-4",
|
||||
[
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
FrameInfo(
|
||||
[tasks.__file__, ANY, "sleep"]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
script_name,
|
||||
38,
|
||||
"echo_client",
|
||||
]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
],
|
||||
[
|
||||
CoroInfo(
|
||||
[
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
script_name,
|
||||
41,
|
||||
"echo_client_spam",
|
||||
]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
],
|
||||
]
|
||||
),
|
||||
entries,
|
||||
)
|
||||
|
||||
expected_stack = [
|
||||
[
|
||||
expected_awaited_by = [
|
||||
CoroInfo(
|
||||
[
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
),
|
||||
(
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
),
|
||||
(script_name, 41, "echo_client_spam"),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
[
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup._aexit",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[
|
||||
taskgroups.__file__,
|
||||
ANY,
|
||||
"TaskGroup.__aexit__",
|
||||
]
|
||||
),
|
||||
FrameInfo(
|
||||
[script_name, 41, "echo_client_spam"]
|
||||
),
|
||||
],
|
||||
ANY,
|
||||
]
|
||||
)
|
||||
]
|
||||
tasks_with_stack = [
|
||||
task for task in entries if task[2] == expected_stack
|
||||
tasks_with_awaited = [
|
||||
task
|
||||
for task in entries
|
||||
if task.awaited_by == expected_awaited_by
|
||||
]
|
||||
self.assertGreaterEqual(len(tasks_with_stack), 1000)
|
||||
self.assertGreaterEqual(len(tasks_with_awaited), 1000)
|
||||
|
||||
# the final task will have some random number, but it should for
|
||||
# sure be one of the echo client spam horde (In windows this is not true
|
||||
# for some reason)
|
||||
if sys.platform != "win32":
|
||||
self.assertEqual(
|
||||
expected_stack,
|
||||
entries[-1][2],
|
||||
tasks_with_awaited[-1].awaited_by,
|
||||
entries[-1].awaited_by,
|
||||
)
|
||||
except PermissionError:
|
||||
self.skipTest("Insufficient permissions to read the stack trace")
|
||||
self.skipTest(
|
||||
"Insufficient permissions to read the stack trace"
|
||||
)
|
||||
finally:
|
||||
if client_socket is not None:
|
||||
client_socket.close()
|
||||
|
@ -740,17 +859,21 @@ class TestGetStackTrace(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
stack[:2],
|
||||
[
|
||||
(
|
||||
__file__,
|
||||
get_stack_trace.__code__.co_firstlineno + 2,
|
||||
"get_stack_trace",
|
||||
FrameInfo(
|
||||
[
|
||||
__file__,
|
||||
get_stack_trace.__code__.co_firstlineno + 2,
|
||||
"get_stack_trace",
|
||||
]
|
||||
),
|
||||
(
|
||||
__file__,
|
||||
self.test_self_trace.__code__.co_firstlineno + 6,
|
||||
"TestGetStackTrace.test_self_trace",
|
||||
FrameInfo(
|
||||
[
|
||||
__file__,
|
||||
self.test_self_trace.__code__.co_firstlineno + 6,
|
||||
"TestGetStackTrace.test_self_trace",
|
||||
]
|
||||
),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Fixed :mod:`asyncio` debugging tools to properly display internal coroutine
|
||||
call stacks alongside external task dependencies. The ``python -m asyncio
|
||||
ps`` and ``python -m asyncio pstree`` commands now show complete execution
|
||||
context. Patch by Pablo Galindo.
|
|
@ -97,24 +97,82 @@ struct _Py_AsyncioModuleDebugOffsets {
|
|||
} asyncio_thread_state;
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
proc_handle_t handle;
|
||||
uintptr_t runtime_start_address;
|
||||
struct _Py_DebugOffsets debug_offsets;
|
||||
int async_debug_offsets_available;
|
||||
struct _Py_AsyncioModuleDebugOffsets async_debug_offsets;
|
||||
uintptr_t interpreter_addr;
|
||||
uintptr_t tstate_addr;
|
||||
uint64_t code_object_generation;
|
||||
_Py_hashtable_t *code_object_cache;
|
||||
int debug;
|
||||
#ifdef Py_GIL_DISABLED
|
||||
// TLBC cache invalidation tracking
|
||||
uint32_t tlbc_generation; // Track TLBC index pool changes
|
||||
_Py_hashtable_t *tlbc_cache; // Cache of TLBC arrays by code object address
|
||||
#endif
|
||||
} RemoteUnwinderObject;
|
||||
/* ============================================================================
|
||||
* STRUCTSEQ TYPE DEFINITIONS
|
||||
* ============================================================================ */
|
||||
|
||||
// TaskInfo structseq type - replaces 4-tuple (task_id, task_name, coroutine_stack, awaited_by)
|
||||
static PyStructSequence_Field TaskInfo_fields[] = {
|
||||
{"task_id", "Task ID (memory address)"},
|
||||
{"task_name", "Task name"},
|
||||
{"coroutine_stack", "Coroutine call stack"},
|
||||
{"awaited_by", "Tasks awaiting this task"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc TaskInfo_desc = {
|
||||
"_remote_debugging.TaskInfo",
|
||||
"Information about an asyncio task",
|
||||
TaskInfo_fields,
|
||||
4
|
||||
};
|
||||
|
||||
// FrameInfo structseq type - replaces 3-tuple (filename, lineno, funcname)
|
||||
static PyStructSequence_Field FrameInfo_fields[] = {
|
||||
{"filename", "Source code filename"},
|
||||
{"lineno", "Line number"},
|
||||
{"funcname", "Function name"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc FrameInfo_desc = {
|
||||
"_remote_debugging.FrameInfo",
|
||||
"Information about a frame",
|
||||
FrameInfo_fields,
|
||||
3
|
||||
};
|
||||
|
||||
// CoroInfo structseq type - replaces 2-tuple (call_stack, task_name)
|
||||
static PyStructSequence_Field CoroInfo_fields[] = {
|
||||
{"call_stack", "Coroutine call stack"},
|
||||
{"task_name", "Task name"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc CoroInfo_desc = {
|
||||
"_remote_debugging.CoroInfo",
|
||||
"Information about a coroutine",
|
||||
CoroInfo_fields,
|
||||
2
|
||||
};
|
||||
|
||||
// ThreadInfo structseq type - replaces 2-tuple (thread_id, frame_info)
|
||||
static PyStructSequence_Field ThreadInfo_fields[] = {
|
||||
{"thread_id", "Thread ID"},
|
||||
{"frame_info", "Frame information"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc ThreadInfo_desc = {
|
||||
"_remote_debugging.ThreadInfo",
|
||||
"Information about a thread",
|
||||
ThreadInfo_fields,
|
||||
2
|
||||
};
|
||||
|
||||
// AwaitedInfo structseq type - replaces 2-tuple (tid, awaited_by_list)
|
||||
static PyStructSequence_Field AwaitedInfo_fields[] = {
|
||||
{"thread_id", "Thread ID"},
|
||||
{"awaited_by", "List of tasks awaited by this thread"},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
static PyStructSequence_Desc AwaitedInfo_desc = {
|
||||
"_remote_debugging.AwaitedInfo",
|
||||
"Information about what a thread is awaiting",
|
||||
AwaitedInfo_fields,
|
||||
2
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
PyObject *func_name;
|
||||
|
@ -127,8 +185,33 @@ typedef struct {
|
|||
typedef struct {
|
||||
/* Types */
|
||||
PyTypeObject *RemoteDebugging_Type;
|
||||
PyTypeObject *TaskInfo_Type;
|
||||
PyTypeObject *FrameInfo_Type;
|
||||
PyTypeObject *CoroInfo_Type;
|
||||
PyTypeObject *ThreadInfo_Type;
|
||||
PyTypeObject *AwaitedInfo_Type;
|
||||
} RemoteDebuggingState;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
proc_handle_t handle;
|
||||
uintptr_t runtime_start_address;
|
||||
struct _Py_DebugOffsets debug_offsets;
|
||||
int async_debug_offsets_available;
|
||||
struct _Py_AsyncioModuleDebugOffsets async_debug_offsets;
|
||||
uintptr_t interpreter_addr;
|
||||
uintptr_t tstate_addr;
|
||||
uint64_t code_object_generation;
|
||||
_Py_hashtable_t *code_object_cache;
|
||||
int debug;
|
||||
RemoteDebuggingState *cached_state; // Cached module state
|
||||
#ifdef Py_GIL_DISABLED
|
||||
// TLBC cache invalidation tracking
|
||||
uint32_t tlbc_generation; // Track TLBC index pool changes
|
||||
_Py_hashtable_t *tlbc_cache; // Cache of TLBC arrays by code object address
|
||||
#endif
|
||||
} RemoteUnwinderObject;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int lineno;
|
||||
|
@ -218,6 +301,24 @@ RemoteDebugging_GetState(PyObject *module)
|
|||
return (RemoteDebuggingState *)state;
|
||||
}
|
||||
|
||||
static inline RemoteDebuggingState *
|
||||
RemoteDebugging_GetStateFromType(PyTypeObject *type)
|
||||
{
|
||||
PyObject *module = PyType_GetModule(type);
|
||||
assert(module != NULL);
|
||||
return RemoteDebugging_GetState(module);
|
||||
}
|
||||
|
||||
static inline RemoteDebuggingState *
|
||||
RemoteDebugging_GetStateFromObject(PyObject *obj)
|
||||
{
|
||||
RemoteUnwinderObject *unwinder = (RemoteUnwinderObject *)obj;
|
||||
if (unwinder->cached_state == NULL) {
|
||||
unwinder->cached_state = RemoteDebugging_GetStateFromType(Py_TYPE(obj));
|
||||
}
|
||||
return unwinder->cached_state;
|
||||
}
|
||||
|
||||
static inline int
|
||||
RemoteDebugging_InitState(RemoteDebuggingState *st)
|
||||
{
|
||||
|
@ -854,24 +955,14 @@ create_task_result(
|
|||
char task_obj[SIZEOF_TASK_OBJ];
|
||||
uintptr_t coro_addr;
|
||||
|
||||
result = PyList_New(0);
|
||||
if (result == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create task result list");
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Create call_stack first since it's the first tuple element
|
||||
call_stack = PyList_New(0);
|
||||
if (call_stack == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create call stack list");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (PyList_Append(result, call_stack)) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append call stack to task result");
|
||||
goto error;
|
||||
}
|
||||
Py_CLEAR(call_stack);
|
||||
|
||||
// Create task name/address for second tuple element
|
||||
if (recurse_task) {
|
||||
tn = parse_task_name(unwinder, task_address);
|
||||
} else {
|
||||
|
@ -882,12 +973,6 @@ create_task_result(
|
|||
goto error;
|
||||
}
|
||||
|
||||
if (PyList_Append(result, tn)) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task name to result");
|
||||
goto error;
|
||||
}
|
||||
Py_CLEAR(tn);
|
||||
|
||||
// Parse coroutine chain
|
||||
if (_Py_RemoteDebug_PagedReadRemoteMemory(&unwinder->handle, task_address,
|
||||
unwinder->async_debug_offsets.asyncio_task_object.size,
|
||||
|
@ -900,31 +985,29 @@ create_task_result(
|
|||
coro_addr &= ~Py_TAG_BITS;
|
||||
|
||||
if ((void*)coro_addr != NULL) {
|
||||
call_stack = PyList_New(0);
|
||||
if (call_stack == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create coro call stack list");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (parse_coro_chain(unwinder, coro_addr, call_stack) < 0) {
|
||||
Py_DECREF(call_stack);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse coroutine chain");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (PyList_Reverse(call_stack)) {
|
||||
Py_DECREF(call_stack);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to reverse call stack");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (PyList_SetItem(result, 0, call_stack) < 0) {
|
||||
Py_DECREF(call_stack);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to set call stack in result");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create final CoroInfo result
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
result = PyStructSequence_New(state->CoroInfo_Type);
|
||||
if (result == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create CoroInfo");
|
||||
goto error;
|
||||
}
|
||||
|
||||
// PyStructSequence_SetItem steals references, so we don't need to DECREF on success
|
||||
PyStructSequence_SetItem(result, 0, call_stack); // This steals the reference
|
||||
PyStructSequence_SetItem(result, 1, tn); // This steals the reference
|
||||
|
||||
return result;
|
||||
|
||||
error:
|
||||
|
@ -943,7 +1026,6 @@ parse_task(
|
|||
) {
|
||||
char is_task;
|
||||
PyObject* result = NULL;
|
||||
PyObject* awaited_by = NULL;
|
||||
int err;
|
||||
|
||||
err = read_char(
|
||||
|
@ -962,48 +1044,37 @@ parse_task(
|
|||
goto error;
|
||||
}
|
||||
} else {
|
||||
result = PyList_New(0);
|
||||
// Create an empty CoroInfo for non-task objects
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
result = PyStructSequence_New(state->CoroInfo_Type);
|
||||
if (result == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty task result");
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty CoroInfo");
|
||||
goto error;
|
||||
}
|
||||
PyObject *empty_list = PyList_New(0);
|
||||
if (empty_list == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty list");
|
||||
goto error;
|
||||
}
|
||||
PyObject *task_name = PyLong_FromUnsignedLongLong(task_address);
|
||||
if (task_name == NULL) {
|
||||
Py_DECREF(empty_list);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task name");
|
||||
goto error;
|
||||
}
|
||||
PyStructSequence_SetItem(result, 0, empty_list); // This steals the reference
|
||||
PyStructSequence_SetItem(result, 1, task_name); // This steals the reference
|
||||
}
|
||||
|
||||
if (PyList_Append(render_to, result)) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task result to render list");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (recurse_task) {
|
||||
awaited_by = PyList_New(0);
|
||||
if (awaited_by == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by list");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (PyList_Append(result, awaited_by)) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by to result");
|
||||
goto error;
|
||||
}
|
||||
Py_DECREF(awaited_by);
|
||||
|
||||
/* awaited_by is borrowed from 'result' to simplify cleanup */
|
||||
if (parse_task_awaited_by(unwinder, task_address, awaited_by, 1) < 0) {
|
||||
// Clear the pointer so the cleanup doesn't try to decref it since
|
||||
// it's borrowed from 'result' and will be decrefed when result is
|
||||
// deleted.
|
||||
awaited_by = NULL;
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task awaited_by relationships");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
Py_DECREF(result);
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
Py_XDECREF(result);
|
||||
Py_XDECREF(awaited_by);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -1161,6 +1232,7 @@ process_single_task_node(
|
|||
PyObject *current_awaited_by = NULL;
|
||||
PyObject *task_id = NULL;
|
||||
PyObject *result_item = NULL;
|
||||
PyObject *coroutine_stack = NULL;
|
||||
|
||||
tn = parse_task_name(unwinder, task_addr);
|
||||
if (tn == NULL) {
|
||||
|
@ -1174,25 +1246,40 @@ process_single_task_node(
|
|||
goto error;
|
||||
}
|
||||
|
||||
// Extract the coroutine stack for this task
|
||||
coroutine_stack = PyList_New(0);
|
||||
if (coroutine_stack == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create coroutine stack list in single task node");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (parse_task(unwinder, task_addr, coroutine_stack, 0) < 0) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse task coroutine stack in single task node");
|
||||
goto error;
|
||||
}
|
||||
|
||||
task_id = PyLong_FromUnsignedLongLong(task_addr);
|
||||
if (task_id == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to create task ID in single task node");
|
||||
goto error;
|
||||
}
|
||||
|
||||
result_item = PyTuple_New(3);
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
result_item = PyStructSequence_New(state->TaskInfo_Type);
|
||||
if (result_item == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create result tuple in single task node");
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create TaskInfo in single task node");
|
||||
goto error;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(result_item, 0, task_id); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 1, tn); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 2, current_awaited_by); // steals ref
|
||||
PyStructSequence_SetItem(result_item, 0, task_id); // steals ref
|
||||
PyStructSequence_SetItem(result_item, 1, tn); // steals ref
|
||||
PyStructSequence_SetItem(result_item, 2, coroutine_stack); // steals ref
|
||||
PyStructSequence_SetItem(result_item, 3, current_awaited_by); // steals ref
|
||||
|
||||
// References transferred to tuple
|
||||
task_id = NULL;
|
||||
tn = NULL;
|
||||
coroutine_stack = NULL;
|
||||
current_awaited_by = NULL;
|
||||
|
||||
if (PyList_Append(result, result_item)) {
|
||||
|
@ -1203,9 +1290,11 @@ process_single_task_node(
|
|||
Py_DECREF(result_item);
|
||||
|
||||
// Get back current_awaited_by reference for parse_task_awaited_by
|
||||
current_awaited_by = PyTuple_GET_ITEM(result_item, 2);
|
||||
current_awaited_by = PyStructSequence_GetItem(result_item, 3);
|
||||
if (parse_task_awaited_by(unwinder, task_addr, current_awaited_by, 0) < 0) {
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to parse awaited_by in single task node");
|
||||
// No cleanup needed here since all references were transferred to result_item
|
||||
// and result_item was already added to result list and decreffed
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -1216,6 +1305,7 @@ error:
|
|||
Py_XDECREF(current_awaited_by);
|
||||
Py_XDECREF(task_id);
|
||||
Py_XDECREF(result_item);
|
||||
Py_XDECREF(coroutine_stack);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
@ -1554,17 +1644,18 @@ done_tlbc:
|
|||
goto error;
|
||||
}
|
||||
|
||||
tuple = PyTuple_New(3);
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
tuple = PyStructSequence_New(state->FrameInfo_Type);
|
||||
if (!tuple) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create result tuple for code object");
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create FrameInfo for code object");
|
||||
goto error;
|
||||
}
|
||||
|
||||
Py_INCREF(meta->func_name);
|
||||
Py_INCREF(meta->file_name);
|
||||
PyTuple_SET_ITEM(tuple, 0, meta->file_name);
|
||||
PyTuple_SET_ITEM(tuple, 1, lineno);
|
||||
PyTuple_SET_ITEM(tuple, 2, meta->func_name);
|
||||
PyStructSequence_SetItem(tuple, 0, meta->file_name);
|
||||
PyStructSequence_SetItem(tuple, 1, lineno);
|
||||
PyStructSequence_SetItem(tuple, 2, meta->func_name);
|
||||
|
||||
*result = tuple;
|
||||
return 0;
|
||||
|
@ -2212,23 +2303,24 @@ append_awaited_by(
|
|||
return -1;
|
||||
}
|
||||
|
||||
PyObject *result_item = PyTuple_New(2);
|
||||
if (result_item == NULL) {
|
||||
Py_DECREF(tid_py);
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by result tuple");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyObject* awaited_by_for_thread = PyList_New(0);
|
||||
if (awaited_by_for_thread == NULL) {
|
||||
Py_DECREF(tid_py);
|
||||
Py_DECREF(result_item);
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by thread list");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(result_item, 0, tid_py); // steals ref
|
||||
PyTuple_SET_ITEM(result_item, 1, awaited_by_for_thread); // steals ref
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
PyObject *result_item = PyStructSequence_New(state->AwaitedInfo_Type);
|
||||
if (result_item == NULL) {
|
||||
Py_DECREF(tid_py);
|
||||
Py_DECREF(awaited_by_for_thread);
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create AwaitedInfo");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyStructSequence_SetItem(result_item, 0, tid_py); // steals ref
|
||||
PyStructSequence_SetItem(result_item, 1, awaited_by_for_thread); // steals ref
|
||||
if (PyList_Append(result, result_item)) {
|
||||
Py_DECREF(result_item);
|
||||
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by result item");
|
||||
|
@ -2352,14 +2444,15 @@ unwind_stack_for_thread(
|
|||
goto error;
|
||||
}
|
||||
|
||||
result = PyTuple_New(2);
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
|
||||
result = PyStructSequence_New(state->ThreadInfo_Type);
|
||||
if (result == NULL) {
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create thread unwind result tuple");
|
||||
set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create ThreadInfo");
|
||||
goto error;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(result, 0, thread_id); // Steals reference
|
||||
PyTuple_SET_ITEM(result, 1, frame_info); // Steals reference
|
||||
PyStructSequence_SetItem(result, 0, thread_id); // Steals reference
|
||||
PyStructSequence_SetItem(result, 1, frame_info); // Steals reference
|
||||
|
||||
cleanup_stack_chunks(&chunks);
|
||||
return result;
|
||||
|
@ -2414,6 +2507,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
|
|||
/*[clinic end generated code: output=3982f2a7eba49334 input=48a762566b828e91]*/
|
||||
{
|
||||
self->debug = debug;
|
||||
self->cached_state = NULL;
|
||||
if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) {
|
||||
set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle");
|
||||
return -1;
|
||||
|
@ -2860,6 +2954,47 @@ _remote_debugging_exec(PyObject *m)
|
|||
if (PyModule_AddType(m, st->RemoteDebugging_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Initialize structseq types
|
||||
st->TaskInfo_Type = PyStructSequence_NewType(&TaskInfo_desc);
|
||||
if (st->TaskInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->TaskInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc);
|
||||
if (st->FrameInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->FrameInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
st->CoroInfo_Type = PyStructSequence_NewType(&CoroInfo_desc);
|
||||
if (st->CoroInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->CoroInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
st->ThreadInfo_Type = PyStructSequence_NewType(&ThreadInfo_desc);
|
||||
if (st->ThreadInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->ThreadInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc);
|
||||
if (st->AwaitedInfo_Type == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (PyModule_AddType(m, st->AwaitedInfo_Type) < 0) {
|
||||
return -1;
|
||||
}
|
||||
#ifdef Py_GIL_DISABLED
|
||||
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
|
||||
#endif
|
||||
|
@ -2878,6 +3013,11 @@ remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg)
|
|||
{
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
|
||||
Py_VISIT(state->RemoteDebugging_Type);
|
||||
Py_VISIT(state->TaskInfo_Type);
|
||||
Py_VISIT(state->FrameInfo_Type);
|
||||
Py_VISIT(state->CoroInfo_Type);
|
||||
Py_VISIT(state->ThreadInfo_Type);
|
||||
Py_VISIT(state->AwaitedInfo_Type);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -2886,6 +3026,11 @@ remote_debugging_clear(PyObject *mod)
|
|||
{
|
||||
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
|
||||
Py_CLEAR(state->RemoteDebugging_Type);
|
||||
Py_CLEAR(state->TaskInfo_Type);
|
||||
Py_CLEAR(state->FrameInfo_Type);
|
||||
Py_CLEAR(state->CoroInfo_Type);
|
||||
Py_CLEAR(state->ThreadInfo_Type);
|
||||
Py_CLEAR(state->AwaitedInfo_Type);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue