5252 │ │ • PID, uptime, time, interval │ │
5353 │ │ • Sample stats & progress bar │ │
5454 │ │ • Efficiency bar │ │
55+ │ │ • Thread status & GC stats │ │
5556 │ │ • Function summary │ │
5657 │ │ • Top 3 hottest functions │ │
5758 │ ├─────────────────────────────────┤ │
109110import sysconfig
110111import time
111112from abc import ABC , abstractmethod
112- from .collector import Collector , THREAD_STATE_RUNNING
113+ from .collector import Collector
114+ from .constants import (
115+ THREAD_STATUS_HAS_GIL ,
116+ THREAD_STATUS_ON_CPU ,
117+ THREAD_STATUS_UNKNOWN ,
118+ THREAD_STATUS_GIL_REQUESTED ,
119+ PROFILING_MODE_CPU ,
120+ PROFILING_MODE_GIL ,
121+ PROFILING_MODE_WALL ,
122+ )
113123
114124# Time conversion constants
115125MICROSECONDS_PER_SECOND = 1_000_000
129139WIDTH_THRESHOLD_CUMTIME = 140
130140
131141# Display layout constants
132- HEADER_LINES = 9
142+ HEADER_LINES = 10 # Increased to include thread status line
133143FOOTER_LINES = 2
134144SAFETY_MARGIN = 1
135145TOP_FUNCTIONS_DISPLAY_COUNT = 3
@@ -274,6 +284,7 @@ def render(self, line, width, **kwargs):
274284 line = self .draw_header_info (line , width , elapsed )
275285 line = self .draw_sample_stats (line , width , elapsed )
276286 line = self .draw_efficiency_bar (line , width )
287+ line = self .draw_thread_status (line , width )
277288 line = self .draw_function_stats (
278289 line , width , kwargs .get ("stats_list" , [])
279290 )
@@ -463,6 +474,61 @@ def draw_efficiency_bar(self, line, width):
463474 self .add_str (line , col + 1 , label , curses .A_NORMAL )
464475 return line + 1
465476
477+ def _add_percentage_stat (self , line , col , value , label , color , add_separator = False ):
478+ """Add a percentage stat to the display.
479+
480+ Args:
481+ line: Line number
482+ col: Starting column
483+ value: Percentage value
484+ label: Label text
485+ color: Color attribute
486+ add_separator: Whether to add separator before the stat
487+
488+ Returns:
489+ Updated column position
490+ """
491+ if add_separator :
492+ self .add_str (line , col , " │ " , curses .A_DIM )
493+ col += 3
494+
495+ self .add_str (line , col , f"{ value :>4.1f} " , color )
496+ col += 4
497+ self .add_str (line , col , f"% { label } " , curses .A_NORMAL )
498+ col += len (label ) + 2
499+
500+ return col
501+
502+ def draw_thread_status (self , line , width ):
503+ """Draw thread status statistics and GC information."""
504+ # Calculate percentages
505+ total_threads = max (1 , self .collector ._thread_status_counts ['total' ])
506+ pct_on_gil = (self .collector ._thread_status_counts ['has_gil' ] / total_threads ) * 100
507+ pct_off_gil = 100.0 - pct_on_gil
508+ pct_gil_requested = (self .collector ._thread_status_counts ['gil_requested' ] / total_threads ) * 100
509+
510+ total_samples = max (1 , self .collector .total_samples )
511+ pct_gc = (self .collector ._gc_frame_samples / total_samples ) * 100
512+
513+ col = 0
514+ self .add_str (line , col , "Threads: " , curses .A_BOLD )
515+ col += 11
516+
517+ # Show GIL stats only if mode is not GIL (GIL mode filters to only GIL holders)
518+ if self .collector .mode != PROFILING_MODE_GIL :
519+ col = self ._add_percentage_stat (line , col , pct_on_gil , "on gil" , self .colors ["green" ])
520+ col = self ._add_percentage_stat (line , col , pct_off_gil , "off gil" , self .colors ["red" ], add_separator = True )
521+
522+ # Show "waiting for gil" only if mode is not GIL
523+ if self .collector .mode != PROFILING_MODE_GIL and col < width - 30 :
524+ col = self ._add_percentage_stat (line , col , pct_gil_requested , "waiting for gil" , self .colors ["yellow" ], add_separator = True )
525+
526+ # Always show GC stats
527+ if col < width - 15 :
528+ col = self ._add_percentage_stat (line , col , pct_gc , "GC" , self .colors ["magenta" ], add_separator = (col > 11 ))
529+
530+ return line + 1
531+
466532 def draw_function_stats (self , line , width , stats_list ):
467533 """Draw function statistics summary."""
468534 total_funcs = len (self .collector .result )
@@ -1204,6 +1270,7 @@ def __init__(
12041270 limit = DEFAULT_DISPLAY_LIMIT ,
12051271 pid = None ,
12061272 display = None ,
1273+ mode = None ,
12071274 ):
12081275 """
12091276 Initialize the live stats collector.
@@ -1215,6 +1282,7 @@ def __init__(
12151282 limit: Maximum number of functions to display
12161283 pid: Process ID being profiled
12171284 display: DisplayInterface implementation (None means curses will be used)
1285+ mode: Profiling mode ('cpu', 'gil', etc.) - affects what stats are shown
12181286 """
12191287 self .result = collections .defaultdict (
12201288 lambda : dict (total_rec_calls = 0 , direct_calls = 0 , cumulative_calls = 0 )
@@ -1232,6 +1300,7 @@ def __init__(
12321300 self .display = display # DisplayInterface implementation
12331301 self .running = True
12341302 self .pid = pid
1303+ self .mode = mode # Profiling mode
12351304 self ._saved_stdout = None
12361305 self ._saved_stderr = None
12371306 self ._devnull = None
@@ -1240,6 +1309,16 @@ def __init__(
12401309 self ._successful_samples = 0 # Track samples that captured frames
12411310 self ._failed_samples = 0 # Track samples that failed to capture frames
12421311
1312+ # Thread status statistics (bit flags)
1313+ self ._thread_status_counts = {
1314+ 'has_gil' : 0 ,
1315+ 'on_cpu' : 0 ,
1316+ 'gil_requested' : 0 ,
1317+ 'unknown' : 0 ,
1318+ 'total' : 0 , # Total thread count across all samples
1319+ }
1320+ self ._gc_frame_samples = 0 # Track samples with GC frames
1321+
12431322 # Interactive controls state
12441323 self .paused = False # Pause UI updates (profiling continues)
12451324 self .show_help = False # Show help screen
@@ -1333,15 +1412,58 @@ def collect(self, stack_frames):
13331412 self .start_time = time .perf_counter ()
13341413 self ._last_display_update = self .start_time
13351414
1415+ # Thread status counts for this sample
1416+ temp_status_counts = {
1417+ 'has_gil' : 0 ,
1418+ 'on_cpu' : 0 ,
1419+ 'gil_requested' : 0 ,
1420+ 'unknown' : 0 ,
1421+ 'total' : 0 ,
1422+ }
1423+ has_gc_frame = False
1424+
13361425 # Always collect data, even when paused
1337- # Track if we got any frames this sample
1338- got_frames = False
1339- for frames , thread_id in self ._iter_all_frames (
1340- stack_frames , skip_idle = self .skip_idle
1341- ):
1342- self ._process_frames (frames )
1343- if frames :
1344- got_frames = True
1426+ # Track thread status flags and GC frames
1427+ for interpreter_info in stack_frames :
1428+ threads = getattr (interpreter_info , 'threads' , [])
1429+ for thread_info in threads :
1430+ temp_status_counts ['total' ] += 1
1431+
1432+ # Track thread status using bit flags
1433+ status_flags = getattr (thread_info , 'status' , 0 )
1434+
1435+ if status_flags & THREAD_STATUS_HAS_GIL :
1436+ temp_status_counts ['has_gil' ] += 1
1437+ if status_flags & THREAD_STATUS_ON_CPU :
1438+ temp_status_counts ['on_cpu' ] += 1
1439+ if status_flags & THREAD_STATUS_GIL_REQUESTED :
1440+ temp_status_counts ['gil_requested' ] += 1
1441+ if status_flags & THREAD_STATUS_UNKNOWN :
1442+ temp_status_counts ['unknown' ] += 1
1443+
1444+ # Process frames (respecting skip_idle)
1445+ if self .skip_idle :
1446+ has_gil = bool (status_flags & THREAD_STATUS_HAS_GIL )
1447+ on_cpu = bool (status_flags & THREAD_STATUS_ON_CPU )
1448+ if not (has_gil or on_cpu ):
1449+ continue
1450+
1451+ frames = getattr (thread_info , 'frame_info' , None )
1452+ if frames :
1453+ self ._process_frames (frames )
1454+ # Check if any frame is in GC
1455+ for frame in frames :
1456+ funcname = getattr (frame , 'funcname' , '' )
1457+ if '<GC>' in funcname or 'gc_collect' in funcname :
1458+ has_gc_frame = True
1459+ break
1460+
1461+ # Update cumulative thread status counts
1462+ for key , count in temp_status_counts .items ():
1463+ self ._thread_status_counts [key ] += count
1464+
1465+ if has_gc_frame :
1466+ self ._gc_frame_samples += 1
13451467
13461468 self ._successful_samples += 1
13471469 self .total_samples += 1
@@ -1633,6 +1755,14 @@ def reset_stats(self):
16331755 self ._successful_samples = 0
16341756 self ._failed_samples = 0
16351757 self ._max_sample_rate = 0
1758+ self ._thread_status_counts = {
1759+ 'has_gil' : 0 ,
1760+ 'on_cpu' : 0 ,
1761+ 'gil_requested' : 0 ,
1762+ 'unknown' : 0 ,
1763+ 'total' : 0 ,
1764+ }
1765+ self ._gc_frame_samples = 0
16361766 self .start_time = time .perf_counter ()
16371767 self ._last_display_update = self .start_time
16381768
0 commit comments