11import _remote_debugging
2+ import contextlib
23import os
34import statistics
45import sys
78from collections import deque
89from _colorize import ANSIColors
910
11+ from .pstats_collector import PstatsCollector
12+ from .stack_collector import CollapsedStackCollector , FlamegraphCollector
13+ from .heatmap_collector import HeatmapCollector
14+ from .gecko_collector import GeckoCollector
1015from .binary_collector import BinaryCollector
16+
17+
18+ @contextlib .contextmanager
19+ def _pause_threads (unwinder , blocking ):
20+ """Context manager to pause/resume threads around sampling if blocking is True."""
21+ if blocking :
22+ unwinder .pause_threads ()
23+ try :
24+ yield
25+ finally :
26+ unwinder .resume_threads ()
27+ else :
28+ yield
29+
30+
1131from .constants import (
1232 PROFILING_MODE_WALL ,
1333 PROFILING_MODE_CPU ,
2545
2646
2747class SampleProfiler :
28- def __init__ (self , pid , sample_interval_usec , all_threads , * , mode = PROFILING_MODE_WALL , native = False , gc = True , opcodes = False , skip_non_matching_threads = True , collect_stats = False ):
48+ def __init__ (self , pid , sample_interval_usec , all_threads , * , mode = PROFILING_MODE_WALL , native = False , gc = True , opcodes = False , skip_non_matching_threads = True , collect_stats = False , blocking = False ):
2949 self .pid = pid
3050 self .sample_interval_usec = sample_interval_usec
3151 self .all_threads = all_threads
3252 self .mode = mode # Store mode for later use
3353 self .collect_stats = collect_stats
54+ self .blocking = blocking
3455 try :
3556 self .unwinder = self ._new_unwinder (native , gc , opcodes , skip_non_matching_threads )
3657 except RuntimeError as err :
@@ -60,12 +81,11 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
6081 running_time = 0
6182 num_samples = 0
6283 errors = 0
84+ interrupted = False
6385 start_time = next_time = time .perf_counter ()
6486 last_sample_time = start_time
6587 realtime_update_interval = 1.0 # Update every second
6688 last_realtime_update = start_time
67- interrupted = False
68-
6989 try :
7090 while running_time < duration_sec :
7191 # Check if live collector wants to stop
@@ -75,14 +95,15 @@ def sample(self, collector, duration_sec=10, *, async_aware=False):
7595 current_time = time .perf_counter ()
7696 if next_time < current_time :
7797 try :
78- if async_aware == "all" :
79- stack_frames = self .unwinder .get_all_awaited_by ()
80- elif async_aware == "running" :
81- stack_frames = self .unwinder .get_async_stack_trace ()
82- else :
83- stack_frames = self .unwinder .get_stack_trace ()
84- collector .collect (stack_frames )
85- except ProcessLookupError :
98+ with _pause_threads (self .unwinder , self .blocking ):
99+ if async_aware == "all" :
100+ stack_frames = self .unwinder .get_all_awaited_by ()
101+ elif async_aware == "running" :
102+ stack_frames = self .unwinder .get_async_stack_trace ()
103+ else :
104+ stack_frames = self .unwinder .get_stack_trace ()
105+ collector .collect (stack_frames )
106+ except ProcessLookupError as e :
86107 duration_sec = current_time - start_time
87108 break
88109 except (RuntimeError , UnicodeDecodeError , MemoryError , OSError ):
@@ -350,6 +371,7 @@ def sample(
350371 native = False ,
351372 gc = True ,
352373 opcodes = False ,
374+ blocking = False ,
353375):
354376 """Sample a process using the provided collector.
355377
@@ -365,6 +387,7 @@ def sample(
365387 native: Whether to include native frames
366388 gc: Whether to include GC frames
367389 opcodes: Whether to include opcode information
390+ blocking: Whether to stop all threads before sampling for consistent snapshots
368391
369392 Returns:
370393 The collector with collected samples
@@ -390,6 +413,7 @@ def sample(
390413 opcodes = opcodes ,
391414 skip_non_matching_threads = skip_non_matching_threads ,
392415 collect_stats = realtime_stats ,
416+ blocking = blocking ,
393417 )
394418 profiler .realtime_stats = realtime_stats
395419
@@ -411,6 +435,7 @@ def sample_live(
411435 native = False ,
412436 gc = True ,
413437 opcodes = False ,
438+ blocking = False ,
414439):
415440 """Sample a process in live/interactive mode with curses TUI.
416441
@@ -426,6 +451,7 @@ def sample_live(
426451 native: Whether to include native frames
427452 gc: Whether to include GC frames
428453 opcodes: Whether to include opcode information
454+ blocking: Whether to stop all threads before sampling for consistent snapshots
429455
430456 Returns:
431457 The collector with collected samples
@@ -451,6 +477,7 @@ def sample_live(
451477 opcodes = opcodes ,
452478 skip_non_matching_threads = skip_non_matching_threads ,
453479 collect_stats = realtime_stats ,
480+ blocking = blocking ,
454481 )
455482 profiler .realtime_stats = realtime_stats
456483
0 commit comments