Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-135953: Add GIL contention markers to sampling profiler Gecko format
This commit enhances the Gecko format reporter in the sampling profiler
to include markers for GIL acquisition events. The implementation adds:

- GIL contention tracking in pystate and ceval_gil
- New marker types in the Gecko collector for GIL wait events
- External inspection support for reading GIL state
- Sample collection of GIL acquisition times and thread states
- Comprehensive tests for marker export functionality

The markers provide visibility into GIL contention patterns, showing
when threads are waiting to acquire the GIL and how long they wait.
  • Loading branch information
pablogsal committed Oct 1, 2025
commit 7b2719e3e801e77d0cc3519f4681671f75a4385c
3 changes: 3 additions & 0 deletions Include/cpython/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ struct _ts {
/* Currently holds the GIL. Must be its own field to avoid data races */
int holds_gil;

/* Currently requesting the GIL */
int gil_requested;

int _whence;

/* Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED).
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_debug_offsets.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ typedef struct _Py_DebugOffsets {
uint64_t native_thread_id;
uint64_t datastack_chunk;
uint64_t status;
uint64_t holds_gil;
uint64_t gil_requested;
} thread_state;

// InterpreterFrame offset;
Expand Down Expand Up @@ -273,6 +275,8 @@ typedef struct _Py_DebugOffsets {
.native_thread_id = offsetof(PyThreadState, native_thread_id), \
.datastack_chunk = offsetof(PyThreadState, datastack_chunk), \
.status = offsetof(PyThreadState, _status), \
.holds_gil = offsetof(PyThreadState, holds_gil), \
.gil_requested = offsetof(PyThreadState, gil_requested), \
}, \
.interpreter_frame = { \
.size = sizeof(_PyInterpreterFrame), \
Expand Down
30 changes: 16 additions & 14 deletions Lib/profiling/sampling/collector.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
from abc import ABC, abstractmethod

# Enums are slow
THREAD_STATE_RUNNING = 0
THREAD_STATE_IDLE = 1
THREAD_STATE_GIL_WAIT = 2
THREAD_STATE_UNKNOWN = 3

STATUS = {
THREAD_STATE_RUNNING: "running",
THREAD_STATE_IDLE: "idle",
THREAD_STATE_GIL_WAIT: "gil_wait",
THREAD_STATE_UNKNOWN: "unknown",
}
# Thread status flags
try:
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN
except ImportError:
# Fallback for tests or when module is not available
THREAD_STATUS_HAS_GIL = (1 << 0)
THREAD_STATUS_ON_CPU = (1 << 1)
THREAD_STATUS_UNKNOWN = (1 << 2)

class Collector(ABC):
@abstractmethod
Expand All @@ -26,8 +22,14 @@ def _iter_all_frames(self, stack_frames, skip_idle=False):
"""Iterate over all frame stacks from all interpreters and threads."""
for interpreter_info in stack_frames:
for thread_info in interpreter_info.threads:
if skip_idle and thread_info.status != THREAD_STATE_RUNNING:
continue
# skip_idle now means: skip if thread is not actively running
# A thread is "active" if it has the GIL OR is on CPU
if skip_idle:
status_flags = thread_info.status
has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL)
on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU)
if not (has_gil or on_cpu):
continue
frames = thread_info.frame_info
if frames:
yield frames, thread_info.thread_id
Loading
Loading