Skip to content

Commit a332095

Browse files
committed
fixup! gh-135953: Add GIL contention markers to sampling profiler Gecko format
1 parent 7b2719e commit a332095

File tree

3 files changed

+118
-132
lines changed

3 files changed

+118
-132
lines changed

Lib/profiling/sampling/collector.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
# Thread status flags
44
try:
5-
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN
5+
from _remote_debugging import THREAD_STATUS_HAS_GIL, THREAD_STATUS_ON_CPU, THREAD_STATUS_UNKNOWN, THREAD_STATUS_GIL_REQUESTED
66
except ImportError:
77
# Fallback for tests or when module is not available
88
THREAD_STATUS_HAS_GIL = (1 << 0)
99
THREAD_STATUS_ON_CPU = (1 << 1)
1010
THREAD_STATUS_UNKNOWN = (1 << 2)
11+
THREAD_STATUS_GIL_REQUESTED = (1 << 3)
1112

1213
class Collector(ABC):
1314
@abstractmethod

Lib/profiling/sampling/gecko_collector.py

Lines changed: 106 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,44 @@ def __init__(self, *, skip_idle=False):
8787
# GC event tracking: track if we're currently in a GC
8888
self.potential_gc_start = None
8989

90+
# Track which threads have been initialized for state tracking
91+
self.initialized_threads = set()
92+
93+
def _track_state_transition(self, tid, condition, active_dict, inactive_dict,
94+
active_name, inactive_name, category, current_time):
95+
"""Track binary state transitions and emit markers.
96+
97+
Args:
98+
tid: Thread ID
99+
condition: Whether the active state is true
100+
active_dict: Dict tracking start time of active state
101+
inactive_dict: Dict tracking start time of inactive state
102+
active_name: Name for active state marker
103+
inactive_name: Name for inactive state marker
104+
category: Gecko category for the markers
105+
current_time: Current timestamp
106+
"""
107+
# On first observation of a thread, just record the current state
108+
# without creating a marker (we don't know what the previous state was)
109+
if tid not in self.initialized_threads:
110+
if condition:
111+
active_dict[tid] = current_time
112+
else:
113+
inactive_dict[tid] = current_time
114+
return
115+
116+
# For already-initialized threads, track transitions
117+
if condition:
118+
active_dict.setdefault(tid, current_time)
119+
if tid in inactive_dict:
120+
self._add_marker(tid, inactive_name, inactive_dict.pop(tid),
121+
current_time, category)
122+
else:
123+
inactive_dict.setdefault(tid, current_time)
124+
if tid in active_dict:
125+
self._add_marker(tid, active_name, active_dict.pop(tid),
126+
current_time, category)
127+
90128
def collect(self, stack_frames):
91129
"""Collect a sample from stack frames."""
92130
current_time = (time.time() * 1000) - self.start_time
@@ -120,83 +158,52 @@ def collect(self, stack_frames):
120158
status_flags = thread_info.status
121159
has_gil = bool(status_flags & THREAD_STATUS_HAS_GIL)
122160
on_cpu = bool(status_flags & THREAD_STATUS_ON_CPU)
123-
unknown = bool(status_flags & THREAD_STATUS_UNKNOWN)
124161
gil_requested = bool(status_flags & THREAD_STATUS_GIL_REQUESTED)
125162

