| 156 | return all_task_ids - all_parent_ids |
| 157 | |
| 158 | def _build_linear_stacks(self, leaf_task_ids, task_map, child_to_parent): |
| 159 | for leaf_id in leaf_task_ids: |
| 160 | frames = [] |
| 161 | visited = set() |
| 162 | current_id = leaf_id |
| 163 | thread_id = None |
| 164 | |
| 165 | # Follow the single parent chain from leaf to root |
| 166 | while current_id is not None: |
| 167 | # Cycle detection |
| 168 | if current_id in visited: |
| 169 | break |
| 170 | visited.add(current_id) |
| 171 | |
| 172 | # Check if task exists in task_map |
| 173 | if current_id not in task_map: |
| 174 | break |
| 175 | |
| 176 | task_info, tid = task_map[current_id] |
| 177 | |
| 178 | # Set thread_id from first task |
| 179 | if thread_id is None: |
| 180 | thread_id = tid |
| 181 | |
| 182 | # Add all frames from all coroutines in this task |
| 183 | if task_info.coroutine_stack: |
| 184 | for coro_info in task_info.coroutine_stack: |
| 185 | for frame in coro_info.call_stack: |
| 186 | frames.append(frame) |
| 187 | |
| 188 | # Get pre-computed parent info (no sorting needed!) |
| 189 | parent_info = child_to_parent.get(current_id) |
| 190 | |
| 191 | # Add task boundary marker with parent count annotation if multiple parents |
| 192 | task_name = task_info.task_name or "Task-" + str(task_info.task_id) |
| 193 | if parent_info: |
| 194 | selected_parent, parent_count = parent_info |
| 195 | if parent_count > 1: |
| 196 | task_name = f"{task_name} ({parent_count} parents)" |
| 197 | frames.append(FrameInfo(("<task>", None, task_name, None))) |
| 198 | current_id = selected_parent |
| 199 | else: |
| 200 | # Root task - no parent |
| 201 | frames.append(FrameInfo(("<task>", None, task_name, None))) |
| 202 | current_id = None |
| 203 | |
| 204 | # Yield the complete stack if we collected any frames |
| 205 | if frames and thread_id is not None: |
| 206 | yield frames, thread_id, leaf_id |
| 207 | |
| 208 | def _is_gc_frame(self, frame): |
| 209 | if isinstance(frame, tuple): |