Skip to content

Commit af65c15

Browse files
It might work but the code is bad
1 parent b85e10f commit af65c15

4 files changed

Lines changed: 119 additions & 9 deletions

File tree

Lib/profiling/sampling/collector.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from abc import ABC, abstractmethod
22

3+
from _remote_debugging import FrameInfo
4+
5+
36
# Enums are slow
47
THREAD_STATE_RUNNING = 0
58
THREAD_STATE_IDLE = 1
@@ -31,3 +34,85 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
3134
frames = thread_info.frame_info
3235
if frames:
3336
yield frames, thread_info.thread_id
37+
38+
def _iter_async_frames(self, awaited_info_list):
39+
"""
40+
Iterate over all async frame stacks from awaited info.
41+
Yields one stack per task in LEAF→ROOT order: [task body, task marker, parent body, ..., Program Root]
42+
"""
43+
# Index all tasks by ID
44+
all_tasks = {}
45+
for awaited_info in awaited_info_list:
46+
for task_info in awaited_info.awaited_by:
47+
all_tasks[task_info.task_id] = (task_info, awaited_info.thread_id)
48+
49+
cache = {} # Memoize parent chains
50+
51+
def build_parent_chain(task_id, parent_id):
52+
"""Build ancestor chain: [await-site frames, grandparent chain..., Program Root]"""
53+
if parent_id in cache:
54+
return cache[parent_id]
55+
56+
if parent_id not in all_tasks:
57+
return []
58+
59+
parent_info, _ = all_tasks[parent_id]
60+
61+
# Find the await-site frames for this parent relationship
62+
await_frames = []
63+
for coro_info in all_tasks[task_id][0].awaited_by:
64+
if coro_info.task_name == parent_id:
65+
await_frames = list(coro_info.call_stack or [])
66+
break
67+
68+
# Recursively build grandparent chain, or terminate with Program Root
69+
if (parent_info.awaited_by and parent_info.awaited_by[0].task_name and
70+
parent_info.awaited_by[0].task_name in all_tasks):
71+
grandparent_id = parent_info.awaited_by[0].task_name
72+
chain = await_frames + build_parent_chain(parent_id, grandparent_id)
73+
else:
74+
# Parent is root or grandparent not tracked
75+
root_frame = FrameInfo(("<thread>", 0, "Program Root"))
76+
chain = await_frames + [root_frame]
77+
78+
cache[parent_id] = chain
79+
return chain
80+
81+
# Find all parent task IDs (tasks that have children)
82+
parent_task_ids = {
83+
coro.task_name
84+
for task_info, _ in all_tasks.values()
85+
for coro in (task_info.awaited_by or [])
86+
if coro.task_name
87+
}
88+
89+
# Yield one stack per leaf task (tasks that are not parents)
90+
for task_id, (task_info, thread_id) in all_tasks.items():
91+
# Skip parent tasks - they'll be included in their children's stacks
92+
if task_id in parent_task_ids:
93+
continue
94+
# Collect task's coroutine frames
95+
body_frames = [
96+
frame
97+
for coro in (task_info.coroutine_stack or [])
98+
for frame in (coro.call_stack or [])
99+
]
100+
101+
# Add synthetic task marker
102+
task_name = task_info.task_name or f"Task-{task_id}"
103+
synthetic = FrameInfo(("<task>", 0, f"running {task_name}"))
104+
105+
# Build complete stack with parent chain
106+
if task_info.awaited_by and task_info.awaited_by[0].task_name:
107+
parent_id = task_info.awaited_by[0].task_name
108+
if parent_id in all_tasks:
109+
parent_chain = build_parent_chain(task_id, parent_id)
110+
yield body_frames + [synthetic] + parent_chain, thread_id, 0
111+
else:
112+
# Parent not tracked, treat as root task
113+
root = FrameInfo(("<thread>", 0, "Program Root"))
114+
yield body_frames + [synthetic, root], thread_id, 0
115+
else:
116+
# Root task (no parents or empty awaited_by)
117+
root = FrameInfo(("<thread>", 0, "Program Root"))
118+
yield body_frames + [synthetic, root], thread_id, 0