126-
# Track GIL possession (interval marker: "Has GIL" / "No GIL")
127-
if has_gil:
128-
if tid not in self.has_gil_start:
129-
self.has_gil_start[tid] = current_time
130-
# End "No GIL" if it was running
131-
if tid in self.no_gil_start:
132-
start_time = self.no_gil_start[tid]
133-
self._add_marker(tid, "No GIL", start_time, current_time, CATEGORY_GIL)
134-
del self.no_gil_start[tid]
135-
else:
136-
if tid not in self.no_gil_start:
137-
self.no_gil_start[tid] = current_time
138-
# End "Has GIL" if it was running
139-
if tid in self.has_gil_start:
140-
start_time = self.has_gil_start[tid]
141-
self._add_marker(tid, "Has GIL", start_time, current_time, CATEGORY_GIL)
142-
del self.has_gil_start[tid]
143-
144-
# Track "On CPU" / "Off CPU" state
145-
if on_cpu:
146-
if tid not in self.on_cpu_start:
147-
self.on_cpu_start[tid] = current_time
148-
# End "Off CPU" if it was running
149-
if tid in self.off_cpu_start:
150-
start_time = self.off_cpu_start[tid]
151-
self._add_marker(tid, "Off CPU", start_time, current_time, CATEGORY_CPU)
152-
del self.off_cpu_start[tid]
153-
else:
154-
if tid not in self.off_cpu_start:
155-
self.off_cpu_start[tid] = current_time
156-
# End "On CPU" if it was running
157-
if tid in self.on_cpu_start:
158-
start_time = self.on_cpu_start[tid]
159-
self._add_marker(tid, "On CPU", start_time, current_time, CATEGORY_CPU)
160-
del self.on_cpu_start[tid]
161-
162-
# Track "Python Code" (has GIL) / "Native Code" (on CPU without GIL)
163+
# Track GIL possession (Has GIL / No GIL)
164+
self._track_state_transition(
165+
tid, has_gil, self.has_gil_start, self.no_gil_start,
166+
"Has GIL", "No GIL", CATEGORY_GIL, current_time
167+
)
168+
169+
# Track CPU state (On CPU / Off CPU)
170+
self._track_state_transition(
171+
tid, on_cpu, self.on_cpu_start, self.off_cpu_start,
172+
"On CPU", "Off CPU", CATEGORY_CPU, current_time
173+
)
174+
175+
# Track code type (Python Code / Native Code)
176+
# This is tri-state: Python (has_gil), Native (on_cpu without gil), or Neither
163177
if has_gil:
164-
if tid not in self.python_code_start:
165-
self.python_code_start[tid] = current_time
166-
# End "Native Code" if it was running
167-
if tid in self.native_code_start:
168-
start_time = self.native_code_start[tid]
169-
self._add_marker(tid, "Native Code", start_time, current_time, CATEGORY_CODE_TYPE)
170-
del self.native_code_start[tid]
178+
self._track_state_transition(
179+
tid, True, self.python_code_start, self.native_code_start,
180+
"Python Code", "Native Code", CATEGORY_CODE_TYPE, current_time
181+
)
171182
elif on_cpu:
172-
# Native code: on CPU without GIL
173-
if tid not in self.native_code_start:
174-
self.native_code_start[tid] = current_time
175-
# End "Python Code" if it was running
176-
if tid in self.python_code_start:
177-
start_time = self.python_code_start[tid]
178-
self._add_marker(tid, "Python Code", start_time, current_time, CATEGORY_CODE_TYPE)
179-
del self.python_code_start[tid]
183+
self._track_state_transition(
184+
tid, True, self.native_code_start, self.python_code_start,
185+
"Native Code", "Python Code", CATEGORY_CODE_TYPE, current_time
186+
)
180187
else:
181-
# Neither has GIL nor on CPU - end both if running
182-
if tid in self.python_code_start:
183-
start_time = self.python_code_start[tid]
184-
self._add_marker(tid, "Python Code", start_time, current_time, CATEGORY_CODE_TYPE)
185-
del self.python_code_start[tid]
186-
if tid in self.native_code_start:
187-
start_time = self.native_code_start[tid]
188-
self._add_marker(tid, "Native Code", start_time, current_time, CATEGORY_CODE_TYPE)
189-
del self.native_code_start[tid]
190-
191-
# Track "Waiting for GIL" intervals
188+
# Thread is idle (neither has GIL nor on CPU) - close any open code markers
189+
# This handles the third state that _track_state_transition doesn't cover
190+
if tid in self.initialized_threads:
191+
if tid in self.python_code_start:
192+
self._add_marker(tid, "Python Code", self.python_code_start.pop(tid),
193+
current_time, CATEGORY_CODE_TYPE)
194+
if tid in self.native_code_start:
195+
self._add_marker(tid, "Native Code", self.native_code_start.pop(tid),
196+
current_time, CATEGORY_CODE_TYPE)
197+
198+
# Track "Waiting for GIL" intervals (one-sided tracking)
192199
if gil_requested:
193-
if tid not in self.gil_wait_start:
194-
self.gil_wait_start[tid] = current_time
195-
else:
196-
if tid in self.gil_wait_start:
197-
start_time = self.gil_wait_start[tid]
198-
self._add_marker(tid, "Waiting for GIL", start_time, current_time, CATEGORY_GIL)
199-
del self.gil_wait_start[tid]
200+
self.gil_wait_start.setdefault(tid, current_time)
201+
elif tid in self.gil_wait_start:
202+
self._add_marker(tid, "Waiting for GIL", self.gil_wait_start.pop(tid),
203+
current_time, CATEGORY_GIL)
204+
205+
# Mark thread as initialized after processing all state transitions
206+
self.initialized_threads.add(tid)
200207

