2025-07-10 18:44:24 +01:00
import _remote_debugging
2025-12-23 10:49:47 +00:00
import contextlib
2025-07-10 18:44:24 +01:00
import os
import statistics
import sys
import sysconfig
import time
from collections import deque
from _colorize import ANSIColors
2025-12-23 10:49:47 +00:00
from . pstats_collector import PstatsCollector
from . stack_collector import CollapsedStackCollector , FlamegraphCollector
from . heatmap_collector import HeatmapCollector
from . gecko_collector import GeckoCollector
2025-12-22 23:57:20 +00:00
from . binary_collector import BinaryCollector
2025-12-23 10:49:47 +00:00
@contextlib.contextmanager
def _pause_threads ( unwinder , blocking ) :
""" Context manager to pause/resume threads around sampling if blocking is True. """
if blocking :
unwinder . pause_threads ( )
try :
yield
finally :
unwinder . resume_threads ( )
else :
yield
2025-11-20 18:27:17 +00:00
from . constants import (
PROFILING_MODE_WALL ,
PROFILING_MODE_CPU ,
PROFILING_MODE_GIL ,
PROFILING_MODE_ALL ,
2025-12-11 20:46:34 +00:00
PROFILING_MODE_EXCEPTION ,
2025-11-20 18:27:17 +00:00
)
2025-12-22 16:15:57 +02:00
from . _format_utils import fmt
2025-11-20 18:27:17 +00:00
try :
from . live_collector import LiveStatsCollector
except ImportError :
LiveStatsCollector = None
2025-07-10 18:44:24 +01:00
2025-08-11 08:36:43 -03:00
_FREE_THREADED_BUILD = sysconfig . get_config_var ( " Py_GIL_DISABLED " ) is not None
2025-12-27 01:36:15 +01:00
# Minimum number of samples required before showing the TUI
# If fewer samples are collected, we skip the TUI and just print a message
MIN_SAMPLES_FOR_TUI = 200
2025-09-19 19:17:28 +01:00
2025-07-10 18:44:24 +01:00
class SampleProfiler :
2025-12-23 10:49:47 +00:00
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 ) :
2025-07-10 18:44:24 +01:00
self . pid = pid
self . sample_interval_usec = sample_interval_usec
self . all_threads = all_threads
2025-11-30 01:42:39 +00:00
self . mode = mode # Store mode for later use
2025-12-06 22:37:34 +00:00
self . collect_stats = collect_stats
2025-12-23 10:49:47 +00:00
self . blocking = blocking
2025-12-17 22:15:22 +08:00
try :
self . unwinder = self . _new_unwinder ( native , gc , opcodes , skip_non_matching_threads )
except RuntimeError as err :
raise SystemExit ( err ) from err
# Track sample intervals and total sample count
self . sample_intervals = deque ( maxlen = 100 )
self . total_samples = 0
self . realtime_stats = False
def _new_unwinder ( self , native , gc , opcodes , skip_non_matching_threads ) :
2025-08-11 08:36:43 -03:00
if _FREE_THREADED_BUILD :
2025-12-17 22:15:22 +08:00
unwinder = _remote_debugging . RemoteUnwinder (
self . pid , all_threads = self . all_threads , mode = self . mode , native = native , gc = gc ,
2025-12-11 03:41:47 +00:00
opcodes = opcodes , skip_non_matching_threads = skip_non_matching_threads ,
2025-12-17 22:15:22 +08:00
cache_frames = True , stats = self . collect_stats
2025-07-10 18:44:24 +01:00
)
else :
2025-12-17 22:15:22 +08:00
unwinder = _remote_debugging . RemoteUnwinder (
self . pid , only_active_thread = bool ( self . all_threads ) , mode = self . mode , native = native , gc = gc ,
2025-12-11 03:41:47 +00:00
opcodes = opcodes , skip_non_matching_threads = skip_non_matching_threads ,
2025-12-17 22:15:22 +08:00
cache_frames = True , stats = self . collect_stats
2025-07-10 18:44:24 +01:00
)
2025-12-17 22:15:22 +08:00
return unwinder
2025-07-10 18:44:24 +01:00
2025-12-25 19:21:16 +00:00
def sample ( self , collector , duration_sec = None , * , async_aware = False ) :
2025-07-10 18:44:24 +01:00
sample_interval_sec = self . sample_interval_usec / 1_000_000
num_samples = 0
errors = 0
2025-12-23 10:49:47 +00:00
interrupted = False
2025-12-25 19:21:16 +00:00
running_time_sec = 0
2025-07-10 18:44:24 +01:00
start_time = next_time = time . perf_counter ( )
last_sample_time = start_time
realtime_update_interval = 1.0 # Update every second
last_realtime_update = start_time
2025-11-30 10:49:13 +08:00
try :
2025-12-25 19:21:16 +00:00
while duration_sec is None or running_time_sec < duration_sec :
2025-11-30 10:49:13 +08:00
# Check if live collector wants to stop
if hasattr ( collector , ' running ' ) and not collector . running :
2025-07-10 18:44:24 +01:00
break
2025-11-30 10:49:13 +08:00
current_time = time . perf_counter ( )
if next_time < current_time :
try :
2025-12-23 10:49:47 +00:00
with _pause_threads ( self . unwinder , self . blocking ) :
if async_aware == " all " :
stack_frames = self . unwinder . get_all_awaited_by ( )
elif async_aware == " running " :
stack_frames = self . unwinder . get_async_stack_trace ( )
else :
stack_frames = self . unwinder . get_stack_trace ( )
collector . collect ( stack_frames )
except ProcessLookupError as e :
2025-12-25 19:21:16 +00:00
running_time_sec = current_time - start_time
2025-11-30 10:49:13 +08:00
break
except ( RuntimeError , UnicodeDecodeError , MemoryError , OSError ) :
collector . collect_failed_sample ( )
errors + = 1
except Exception as e :
2025-12-17 22:15:22 +08:00
if not _is_process_running ( self . pid ) :
2025-11-30 10:49:13 +08:00
break
raise e from None
# Track actual sampling intervals for real-time stats
if num_samples > 0 :
actual_interval = current_time - last_sample_time
self . sample_intervals . append (
1.0 / actual_interval
) # Convert to Hz
self . total_samples + = 1
# Print real-time statistics if enabled
if (
self . realtime_stats
and ( current_time - last_realtime_update )
> = realtime_update_interval
) :
self . _print_realtime_stats ( )
last_realtime_update = current_time
last_sample_time = current_time
num_samples + = 1
next_time + = sample_interval_sec
2025-12-25 19:21:16 +00:00
running_time_sec = time . perf_counter ( ) - start_time
2025-11-30 10:49:13 +08:00
except KeyboardInterrupt :
interrupted = True
2025-12-25 19:21:16 +00:00
running_time_sec = time . perf_counter ( ) - start_time
2025-11-30 10:49:13 +08:00
print ( " Interrupted by user. " )
2025-07-10 18:44:24 +01:00
# Clear real-time stats line if it was being displayed
if self . realtime_stats and len ( self . sample_intervals ) > 0 :
print ( ) # Add newline after real-time stats
2025-12-25 19:21:16 +00:00
sample_rate = num_samples / running_time_sec if running_time_sec > 0 else 0
2025-09-09 23:06:45 +01:00
error_rate = ( errors / num_samples ) * 100 if num_samples > 0 else 0
2025-12-25 19:21:16 +00:00
expected_samples = int ( running_time_sec / sample_interval_sec )
2025-12-01 17:34:14 +00:00
missed_samples = ( expected_samples - num_samples ) / expected_samples * 100 if expected_samples > 0 else 0
2025-09-09 23:06:45 +01:00
2025-11-20 18:27:17 +00:00
# Don't print stats for live mode (curses is handling display)
is_live_mode = LiveStatsCollector is not None and isinstance ( collector , LiveStatsCollector )
if not is_live_mode :
2025-12-25 19:21:16 +00:00
print ( f " Captured { num_samples : n } samples in { fmt ( running_time_sec , 2 ) } seconds " )
2025-12-22 16:15:57 +02:00
print ( f " Sample rate: { fmt ( sample_rate , 2 ) } samples/sec " )
print ( f " Error rate: { fmt ( error_rate , 2 ) } " )
2025-09-09 23:06:45 +01:00
2025-12-06 22:37:34 +00:00
# Print unwinder stats if stats collection is enabled
if self . collect_stats :
self . _print_unwinder_stats ( )
2025-12-22 23:57:20 +00:00
if isinstance ( collector , BinaryCollector ) :
self . _print_binary_stats ( collector )
2025-09-09 23:06:45 +01:00
# Pass stats to flamegraph collector if it's the right type
if hasattr ( collector , ' set_stats ' ) :
2025-12-25 19:21:16 +00:00
collector . set_stats ( self . sample_interval_usec , running_time_sec , sample_rate , error_rate , missed_samples , mode = self . mode )
2025-07-10 18:44:24 +01:00
2025-11-30 10:49:13 +08:00
if num_samples < expected_samples and not is_live_mode and not interrupted :
2025-07-10 18:44:24 +01:00
print (
f " Warning: missed { expected_samples - num_samples } samples "
f " from the expected total of { expected_samples } "
2025-12-22 16:15:57 +02:00
f " ( { fmt ( ( expected_samples - num_samples ) / expected_samples * 100 , 2 ) } %) "
2025-07-10 18:44:24 +01:00
)
def _print_realtime_stats ( self ) :
""" Print real-time sampling statistics. """
if len ( self . sample_intervals ) < 2 :
return
# Calculate statistics on the Hz values (deque automatically maintains rolling window)
hz_values = list ( self . sample_intervals )
mean_hz = statistics . mean ( hz_values )
min_hz = min ( hz_values )
max_hz = max ( hz_values )
# Calculate microseconds per sample for all metrics (1/Hz * 1,000,000)
mean_us_per_sample = ( 1.0 / mean_hz ) * 1_000_000 if mean_hz > 0 else 0
min_us_per_sample = (
( 1.0 / max_hz ) * 1_000_000 if max_hz > 0 else 0
) # Min time = Max Hz
max_us_per_sample = (
( 1.0 / min_hz ) * 1_000_000 if min_hz > 0 else 0
) # Max time = Min Hz
2025-12-06 22:37:34 +00:00
# Build cache stats string if stats collection is enabled
cache_stats_str = " "
if self . collect_stats :
try :
stats = self . unwinder . get_stats ( )
hits = stats . get ( ' frame_cache_hits ' , 0 )
partial = stats . get ( ' frame_cache_partial_hits ' , 0 )
misses = stats . get ( ' frame_cache_misses ' , 0 )
total = hits + partial + misses
if total > 0 :
hit_pct = ( hits + partial ) / total * 100
2025-12-22 16:15:57 +02:00
cache_stats_str = f " { ANSIColors . MAGENTA } Cache: { fmt ( hit_pct ) } % ( { hits } + { partial } / { misses } ) { ANSIColors . RESET } "
2025-12-06 22:37:34 +00:00
except RuntimeError :
pass
2025-07-10 18:44:24 +01:00
# Clear line and print stats
print (
2025-12-06 22:37:34 +00:00
f " \r \033 [K { ANSIColors . BOLD_BLUE } Stats: { ANSIColors . RESET } "
2025-12-22 16:15:57 +02:00
f " { ANSIColors . YELLOW } { fmt ( mean_hz ) } Hz ( { fmt ( mean_us_per_sample ) } µs) { ANSIColors . RESET } "
f " { ANSIColors . GREEN } Min: { fmt ( min_hz ) } Hz { ANSIColors . RESET } "
f " { ANSIColors . RED } Max: { fmt ( max_hz ) } Hz { ANSIColors . RESET } "
2025-12-06 22:37:34 +00:00
f " { ANSIColors . CYAN } N= { self . total_samples } { ANSIColors . RESET } "
f " { cache_stats_str } " ,
2025-07-10 18:44:24 +01:00
end = " " ,
flush = True ,
)
2025-12-06 22:37:34 +00:00
def _print_unwinder_stats ( self ) :
""" Print unwinder statistics including cache performance. """
try :
stats = self . unwinder . get_stats ( )
except RuntimeError :
return # Stats not enabled
print ( f " \n { ANSIColors . BOLD_BLUE } { ' = ' * 50 } { ANSIColors . RESET } " )
print ( f " { ANSIColors . BOLD_BLUE } Unwinder Statistics: { ANSIColors . RESET } " )
# Frame cache stats
total_samples = stats . get ( ' total_samples ' , 0 )
frame_cache_hits = stats . get ( ' frame_cache_hits ' , 0 )
frame_cache_partial_hits = stats . get ( ' frame_cache_partial_hits ' , 0 )
frame_cache_misses = stats . get ( ' frame_cache_misses ' , 0 )
total_lookups = frame_cache_hits + frame_cache_partial_hits + frame_cache_misses
# Calculate percentages
hits_pct = ( frame_cache_hits / total_lookups * 100 ) if total_lookups > 0 else 0
partial_pct = ( frame_cache_partial_hits / total_lookups * 100 ) if total_lookups > 0 else 0
misses_pct = ( frame_cache_misses / total_lookups * 100 ) if total_lookups > 0 else 0
print ( f " { ANSIColors . CYAN } Frame Cache: { ANSIColors . RESET } " )
2025-12-22 16:15:57 +02:00
print ( f " Total samples: { total_samples : n } " )
print ( f " Full hits: { frame_cache_hits : n } ( { ANSIColors . GREEN } { fmt ( hits_pct ) } % { ANSIColors . RESET } ) " )
print ( f " Partial hits: { frame_cache_partial_hits : n } ( { ANSIColors . YELLOW } { fmt ( partial_pct ) } % { ANSIColors . RESET } ) " )
print ( f " Misses: { frame_cache_misses : n } ( { ANSIColors . RED } { fmt ( misses_pct ) } % { ANSIColors . RESET } ) " )
2025-12-06 22:37:34 +00:00
# Frame read stats
frames_from_cache = stats . get ( ' frames_read_from_cache ' , 0 )
frames_from_memory = stats . get ( ' frames_read_from_memory ' , 0 )
total_frames = frames_from_cache + frames_from_memory
cache_frame_pct = ( frames_from_cache / total_frames * 100 ) if total_frames > 0 else 0
memory_frame_pct = ( frames_from_memory / total_frames * 100 ) if total_frames > 0 else 0
print ( f " { ANSIColors . CYAN } Frame Reads: { ANSIColors . RESET } " )
2025-12-22 16:15:57 +02:00
print ( f " From cache: { frames_from_cache : n } ( { ANSIColors . GREEN } { fmt ( cache_frame_pct ) } % { ANSIColors . RESET } ) " )
print ( f " From memory: { frames_from_memory : n } ( { ANSIColors . RED } { fmt ( memory_frame_pct ) } % { ANSIColors . RESET } ) " )
2025-12-06 22:37:34 +00:00
# Code object cache stats
code_hits = stats . get ( ' code_object_cache_hits ' , 0 )
code_misses = stats . get ( ' code_object_cache_misses ' , 0 )
total_code = code_hits + code_misses
code_hits_pct = ( code_hits / total_code * 100 ) if total_code > 0 else 0
code_misses_pct = ( code_misses / total_code * 100 ) if total_code > 0 else 0
print ( f " { ANSIColors . CYAN } Code Object Cache: { ANSIColors . RESET } " )
2025-12-22 16:15:57 +02:00
print ( f " Hits: { code_hits : n } ( { ANSIColors . GREEN } { fmt ( code_hits_pct ) } % { ANSIColors . RESET } ) " )
print ( f " Misses: { code_misses : n } ( { ANSIColors . RED } { fmt ( code_misses_pct ) } % { ANSIColors . RESET } ) " )
2025-12-06 22:37:34 +00:00
# Memory operations
memory_reads = stats . get ( ' memory_reads ' , 0 )
memory_bytes = stats . get ( ' memory_bytes_read ' , 0 )
if memory_bytes > = 1024 * 1024 :
2025-12-22 16:15:57 +02:00
memory_str = f " { fmt ( memory_bytes / ( 1024 * 1024 ) ) } MB "
2025-12-06 22:37:34 +00:00
elif memory_bytes > = 1024 :
2025-12-22 16:15:57 +02:00
memory_str = f " { fmt ( memory_bytes / 1024 ) } KB "
2025-12-06 22:37:34 +00:00
else :
memory_str = f " { memory_bytes } B "
print ( f " { ANSIColors . CYAN } Memory: { ANSIColors . RESET } " )
2025-12-22 16:15:57 +02:00
print ( f " Read operations: { memory_reads : n } ( { memory_str } ) " )
2025-12-06 22:37:34 +00:00
# Stale invalidations
stale_invalidations = stats . get ( ' stale_cache_invalidations ' , 0 )
if stale_invalidations > 0 :
print ( f " { ANSIColors . YELLOW } Stale cache invalidations: { stale_invalidations } { ANSIColors . RESET } " )
2025-12-22 23:57:20 +00:00
def _print_binary_stats ( self , collector ) :
""" Print binary I/O encoding statistics. """
try :
stats = collector . get_stats ( )
except ( ValueError , RuntimeError ) :
return # Collector closed or stats unavailable
print ( f " { ANSIColors . CYAN } Binary Encoding: { ANSIColors . RESET } " )
repeat_records = stats . get ( ' repeat_records ' , 0 )
repeat_samples = stats . get ( ' repeat_samples ' , 0 )
full_records = stats . get ( ' full_records ' , 0 )
suffix_records = stats . get ( ' suffix_records ' , 0 )
pop_push_records = stats . get ( ' pop_push_records ' , 0 )
total_records = stats . get ( ' total_records ' , 0 )
if total_records > 0 :
repeat_pct = repeat_records / total_records * 100
full_pct = full_records / total_records * 100
suffix_pct = suffix_records / total_records * 100
pop_push_pct = pop_push_records / total_records * 100
else :
repeat_pct = full_pct = suffix_pct = pop_push_pct = 0
print ( f " Records: { total_records : , } " )
print ( f " RLE repeat: { repeat_records : , } ( { ANSIColors . GREEN } { repeat_pct : .1f } % { ANSIColors . RESET } ) [ { repeat_samples : , } samples] " )
print ( f " Full stack: { full_records : , } ( { full_pct : .1f } %) " )
print ( f " Suffix match: { suffix_records : , } ( { suffix_pct : .1f } %) " )
print ( f " Pop-push: { pop_push_records : , } ( { pop_push_pct : .1f } %) " )
frames_written = stats . get ( ' total_frames_written ' , 0 )
frames_saved = stats . get ( ' frames_saved ' , 0 )
compression_pct = stats . get ( ' frame_compression_pct ' , 0 )
print ( f " { ANSIColors . CYAN } Frame Efficiency: { ANSIColors . RESET } " )
print ( f " Frames written: { frames_written : , } " )
print ( f " Frames saved: { frames_saved : , } ( { ANSIColors . GREEN } { compression_pct : .1f } % { ANSIColors . RESET } ) " )
bytes_written = stats . get ( ' bytes_written ' , 0 )
if bytes_written > = 1024 * 1024 :
bytes_str = f " { bytes_written / ( 1024 * 1024 ) : .1f } MB "
elif bytes_written > = 1024 :
bytes_str = f " { bytes_written / 1024 : .1f } KB "
else :
bytes_str = f " { bytes_written } B "
print ( f " Bytes (pre-zstd): { bytes_str } " )
2025-07-10 18:44:24 +01:00
2025-12-17 22:15:22 +08:00
def _is_process_running ( pid ) :
if pid < = 0 :
return False
if os . name == " posix " :
try :
os . kill ( pid , 0 )
return True
except ProcessLookupError :
return False
except PermissionError :
# EPERM means process exists but we can't signal it
return True
elif sys . platform == " win32 " :
try :
_remote_debugging . RemoteUnwinder ( pid )
except Exception :
return False
return True
else :
raise ValueError ( f " Unsupported platform: { sys . platform } " )
2025-07-10 18:44:24 +01:00
def sample (
pid ,
2025-11-24 11:45:08 +00:00
collector ,
2025-07-10 18:44:24 +01:00
* ,
2025-12-25 19:21:16 +00:00
duration_sec = None ,
2025-07-10 18:44:24 +01:00
all_threads = False ,
realtime_stats = False ,
2025-09-19 19:17:28 +01:00
mode = PROFILING_MODE_WALL ,
2025-12-06 11:31:40 -08:00
async_aware = None ,
2025-11-17 05:39:00 -08:00
native = False ,
gc = True ,
2025-12-11 03:41:47 +00:00
opcodes = False ,
2025-12-23 10:49:47 +00:00
blocking = False ,
2025-07-10 18:44:24 +01:00
) :
2025-11-24 11:45:08 +00:00
""" Sample a process using the provided collector.
Args :
pid : Process ID to sample
collector : Collector instance to use for gathering samples
2025-12-25 19:21:16 +00:00
duration_sec : How long to sample for ( seconds ) , or None to run until
the process exits or interrupted
2025-11-24 11:45:08 +00:00
all_threads : Whether to sample all threads
realtime_stats : Whether to print real - time sampling statistics
mode : Profiling mode - WALL ( all samples ) , CPU ( only when on CPU ) ,
2025-12-11 20:46:34 +00:00
GIL ( only when holding GIL ) , ALL ( includes GIL and CPU status ) ,
EXCEPTION ( only when thread has an active exception )
2025-11-24 11:45:08 +00:00
native : Whether to include native frames
gc : Whether to include GC frames
2025-12-11 03:41:47 +00:00
opcodes : Whether to include opcode information
2025-12-23 10:49:47 +00:00
blocking : Whether to stop all threads before sampling for consistent snapshots
2025-11-24 11:45:08 +00:00
Returns :
The collector with collected samples
"""
# Get sample interval from collector
sample_interval_usec = collector . sample_interval_usec
2025-11-17 12:46:26 +00:00
# PROFILING_MODE_ALL implies no skipping at all
if mode == PROFILING_MODE_ALL :
skip_non_matching_threads = False
else :
2025-11-24 11:45:08 +00:00
# For most modes, skip non-matching threads
# Gecko collector overrides this by setting skip_idle=False
skip_non_matching_threads = True
2025-11-17 12:46:26 +00:00
2025-07-10 18:44:24 +01:00
profiler = SampleProfiler (
2025-11-24 11:45:08 +00:00
pid ,
sample_interval_usec ,
all_threads = all_threads ,
mode = mode ,
native = native ,
gc = gc ,
2025-12-11 03:41:47 +00:00
opcodes = opcodes ,
2025-12-06 22:37:34 +00:00
skip_non_matching_threads = skip_non_matching_threads ,
collect_stats = realtime_stats ,
2025-12-23 10:49:47 +00:00
blocking = blocking ,
2025-07-10 18:44:24 +01:00
)
profiler . realtime_stats = realtime_stats
2025-11-24 11:45:08 +00:00
# Run the sampling
2025-12-06 11:31:40 -08:00
profiler . sample ( collector , duration_sec , async_aware = async_aware )
2025-07-10 18:44:24 +01:00
2025-11-24 11:45:08 +00:00
return collector
2025-11-20 18:27:17 +00:00
2025-07-10 18:44:24 +01:00
2025-11-24 11:45:08 +00:00
def sample_live (
pid ,
collector ,
* ,
2025-12-25 19:21:16 +00:00
duration_sec = None ,
2025-11-24 11:45:08 +00:00
all_threads = False ,
realtime_stats = False ,
mode = PROFILING_MODE_WALL ,
2025-12-06 11:31:40 -08:00
async_aware = None ,
2025-11-24 11:45:08 +00:00
native = False ,
gc = True ,
2025-12-11 03:41:47 +00:00
opcodes = False ,
2025-12-23 10:49:47 +00:00
blocking = False ,
2025-11-24 11:45:08 +00:00
) :
""" Sample a process in live/interactive mode with curses TUI.
Args :
pid : Process ID to sample
collector : LiveStatsCollector instance
duration_sec : How long to sample for ( seconds )
all_threads : Whether to sample all threads
realtime_stats : Whether to print real - time sampling statistics
mode : Profiling mode - WALL ( all samples ) , CPU ( only when on CPU ) ,
2025-12-11 20:46:34 +00:00
GIL ( only when holding GIL ) , ALL ( includes GIL and CPU status ) ,
EXCEPTION ( only when thread has an active exception )
2025-11-24 11:45:08 +00:00
native : Whether to include native frames
gc : Whether to include GC frames
2025-12-11 03:41:47 +00:00
opcodes : Whether to include opcode information
2025-12-23 10:49:47 +00:00
blocking : Whether to stop all threads before sampling for consistent snapshots
2025-11-24 11:45:08 +00:00
Returns :
The collector with collected samples
2025-11-20 18:27:17 +00:00
"""
2025-11-24 11:45:08 +00:00
import curses
2025-11-20 18:27:17 +00:00
2025-12-27 01:36:15 +01:00
# Check if process is alive before doing any heavy initialization
if not _is_process_running ( pid ) :
print ( f " No samples collected - process { pid } exited before profiling could begin. " , file = sys . stderr )
return collector
2025-11-24 11:45:08 +00:00
# Get sample interval from collector
sample_interval_usec = collector . sample_interval_usec
2025-08-11 08:36:43 -03:00
2025-11-24 11:45:08 +00:00
# PROFILING_MODE_ALL implies no skipping at all
if mode == PROFILING_MODE_ALL :
skip_non_matching_threads = False
else :
skip_non_matching_threads = True
2025-09-19 19:17:28 +01:00
2025-11-24 11:45:08 +00:00
profiler = SampleProfiler (
2025-08-11 08:36:43 -03:00
pid ,
2025-11-24 11:45:08 +00:00
sample_interval_usec ,
all_threads = all_threads ,
2025-09-19 19:17:28 +01:00
mode = mode ,
2025-11-24 11:45:08 +00:00
native = native ,
gc = gc ,
2025-12-11 03:41:47 +00:00
opcodes = opcodes ,
2025-12-06 22:37:34 +00:00
skip_non_matching_threads = skip_non_matching_threads ,
collect_stats = realtime_stats ,
2025-12-23 10:49:47 +00:00
blocking = blocking ,
2025-07-10 18:44:24 +01:00
)
2025-11-24 11:45:08 +00:00
profiler . realtime_stats = realtime_stats
2025-07-10 18:44:24 +01:00
2025-11-24 11:45:08 +00:00
def curses_wrapper_func ( stdscr ) :
collector . init_curses ( stdscr )
2025-08-11 08:36:43 -03:00
try :
2025-12-06 11:31:40 -08:00
profiler . sample ( collector , duration_sec , async_aware = async_aware )
2025-12-27 01:36:15 +01:00
# If too few samples were collected, exit cleanly without showing TUI
if collector . successful_samples < MIN_SAMPLES_FOR_TUI :
# Clear screen before exiting to avoid visual artifacts
stdscr . clear ( )
stdscr . refresh ( )
return
2025-11-24 11:45:08 +00:00
# Mark as finished and keep the TUI running until user presses 'q'
collector . mark_finished ( )
# Keep processing input until user quits
while collector . running :
collector . _handle_input ( )
time . sleep ( 0.05 ) # Small sleep to avoid busy waiting
2025-08-11 08:36:43 -03:00
finally :
2025-11-24 11:45:08 +00:00
collector . cleanup_curses ( )
try :
curses . wrapper ( curses_wrapper_func )
except KeyboardInterrupt :
pass
2025-07-10 18:44:24 +01:00
2025-12-27 01:36:15 +01:00
# If too few samples were collected, print a message
if collector . successful_samples < MIN_SAMPLES_FOR_TUI :
if collector . successful_samples == 0 :
print ( f " No samples collected - process { pid } exited before profiling could begin. " , file = sys . stderr )
else :
print ( f " Only { collector . successful_samples } sample(s) collected (minimum { MIN_SAMPLES_FOR_TUI } required for TUI) - process { pid } exited too quickly. " , file = sys . stderr )
2025-11-24 11:45:08 +00:00
return collector