]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135371: Fix asyncio introspection output to include internal coroutine chains...
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Sat, 14 Jun 2025 12:48:25 +0000 (13:48 +0100)
committerGitHub <noreply@github.com>
Sat, 14 Jun 2025 12:48:25 +0000 (13:48 +0100)
Doc/whatsnew/3.14.rst
Lib/asyncio/tools.py
Lib/test/test_asyncio/test_tools.py
Lib/test/test_external_inspection.py
Misc/NEWS.d/next/Core_and_Builtins/2025-06-12-18-12-42.gh-issue-135371.R_YUtR.rst [new file with mode: 0644]
Modules/_remote_debugging_module.c

index ca330a32b33c4b005097f17d7834bc0444402868..705bf46d603697f43eb55415393a69f542af9723 100644 (file)
@@ -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
-
-
-or:
+  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 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`.)
index 3fc4524c008db6a65efca59068030e8ad9e4dfa4..2683f34cc7113b573286b43280e4cd52df5a01bf 100644 (file)
@@ -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
-
-
-def _build_tree(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, 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)
-
-    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
+    cor_nodes = defaultdict(dict)  # Maps parent -> {frame_name: node_key}
+    next_cor_id = count(1)
+
+    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:
index ba36e759ccdd616f6e2c9b7bf09ba7658094ef20..34e94830204cd8c53f0ba7d1ca4385ce58b889f3 100644 (file)
@@ -2,6 +2,13 @@ import unittest
 
 from asyncio import tools
 
+from collections import namedtuple
+
+FrameInfo = namedtuple('FrameInfo', ['funcname', 'filename', 'lineno'])
+CoroInfo = namedtuple('CoroInfo', ['call_stack', 'task_name'])
+TaskInfo = namedtuple('TaskInfo', ['task_id', 'task_name', 'coroutine_stack', 'awaited_by'])
+AwaitedInfo = namedtuple('AwaitedInfo', ['thread_id', 'awaited_by'])
+
 
 # mock output of get_all_awaited_by function.
 TEST_INPUTS_TREE = [
@@ -10,81 +17,151 @@ TEST_INPUTS_TREE = [
         # different subtasks part of a TaskGroup (root1 and root2) which call
         # awaiter functions.
         (
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                    (
-                        3,
-                        "timer",
-                        [
-                            [[("awaiter3", "/path/to/app.py", 130),
-                              ("awaiter2", "/path/to/app.py", 120),
-                              ("awaiter", "/path/to/app.py", 110)], 4],
-                            [[("awaiterB3", "/path/to/app.py", 190),
-                              ("awaiterB2", "/path/to/app.py", 180),
-                              ("awaiterB", "/path/to/app.py", 170)], 5],
-                            [[("awaiterB3", "/path/to/app.py", 190),
-                              ("awaiterB2", "/path/to/app.py", 180),
-                              ("awaiterB", "/path/to/app.py", 170)], 6],
-                            [[("awaiter3", "/path/to/app.py", 130),
-                              ("awaiter2", "/path/to/app.py", 120),
-                              ("awaiter", "/path/to/app.py", 110)], 7],
-                        ],
-                    ),
-                    (
-                        8,
-                        "root1",
-                        [[["_aexit", "__aexit__", "main"], 2]],
-                    ),
-                    (
-                        9,
-                        "root2",
-                        [[["_aexit", "__aexit__", "main"], 2]],
-                    ),
-                    (
-                        4,
-                        "child1_1",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                8,
-                            ]
-                        ],
-                    ),
-                    (
-                        6,
-                        "child2_1",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                8,
-                            ]
-                        ],
-                    ),
-                    (
-                        7,
-                        "child1_2",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                9,
-                            ]
-                        ],
-                    ),
-                    (
-                        5,
-                        "child2_2",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                9,
-                            ]
-                        ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
                     ),
-                ],
+                    TaskInfo(
+                        task_id=3,
+                        task_name="timer",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter3", "/path/to/app.py", 130),
+                                    FrameInfo("awaiter2", "/path/to/app.py", 120),
+                                    FrameInfo("awaiter", "/path/to/app.py", 110)
+                                ],
+                                task_name=4
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiterB3", "/path/to/app.py", 190),
+                                    FrameInfo("awaiterB2", "/path/to/app.py", 180),
+                                    FrameInfo("awaiterB", "/path/to/app.py", 170)
+                                ],
+                                task_name=5
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiterB3", "/path/to/app.py", 190),
+                                    FrameInfo("awaiterB2", "/path/to/app.py", 180),
+                                    FrameInfo("awaiterB", "/path/to/app.py", 170)
+                                ],
+                                task_name=6
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter3", "/path/to/app.py", 130),
+                                    FrameInfo("awaiter2", "/path/to/app.py", 120),
+                                    FrameInfo("awaiter", "/path/to/app.py", 110)
+                                ],
+                                task_name=7
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=8,
+                        task_name="root1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("main", "", 0)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=9,
+                        task_name="root2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("main", "", 0)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="child1_1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=8
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=6,
+                        task_name="child2_1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=8
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=7,
+                        task_name="child1_2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=9
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=5,
+                        task_name="child2_2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=9
+                            )
+                        ]
+                    )
+                ]
             ),
-            (0, []),
+            AwaitedInfo(thread_id=0, awaited_by=[])
         ),
         (
             [
@@ -130,26 +207,96 @@ TEST_INPUTS_TREE = [
     [
         # test case containing two roots
         (
-            (
-                9,
-                [
-                    (5, "Task-5", []),
-                    (6, "Task-6", [[["main2"], 5]]),
-                    (7, "Task-7", [[["main2"], 5]]),
-                    (8, "Task-8", [[["main2"], 5]]),
-                ],
+            AwaitedInfo(
+                thread_id=9,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=5,
+                        task_name="Task-5",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=6,
+                        task_name="Task-6",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=7,
+                        task_name="Task-7",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=8,
+                        task_name="Task-8",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    )
+                ]
             ),
-            (
-                10,
-                [
-                    (1, "Task-1", []),
-                    (2, "Task-2", [[["main"], 1]]),
-                    (3, "Task-3", [[["main"], 1]]),
-                    (4, "Task-4", [[["main"], 1]]),
-                ],
+            AwaitedInfo(
+                thread_id=10,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-3",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="Task-4",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    )
+                ]
             ),
-            (11, []),
-            (0, []),
+            AwaitedInfo(thread_id=11, awaited_by=[]),
+            AwaitedInfo(thread_id=0, awaited_by=[])
         ),
         (
             [
@@ -174,18 +321,63 @@ TEST_INPUTS_TREE = [
         # test case containing two roots, one of them without subtasks
         (
             [
-                (1, [(2, "Task-5", [])]),
-                (
-                    3,
-                    [
-                        (4, "Task-1", []),
-                        (5, "Task-2", [[["main"], 4]]),
-                        (6, "Task-3", [[["main"], 4]]),
-                        (7, "Task-4", [[["main"], 4]]),
-                    ],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-5",
+                            coroutine_stack=[],
+                            awaited_by=[]
+                        )
+                    ]
                 ),
-                (8, []),
-                (0, []),
+                AwaitedInfo(
+                    thread_id=3,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=4,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
+                        ),
+                        TaskInfo(
+                            task_id=5,
+                            task_name="Task-2",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=6,
+                            task_name="Task-3",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=7,
+                            task_name="Task-4",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        )
+                    ]
+                ),
+                AwaitedInfo(thread_id=8, awaited_by=[]),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         (
@@ -208,19 +400,44 @@ TEST_INPUTS_CYCLES_TREE = [
         # this test case contains a cycle: two tasks awaiting each other.
         (
             [
-                (
-                    1,
-                    [
-                        (2, "Task-1", []),
-                        (
-                            3,
-                            "a",
-                            [[["awaiter2"], 4], [["main"], 2]],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
                         ),
-                        (4, "b", [[["awaiter"], 3]]),
-                    ],
+                        TaskInfo(
+                            task_id=3,
+                            task_name="a",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("awaiter2", "", 0)],
+                                    task_name=4
+                                ),
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=2
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=4,
+                            task_name="b",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("awaiter", "", 0)],
+                                    task_name=3
+                                )
+                            ]
+                        )
+                    ]
                 ),
-                (0, []),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         ([[4, 3, 4]]),
@@ -229,32 +446,85 @@ TEST_INPUTS_CYCLES_TREE = [
         # this test case contains two cycles
         (
             [
-                (
-                    1,
-                    [
-                        (2, "Task-1", []),
-                        (
-                            3,
-                            "A",
-                            [[["nested", "nested", "task_b"], 4]],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
                         ),
-                        (
-                            4,
-                            "B",
-                            [
-                                [["nested", "nested", "task_c"], 5],
-                                [["nested", "nested", "task_a"], 3],
-                            ],
+                        TaskInfo(
+                            task_id=3,
+                            task_name="A",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_b", "", 0)
+                                    ],
+                                    task_name=4
+                                )
+                            ]
                         ),
-                        (5, "C", [[["nested", "nested"], 6]]),
-                        (
-                            6,
-                            "Task-2",
-                            [[["nested", "nested", "task_b"], 4]],
+                        TaskInfo(
+                            task_id=4,
+                            task_name="B",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_c", "", 0)
+                                    ],
+                                    task_name=5
+                                ),
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_a", "", 0)
+                                    ],
+                                    task_name=3
+                                )
+                            ]
                         ),
-                    ],
+                        TaskInfo(
+                            task_id=5,
+                            task_name="C",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0)
+                                    ],
+                                    task_name=6
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=6,
+                            task_name="Task-2",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_b", "", 0)
+                                    ],
+                                    task_name=4
+                                )
+                            ]
+                        )
+                    ]
                 ),
-                (0, []),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         ([[4, 3, 4], [4, 6, 5, 4]]),
@@ -267,81 +537,160 @@ TEST_INPUTS_TABLE = [
         # different subtasks part of a TaskGroup (root1 and root2) which call
         # awaiter functions.
         (
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                    (
-                        3,
-                        "timer",
-                        [
-                            [["awaiter3", "awaiter2", "awaiter"], 4],
-                            [["awaiter1_3", "awaiter1_2", "awaiter1"], 5],
-                            [["awaiter1_3", "awaiter1_2", "awaiter1"], 6],
-                            [["awaiter3", "awaiter2", "awaiter"], 7],
-                        ],
-                    ),
-                    (
-                        8,
-                        "root1",
-                        [[["_aexit", "__aexit__", "main"], 2]],
-                    ),
-                    (
-                        9,
-                        "root2",
-                        [[["_aexit", "__aexit__", "main"], 2]],
-                    ),
-                    (
-                        4,
-                        "child1_1",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                8,
-                            ]
-                        ],
-                    ),
-                    (
-                        6,
-                        "child2_1",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                8,
-                            ]
-                        ],
-                    ),
-                    (
-                        7,
-                        "child1_2",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                9,
-                            ]
-                        ],
-                    ),
-                    (
-                        5,
-                        "child2_2",
-                        [
-                            [
-                                ["_aexit", "__aexit__", "blocho_caller", "bloch"],
-                                9,
-                            ]
-                        ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
                     ),
-                ],
+                    TaskInfo(
+                        task_id=3,
+                        task_name="timer",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter3", "", 0),
+                                    FrameInfo("awaiter2", "", 0),
+                                    FrameInfo("awaiter", "", 0)
+                                ],
+                                task_name=4
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter1_3", "", 0),
+                                    FrameInfo("awaiter1_2", "", 0),
+                                    FrameInfo("awaiter1", "", 0)
+                                ],
+                                task_name=5
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter1_3", "", 0),
+                                    FrameInfo("awaiter1_2", "", 0),
+                                    FrameInfo("awaiter1", "", 0)
+                                ],
+                                task_name=6
+                            ),
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("awaiter3", "", 0),
+                                    FrameInfo("awaiter2", "", 0),
+                                    FrameInfo("awaiter", "", 0)
+                                ],
+                                task_name=7
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=8,
+                        task_name="root1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("main", "", 0)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=9,
+                        task_name="root2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("main", "", 0)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="child1_1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=8
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=6,
+                        task_name="child2_1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=8
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=7,
+                        task_name="child1_2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=9
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=5,
+                        task_name="child2_2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("_aexit", "", 0),
+                                    FrameInfo("__aexit__", "", 0),
+                                    FrameInfo("blocho_caller", "", 0),
+                                    FrameInfo("bloch", "", 0)
+                                ],
+                                task_name=9
+                            )
+                        ]
+                    )
+                ]
             ),
-            (0, []),
+            AwaitedInfo(thread_id=0, awaited_by=[])
         ),
         (
             [
-                [1, "0x2", "Task-1", "", "", "0x0"],
+                [1, "0x2", "Task-1", "", "", "", "0x0"],
                 [
                     1,
                     "0x3",
                     "timer",
+                    "",
                     "awaiter3 -> awaiter2 -> awaiter",
                     "child1_1",
                     "0x4",
@@ -350,6 +699,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x3",
                     "timer",
+                    "",
                     "awaiter1_3 -> awaiter1_2 -> awaiter1",
                     "child2_2",
                     "0x5",
@@ -358,6 +708,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x3",
                     "timer",
+                    "",
                     "awaiter1_3 -> awaiter1_2 -> awaiter1",
                     "child2_1",
                     "0x6",
@@ -366,6 +717,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x3",
                     "timer",
+                    "",
                     "awaiter3 -> awaiter2 -> awaiter",
                     "child1_2",
                     "0x7",
@@ -374,6 +726,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x8",
                     "root1",
+                    "",
                     "_aexit -> __aexit__ -> main",
                     "Task-1",
                     "0x2",
@@ -382,6 +735,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x9",
                     "root2",
+                    "",
                     "_aexit -> __aexit__ -> main",
                     "Task-1",
                     "0x2",
@@ -390,6 +744,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x4",
                     "child1_1",
+                    "",
                     "_aexit -> __aexit__ -> blocho_caller -> bloch",
                     "root1",
                     "0x8",
@@ -398,6 +753,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x6",
                     "child2_1",
+                    "",
                     "_aexit -> __aexit__ -> blocho_caller -> bloch",
                     "root1",
                     "0x8",
@@ -406,6 +762,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x7",
                     "child1_2",
+                    "",
                     "_aexit -> __aexit__ -> blocho_caller -> bloch",
                     "root2",
                     "0x9",
@@ -414,6 +771,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x5",
                     "child2_2",
+                    "",
                     "_aexit -> __aexit__ -> blocho_caller -> bloch",
                     "root2",
                     "0x9",
@@ -424,37 +782,107 @@ TEST_INPUTS_TABLE = [
     [
         # test case containing two roots
         (
-            (
-                9,
-                [
-                    (5, "Task-5", []),
-                    (6, "Task-6", [[["main2"], 5]]),
-                    (7, "Task-7", [[["main2"], 5]]),
-                    (8, "Task-8", [[["main2"], 5]]),
-                ],
+            AwaitedInfo(
+                thread_id=9,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=5,
+                        task_name="Task-5",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=6,
+                        task_name="Task-6",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=7,
+                        task_name="Task-7",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=8,
+                        task_name="Task-8",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main2", "", 0)],
+                                task_name=5
+                            )
+                        ]
+                    )
+                ]
             ),
-            (
-                10,
-                [
-                    (1, "Task-1", []),
-                    (2, "Task-2", [[["main"], 1]]),
-                    (3, "Task-3", [[["main"], 1]]),
-                    (4, "Task-4", [[["main"], 1]]),
-                ],
+            AwaitedInfo(
+                thread_id=10,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-3",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="Task-4",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    )
+                ]
             ),
-            (11, []),
-            (0, []),
+            AwaitedInfo(thread_id=11, awaited_by=[]),
+            AwaitedInfo(thread_id=0, awaited_by=[])
         ),
         (
             [
-                [9, "0x5", "Task-5", "", "", "0x0"],
-                [9, "0x6", "Task-6", "main2", "Task-5", "0x5"],
-                [9, "0x7", "Task-7", "main2", "Task-5", "0x5"],
-                [9, "0x8", "Task-8", "main2", "Task-5", "0x5"],
-                [10, "0x1", "Task-1", "", "", "0x0"],
-                [10, "0x2", "Task-2", "main", "Task-1", "0x1"],
-                [10, "0x3", "Task-3", "main", "Task-1", "0x1"],
-                [10, "0x4", "Task-4", "main", "Task-1", "0x1"],
+                [9, "0x5", "Task-5", "", "", "", "0x0"],
+                [9, "0x6", "Task-6", "", "main2", "Task-5", "0x5"],
+                [9, "0x7", "Task-7", "", "main2", "Task-5", "0x5"],
+                [9, "0x8", "Task-8", "", "main2", "Task-5", "0x5"],
+                [10, "0x1", "Task-1", "", "", "", "0x0"],
+                [10, "0x2", "Task-2", "", "main", "Task-1", "0x1"],
+                [10, "0x3", "Task-3", "", "main", "Task-1", "0x1"],
+                [10, "0x4", "Task-4", "", "main", "Task-1", "0x1"],
             ]
         ),
     ],
@@ -462,27 +890,72 @@ TEST_INPUTS_TABLE = [
         # test case containing two roots, one of them without subtasks
         (
             [
-                (1, [(2, "Task-5", [])]),
-                (
-                    3,
-                    [
-                        (4, "Task-1", []),
-                        (5, "Task-2", [[["main"], 4]]),
-                        (6, "Task-3", [[["main"], 4]]),
-                        (7, "Task-4", [[["main"], 4]]),
-                    ],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-5",
+                            coroutine_stack=[],
+                            awaited_by=[]
+                        )
+                    ]
                 ),
-                (8, []),
-                (0, []),
+                AwaitedInfo(
+                    thread_id=3,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=4,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
+                        ),
+                        TaskInfo(
+                            task_id=5,
+                            task_name="Task-2",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=6,
+                            task_name="Task-3",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=7,
+                            task_name="Task-4",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=4
+                                )
+                            ]
+                        )
+                    ]
+                ),
+                AwaitedInfo(thread_id=8, awaited_by=[]),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         (
             [
-                [1, "0x2", "Task-5", "", "", "0x0"],
-                [3, "0x4", "Task-1", "", "", "0x0"],
-                [3, "0x5", "Task-2", "main", "Task-1", "0x4"],
-                [3, "0x6", "Task-3", "main", "Task-1", "0x4"],
-                [3, "0x7", "Task-4", "main", "Task-1", "0x4"],
+                [1, "0x2", "Task-5", "", "", "", "0x0"],
+                [3, "0x4", "Task-1", "", "", "", "0x0"],
+                [3, "0x5", "Task-2", "", "main", "Task-1", "0x4"],
+                [3, "0x6", "Task-3", "", "main", "Task-1", "0x4"],
+                [3, "0x7", "Task-4", "", "main", "Task-1", "0x4"],
             ]
         ),
     ],
@@ -491,27 +964,52 @@ TEST_INPUTS_TABLE = [
         # this test case contains a cycle: two tasks awaiting each other.
         (
             [
-                (
-                    1,
-                    [
-                        (2, "Task-1", []),
-                        (
-                            3,
-                            "a",
-                            [[["awaiter2"], 4], [["main"], 2]],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
                         ),
-                        (4, "b", [[["awaiter"], 3]]),
-                    ],
+                        TaskInfo(
+                            task_id=3,
+                            task_name="a",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("awaiter2", "", 0)],
+                                    task_name=4
+                                ),
+                                CoroInfo(
+                                    call_stack=[FrameInfo("main", "", 0)],
+                                    task_name=2
+                                )
+                            ]
+                        ),
+                        TaskInfo(
+                            task_id=4,
+                            task_name="b",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[FrameInfo("awaiter", "", 0)],
+                                    task_name=3
+                                )
+                            ]
+                        )
+                    ]
                 ),
-                (0, []),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         (
             [
-                [1, "0x2", "Task-1", "", "", "0x0"],
-                [1, "0x3", "a", "awaiter2", "b", "0x4"],
-                [1, "0x3", "a", "main", "Task-1", "0x2"],
-                [1, "0x4", "b", "awaiter", "a", "0x3"],
+                [1, "0x2", "Task-1", "", "", "", "0x0"],
+                [1, "0x3", "a", "", "awaiter2", "b", "0x4"],
+                [1, "0x3", "a", "", "main", "Task-1", "0x2"],
+                [1, "0x4", "b", "", "awaiter", "a", "0x3"],
             ]
         ),
     ],
@@ -519,41 +1017,95 @@ TEST_INPUTS_TABLE = [
         # this test case contains two cycles
         (
             [
-                (
-                    1,
-                    [
-                        (2, "Task-1", []),
-                        (
-                            3,
-                            "A",
-                            [[["nested", "nested", "task_b"], 4]],
+                AwaitedInfo(
+                    thread_id=1,
+                    awaited_by=[
+                        TaskInfo(
+                            task_id=2,
+                            task_name="Task-1",
+                            coroutine_stack=[],
+                            awaited_by=[]
+                        ),
+                        TaskInfo(
+                            task_id=3,
+                            task_name="A",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_b", "", 0)
+                                    ],
+                                    task_name=4
+                                )
+                            ]
                         ),
-                        (
-                            4,
-                            "B",
-                            [
-                                [["nested", "nested", "task_c"], 5],
-                                [["nested", "nested", "task_a"], 3],
-                            ],
+                        TaskInfo(
+                            task_id=4,
+                            task_name="B",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_c", "", 0)
+                                    ],
+                                    task_name=5
+                                ),
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_a", "", 0)
+                                    ],
+                                    task_name=3
+                                )
+                            ]
                         ),
-                        (5, "C", [[["nested", "nested"], 6]]),
-                        (
-                            6,
-                            "Task-2",
-                            [[["nested", "nested", "task_b"], 4]],
+                        TaskInfo(
+                            task_id=5,
+                            task_name="C",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0)
+                                    ],
+                                    task_name=6
+                                )
+                            ]
                         ),
-                    ],
+                        TaskInfo(
+                            task_id=6,
+                            task_name="Task-2",
+                            coroutine_stack=[],
+                            awaited_by=[
+                                CoroInfo(
+                                    call_stack=[
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("nested", "", 0),
+                                        FrameInfo("task_b", "", 0)
+                                    ],
+                                    task_name=4
+                                )
+                            ]
+                        )
+                    ]
                 ),
-                (0, []),
+                AwaitedInfo(thread_id=0, awaited_by=[])
             ]
         ),
         (
             [
-                [1, "0x2", "Task-1", "", "", "0x0"],
+                [1, "0x2", "Task-1", "", "", "", "0x0"],
                 [
                     1,
                     "0x3",
                     "A",
+                    "",
                     "nested -> nested -> task_b",
                     "B",
                     "0x4",
@@ -562,6 +1114,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x4",
                     "B",
+                    "",
                     "nested -> nested -> task_c",
                     "C",
                     "0x5",
@@ -570,6 +1123,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x4",
                     "B",
+                    "",
                     "nested -> nested -> task_a",
                     "A",
                     "0x3",
@@ -578,6 +1132,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x5",
                     "C",
+                    "",
                     "nested -> nested",
                     "Task-2",
                     "0x6",
@@ -586,6 +1141,7 @@ TEST_INPUTS_TABLE = [
                     1,
                     "0x6",
                     "Task-2",
+                    "",
                     "nested -> nested -> task_b",
                     "B",
                     "0x4",
@@ -600,7 +1156,8 @@ class TestAsyncioToolsTree(unittest.TestCase):
     def test_asyncio_utils(self):
         for input_, tree in TEST_INPUTS_TREE:
             with self.subTest(input_):
-                self.assertEqual(tools.build_async_tree(input_), tree)
+                result = tools.build_async_tree(input_)
+                self.assertEqual(result, tree)
 
     def test_asyncio_utils_cycles(self):
         for input_, cycles in TEST_INPUTS_CYCLES_TREE:
@@ -615,7 +1172,8 @@ class TestAsyncioToolsTable(unittest.TestCase):
     def test_asyncio_utils(self):
         for input_, table in TEST_INPUTS_TABLE:
             with self.subTest(input_):
-                self.assertEqual(tools.build_task_table(input_), table)
+                result = tools.build_task_table(input_)
+                self.assertEqual(result, table)
 
 
 class TestAsyncioToolsBasic(unittest.TestCase):
@@ -632,26 +1190,67 @@ class TestAsyncioToolsBasic(unittest.TestCase):
         self.assertEqual(tools.build_task_table(result), expected_output)
 
     def test_only_independent_tasks_tree(self):
-        input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=10,
+                        task_name="taskA",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=11,
+                        task_name="taskB",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
+            )
+        ]
         expected = [["└── (T) taskA"], ["└── (T) taskB"]]
         result = tools.build_async_tree(input_)
         self.assertEqual(sorted(result), sorted(expected))
 
     def test_only_independent_tasks_table(self):
-        input_ = [(1, [(10, "taskA", []), (11, "taskB", [])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=10,
+                        task_name="taskA",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=11,
+                        task_name="taskB",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
+            )
+        ]
         self.assertEqual(
             tools.build_task_table(input_),
-            [[1, "0xa", "taskA", "", "", "0x0"], [1, "0xb", "taskB", "", "", "0x0"]],
+            [[1, '0xa', 'taskA', '', '', '', '0x0'], [1, '0xb', 'taskB', '', '', '', '0x0']]
         )
 
     def test_single_task_tree(self):
         """Test build_async_tree with a single task and no awaits."""
         result = [
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
             )
         ]
         expected_output = [
@@ -664,25 +1263,50 @@ class TestAsyncioToolsBasic(unittest.TestCase):
     def test_single_task_table(self):
         """Test build_task_table with a single task and no awaits."""
         result = [
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
             )
         ]
-        expected_output = [[1, "0x2", "Task-1", "", "", "0x0"]]
+        expected_output = [[1, '0x2', 'Task-1', '', '', '', '0x0']]
         self.assertEqual(tools.build_task_table(result), expected_output)
 
     def test_cycle_detection(self):
         """Test build_async_tree raises CycleFoundException for cyclic input."""
         result = [
-            (
-                1,
-                [
-                    (2, "Task-1", [[["main"], 3]]),
-                    (3, "Task-2", [[["main"], 2]]),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=3
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    )
+                ]
             )
         ]
         with self.assertRaises(tools.CycleFoundException) as context:
@@ -692,13 +1316,38 @@ class TestAsyncioToolsBasic(unittest.TestCase):
     def test_complex_tree(self):
         """Test build_async_tree with a more complex tree structure."""
         result = [
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                    (3, "Task-2", [[["main"], 2]]),
-                    (4, "Task-3", [[["main"], 3]]),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="Task-3",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=3
+                            )
+                        ]
+                    )
+                ]
             )
         ]
         expected_output = [
@@ -715,30 +1364,76 @@ class TestAsyncioToolsBasic(unittest.TestCase):
     def test_complex_table(self):
         """Test build_task_table with a more complex tree structure."""
         result = [
-            (
-                1,
-                [
-                    (2, "Task-1", []),
-                    (3, "Task-2", [[["main"], 2]]),
-                    (4, "Task-3", [[["main"], 3]]),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=4,
+                        task_name="Task-3",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("main", "", 0)],
+                                task_name=3
+                            )
+                        ]
+                    )
+                ]
             )
         ]
         expected_output = [
-            [1, "0x2", "Task-1", "", "", "0x0"],
-            [1, "0x3", "Task-2", "main", "Task-1", "0x2"],
-            [1, "0x4", "Task-3", "main", "Task-2", "0x3"],
+            [1, '0x2', 'Task-1', '', '', '', '0x0'],
+            [1, '0x3', 'Task-2', '', 'main', 'Task-1', '0x2'],
+            [1, '0x4', 'Task-3', '', 'main', 'Task-2', '0x3']
         ]
         self.assertEqual(tools.build_task_table(result), expected_output)
 
     def test_deep_coroutine_chain(self):
         input_ = [
-            (
-                1,
-                [
-                    (10, "leaf", [[["c1", "c2", "c3", "c4", "c5"], 11]]),
-                    (11, "root", []),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=10,
+                        task_name="leaf",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("c1", "", 0),
+                                    FrameInfo("c2", "", 0),
+                                    FrameInfo("c3", "", 0),
+                                    FrameInfo("c4", "", 0),
+                                    FrameInfo("c5", "", 0)
+                                ],
+                                task_name=11
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=11,
+                        task_name="root",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
             )
         ]
         expected = [
@@ -757,13 +1452,47 @@ class TestAsyncioToolsBasic(unittest.TestCase):
 
     def test_multiple_cycles_same_node(self):
         input_ = [
-            (
-                1,
-                [
-                    (1, "Task-A", [[["call1"], 2]]),
-                    (2, "Task-B", [[["call2"], 3]]),
-                    (3, "Task-C", [[["call3"], 1], [["call4"], 2]]),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-A",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("call1", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-B",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("call2", "", 0)],
+                                task_name=3
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-C",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("call3", "", 0)],
+                                task_name=1
+                            ),
+                            CoroInfo(
+                                call_stack=[FrameInfo("call4", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    )
+                ]
             )
         ]
         with self.assertRaises(tools.CycleFoundException) as ctx:
@@ -772,19 +1501,43 @@ class TestAsyncioToolsBasic(unittest.TestCase):
         self.assertTrue(any(set(c) == {1, 2, 3} for c in cycles))
 
     def test_table_output_format(self):
-        input_ = [(1, [(1, "Task-A", [[["foo"], 2]]), (2, "Task-B", [])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-A",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("foo", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-B",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
+            )
+        ]
         table = tools.build_task_table(input_)
         for row in table:
-            self.assertEqual(len(row), 6)
+            self.assertEqual(len(row), 7)
             self.assertIsInstance(row[0], int)  # thread ID
             self.assertTrue(
                 isinstance(row[1], str) and row[1].startswith("0x")
             )  # hex task ID
             self.assertIsInstance(row[2], str)  # task name
-            self.assertIsInstance(row[3], str)  # coroutine chain
-            self.assertIsInstance(row[4], str)  # awaiter name
+            self.assertIsInstance(row[3], str)  # coroutine stack
+            self.assertIsInstance(row[4], str)  # coroutine chain
+            self.assertIsInstance(row[5], str)  # awaiter name
             self.assertTrue(
-                isinstance(row[5], str) and row[5].startswith("0x")
+                isinstance(row[6], str) and row[6].startswith("0x")
             )  # hex awaiter ID
 
 
@@ -792,28 +1545,86 @@ class TestAsyncioToolsEdgeCases(unittest.TestCase):
 
     def test_task_awaits_self(self):
         """A task directly awaits itself - should raise a cycle."""
-        input_ = [(1, [(1, "Self-Awaiter", [[["loopback"], 1]])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Self-Awaiter",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("loopback", "", 0)],
+                                task_name=1
+                            )
+                        ]
+                    )
+                ]
+            )
+        ]
         with self.assertRaises(tools.CycleFoundException) as ctx:
             tools.build_async_tree(input_)
         self.assertIn([1, 1], ctx.exception.cycles)
 
     def test_task_with_missing_awaiter_id(self):
         """Awaiter ID not in task list - should not crash, just show 'Unknown'."""
-        input_ = [(1, [(1, "Task-A", [[["coro"], 999]])])]  # 999 not defined
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-A",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("coro", "", 0)],
+                                task_name=999
+                            )
+                        ]
+                    )
+                ]
+            )
+        ]
         table = tools.build_task_table(input_)
         self.assertEqual(len(table), 1)
-        self.assertEqual(table[0][4], "Unknown")
+        self.assertEqual(table[0][5], "Unknown")
 
     def test_duplicate_coroutine_frames(self):
         """Same coroutine frame repeated under a parent - should deduplicate."""
         input_ = [
-            (
-                1,
-                [
-                    (1, "Task-1", [[["frameA"], 2], [["frameA"], 3]]),
-                    (2, "Task-2", []),
-                    (3, "Task-3", []),
-                ],
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="Task-1",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("frameA", "", 0)],
+                                task_name=2
+                            ),
+                            CoroInfo(
+                                call_stack=[FrameInfo("frameA", "", 0)],
+                                task_name=3
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="Task-2",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    ),
+                    TaskInfo(
+                        task_id=3,
+                        task_name="Task-3",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
             )
         ]
         tree = tools.build_async_tree(input_)
@@ -830,14 +1641,63 @@ class TestAsyncioToolsEdgeCases(unittest.TestCase):
 
     def test_task_with_no_name(self):
         """Task with no name in id2name - should still render with fallback."""
-        input_ = [(1, [(1, "root", [[["f1"], 2]]), (2, None, [])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="root",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[FrameInfo("f1", "", 0)],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name=None,
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
+            )
+        ]
         # If name is None, fallback to string should not crash
         tree = tools.build_async_tree(input_)
         self.assertIn("(T) None", "\n".join(tree[0]))
 
     def test_tree_rendering_with_custom_emojis(self):
         """Pass custom emojis to the tree renderer."""
-        input_ = [(1, [(1, "MainTask", [[["f1", "f2"], 2]]), (2, "SubTask", [])])]
+        input_ = [
+            AwaitedInfo(
+                thread_id=1,
+                awaited_by=[
+                    TaskInfo(
+                        task_id=1,
+                        task_name="MainTask",
+                        coroutine_stack=[],
+                        awaited_by=[
+                            CoroInfo(
+                                call_stack=[
+                                    FrameInfo("f1", "", 0),
+                                    FrameInfo("f2", "", 0)
+                                ],
+                                task_name=2
+                            )
+                        ]
+                    ),
+                    TaskInfo(
+                        task_id=2,
+                        task_name="SubTask",
+                        coroutine_stack=[],
+                        awaited_by=[]
+                    )
+                ]
+            )
+        ]
         tree = tools.build_async_tree(input_, task_emoji="🧵", cor_emoji="🔁")
         flat = "\n".join(tree[0])
         self.assertIn("🧵 MainTask", flat)
index 303af25fc7a715fd88689edbdc624105232b63d7..90214e814f2b35efd0301b5f2f685b3c461159d9 100644 (file)
@@ -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",
+                    ]
                 ),
-            ]
+            ],
         )
 
 
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-12-18-12-42.gh-issue-135371.R_YUtR.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-12-18-12-42.gh-issue-135371.R_YUtR.rst
new file mode 100644 (file)
index 0000000..9f2e825
--- /dev/null
@@ -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.
index ea58f38006e199da42924590176a2282eec42421..19f12c3b02e5a40af10c77ff817c2087cd79dfd7 100644 (file)
@@ -97,6 +97,101 @@ struct _Py_AsyncioModuleDebugOffsets {
     } asyncio_thread_state;
 };
 
+/* ============================================================================
+ * 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;
+    PyObject *file_name;
+    int first_lineno;
+    PyObject *linetable;  // bytes
+    uintptr_t addr_code_adaptive;
+} CachedCodeMetadata;
+
+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;
@@ -109,6 +204,7 @@ typedef struct {
     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
@@ -116,19 +212,6 @@ typedef struct {
 #endif
 } RemoteUnwinderObject;
 
-typedef struct {
-    PyObject *func_name;
-    PyObject *file_name;
-    int first_lineno;
-    PyObject *linetable;  // bytes
-    uintptr_t addr_code_adaptive;
-} CachedCodeMetadata;
-
-typedef struct {
-    /* Types */
-    PyTypeObject *RemoteDebugging_Type;
-} RemoteDebuggingState;
-
 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;
         }
-    }
-
-    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");
+        PyObject *empty_list = PyList_New(0);
+        if (empty_list == NULL) {
+            set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create empty list");
             goto error;
         }
-
-        if (PyList_Append(result, awaited_by)) {
-            set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append awaited_by to result");
+        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;
         }
-        Py_DECREF(awaited_by);
+        PyStructSequence_SetItem(result, 0, empty_list);  // This steals the reference
+        PyStructSequence_SetItem(result, 1, task_name);  // This steals the reference
+    }
 
-        /* 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;
-        }
+    if (PyList_Append(render_to, result)) {
+        set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to append task result to render list");
+        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) {
+    PyObject* awaited_by_for_thread = PyList_New(0);
+    if (awaited_by_for_thread == NULL) {
         Py_DECREF(tid_py);
-        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by result tuple");
+        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by thread list");
         return -1;
     }
 
-    PyObject* awaited_by_for_thread = PyList_New(0);
-    if (awaited_by_for_thread == NULL) {
+    RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)unwinder);
+    PyObject *result_item = PyStructSequence_New(state->AwaitedInfo_Type);
+    if (result_item == NULL) {
         Py_DECREF(tid_py);
-        Py_DECREF(result_item);
-        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create awaited_by thread list");
+        Py_DECREF(awaited_by_for_thread);
+        set_exception_cause(unwinder, PyExc_MemoryError, "Failed to create AwaitedInfo");
         return -1;
     }
 
-    PyTuple_SET_ITEM(result_item, 0, tid_py);  // steals ref
-    PyTuple_SET_ITEM(result_item, 1, awaited_by_for_thread);  // steals ref
+    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;
 }