Skip to content

Commit 4a7df35

Browse files
committed
Add GIL flags
1 parent 85bada1 commit 4a7df35

5 files changed

Lines changed: 197 additions & 36 deletions

File tree

Lib/profiling/sampling/collector.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
from abc import ABC, abstractmethod
2-
3-
# Thread status flags
4-
try:
5-
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED
6-
except ImportError:
7-
# Fallback for tests or when module is not available
8-
THREAD_STATUS_HAS_GIL = (1 << 0)
9-
THREAD_STATUS_ON_CPU = (1 << 1)
10-
THREAD_STATUS_UNKNOWN = (1 << 2)
11-
THREAD_STATUS_GIL_REQUESTED = (1 << 3)
2+
from .constants import (
3+
THREAD_STATUS_HAS_GIL,
4+
THREAD_STATUS_ON_CPU,
5+
THREAD_STATUS_UNKNOWN,
6+
THREAD_STATUS_GIL_REQUESTED,
7+
)
128

139
class Collector(ABC):
1410
@abstractmethod
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Constants for the sampling profiler."""
2+
3+
# Profiling mode constants
4+
PROFILING_MODE_WALL = 0
5+
PROFILING_MODE_CPU = 1
6+
PROFILING_MODE_GIL = 2
7+
PROFILING_MODE_ALL = 3 # Combines GIL + CPU checks
8+
9+
# Sort mode constants
10+
SORT_MODE_NSAMPLES = 0
11+
SORT_MODE_TOTTIME = 1
12+
SORT_MODE_CUMTIME = 2
13+
SORT_MODE_SAMPLE_PCT = 3
14+
SORT_MODE_CUMUL_PCT = 4
15+
SORT_MODE_NSAMPLES_CUMUL = 5
16+
17+
# Thread status flags
18+
try:
19+
from _remote_debugging import (
20+
THREAD_STATUS_HAS_GIL,
21+
THREAD_STATUS_ON_CPU,
22+
THREAD_STATUS_UNKNOWN,
23+
THREAD_STATUS_GIL_REQUESTED,
24+
)
25+
except ImportError:
26+
# Fallback for tests or when module is not available
27+
THREAD_STATUS_HAS_GIL = (1 << 0)
28+
THREAD_STATUS_ON_CPU = (1 << 1)
29+
THREAD_STATUS_UNKNOWN = (1 << 2)
30+
THREAD_STATUS_GIL_REQUESTED = (1 << 3)

Lib/profiling/sampling/live_collector.py

Lines changed: 140 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
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
│ ├─────────────────────────────────┤ │
@@ -109,7 +110,16 @@
109110
import sysconfig
110111
import time
111112
from 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
115125
MICROSECONDS_PER_SECOND = 1_000_000
@@ -129,7 +139,7 @@
129139
WIDTH_THRESHOLD_CUMTIME = 140
130140

131141
# Display layout constants
132-
HEADER_LINES = 9
142+
HEADER_LINES = 10 # Increased to include thread status line
133143
FOOTER_LINES = 2
134144
SAFETY_MARGIN = 1
135145
TOP_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

Lib/profiling/sampling/sample.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,25 @@
1414
from .pstats_collector import PstatsCollector
1515
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
1616
from .gecko_collector import GeckoCollector
17+
from .constants import (
18+
PROFILING_MODE_WALL,
19+
PROFILING_MODE_CPU,
20+
PROFILING_MODE_GIL,
21+
PROFILING_MODE_ALL,
22+
SORT_MODE_NSAMPLES,
23+
SORT_MODE_TOTTIME,
24+
SORT_MODE_CUMTIME,
25+
SORT_MODE_SAMPLE_PCT,
26+
SORT_MODE_CUMUL_PCT,
27+
SORT_MODE_NSAMPLES_CUMUL,
28+
)
1729
try:
1830
from .live_collector import LiveStatsCollector
1931
except ImportError:
2032
LiveStatsCollector = None
2133

2234
_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None
2335

24-
# Profiling mode constants
25-
PROFILING_MODE_WALL = 0
26-
PROFILING_MODE_CPU = 1
27-
PROFILING_MODE_GIL = 2
28-
PROFILING_MODE_ALL = 3 # Combines GIL + CPU checks
29-
30-
# Sort mode constants
31-
SORT_MODE_NSAMPLES = 0
32-
SORT_MODE_TOTTIME = 1
33-
SORT_MODE_CUMTIME = 2
34-
SORT_MODE_SAMPLE_PCT = 3
35-
SORT_MODE_CUMUL_PCT = 4
36-
SORT_MODE_NSAMPLES_CUMUL = 5
37-
3836

3937
def _parse_mode(mode_string):
4038
"""Convert mode string to mode constant."""
@@ -694,7 +692,8 @@ def sample(
694692
skip_idle=skip_idle,
695693
sort_by=sort_by,
696694
limit=limit or 20,
697-
pid=pid
695+
pid=pid,
696+
mode=mode,
698697
)
699698
# Live mode is interactive, don't save file by default
700699
# User can specify -o if they want to save stats

Modules/_remote_debugging_module.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2758,7 +2758,13 @@ unwind_stack_for_thread(
27582758

27592759
// Check CPU status
27602760
long pthread_id = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.thread_id);
2761-
int cpu_status = get_thread_status(unwinder, tid, pthread_id);
2761+
2762+
// Optimization: only check CPU status if needed by mode because it's expensive
2763+
int cpu_status = -1;
2764+
if (unwinder->mode == PROFILING_MODE_CPU || unwinder->mode == PROFILING_MODE_ALL) {
2765+
cpu_status = get_thread_status(unwinder, tid, pthread_id);
2766+
}
2767+
27622768
if (cpu_status == -1) {
27632769
status_flags |= THREAD_STATUS_UNKNOWN;
27642770
} else if (cpu_status == THREAD_STATE_RUNNING) {

0 commit comments

Comments
 (0)