Skip to content

Commit 8859bf8

Browse files
committed
Implement GIL mode and filtering
1 parent fb8983c commit 8859bf8

File tree

3 files changed

+92
-43
lines changed

3 files changed

+92
-43
lines changed

Lib/profiling/sampling/sample.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@
1515
from .stack_collector import CollapsedStackCollector, FlamegraphCollector
1616

1717
_FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") is not None
18+
19+
# Profiling mode constants
20+
PROFILING_MODE_WALL = 0
21+
PROFILING_MODE_CPU = 1
22+
PROFILING_MODE_GIL = 2
23+
24+
25+
def _parse_mode(mode_string):
26+
"""Convert mode string to mode constant."""
27+
mode_map = {
28+
"wall": PROFILING_MODE_WALL,
29+
"cpu": PROFILING_MODE_CPU,
30+
"gil": PROFILING_MODE_GIL,
31+
}
32+
return mode_map[mode_string]
1833
_HELP_DESCRIPTION = """Sample a process's stack frames and generate profiling data.
1934
Supports the following target modes:
2035
- -p PID: Profile an existing process by PID
@@ -120,18 +135,18 @@ def _run_with_sync(original_cmd):
120135

121136

122137
class SampleProfiler:
123-
def __init__(self, pid, sample_interval_usec, all_threads, *, cpu_time=False):
138+
def __init__(self, pid, sample_interval_usec, all_threads, *, mode=PROFILING_MODE_WALL):
124139
self.pid = pid
125140
self.sample_interval_usec = sample_interval_usec
126141
self.all_threads = all_threads
127142
if _FREE_THREADED_BUILD:
128143
self.unwinder = _remote_debugging.RemoteUnwinder(
129-
self.pid, all_threads=self.all_threads, cpu_time=cpu_time
144+
self.pid, all_threads=self.all_threads, mode=mode
130145
)
131146
else:
132147
only_active_threads = bool(self.all_threads)
133148
self.unwinder = _remote_debugging.RemoteUnwinder(
134-
self.pid, only_active_thread=only_active_threads, cpu_time=cpu_time
149+
self.pid, only_active_thread=only_active_threads, mode=mode
135150
)
136151
# Track sample intervals and total sample count
137152
self.sample_intervals = deque(maxlen=100)
@@ -596,13 +611,16 @@ def sample(
596611
show_summary=True,
597612
output_format="pstats",
598613
realtime_stats=False,
599-
skip_idle=False,
614+
mode=PROFILING_MODE_WALL,
600615
):
601616
profiler = SampleProfiler(
602-
pid, sample_interval_usec, all_threads=all_threads, cpu_time=skip_idle
617+
pid, sample_interval_usec, all_threads=all_threads, mode=mode
603618
)
604619
profiler.realtime_stats = realtime_stats
605620

621+
# Determine skip_idle for collector compatibility
622+
skip_idle = mode != PROFILING_MODE_WALL
623+
606624
collector = None
607625
match output_format:
608626
case "pstats":
@@ -661,7 +679,8 @@ def wait_for_process_and_sample(pid, sort_value, args):
661679
filename = args.outfile
662680
if not filename and args.format == "collapsed":
663681
filename = f"collapsed.{pid}.txt"
664-
skip_idle = True if args.mode == "cpu" else False
682+
683+
mode = _parse_mode(args.mode)
665684

666685
sample(
667686
pid,
@@ -674,7 +693,7 @@ def wait_for_process_and_sample(pid, sort_value, args):
674693
show_summary=not args.no_summary,
675694
output_format=args.format,
676695
realtime_stats=args.realtime_stats,
677-
skip_idle=skip_idle,
696+
mode=mode,
678697
)
679698

680699

@@ -733,9 +752,9 @@ def main():
733752
mode_group = parser.add_argument_group("Mode options")
734753
mode_group.add_argument(
735754
"--mode",
736-
choices=["wall", "cpu"],
737-
default="wall-time",
738-
help="Sampling mode: wall-time (default, skip_idle=False) or cpu-time (skip_idle=True)",
755+
choices=["wall", "cpu", "gil"],
756+
default="wall",
757+
help="Sampling mode: wall (all threads), cpu (only CPU-running threads), gil (only GIL-holding threads)",
739758
)
740759

741760
# Output format selection
@@ -862,8 +881,7 @@ def main():
862881
elif target_count > 1:
863882
parser.error("only one target type can be specified: -p/--pid, -m/--module, or script")
864883

865-
# Set skip_idle based on mode
866-
skip_idle = True if args.mode == "cpu" else False
884+
mode = _parse_mode(args.mode)
867885

868886
if args.pid:
869887
sample(
@@ -877,7 +895,7 @@ def main():
877895
show_summary=not args.no_summary,
878896
output_format=args.format,
879897
realtime_stats=args.realtime_stats,
880-
skip_idle=skip_idle,
898+
mode=mode,
881899
)
882900
elif args.module or args.args:
883901
if args.module:

Modules/_remote_debugging_module.c

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,12 @@ enum _ThreadState {
244244
THREAD_STATE_UNKNOWN
245245
};
246246

247+
enum _ProfilingMode {
248+
PROFILING_MODE_WALL = 0,
249+
PROFILING_MODE_CPU = 1,
250+
PROFILING_MODE_GIL = 2
251+
};
252+
247253
typedef struct {
248254
PyObject_HEAD
249255
proc_handle_t handle;
@@ -257,7 +263,7 @@ typedef struct {
257263
_Py_hashtable_t *code_object_cache;
258264
int debug;
259265
int only_active_thread;
260-
int cpu_time;
266+
int mode; // Use enum _ProfilingMode values
261267
RemoteDebuggingState *cached_state; // Cached module state
262268
#ifdef Py_GIL_DISABLED
263269
// TLBC cache invalidation tracking
@@ -2629,6 +2635,39 @@ unwind_stack_for_thread(
26292635
goto error;
26302636
}
26312637

2638+
long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id);
2639+
2640+
// Calculate thread status based on mode
2641+
int status = THREAD_STATE_UNKNOWN;
2642+
if (unwinder->mode == PROFILING_MODE_CPU) {
2643+
long pthread_id = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.thread_id);
2644+
status = get_thread_status(unwinder, tid, pthread_id);
2645+
if (status == -1) {
2646+
PyErr_Print();
2647+
PyErr_SetString(PyExc_RuntimeError, "Failed to get thread status");
2648+
goto error;
2649+
}
2650+
} else if (unwinder->mode == PROFILING_MODE_GIL) {
2651+
status = (*current_tstate == gil_holder_tstate) ? THREAD_STATE_RUNNING : THREAD_STATE_GIL_WAIT;
2652+
} else {
2653+
// PROFILING_MODE_WALL - all threads are considered running
2654+
status = THREAD_STATE_RUNNING;
2655+
}
2656+
2657+
// Check if we should skip this thread based on mode
2658+
int should_skip = 0;
2659+
if (unwinder->mode == PROFILING_MODE_CPU && status != THREAD_STATE_RUNNING) {
2660+
should_skip = 1;
2661+
} else if (unwinder->mode == PROFILING_MODE_GIL && status != THREAD_STATE_RUNNING) {
2662+
should_skip = 1;
2663+
}
2664+
2665+
if (should_skip) {
2666+
// Advance to next thread and return NULL to skip processing
2667+
*current_tstate = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.next);
2668+
return NULL;
2669+
}
2670+
26322671
uintptr_t frame_addr = GET_MEMBER(uintptr_t, ts, unwinder->debug_offsets.thread_state.current_frame);
26332672

26342673
frame_info = PyList_New(0);
@@ -2642,20 +2681,6 @@ unwind_stack_for_thread(
26422681
goto error;
26432682
}
26442683

2645-
long tid = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.native_thread_id);
2646-
int status = THREAD_STATE_UNKNOWN;
2647-
if (unwinder->cpu_time == 1) {
2648-
long pthread_id = GET_MEMBER(long, ts, unwinder->debug_offsets.thread_state.thread_id);
2649-
status = get_thread_status(unwinder, tid, pthread_id);
2650-
if (status == -1) {
2651-
PyErr_Print();
2652-
PyErr_SetString(PyExc_RuntimeError, "Failed to get thread status");
2653-
goto error;
2654-
}
2655-
} else {
2656-
status = (*current_tstate == gil_holder_tstate) ? THREAD_STATE_RUNNING : THREAD_STATE_GIL_WAIT;
2657-
}
2658-
26592684
if (process_frame_chain(unwinder, frame_addr, &chunks, frame_info) < 0) {
26602685
set_exception_cause(unwinder, PyExc_RuntimeError, "Failed to process frame chain");
26612686
goto error;
@@ -2716,7 +2741,7 @@ _remote_debugging.RemoteUnwinder.__init__
27162741
*
27172742
all_threads: bool = False
27182743
only_active_thread: bool = False
2719-
cpu_time: bool = False
2744+
mode: int = 0
27202745
debug: bool = False
27212746
27222747
Initialize a new RemoteUnwinder object for debugging a remote Python process.
@@ -2726,7 +2751,7 @@ Initialize a new RemoteUnwinder object for debugging a remote Python process.
27262751
all_threads: If True, initialize state for all threads in the process.
27272752
If False, only initialize for the main thread.
27282753
only_active_thread: If True, only sample the thread holding the GIL.
2729-
cpu_time: If True, enable CPU time tracking for unwinder operations.
2754+
mode: Profiling mode: 0=WALL (wall-time), 1=CPU (cpu-time), 2=GIL (gil-time).
27302755
Cannot be used together with all_threads=True.
27312756
debug: If True, chain exceptions to explain the sequence of events that
27322757
lead to the exception.
@@ -2745,8 +2770,8 @@ static int
27452770
_remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
27462771
int pid, int all_threads,
27472772
int only_active_thread,
2748-
int cpu_time, int debug)
2749-
/*[clinic end generated code: output=2598ce54f6335ac7 input=0cf2038cc304c165]*/
2773+
int mode, int debug)
2774+
/*[clinic end generated code: output=784e9990115aa569 input=d082d792d2ba9924]*/
27502775
{
27512776
// Validate that all_threads and only_active_thread are not both True
27522777
if (all_threads && only_active_thread) {
@@ -2765,7 +2790,7 @@ _remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
27652790

27662791
self->debug = debug;
27672792
self->only_active_thread = only_active_thread;
2768-
self->cpu_time = cpu_time;
2793+
self->mode = mode;
27692794
self->cached_state = NULL;
27702795
if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) {
27712796
set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle");
@@ -2983,6 +3008,12 @@ _remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self
29833008
while (current_tstate != 0) {
29843009
PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate, gil_holder_tstate);
29853010
if (!frame_info) {
3011+
// Check if this was an intentional skip due to mode-based filtering
3012+
if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL) && !PyErr_Occurred()) {
3013+
// Thread was skipped due to mode filtering, continue to next thread
3014+
continue;
3015+
}
3016+
// This was an actual error
29863017
Py_DECREF(interpreter_threads);
29873018
set_exception_cause(self, PyExc_RuntimeError, "Failed to unwind stack for thread");
29883019
Py_CLEAR(result);

Modules/clinic/_remote_debugging_module.c.h

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)