201208
# Categorize: idle if neither has GIL nor on CPU
202209
is_idle = not has_gil and not on_cpu
@@ -366,13 +373,22 @@ def _add_marker(self, tid, name, start_time, end_time, category):
366373
})
367374

368375
def _add_gc_marker(self, start_time, end_time):
369-
"""Add a GC Collecting event marker to the main thread (or first thread we see)."""
376+
"""Add a GC Collecting event marker to the main thread."""
370377
if not self.threads:
371378
return
372379

373-
# Add GC marker to the first thread (typically the main thread)
374-
first_tid = next(iter(self.threads))
375-
self._add_marker(first_tid, "GC Collecting", start_time, end_time, CATEGORY_GC)
380+
# Find the main thread by checking isMainThread flag
381+
main_tid = None
382+
for tid, thread_data in self.threads.items():
383+
if thread_data.get("isMainThread", False):
384+
main_tid = tid
385+
break
386+
387+
# If we can't find the main thread, use the first thread as fallback
388+
if main_tid is None:
389+
main_tid = next(iter(self.threads))
390+
391+
self._add_marker(main_tid, "GC Collecting", start_time, end_time, CATEGORY_GC)
376392

377393
def _process_stack(self, thread_data, frames):
378394
"""Process a stack and return the stack index."""
@@ -546,41 +562,21 @@ def _finalize_markers(self):
546562
"""Close any open markers at the end of profiling."""
547563
end_time = self.last_sample_time
548564

549-
# Close all open markers for each thread
550-
for tid in list(self.has_gil_start.keys()):
551-
start_time = self.has_gil_start[tid]
552-
self._add_marker(tid, "Has GIL", start_time, end_time, CATEGORY_GIL)
553-
del self.has_gil_start[tid]
554-
555-
for tid in list(self.no_gil_start.keys()):
556-
start_time = self.no_gil_start[tid]
557-
self._add_marker(tid, "No GIL", start_time, end_time, CATEGORY_GIL)
558-
del self.no_gil_start[tid]
559-
560-
for tid in list(self.on_cpu_start.keys()):
561-
start_time = self.on_cpu_start[tid]
562-
self._add_marker(tid, "On CPU", start_time, end_time, CATEGORY_CPU)
563-
del self.on_cpu_start[tid]
564-
565-
for tid in list(self.off_cpu_start.keys()):
566-
start_time = self.off_cpu_start[tid]
567-
self._add_marker(tid, "Off CPU", start_time, end_time, CATEGORY_CPU)
568-
del self.off_cpu_start[tid]
569-
570-
for tid in list(self.python_code_start.keys()):
571-
start_time = self.python_code_start[tid]
572-
self._add_marker(tid, "Python Code", start_time, end_time, CATEGORY_CODE_TYPE)
573-
del self.python_code_start[tid]
574-
575-
for tid in list(self.native_code_start.keys()):
576-
start_time = self.native_code_start[tid]
577-
self._add_marker(tid, "Native Code", start_time, end_time, CATEGORY_CODE_TYPE)
578-
del self.native_code_start[tid]
579-
580-
for tid in list(self.gil_wait_start.keys()):
581-
start_time = self.gil_wait_start[tid]
582-
self._add_marker(tid, "Waiting for GIL", start_time, end_time, CATEGORY_GIL)
583-
del self.gil_wait_start[tid]
565+
# Close all open markers for each thread using a generic approach
566+
marker_states = [
567+
(self.has_gil_start, "Has GIL", CATEGORY_GIL),
568+
(self.no_gil_start, "No GIL", CATEGORY_GIL),
569+
(self.on_cpu_start, "On CPU", CATEGORY_CPU),
570+
(self.off_cpu_start, "Off CPU", CATEGORY_CPU),
571+
(self.python_code_start, "Python Code", CATEGORY_CODE_TYPE),
572+
(self.native_code_start, "Native Code", CATEGORY_CODE_TYPE),
573+
(self.gil_wait_start, "Waiting for GIL", CATEGORY_GIL),
574+
]
575+
576+
for state_dict, marker_name, category in marker_states:
577+
for tid in list(state_dict.keys()):
578+
self._add_marker(tid, marker_name, state_dict[tid], end_time, category)
579+
del state_dict[tid]
584580