Lib/profiling/sampling/pstats_collector.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,14 @@ def _process_frames(self, frames):
4141
self.callers[callee][caller] += 1
4242

4343
def collect(self, stack_frames):
44-
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle):
45-
self._process_frames(frames)
44+
if stack_frames and hasattr(stack_frames[0], "awaited_by"):
45+
# Async frame processing
46+
for frames, thread_id, _depth in self._iter_async_frames(stack_frames):
47+
self._process_frames(frames)
48+
else:
49+
# Regular frame processing
50+
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=self.skip_idle):
51+
self._process_frames(frames)
4652

4753
def export(self, filename):
4854
self.create_stats()

Lib/profiling/sampling/sample.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MOD
154154
self.total_samples = 0
155155
self.realtime_stats = False
156156

157-
def sample(self, collector, duration_sec=10):
157+
def sample(self, collector, async_aware, duration_sec=10):
158158
sample_interval_sec = self.sample_interval_usec / 1_000_000
159159
running_time = 0
160160
num_samples = 0
@@ -168,7 +168,10 @@ def sample(self, collector, duration_sec=10):
168168
current_time = time.perf_counter()
169169
if next_time < current_time:
170170
try:
171-
stack_frames = self.unwinder.get_stack_trace()
171+
if async_aware:
172+
stack_frames = self.unwinder.get_all_awaited_by()
173+
else:
174+
stack_frames = self.unwinder.get_stack_trace()
172175
collector.collect(stack_frames)
173176
except ProcessLookupError:
174177
duration_sec = current_time - start_time
@@ -613,6 +616,7 @@ def sample(
613616
output_format="pstats",
614617
realtime_stats=False,
615618
mode=PROFILING_MODE_WALL,
619+
async_aware=False,
616620
):
617621
profiler = SampleProfiler(
618622
pid, sample_interval_usec, all_threads=all_threads, mode=mode
@@ -638,7 +642,7 @@ def sample(
638642
case _:
639643
raise ValueError(f"Invalid output format: {output_format}")
640644

641-
profiler.sample(collector, duration_sec)
645+
profiler.sample(collector, async_aware, duration_sec)
642646

643647
if output_format == "pstats" and not filename:
644648
stats = pstats.SampledStats(collector).strip_dirs()
@@ -706,6 +710,7 @@ def wait_for_process_and_sample(pid, sort_value, args):
706710
output_format=args.format,
707711
realtime_stats=args.realtime_stats,
708712
mode=mode,
713+
async_aware=args.async_aware,
709714
)
710715

711716

@@ -760,6 +765,13 @@ def main():
760765
help="Print real-time sampling statistics (Hz, mean, min, max, stdev) during profiling",
761766
)
762767

768+
sampling_group.add_argument(
769+
"--async-aware",
770+
action="store_true",
771+
default=False,
772+
help="Enable async-aware sampling (experimental)",
773+
)
774+
763775
# Mode options
764776
mode_group = parser.add_argument_group("Mode options")
765777
mode_group.add_argument(
@@ -915,6 +927,7 @@ def main():
915927
output_format=args.format,
916928
realtime_stats=args.realtime_stats,
917929
mode=mode,
930+
async_aware=args.async_aware,
918931
)
919932
elif args.module or args.args:
920933
if args.module:

Lib/profiling/sampling/stack_collector.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@ def __init__(self, *, skip_idle=False):
1515
self.skip_idle = skip_idle
1616

1717
def collect(self, stack_frames, skip_idle=False):
18-
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle):
19-
if not frames:
20-
continue
21-
self.process_frames(frames, thread_id)
18+
if stack_frames and hasattr(stack_frames[0], "awaited_by"):
19+
for frames, thread_id, _depth in self._iter_async_frames(stack_frames):
20+
if not frames:
21+
continue
22+
self.process_frames(frames, thread_id)
23+
else:
24+
for frames, thread_id in self._iter_all_frames(stack_frames, skip_idle=skip_idle):
25+
if not frames:
26+
continue
27+
self.process_frames(frames, thread_id)
2228

2329
def process_frames(self, frames, thread_id):
2430
pass

0 commit comments

Comments
 (0)