Skip to content

Commit 56661dc

Browse files
committed
Update to latest main
1 parent f0242e1 commit 56661dc

6 files changed

Lines changed: 343 additions & 71 deletions

File tree

Lib/profiling/sampling/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,7 @@ def _handle_live_attach(args, pid):
660660
limit=20, # Default limit
661661
pid=pid,
662662
mode=mode,
663+
async_aware=args.async_aware,
663664
)
664665

665666
# Sample in live mode
@@ -700,6 +701,7 @@ def _handle_live_run(args):
700701
limit=20, # Default limit
701702
pid=process.pid,
702703
mode=mode,
704+
async_aware=args.async_aware,
703705
)
704706

705707
# Profile the subprocess in live mode

Lib/profiling/sampling/collector.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def _build_linear_stacks(self, leaf_task_ids, task_map, child_to_parent):
127127
# Yield the complete stack if we collected any frames
128128
if frames and thread_id is not None:
129129
yield frames, thread_id, leaf_id
130+
130131
def _is_gc_frame(self, frame):
131132
if isinstance(frame, tuple):
132133
funcname = frame[2] if len(frame) >= 3 else ""

Lib/profiling/sampling/live_collector/collector.py

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def __init__(
103103
pid=None,
104104
display=None,
105105
mode=None,
106+
async_aware=None,
106107
):
107108
"""
108109
Initialize the live stats collector.
@@ -115,6 +116,7 @@ def __init__(
115116
pid: Process ID being profiled
116117
display: DisplayInterface implementation (None means curses will be used)
117118
mode: Profiling mode ('cpu', 'gil', etc.) - affects what stats are shown
119+
async_aware: Async tracing mode - None (sync only), "all" or "running"
118120
"""
119121
self.result = collections.defaultdict(
120122
lambda: dict(total_rec_calls=0, direct_calls=0, cumulative_calls=0)
@@ -133,6 +135,9 @@ def __init__(
133135
self.running = True
134136
self.pid = pid
135137
self.mode = mode # Profiling mode
138+
self.async_aware = async_aware # Async tracing mode
139+
# Pre-select frame iterator method to avoid per-call dispatch overhead
140+
self._get_frame_iterator = self._get_async_frame_iterator if async_aware else self._get_sync_frame_iterator
136141
self._saved_stdout = None
137142
self._saved_stderr = None
138143
self._devnull = None
@@ -294,6 +299,35 @@ def process_frames(self, frames, thread_id=None):
294299
if thread_data:
295300
thread_data.result[top_location]["direct_calls"] += 1
296301

302+
def _get_sync_frame_iterator(self, stack_frames):
303+
"""Iterator for sync frames."""
304+
return self._iter_all_frames(stack_frames, skip_idle=self.skip_idle)
305+
306+
def _get_async_frame_iterator(self, stack_frames):
307+
"""Iterator for async frames, yielding (frames, thread_id) tuples."""
308+
for frames, thread_id, task_id in self._iter_async_frames(stack_frames):
309+
yield frames, thread_id
310+
311+
def _collect_sync_thread_stats(self, stack_frames, has_gc_frame):
312+
"""Collect thread status stats for sync mode."""
313+
status_counts, sample_has_gc, per_thread_stats = self._collect_thread_status_stats(stack_frames)
314+
for key, count in status_counts.items():
315+
self.thread_status_counts[key] += count
316+
if sample_has_gc:
317+
has_gc_frame = True
318+
319+
for thread_id, stats in per_thread_stats.items():
320+
thread_data = self._get_or_create_thread_data(thread_id)
321+
thread_data.has_gil += stats.get("has_gil", 0)
322+
thread_data.on_cpu += stats.get("on_cpu", 0)
323+
thread_data.gil_requested += stats.get("gil_requested", 0)
324+
thread_data.unknown += stats.get("unknown", 0)
325+
thread_data.total += stats.get("total", 0)
326+
if stats.get("gc_samples", 0):
327+
thread_data.gc_frame_samples += stats["gc_samples"]
328+
329+
return has_gc_frame
330+
297331
def collect_failed_sample(self):
298332
self.failed_samples += 1
299333
self.total_samples += 1
@@ -304,78 +338,37 @@ def collect(self, stack_frames):
304338
self.start_time = time.perf_counter()
305339
self._last_display_update = self.start_time
306340

307-
# Thread status counts for this sample
308-
temp_status_counts = {
309-
"has_gil": 0,
310-
"on_cpu": 0,
311-
"gil_requested": 0,
312-
"unknown": 0,
313-
"total": 0,
314-
}
315341
has_gc_frame = False
316342

317-
# Always collect data, even when paused
318-
# Track thread status flags and GC frames
319-
for interpreter_info in stack_frames:
320-
threads = getattr(interpreter_info, "threads", [])
321-
for thread_info in threads:
322-
temp_status_counts["total"] += 1
323-
324-
# Track thread status using bit flags
325-
status_flags = getattr(thread_info, "status", 0)
326-
thread_id = getattr(thread_info, "thread_id", None)
327-
328-
# Update aggregated counts
329-
if status_flags & THREAD_STATUS_HAS_GIL:
330-
temp_status_counts["has_gil"] += 1
331-
if status_flags & THREAD_STATUS_ON_CPU:
332-
temp_status_counts["on_cpu"] += 1
333-
if status_flags & THREAD_STATUS_GIL_REQUESTED:
334-
temp_status_counts["gil_requested"] += 1
335-
if status_flags & THREAD_STATUS_UNKNOWN:
336-
temp_status_counts["unknown"] += 1
337-
338-
# Update per-thread status counts
339-
if thread_id is not None:
340-
thread_data = self._get_or_create_thread_data(thread_id)
341-
thread_data.increment_status_flag(status_flags)
342-
343-
# Process frames (respecting skip_idle)
344-
if self.skip_idle:
345-
has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL)
346-
on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU)
347-
if not (has_gil or on_cpu):
348-
continue
349-
350-
frames = getattr(thread_info, "frame_info", None)
351-
if frames:
352-
self.process_frames(frames, thread_id=thread_id)
353-
354-
# Track thread IDs only for threads that actually have samples
355-
if (
356-
thread_id is not None
357-
and thread_id not in self.thread_ids
358-
):
359-
self.thread_ids.append(thread_id)
360-
361-
# Increment per-thread sample count and check for GC frames
362-
thread_has_gc_frame = False
363-
for frame in frames:
364-
funcname = getattr(frame, "funcname", "")
365-
if "<GC>" in funcname or "gc_collect" in funcname:
366-
has_gc_frame = True
367-
thread_has_gc_frame = True
368-
break
369-
370-
if thread_id is not None:
371-
thread_data = self._get_or_create_thread_data(thread_id)
372-
thread_data.sample_count += 1
373-
if thread_has_gc_frame:
374-
thread_data.gc_frame_samples += 1
375-
376-
# Update cumulative thread status counts
377-
for key, count in temp_status_counts.items():
378-
self.thread_status_counts[key] += count
343+
# Collect thread status stats (only available in sync mode)
344+
if not self.async_aware:
345+
has_gc_frame = self._collect_sync_thread_stats(stack_frames, has_gc_frame)
346+
347+
# Process frames using pre-selected iterator
348+
for frames, thread_id in self._get_frame_iterator(stack_frames):
349+
if not frames:
350+
continue
351+
352+
self.process_frames(frames, thread_id=thread_id)
353+
354+
# Track thread IDs
355+
if thread_id is not None and thread_id not in self.thread_ids:
356+
self.thread_ids.append(thread_id)
357+
358+
# Check for GC frames and update per-thread sample count
359+
thread_has_gc_frame = False
360+
for frame in frames:
361+
funcname = getattr(frame, "funcname", "")
362+
if "<GC>" in funcname or "gc_collect" in funcname:
363+
has_gc_frame = True
364+
thread_has_gc_frame = True
365+
break
366+
367+
if thread_id is not None:
368+
thread_data = self._get_or_create_thread_data(thread_id)
369+
thread_data.sample_count += 1
370+
if thread_has_gc_frame:
371+
thread_data.gc_frame_samples += 1
379372

380373
if has_gc_frame:
381374
self.gc_frame_samples += 1

Lib/profiling/sampling/sample.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def sample_live(
297297
def curses_wrapper_func(stdscr):
298298
collector.init_curses(stdscr)
299299
try:
300-
profiler.sample(collector, duration_sec)
300+
profiler.sample(collector, duration_sec, async_aware=async_aware)
301301
# Mark as finished and keep the TUI running until user presses 'q'
302302
collector.mark_finished()
303303
# Keep processing input until user quits

0 commit comments

Comments
 (0)