585581
# Close any open GC marker
586582
if self.potential_gc_start is not None:

Modules/_remote_debugging_module.c

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,27 +2714,17 @@ unwind_stack_for_thread(
27142714
status_flags |= THREAD_STATUS_ON_CPU;
27152715
}
27162716

2717-
// Determine if we should skip based on mode
2718-
int status = THREAD_STATE_RUNNING; // Default for skip logic
2719-
if (unwinder->mode == PROFILING_MODE_CPU) {
2720-
status = cpu_status;
2721-
} else if (unwinder->mode == PROFILING_MODE_GIL) {
2722-
status = has_gil ? THREAD_STATE_RUNNING : THREAD_STATE_GIL_WAIT;
2723-
} else if (unwinder->mode == PROFILING_MODE_ALL) {
2724-
// In ALL mode, considered running if has GIL or on CPU
2725-
if (has_gil || (status_flags & THREAD_STATUS_ON_CPU)) {
2726-
status = THREAD_STATE_RUNNING;
2727-
} else {
2728-
status = THREAD_STATE_IDLE;
2729-
}
2730-
}
2731-
// PROFILING_MODE_WALL defaults to THREAD_STATE_RUNNING
2732-
27332717
// Check if we should skip this thread based on mode
27342718
int should_skip = 0;
2735-
if (unwinder->skip_non_matching_threads && status != THREAD_STATE_RUNNING &&
2736-
(unwinder->mode == PROFILING_MODE_CPU || unwinder->mode == PROFILING_MODE_GIL)) {
2737-
should_skip = 1;
2719+
if (unwinder->skip_non_matching_threads) {
2720+
if (unwinder->mode == PROFILING_MODE_CPU) {
2721+
// Skip if not on CPU
2722+
should_skip = !(status_flags & THREAD_STATUS_ON_CPU);
2723+
} else if (unwinder->mode == PROFILING_MODE_GIL) {
2724+
// Skip if doesn't have GIL
2725+
should_skip = !(status_flags & THREAD_STATUS_HAS_GIL);
2726+
}
2727+
// PROFILING_MODE_WALL and PROFILING_MODE_ALL never skip
27382728
}
27392729

27402730
if (should_skip) {
@@ -2789,9 +2779,8 @@ unwind_stack_for_thread(
27892779
Py_DECREF(py_status);
27902780
goto error;
27912781
}
2792-
PyErr_Print();
27932782

2794-
// In PROFILING_MODE_ALL, py_status contains flags, otherwise it contains legacy enum
2783+
// py_status contains status flags (bitfield)
27952784
PyStructSequence_SetItem(result, 0, thread_id);
27962785
PyStructSequence_SetItem(result, 1, py_status); // Steals reference
27972786
PyStructSequence_SetItem(result, 2, frame_info); // Steals reference

0 commit comments

Comments
 (0)