cpython/Modules/_remote_debugging/module.c
Pablo Galindo Salgado 661df25692
gh-149584: Fix excessive overhead in the Tachyon profiler regarding the cache behavior (#149649)
Use exact remote reads for interpreter state, thread state, and
interpreter frame structs instead of pulling full remote pages into the
profiler page cache. This matches the core change from
python/cpython#149585.

The profiler clears the page cache between samples, so live entries are
always packed at the front. Track the live count and only clear/search
that prefix instead of scanning all 1024 slots on the hot path.

Use the frame cache to predict the next thread state and top frame
address, then batch interpreter/thread/frame reads with process_vm_readv
when profiling a Linux target. Reuse prefetched frame buffers in the
frame walker when the prediction is valid.

Cache the last FrameInfo tuple per code object/instruction offset, reuse
cached thread id objects, and append cached parent frames directly on
full frame-cache hits. This cuts Python allocation churn in the
steady-state profiler path.
2026-05-20 04:32:08 -07:00

2336 lines
76 KiB
C

/******************************************************************************
* Remote Debugging Module - Main Module Implementation
*
* This file contains the main module initialization, the RemoteUnwinder
* class implementation, and utility functions.
******************************************************************************/
#include "_remote_debugging.h"
#include "binary_io.h"
#include "debug_offsets_validation.h"
#include "gc_stats.h"
/* Forward declarations for clinic-generated code */
typedef struct {
PyObject_HEAD
BinaryWriter *writer;
uint32_t cached_total_samples; /* Preserved after finalize */
} BinaryWriterObject;
typedef struct {
PyObject_HEAD
BinaryReader *reader;
} BinaryReaderObject;
#include "clinic/module.c.h"
/* ============================================================================
* STRUCTSEQ TYPE DEFINITIONS
* ============================================================================ */
// TaskInfo structseq type
static PyStructSequence_Field TaskInfo_fields[] = {
{"task_id", "Task ID (memory address)"},
{"task_name", "Task name"},
{"coroutine_stack", "Coroutine call stack"},
{"awaited_by", "Tasks awaiting this task"},
{NULL}
};
PyStructSequence_Desc TaskInfo_desc = {
"_remote_debugging.TaskInfo",
"Information about an asyncio task",
TaskInfo_fields,
4
};
// LocationInfo structseq type
static PyStructSequence_Field LocationInfo_fields[] = {
{"lineno", "Line number"},
{"end_lineno", "End line number"},
{"col_offset", "Column offset"},
{"end_col_offset", "End column offset"},
{NULL}
};
PyStructSequence_Desc LocationInfo_desc = {
"_remote_debugging.LocationInfo",
"Source location information: (lineno, end_lineno, col_offset, end_col_offset)",
LocationInfo_fields,
4
};
// FrameInfo structseq type
static PyStructSequence_Field FrameInfo_fields[] = {
{"filename", "Source code filename"},
{"location", "LocationInfo structseq or None for synthetic frames"},
{"funcname", "Function name"},
{"opcode", "Opcode being executed (None if not gathered)"},
{NULL}
};
PyStructSequence_Desc FrameInfo_desc = {
"_remote_debugging.FrameInfo",
"Information about a frame",
FrameInfo_fields,
4
};
// CoroInfo structseq type
static PyStructSequence_Field CoroInfo_fields[] = {
{"call_stack", "Coroutine call stack"},
{"task_name", "Task name"},
{NULL}
};
PyStructSequence_Desc CoroInfo_desc = {
"_remote_debugging.CoroInfo",
"Information about a coroutine",
CoroInfo_fields,
2
};
// ThreadInfo structseq type
static PyStructSequence_Field ThreadInfo_fields[] = {
{"thread_id", "Thread ID"},
{"status", "Thread status (flags: HAS_GIL, ON_CPU, UNKNOWN or legacy enum)"},
{"frame_info", "Frame information"},
{NULL}
};
PyStructSequence_Desc ThreadInfo_desc = {
"_remote_debugging.ThreadInfo",
"Information about a thread",
ThreadInfo_fields,
3
};
// InterpreterInfo structseq type
static PyStructSequence_Field InterpreterInfo_fields[] = {
{"interpreter_id", "Interpreter ID"},
{"threads", "List of threads in this interpreter"},
{NULL}
};
PyStructSequence_Desc InterpreterInfo_desc = {
"_remote_debugging.InterpreterInfo",
"Information about an interpreter",
InterpreterInfo_fields,
2
};
// AwaitedInfo structseq type
static PyStructSequence_Field AwaitedInfo_fields[] = {
{"thread_id", "Thread ID"},
{"awaited_by", "List of tasks awaited by this thread"},
{NULL}
};
PyStructSequence_Desc AwaitedInfo_desc = {
"_remote_debugging.AwaitedInfo",
"Information about what a thread is awaiting",
AwaitedInfo_fields,
2
};
// GCStatsInfo structseq type
static PyStructSequence_Field GCStatsInfo_fields[] = {
{"gen", "GC generation number"},
{"iid", "Interpreter ID"},
{"ts_start", "Raw timestamp at collection start"},
{"ts_stop", "Raw timestamp at collection stop"},
{"collections", "Total number of collections"},
{"collected", "Total number of collected objects"},
{"uncollectable", "Total number of uncollectable objects"},
{"candidates", "Total objects considered and traversed"},
{"heap_size", "Number of live objects"},
{"duration", "Total collection time, in seconds"},
{NULL}
};
PyStructSequence_Desc GCStatsInfo_desc = {
"_remote_debugging.GCStatsInfo",
"Information about a garbage collector stats sample",
GCStatsInfo_fields,
10
};
/* ============================================================================
* UTILITY FUNCTIONS
* ============================================================================ */
void
cached_code_metadata_destroy(void *ptr)
{
CachedCodeMetadata *meta = (CachedCodeMetadata *)ptr;
Py_DECREF(meta->func_name);
Py_DECREF(meta->file_name);
Py_DECREF(meta->linetable);
Py_XDECREF(meta->last_frame_info);
PyMem_RawFree(meta);
}
RemoteDebuggingState *
RemoteDebugging_GetState(PyObject *module)
{
void *state = _PyModule_GetState(module);
assert(state != NULL);
return (RemoteDebuggingState *)state;
}
RemoteDebuggingState *
RemoteDebugging_GetStateFromType(PyTypeObject *type)
{
PyObject *module = PyType_GetModule(type);
assert(module != NULL);
return RemoteDebugging_GetState(module);
}
RemoteDebuggingState *
RemoteDebugging_GetStateFromObject(PyObject *obj)
{
RemoteUnwinderObject *unwinder = (RemoteUnwinderObject *)obj;
if (unwinder->cached_state == NULL) {
unwinder->cached_state = RemoteDebugging_GetStateFromType(Py_TYPE(obj));
}
return unwinder->cached_state;
}
int
RemoteDebugging_InitState(RemoteDebuggingState *st)
{
return 0;
}
int
is_prerelease_version(uint64_t version)
{
return (version & 0xF0) != 0xF0;
}
int
validate_debug_offsets(struct _Py_DebugOffsets *debug_offsets)
{
if (memcmp(debug_offsets->cookie, _Py_Debug_Cookie, sizeof(debug_offsets->cookie)) != 0) {
// The remote is probably running a Python version predating debug offsets.
PyErr_SetString(
PyExc_RuntimeError,
"Can't determine the Python version of the remote process");
return -1;
}
// Assume debug offsets could change from one pre-release version to another,
// or one minor version to another, but are stable across patch versions.
if (is_prerelease_version(Py_Version) && Py_Version != debug_offsets->version) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't attach from a pre-release Python interpreter"
" to a process running a different Python version");
return -1;
}
if (is_prerelease_version(debug_offsets->version) && Py_Version != debug_offsets->version) {
PyErr_SetString(
PyExc_RuntimeError,
"Can't attach to a pre-release Python interpreter"
" from a process running a different Python version");
return -1;
}
unsigned int remote_major = (debug_offsets->version >> 24) & 0xFF;
unsigned int remote_minor = (debug_offsets->version >> 16) & 0xFF;
if (PY_MAJOR_VERSION != remote_major || PY_MINOR_VERSION != remote_minor) {
PyErr_Format(
PyExc_RuntimeError,
"Can't attach from a Python %d.%d process to a Python %d.%d process",
PY_MAJOR_VERSION, PY_MINOR_VERSION, remote_major, remote_minor);
return -1;
}
// The debug offsets differ between free threaded and non-free threaded builds.
if (_Py_Debug_Free_Threaded && !debug_offsets->free_threaded) {
PyErr_SetString(
PyExc_RuntimeError,
"Cannot attach from a free-threaded Python process"
" to a process running a non-free-threaded version");
return -1;
}
if (!_Py_Debug_Free_Threaded && debug_offsets->free_threaded) {
PyErr_SetString(
PyExc_RuntimeError,
"Cannot attach to a free-threaded Python process"
" from a process running a non-free-threaded version");
return -1;
}
return _PyRemoteDebug_ValidateDebugOffsetsLayout(debug_offsets);
}
/* ============================================================================
* REMOTEUNWINDER CLASS IMPLEMENTATION
* ============================================================================ */
/*[clinic input]
module _remote_debugging
class _remote_debugging.RemoteUnwinder "RemoteUnwinderObject *" "&RemoteUnwinder_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=12b4dce200381115]*/
/*[clinic input]
@permit_long_summary
@permit_long_docstring_body
_remote_debugging.RemoteUnwinder.__init__
pid: int
*
all_threads: bool = False
only_active_thread: bool = False
mode: int = 0
debug: bool = False
skip_non_matching_threads: bool = True
native: bool = False
gc: bool = False
opcodes: bool = False
cache_frames: bool = False
stats: bool = False
Initialize a new RemoteUnwinder object for debugging a remote Python process.
Args:
pid: Process ID of the target Python process to debug
all_threads: If True, initialize state for all threads in the process.
If False, only initialize for the main thread.
only_active_thread: If True, only sample the thread holding the GIL.
mode: Profiling mode: 0=WALL (wall-time), 1=CPU (cpu-time), 2=GIL (gil-time).
Cannot be used together with all_threads=True.
debug: If True, chain exceptions to explain the sequence of events that
lead to the exception.
skip_non_matching_threads: If True, skip threads that don't match the selected mode.
If False, include all threads regardless of mode.
native: If True, include artificial "<native>" frames to denote calls to
non-Python code.
gc: If True, include artificial "<GC>" frames to denote active garbage
collection.
opcodes: If True, gather bytecode opcode information for instruction-level
profiling.
cache_frames: If True, enable frame caching optimization to avoid re-reading
unchanged parent frames between samples.
stats: If True, collect statistics about cache hits, memory reads, etc.
Use get_stats() to retrieve the collected statistics.
The RemoteUnwinder provides functionality to inspect and debug a running Python
process, including examining thread states, stack frames and other runtime data.
Raises:
PermissionError: If access to the target process is denied
OSError: If unable to attach to the target process or access its memory
RuntimeError: If unable to read debug information from the target process
ValueError: If both all_threads and only_active_thread are True
[clinic start generated code]*/
static int
_remote_debugging_RemoteUnwinder___init___impl(RemoteUnwinderObject *self,
int pid, int all_threads,
int only_active_thread,
int mode, int debug,
int skip_non_matching_threads,
int native, int gc,
int opcodes, int cache_frames,
int stats)
/*[clinic end generated code: output=0031f743f4b9ad52 input=8fb61b24102dec6e]*/
{
// Validate that all_threads and only_active_thread are not both True
if (all_threads && only_active_thread) {
PyErr_SetString(PyExc_ValueError,
"all_threads and only_active_thread cannot both be True");
return -1;
}
#ifdef Py_GIL_DISABLED
if (only_active_thread) {
PyErr_SetString(PyExc_ValueError,
"only_active_thread is not supported in free-threaded builds");
return -1;
}
#endif
self->native = native;
self->gc = gc;
self->opcodes = opcodes;
self->cache_frames = cache_frames;
self->collect_stats = stats;
self->stale_invalidation_counter = 0;
self->cached_tstate_interpreter_addr = 0;
self->cached_tstate_addr = 0;
memset(self->cached_tstates, 0, sizeof(self->cached_tstates));
memset(self->cached_generations, 0, sizeof(self->cached_generations));
self->debug = debug;
self->only_active_thread = only_active_thread;
self->mode = mode;
self->skip_non_matching_threads = skip_non_matching_threads;
self->cached_state = NULL;
self->frame_cache = NULL;
#ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING
self->threads_stopped = 0;
#endif
// Initialize stats to zero
memset(&self->stats, 0, sizeof(self->stats));
if (_Py_RemoteDebug_InitProcHandle(&self->handle, pid) < 0) {
set_exception_cause(self, PyExc_RuntimeError, "Failed to initialize process handle");
return -1;
}
self->runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(&self->handle);
if (self->runtime_start_address == 0) {
set_exception_cause(self, PyExc_RuntimeError, "Failed to get Python runtime address");
return -1;
}
if (_Py_RemoteDebug_ReadDebugOffsets(&self->handle,
&self->runtime_start_address,
&self->debug_offsets) < 0)
{
set_exception_cause(self, PyExc_RuntimeError, "Failed to read debug offsets");
return -1;
}
// Validate that the debug offsets are valid
if (validate_debug_offsets(&self->debug_offsets) == -1) {
set_exception_cause(self, PyExc_RuntimeError, "Invalid debug offsets found");
return -1;
}
// Try to read async debug offsets, but don't fail if they're not available
self->async_debug_offsets_available = 1;
int async_debug_result = read_async_debug(self);
if (async_debug_result == PY_REMOTE_DEBUG_INVALID_ASYNC_DEBUG_OFFSETS) {
return -1;
}
if (async_debug_result < 0) {
PyErr_Clear();
memset(&self->async_debug_offsets, 0, sizeof(self->async_debug_offsets));
self->async_debug_offsets_available = 0;
}
if (populate_initial_state_data(all_threads, self, self->runtime_start_address,
&self->interpreter_addr ,&self->tstate_addr) < 0)
{
set_exception_cause(self, PyExc_RuntimeError, "Failed to populate initial state data");
return -1;
}
self->code_object_cache = _Py_hashtable_new_full(
_Py_hashtable_hash_ptr,
_Py_hashtable_compare_direct,
NULL, // keys are stable pointers, don't destroy
cached_code_metadata_destroy,
NULL
);
if (self->code_object_cache == NULL) {
PyErr_NoMemory();
set_exception_cause(self, PyExc_MemoryError, "Failed to create code object cache");
return -1;
}
#ifdef Py_GIL_DISABLED
// Initialize TLBC cache
self->tlbc_generation = 0;
self->tlbc_cache = _Py_hashtable_new_full(
_Py_hashtable_hash_ptr,
_Py_hashtable_compare_direct,
NULL, // keys are stable pointers, don't destroy
tlbc_cache_entry_destroy,
NULL
);
if (self->tlbc_cache == NULL) {
_Py_hashtable_destroy(self->code_object_cache);
PyErr_NoMemory();
set_exception_cause(self, PyExc_MemoryError, "Failed to create TLBC cache");
return -1;
}
#endif
#if defined(__APPLE__)
self->thread_id_offset = 0;
self->thread_id_offset_initialized = 0;
#endif
#ifdef MS_WINDOWS
self->win_process_buffer = NULL;
self->win_process_buffer_size = 0;
#endif
#ifdef __linux__
self->thread_tids = NULL;
self->thread_tids_capacity = 0;
#endif
if (cache_frames && frame_cache_init(self) < 0) {
return -1;
}
// Clear stale last_profiled_frame values from previous profilers
// This prevents us from stopping frame walking early due to stale values
if (cache_frames) {
clear_last_profiled_frames(self);
}
return 0;
}
static inline size_t
interpreter_thread_cache_index(uintptr_t interpreter_addr)
{
// Direct-mapped table indexed by the remote interpreter address. Each entry
// stores the full address and verifies it on lookup, so hash collisions
// degrade to misses and cannot return a value from the wrong interpreter.
return (size_t)_Py_HashPointerRaw((const void *)interpreter_addr)
& (INTERPRETER_THREAD_CACHE_SIZE - 1);
}
static inline uintptr_t
get_cached_tstate_for_interpreter(
RemoteUnwinderObject *self,
uintptr_t interpreter_addr)
{
if (interpreter_addr == 0) {
return 0;
}
if (self->cached_tstate_interpreter_addr == interpreter_addr) {
return self->cached_tstate_addr;
}
InterpreterTstateCacheEntry *entry =
&self->cached_tstates[interpreter_thread_cache_index(interpreter_addr)];
if (entry->interpreter_addr == interpreter_addr) {
self->cached_tstate_interpreter_addr = interpreter_addr;
self->cached_tstate_addr = entry->thread_state_addr;
return entry->thread_state_addr;
}
return 0;
}
static inline void
set_cached_tstate_for_interpreter(
RemoteUnwinderObject *self,
uintptr_t interpreter_addr,
uintptr_t thread_state_addr)
{
if (interpreter_addr == 0 || thread_state_addr == 0) {
return;
}
self->cached_tstate_interpreter_addr = interpreter_addr;
self->cached_tstate_addr = thread_state_addr;
InterpreterTstateCacheEntry *entry =
&self->cached_tstates[interpreter_thread_cache_index(interpreter_addr)];
entry->interpreter_addr = interpreter_addr;
entry->thread_state_addr = thread_state_addr;
}
static void
refresh_generation_caches_from_interp_state(
RemoteUnwinderObject *self,
uintptr_t interpreter_addr,
const char *interp_state_buffer)
{
uint64_t code_object_generation = GET_MEMBER(uint64_t, interp_state_buffer,
self->debug_offsets.interpreter_state.code_object_generation);
if (self->cached_generation_interpreter_addr == interpreter_addr) {
if (code_object_generation != self->cached_code_object_generation) {
self->cached_code_object_generation = code_object_generation;
_Py_hashtable_clear(self->code_object_cache);
}
}
else {
InterpreterGenerationCacheEntry *entry =
&self->cached_generations[interpreter_thread_cache_index(interpreter_addr)];
// A slot rebound from another interpreter must be treated as changed:
// the code_object_cache is global, so even if the new generation
// numerically matches what the previous occupant had, stale entries
// from that occupant could still be served.
int changed = entry->interpreter_addr != interpreter_addr
|| entry->code_object_generation != code_object_generation;
entry->interpreter_addr = interpreter_addr;
entry->code_object_generation = code_object_generation;
if (changed) {
_Py_hashtable_clear(self->code_object_cache);
}
self->cached_generation_interpreter_addr = interpreter_addr;
self->cached_code_object_generation = code_object_generation;
}
#ifdef Py_GIL_DISABLED
uint32_t current_tlbc_generation = GET_MEMBER(uint32_t, interp_state_buffer,
self->debug_offsets.interpreter_state.tlbc_generation);
if (current_tlbc_generation != self->tlbc_generation) {
self->tlbc_generation = current_tlbc_generation;
_Py_hashtable_clear(self->tlbc_cache);
}
#endif
}
static int
refresh_generation_caches_for_interpreter(
RemoteUnwinderObject *self,
uintptr_t interpreter_addr)
{
char interp_state_buffer[INTERP_STATE_BUFFER_SIZE];
if (_Py_RemoteDebug_ReadRemoteMemory(
&self->handle,
interpreter_addr,
INTERP_STATE_BUFFER_SIZE,
interp_state_buffer) < 0) {
set_exception_cause(self, PyExc_RuntimeError,
"Failed to read interpreter state buffer");
return -1;
}
refresh_generation_caches_from_interp_state(self, interpreter_addr, interp_state_buffer);
return 0;
}
static int
read_interp_state_and_maybe_thread_frame(
RemoteUnwinderObject *unwinder,
uintptr_t interpreter_addr,
char *interp_state_buffer,
char *tstate_buffer,
char *frame_buffer,
RemoteReadPrefetch *prefetch)
{
prefetch->tstate = NULL;
prefetch->frame = NULL;
if (prefetch->tstate_addr != 0) {
size_t tstate_size = (size_t)unwinder->debug_offsets.thread_state.size;
_Py_RemoteReadSegment segments[3] = {
{interpreter_addr, interp_state_buffer, INTERP_STATE_BUFFER_SIZE},
{prefetch->tstate_addr, tstate_buffer, tstate_size},
{prefetch->frame_addr, frame_buffer, SIZEOF_INTERP_FRAME},
};
int nsegs = prefetch->frame_addr != 0 ? 3 : 2;
Py_ssize_t nread = _Py_RemoteDebug_BatchedReadRemoteMemory(
&unwinder->handle, segments, nsegs);
int completed = 0;
if (nread >= (Py_ssize_t)INTERP_STATE_BUFFER_SIZE) {
completed = 1;
Py_ssize_t with_tstate = (Py_ssize_t)INTERP_STATE_BUFFER_SIZE
+ (Py_ssize_t)tstate_size;
if (nread >= with_tstate) {
completed = 2;
}
if (nsegs == 3
&& nread == with_tstate + (Py_ssize_t)SIZEOF_INTERP_FRAME) {
completed = 3;
}
}
STATS_BATCHED_READ(unwinder, nsegs, completed);
if (completed >= 1) {
if (completed >= 2) {
prefetch->tstate = tstate_buffer;
}
if (completed >= 3) {
prefetch->frame = frame_buffer;
}
return 0;
}
}
return _Py_RemoteDebug_ReadRemoteMemory(
&unwinder->handle,
interpreter_addr,
INTERP_STATE_BUFFER_SIZE,
interp_state_buffer);
}
/*[clinic input]
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.get_stack_trace
Returns stack traces for all interpreters and threads in process.
Each element in the returned list is a tuple of (interpreter_id, thread_list), where:
- interpreter_id is the interpreter identifier
- thread_list is a list of tuples (thread_id, frame_list) for threads in that interpreter
- thread_id is the OS thread identifier
- frame_list is a list of tuples (function_name, filename, line_number) representing
the Python stack frames for that thread, ordered from most recent to oldest
The threads returned depend on the initialization parameters:
- If only_active_thread was True: returns only the thread holding the GIL across all interpreters
- If all_threads was True: returns all threads across all interpreters
- Otherwise: returns only the main thread of each interpreter
Example:
[
(0, [ # Main interpreter
(1234, [
('process_data', 'worker.py', 127),
('run_worker', 'worker.py', 45),
('main', 'app.py', 23)
]),
(1235, [
('handle_request', 'server.py', 89),
('serve_forever', 'server.py', 52)
])
]),
(1, [ # Sub-interpreter
(1236, [
('sub_worker', 'sub.py', 15)
])
])
]
Raises:
RuntimeError: If there is an error copying memory from the target process
OSError: If there is an error accessing the target process
PermissionError: If access to the target process is denied
UnicodeDecodeError: If there is an error decoding strings from the target process
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_get_stack_trace_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=666192b90c69d567 input=bcff01c73cccc1c0]*/
{
STATS_INC(self, total_samples);
PyObject* result = PyList_New(0);
if (!result) {
set_exception_cause(self, PyExc_MemoryError, "Failed to create stack trace result list");
return NULL;
}
// Iterate over all interpreters
uintptr_t current_interpreter = self->interpreter_addr;
while (current_interpreter != 0) {
// Read interpreter state to get the interpreter ID
char interp_state_buffer[INTERP_STATE_BUFFER_SIZE];
char prefetched_tstate[SIZEOF_THREAD_STATE];
char prefetched_frame[SIZEOF_INTERP_FRAME];
RemoteReadPrefetch prefetch = {0};
if (self->cache_frames) {
prefetch.tstate_addr = get_cached_tstate_for_interpreter(
self, current_interpreter);
}
if (prefetch.tstate_addr != 0) {
FrameCacheEntry *entry = frame_cache_find_by_tstate(self, prefetch.tstate_addr);
if (entry && entry->num_addrs > 0) {
prefetch.frame_addr = entry->addrs[0];
}
}
if (read_interp_state_and_maybe_thread_frame(
self,
current_interpreter,
interp_state_buffer,
prefetched_tstate,
prefetched_frame,
&prefetch) < 0) {
set_exception_cause(self, PyExc_RuntimeError, "Failed to read interpreter state buffer");
Py_CLEAR(result);
goto exit;
}
refresh_generation_caches_from_interp_state(self, current_interpreter, interp_state_buffer);
uintptr_t gc_frame = 0;
if (self->gc) {
gc_frame = GET_MEMBER(uintptr_t, interp_state_buffer,
self->debug_offsets.interpreter_state.gc
+ self->debug_offsets.gc.frame);
}
int64_t interpreter_id = GET_MEMBER(int64_t, interp_state_buffer,
self->debug_offsets.interpreter_state.id);
// Create a list to hold threads for this interpreter
PyObject *interpreter_threads = PyList_New(0);
if (!interpreter_threads) {
set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter threads list");
Py_CLEAR(result);
goto exit;
}
// Get the GIL holder for this interpreter (needed for GIL_WAIT logic)
uintptr_t gil_holder_tstate = 0;
int gil_locked = GET_MEMBER(int, interp_state_buffer,
self->debug_offsets.interpreter_state.gil_runtime_state_locked);
if (gil_locked) {
gil_holder_tstate = (uintptr_t)GET_MEMBER(PyThreadState*, interp_state_buffer,
self->debug_offsets.interpreter_state.gil_runtime_state_holder);
}
uintptr_t current_tstate;
if (self->only_active_thread) {
// Find the GIL holder for THIS interpreter
if (!gil_locked) {
// This interpreter's GIL is not locked, skip it
Py_DECREF(interpreter_threads);
goto next_interpreter;
}
current_tstate = gil_holder_tstate;
} else if (self->tstate_addr == 0) {
// Get all threads for this interpreter
current_tstate = GET_MEMBER(uintptr_t, interp_state_buffer,
self->debug_offsets.interpreter_state.threads_head);
} else {
// Target specific thread (only process first interpreter)
current_tstate = self->tstate_addr;
}
if (current_tstate != 0 && self->cache_frames) {
set_cached_tstate_for_interpreter(self, current_interpreter, current_tstate);
}
// Acquire main thread state information
uintptr_t main_thread_tstate = GET_MEMBER(uintptr_t, interp_state_buffer,
self->debug_offsets.interpreter_state.threads_main);
while (current_tstate != 0) {
uintptr_t prev_tstate = current_tstate;
PyObject* frame_info = unwind_stack_for_thread(self, &current_tstate,
gil_holder_tstate,
gc_frame,
main_thread_tstate,
&prefetch);
if (!frame_info) {
// Check if this was an intentional skip due to mode-based filtering
if ((self->mode == PROFILING_MODE_CPU || self->mode == PROFILING_MODE_GIL ||
self->mode == PROFILING_MODE_EXCEPTION) && !PyErr_Occurred()) {
// Detect cycle: if current_tstate didn't advance, we have corrupted data
if (current_tstate == prev_tstate) {
Py_DECREF(interpreter_threads);
PyErr_Format(PyExc_RuntimeError,
"Thread list cycle detected at address 0x%lx (corrupted remote memory)",
current_tstate);
set_exception_cause(self, PyExc_RuntimeError,
"Thread list cycle detected (corrupted remote memory)");
Py_CLEAR(result);
goto exit;
}
// Thread was skipped due to mode filtering, continue to next thread
continue;
}
// This was an actual error
Py_DECREF(interpreter_threads);
set_exception_cause(self, PyExc_RuntimeError, "Failed to unwind stack for thread");
Py_CLEAR(result);
goto exit;
}
if (PyList_Append(interpreter_threads, frame_info) == -1) {
Py_DECREF(frame_info);
Py_DECREF(interpreter_threads);
set_exception_cause(self, PyExc_RuntimeError, "Failed to append thread frame info");
Py_CLEAR(result);
goto exit;
}
Py_DECREF(frame_info);
// If targeting specific thread or only active thread, process just one
if (self->tstate_addr || self->only_active_thread) {
break;
}
}
// Create the InterpreterInfo StructSequence
RemoteDebuggingState *state = RemoteDebugging_GetStateFromObject((PyObject*)self);
PyObject *interpreter_info = PyStructSequence_New(state->InterpreterInfo_Type);
if (!interpreter_info) {
Py_DECREF(interpreter_threads);
set_exception_cause(self, PyExc_MemoryError, "Failed to create InterpreterInfo");
Py_CLEAR(result);
goto exit;
}
PyObject *interp_id = PyLong_FromLongLong(interpreter_id);
if (!interp_id) {
Py_DECREF(interpreter_threads);
Py_DECREF(interpreter_info);
set_exception_cause(self, PyExc_MemoryError, "Failed to create interpreter ID");
Py_CLEAR(result);
goto exit;
}
PyStructSequence_SetItem(interpreter_info, 0, interp_id); // steals reference
PyStructSequence_SetItem(interpreter_info, 1, interpreter_threads); // steals reference
// Add this interpreter to the result list
if (PyList_Append(result, interpreter_info) == -1) {
Py_DECREF(interpreter_info);
set_exception_cause(self, PyExc_RuntimeError, "Failed to append interpreter info");
Py_CLEAR(result);
goto exit;
}
Py_DECREF(interpreter_info);
next_interpreter:
// Get the next interpreter address
current_interpreter = GET_MEMBER(uintptr_t, interp_state_buffer,
self->debug_offsets.interpreter_state.next);
// If we're targeting a specific thread, stop after first interpreter
if (self->tstate_addr != 0) {
break;
}
}
exit:
// Invalidate cache entries for threads not seen in this sample.
// Only do this every 1024 iterations to avoid performance overhead.
if (self->cache_frames && result) {
if (++self->stale_invalidation_counter >= 1024) {
self->stale_invalidation_counter = 0;
frame_cache_invalidate_stale(self, result);
}
}
_Py_RemoteDebug_ClearCache(&self->handle);
return result;
}
/*[clinic input]
@permit_long_summary
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.get_all_awaited_by
Get all tasks and their awaited_by relationships from the remote process.
This provides a tree structure showing which tasks are waiting for other tasks.
For each task, returns:
1. The call stack frames leading to where the task is currently executing
2. The name of the task
3. A list of tasks that this task is waiting for, with their own frames/names/etc
Returns a list of [frames, task_name, subtasks] where:
- frames: List of (func_name, filename, lineno) showing the call stack
- task_name: String identifier for the task
- subtasks: List of tasks being awaited by this task, in same format
Raises:
RuntimeError: If AsyncioDebug section is not available in the remote process
MemoryError: If memory allocation fails
OSError: If reading from the remote process fails
Example output:
[
# Task c2_root waiting for two subtasks
[
# Call stack of c2_root
[("c5", "script.py", 10), ("c4", "script.py", 14)],
"c2_root",
[
# First subtask (sub_main_2) and what it's waiting for
[
[("c1", "script.py", 23)],
"sub_main_2",
[...]
],
# Second subtask and its waiters
[...]
]
]
]
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_get_all_awaited_by_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=6a49cd345e8aec53 input=307f754cbe38250c]*/
{
if (ensure_async_debug_offsets(self) < 0) {
return NULL;
}
if (refresh_generation_caches_for_interpreter(self, self->interpreter_addr) < 0) {
return NULL;
}
PyObject *result = PyList_New(0);
if (result == NULL) {
set_exception_cause(self, PyExc_MemoryError, "Failed to create awaited_by result list");
goto result_err;
}
// Process all threads
if (iterate_threads(self, process_thread_for_awaited_by, result) < 0) {
goto result_err;
}
uintptr_t head_addr = self->interpreter_addr
+ (uintptr_t)self->async_debug_offsets.asyncio_interpreter_state.asyncio_tasks_head;
// On top of a per-thread task lists used by default by asyncio to avoid
// contention, there is also a fallback per-interpreter list of tasks;
// any tasks still pending when a thread is destroyed will be moved to the
// per-interpreter task list. It's unlikely we'll find anything here, but
// interesting for debugging.
if (append_awaited_by(self, 0, head_addr, result))
{
set_exception_cause(self, PyExc_RuntimeError, "Failed to append interpreter awaited_by in get_all_awaited_by");
goto result_err;
}
_Py_RemoteDebug_ClearCache(&self->handle);
return result;
result_err:
_Py_RemoteDebug_ClearCache(&self->handle);
Py_XDECREF(result);
return NULL;
}
/*[clinic input]
@permit_long_summary
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.get_async_stack_trace
Get the currently running async tasks and their dependency graphs from the remote process.
This returns information about running tasks and all tasks that are waiting for them,
forming a complete dependency graph for each thread's active task.
For each thread with a running task, returns the running task plus all tasks that
transitively depend on it (tasks waiting for the running task, tasks waiting for
those tasks, etc.).
Returns a list of per-thread results, where each thread result contains:
- Thread ID
- List of task information for the running task and all its waiters
Each task info contains:
- Task ID (memory address)
- Task name
- Call stack frames: List of (func_name, filename, lineno)
- List of tasks waiting for this task (recursive structure)
Raises:
RuntimeError: If AsyncioDebug section is not available in the target process
MemoryError: If memory allocation fails
OSError: If reading from the remote process fails
Example output (similar structure to get_all_awaited_by but only for running tasks):
[
# Thread 140234 results
(140234, [
# Running task and its complete waiter dependency graph
(4345585712, 'main_task',
[("run_server", "server.py", 127), ("main", "app.py", 23)],
[
# Tasks waiting for main_task
(4345585800, 'worker_1', [...], [...]),
(4345585900, 'worker_2', [...], [...])
])
])
]
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_get_async_stack_trace_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=6433d52b55e87bbe input=6129b7d509a887c9]*/
{
if (ensure_async_debug_offsets(self) < 0) {
return NULL;
}
if (refresh_generation_caches_for_interpreter(self, self->interpreter_addr) < 0) {
return NULL;
}
PyObject *result = PyList_New(0);
if (result == NULL) {
set_exception_cause(self, PyExc_MemoryError, "Failed to create result list in get_async_stack_trace");
return NULL;
}
// Process all threads
if (iterate_threads(self, process_thread_for_async_stack_trace, result) < 0) {
goto result_err;
}
_Py_RemoteDebug_ClearCache(&self->handle);
return result;
result_err:
_Py_RemoteDebug_ClearCache(&self->handle);
Py_XDECREF(result);
return NULL;
}
/*[clinic input]
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.get_stats
Get collected statistics about profiling performance.
Returns a dictionary containing statistics about cache performance,
memory reads, and other profiling metrics. Only available if the
RemoteUnwinder was created with stats=True.
Returns:
dict: A dictionary containing:
- total_samples: Total number of get_stack_trace calls
- frame_cache_hits: Full cache hits (entire stack unchanged)
- frame_cache_misses: Cache misses requiring full walk
- frame_cache_partial_hits: Partial hits (stopped at cached frame)
- frames_read_from_cache: Total frames retrieved from cache
- frames_read_from_memory: Total frames read from remote memory
- memory_reads: Total remote memory read operations
- memory_bytes_read: Total bytes read from remote memory
- code_object_cache_hits: Code object cache hits
- code_object_cache_misses: Code object cache misses
- stale_cache_invalidations: Times stale cache entries were cleared
- batched_read_attempts: Batched remote-read attempts
- batched_read_successes: Attempts that read all requested segments
- batched_read_misses: Attempts that fell back or partially read
- batched_read_segments_requested: Segments requested by batched reads
- batched_read_segments_completed: Segments completed by batched reads
- frame_cache_hit_rate: Percentage of samples that hit the cache
- code_object_cache_hit_rate: Percentage of code object lookups that hit cache
- batched_read_success_rate: Percentage of batched reads that completed all segments
- batched_read_segment_completion_rate: Percentage of requested segments read by batched reads
Raises:
RuntimeError: If stats collection was not enabled (stats=False)
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_get_stats_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=21e36477122be2a0 input=0392d62b278e9c35]*/
{
if (!self->collect_stats) {
PyErr_SetString(PyExc_RuntimeError,
"Statistics collection was not enabled. "
"Create RemoteUnwinder with stats=True to collect statistics.");
return NULL;
}
PyObject *result = PyDict_New();
if (!result) {
return NULL;
}
#define ADD_STAT(name) do { \
PyObject *val = PyLong_FromUnsignedLongLong(self->stats.name); \
if (!val || PyDict_SetItemString(result, #name, val) < 0) { \
Py_XDECREF(val); \
Py_DECREF(result); \
return NULL; \
} \
Py_DECREF(val); \
} while(0)
ADD_STAT(total_samples);
ADD_STAT(frame_cache_hits);
ADD_STAT(frame_cache_misses);
ADD_STAT(frame_cache_partial_hits);
ADD_STAT(frames_read_from_cache);
ADD_STAT(frames_read_from_memory);
ADD_STAT(memory_reads);
ADD_STAT(memory_bytes_read);
ADD_STAT(code_object_cache_hits);
ADD_STAT(code_object_cache_misses);
ADD_STAT(stale_cache_invalidations);
ADD_STAT(batched_read_attempts);
ADD_STAT(batched_read_successes);
ADD_STAT(batched_read_misses);
ADD_STAT(batched_read_segments_requested);
ADD_STAT(batched_read_segments_completed);
#undef ADD_STAT
#define ADD_DERIVED_STAT(name, value) do { \
PyObject *val = PyFloat_FromDouble(value); \
if (!val || PyDict_SetItemString(result, name, val) < 0) { \
Py_XDECREF(val); \
Py_DECREF(result); \
return NULL; \
} \
Py_DECREF(val); \
} while(0)
// Calculate and add derived statistics
// Hit rate is calculated as (hits + partial_hits) / total_cache_lookups
double frame_cache_hit_rate = 0.0;
uint64_t total_cache_lookups = self->stats.frame_cache_hits + self->stats.frame_cache_partial_hits + self->stats.frame_cache_misses;
if (total_cache_lookups > 0) {
frame_cache_hit_rate = 100.0 * (double)(self->stats.frame_cache_hits + self->stats.frame_cache_partial_hits)
/ (double)total_cache_lookups;
}
ADD_DERIVED_STAT("frame_cache_hit_rate", frame_cache_hit_rate);
double code_object_hit_rate = 0.0;
uint64_t total_code_lookups = self->stats.code_object_cache_hits + self->stats.code_object_cache_misses;
if (total_code_lookups > 0) {
code_object_hit_rate = 100.0 * (double)self->stats.code_object_cache_hits / (double)total_code_lookups;
}
ADD_DERIVED_STAT("code_object_cache_hit_rate", code_object_hit_rate);
double batched_read_success_rate = 0.0;
if (self->stats.batched_read_attempts > 0) {
batched_read_success_rate =
100.0 * (double)self->stats.batched_read_successes
/ (double)self->stats.batched_read_attempts;
}
ADD_DERIVED_STAT("batched_read_success_rate", batched_read_success_rate);
double batched_read_segment_completion_rate = 0.0;
if (self->stats.batched_read_segments_requested > 0) {
batched_read_segment_completion_rate =
100.0 * (double)self->stats.batched_read_segments_completed
/ (double)self->stats.batched_read_segments_requested;
}
ADD_DERIVED_STAT("batched_read_segment_completion_rate",
batched_read_segment_completion_rate);
#undef ADD_DERIVED_STAT
return result;
}
/*[clinic input]
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.pause_threads
Pause all threads in the target process.
This stops all threads in the target process to allow for consistent
memory reads during sampling. Must be paired with a call to resume_threads().
Returns True if threads were successfully paused, False if they were already paused.
Raises:
RuntimeError: If there is an error stopping the threads
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_pause_threads_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=aaf2bdc0a725750c input=d8a266f19a81c67e]*/
{
#ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING
if (self->threads_stopped) {
Py_RETURN_FALSE;
}
_Py_RemoteDebug_InitThreadsState(self, &self->threads_state);
if (_Py_RemoteDebug_StopAllThreads(self, &self->threads_state) < 0) {
return NULL;
}
self->threads_stopped = 1;
Py_RETURN_TRUE;
#else
PyErr_SetString(PyExc_NotImplementedError,
"pause_threads is not supported on this platform");
return NULL;
#endif
}
/*[clinic input]
@permit_long_docstring_body
@critical_section
_remote_debugging.RemoteUnwinder.resume_threads
Resume all threads in the target process.
This resumes threads that were previously paused with pause_threads().
Returns True if threads were successfully resumed, False if they were not paused.
[clinic start generated code]*/
static PyObject *
_remote_debugging_RemoteUnwinder_resume_threads_impl(RemoteUnwinderObject *self)
/*[clinic end generated code: output=8d6781ea37095536 input=16baaaab007f4259]*/
{
#ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING
if (!self->threads_stopped) {
Py_RETURN_FALSE;
}
_Py_RemoteDebug_ResumeAllThreads(self, &self->threads_state);
self->threads_stopped = 0;
Py_RETURN_TRUE;
#else
PyErr_SetString(PyExc_NotImplementedError,
"resume_threads is not supported on this platform");
return NULL;
#endif
}
static PyMethodDef RemoteUnwinder_methods[] = {
_REMOTE_DEBUGGING_REMOTEUNWINDER_GET_STACK_TRACE_METHODDEF
_REMOTE_DEBUGGING_REMOTEUNWINDER_GET_ALL_AWAITED_BY_METHODDEF
_REMOTE_DEBUGGING_REMOTEUNWINDER_GET_ASYNC_STACK_TRACE_METHODDEF
_REMOTE_DEBUGGING_REMOTEUNWINDER_GET_STATS_METHODDEF
_REMOTE_DEBUGGING_REMOTEUNWINDER_PAUSE_THREADS_METHODDEF
_REMOTE_DEBUGGING_REMOTEUNWINDER_RESUME_THREADS_METHODDEF
{NULL, NULL}
};
static void
RemoteUnwinder_dealloc(PyObject *op)
{
RemoteUnwinderObject *self = RemoteUnwinder_CAST(op);
PyTypeObject *tp = Py_TYPE(self);
#ifdef Py_REMOTE_DEBUG_SUPPORTS_BLOCKING
if (self->threads_stopped) {
_Py_RemoteDebug_ResumeAllThreads(self, &self->threads_state);
self->threads_stopped = 0;
}
#endif
#ifdef __linux__
if (self->thread_tids != NULL) {
PyMem_RawFree(self->thread_tids);
self->thread_tids = NULL;
}
#endif
if (self->code_object_cache) {
_Py_hashtable_destroy(self->code_object_cache);
}
#ifdef MS_WINDOWS
if (self->win_process_buffer != NULL) {
PyMem_Free(self->win_process_buffer);
}
#endif
#ifdef Py_GIL_DISABLED
if (self->tlbc_cache) {
_Py_hashtable_destroy(self->tlbc_cache);
}
#endif
if (self->handle.pid != 0) {
_Py_RemoteDebug_ClearCache(&self->handle);
_Py_RemoteDebug_CleanupProcHandle(&self->handle);
}
frame_cache_cleanup(self);
PyObject_Del(self);
Py_DECREF(tp);
}
static PyType_Slot RemoteUnwinder_slots[] = {
{Py_tp_doc, (void *)"RemoteUnwinder(pid): Inspect stack of a remote Python process."},
{Py_tp_methods, RemoteUnwinder_methods},
{Py_tp_init, _remote_debugging_RemoteUnwinder___init__},
{Py_tp_dealloc, RemoteUnwinder_dealloc},
{0, NULL}
};
static PyType_Spec RemoteUnwinder_spec = {
.name = "_remote_debugging.RemoteUnwinder",
.basicsize = sizeof(RemoteUnwinderObject),
.flags = (
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_IMMUTABLETYPE
),
.slots = RemoteUnwinder_slots,
};
/* ============================================================================
* GCMONITOR CLASS IMPLEMENTATION
* ============================================================================ */
static void
cleanup_runtime_offsets(RuntimeOffsets *offsets)
{
if (offsets->handle.pid != 0) {
_Py_RemoteDebug_ClearCache(&offsets->handle);
_Py_RemoteDebug_CleanupProcHandle(&offsets->handle);
}
}
static int
init_runtime_offsets(RuntimeOffsets *offsets, int pid, int debug)
{
offsets->debug = debug;
if (_Py_RemoteDebug_InitProcHandle(&offsets->handle, pid) < 0) {
set_exception_cause(offsets, PyExc_RuntimeError, "Failed to initialize process handle");
return -1;
}
offsets->runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(&offsets->handle);
if (offsets->runtime_start_address == 0) {
set_exception_cause(offsets, PyExc_RuntimeError, "Failed to get Python runtime address");
goto error;
}
if (_Py_RemoteDebug_ReadDebugOffsets(&offsets->handle,
&offsets->runtime_start_address,
&offsets->debug_offsets) < 0)
{
set_exception_cause(offsets, PyExc_RuntimeError, "Failed to read debug offsets");
goto error;
}
if (validate_debug_offsets(&offsets->debug_offsets) == -1) {
set_exception_cause(offsets, PyExc_RuntimeError, "Invalid debug offsets found");
goto error;
}
return 0;
error:
cleanup_runtime_offsets(offsets);
return -1;
}
/*[clinic input]
class _remote_debugging.GCMonitor "GCMonitorObject *" "&GCMonitor_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ebc229325a5e5154]*/
/*[clinic input]
@permit_long_summary
@permit_long_docstring_body
_remote_debugging.GCMonitor.__init__
pid: int
*
debug: bool = False
Initialize a new GCMonitor object for monitoring GC events from remote process.
Args:
pid: Process ID of the target Python process to monitor
debug: If True, chain exceptions to explain the sequence of events that
lead to the exception.
The GCMonitor provides functionality to read GC statistics from a running
Python process.
Raises:
PermissionError: If access to the target process is denied
OSError: If unable to attach to the target process or access its memory
RuntimeError: If unable to read debug information from the target process
[clinic start generated code]*/
static int
_remote_debugging_GCMonitor___init___impl(GCMonitorObject *self, int pid,
int debug)
/*[clinic end generated code: output=2cdf351c2f6335db input=1185a48535b808be]*/
{
return init_runtime_offsets(&self->offsets, pid, debug);
}
/*[clinic input]
@critical_section
_remote_debugging.GCMonitor.get_gc_stats
all_interpreters: bool = False
If True, return GC statistics from all interpreters.
If False, return only from main interpreter.
Get garbage collector statistics from external Python process.
Returns a list of GCStatsInfo objects with GC statistics data.
Returns:
list of GCStatsInfo: A list of stats samples containing:
- gen: GC generation number.
- iid: Interpreter ID.
- ts_start: Raw timestamp at collection start.
- ts_stop: Raw timestamp at collection stop.
- collections: Total number of collections.
- collected: Total number of collected objects.
- uncollectable: Total number of uncollectable objects.
- candidates: Total objects considered and traversed.
- heap_size: number of live objects.
- duration: Total collection time, in seconds.
Raises:
RuntimeError: If the target process cannot be inspected or if its
debug offsets or GC stats layout are incompatible.
[clinic start generated code]*/
static PyObject *
_remote_debugging_GCMonitor_get_gc_stats_impl(GCMonitorObject *self,
int all_interpreters)
/*[clinic end generated code: output=f73f365725224f7a input=12f7c1a288cf2741]*/
{
RemoteDebuggingState *st = RemoteDebugging_GetStateFromType(Py_TYPE(self));
return get_gc_stats(&self->offsets, all_interpreters, st->GCStatsInfo_Type);
}
static PyMethodDef GCMonitor_methods[] = {
_REMOTE_DEBUGGING_GCMONITOR_GET_GC_STATS_METHODDEF
{NULL, NULL}
};
static void
GCMonitor_dealloc(PyObject *op)
{
GCMonitorObject *self = GCMonitor_CAST(op);
PyTypeObject *tp = Py_TYPE(self);
cleanup_runtime_offsets(&self->offsets);
PyObject_Del(self);
Py_DECREF(tp);
}
static PyType_Slot GCMonitor_slots[] = {
{Py_tp_doc, (void *)"GCMonitor(pid): Monitor GC events of a remote Python process."},
{Py_tp_methods, GCMonitor_methods},
{Py_tp_init, _remote_debugging_GCMonitor___init__},
{Py_tp_dealloc, GCMonitor_dealloc},
{0, NULL}
};
static PyType_Spec GCMonitor_spec = {
.name = "_remote_debugging.GCMonitor",
.basicsize = sizeof(GCMonitorObject),
.flags = (
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_IMMUTABLETYPE
),
.slots = GCMonitor_slots,
};
/* Forward declarations for type specs defined later */
static PyType_Spec BinaryWriter_spec;
static PyType_Spec BinaryReader_spec;
/* ============================================================================
* MODULE INITIALIZATION
* ============================================================================ */
static int
_remote_debugging_exec(PyObject *m)
{
RemoteDebuggingState *st = RemoteDebugging_GetState(m);
#define CREATE_TYPE(mod, type, spec) \
do { \
type = (PyTypeObject *)PyType_FromMetaclass(NULL, mod, spec, NULL); \
if (type == NULL) { \
return -1; \
} \
} while (0)
CREATE_TYPE(m, st->RemoteDebugging_Type, &RemoteUnwinder_spec);
if (PyModule_AddType(m, st->RemoteDebugging_Type) < 0) {
return -1;
}
CREATE_TYPE(m, st->GCMonitor_Type, &GCMonitor_spec);
if (PyModule_AddType(m, st->GCMonitor_Type) < 0) {
return -1;
}
// Initialize structseq types
st->TaskInfo_Type = PyStructSequence_NewType(&TaskInfo_desc);
if (st->TaskInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->TaskInfo_Type) < 0) {
return -1;
}
st->LocationInfo_Type = PyStructSequence_NewType(&LocationInfo_desc);
if (st->LocationInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->LocationInfo_Type) < 0) {
return -1;
}
st->FrameInfo_Type = PyStructSequence_NewType(&FrameInfo_desc);
if (st->FrameInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->FrameInfo_Type) < 0) {
return -1;
}
st->CoroInfo_Type = PyStructSequence_NewType(&CoroInfo_desc);
if (st->CoroInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->CoroInfo_Type) < 0) {
return -1;
}
st->ThreadInfo_Type = PyStructSequence_NewType(&ThreadInfo_desc);
if (st->ThreadInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->ThreadInfo_Type) < 0) {
return -1;
}
st->InterpreterInfo_Type = PyStructSequence_NewType(&InterpreterInfo_desc);
if (st->InterpreterInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->InterpreterInfo_Type) < 0) {
return -1;
}
st->AwaitedInfo_Type = PyStructSequence_NewType(&AwaitedInfo_desc);
if (st->AwaitedInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->AwaitedInfo_Type) < 0) {
return -1;
}
st->GCStatsInfo_Type = PyStructSequence_NewType(&GCStatsInfo_desc);
if (st->GCStatsInfo_Type == NULL) {
return -1;
}
if (PyModule_AddType(m, st->GCStatsInfo_Type) < 0) {
return -1;
}
// Create BinaryWriter and BinaryReader types
CREATE_TYPE(m, st->BinaryWriter_Type, &BinaryWriter_spec);
if (PyModule_AddType(m, st->BinaryWriter_Type) < 0) {
return -1;
}
CREATE_TYPE(m, st->BinaryReader_Type, &BinaryReader_spec);
if (PyModule_AddType(m, st->BinaryReader_Type) < 0) {
return -1;
}
#ifdef Py_GIL_DISABLED
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
#endif
int rc = PyModule_AddIntConstant(m, "PROCESS_VM_READV_SUPPORTED", HAVE_PROCESS_VM_READV);
if (rc < 0) {
return -1;
}
// Add thread status flag constants
if (PyModule_AddIntConstant(m, "THREAD_STATUS_HAS_GIL", THREAD_STATUS_HAS_GIL) < 0) {
return -1;
}
if (PyModule_AddIntConstant(m, "THREAD_STATUS_ON_CPU", THREAD_STATUS_ON_CPU) < 0) {
return -1;
}
if (PyModule_AddIntConstant(m, "THREAD_STATUS_UNKNOWN", THREAD_STATUS_UNKNOWN) < 0) {
return -1;
}
if (PyModule_AddIntConstant(m, "THREAD_STATUS_GIL_REQUESTED", THREAD_STATUS_GIL_REQUESTED) < 0) {
return -1;
}
if (PyModule_AddIntConstant(m, "THREAD_STATUS_HAS_EXCEPTION", THREAD_STATUS_HAS_EXCEPTION) < 0) {
return -1;
}
if (PyModule_AddIntConstant(m, "THREAD_STATUS_MAIN_THREAD", THREAD_STATUS_MAIN_THREAD) < 0) {
return -1;
}
if (RemoteDebugging_InitState(st) < 0) {
return -1;
}
return 0;
}
static int
remote_debugging_traverse(PyObject *mod, visitproc visit, void *arg)
{
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_VISIT(state->RemoteDebugging_Type);
Py_VISIT(state->TaskInfo_Type);
Py_VISIT(state->LocationInfo_Type);
Py_VISIT(state->FrameInfo_Type);
Py_VISIT(state->CoroInfo_Type);
Py_VISIT(state->ThreadInfo_Type);
Py_VISIT(state->InterpreterInfo_Type);
Py_VISIT(state->AwaitedInfo_Type);
Py_VISIT(state->GCStatsInfo_Type);
Py_VISIT(state->BinaryWriter_Type);
Py_VISIT(state->BinaryReader_Type);
Py_VISIT(state->GCMonitor_Type);
return 0;
}
static int
remote_debugging_clear(PyObject *mod)
{
RemoteDebuggingState *state = RemoteDebugging_GetState(mod);
Py_CLEAR(state->RemoteDebugging_Type);
Py_CLEAR(state->TaskInfo_Type);
Py_CLEAR(state->LocationInfo_Type);
Py_CLEAR(state->FrameInfo_Type);
Py_CLEAR(state->CoroInfo_Type);
Py_CLEAR(state->ThreadInfo_Type);
Py_CLEAR(state->InterpreterInfo_Type);
Py_CLEAR(state->AwaitedInfo_Type);
Py_CLEAR(state->GCStatsInfo_Type);
Py_CLEAR(state->BinaryWriter_Type);
Py_CLEAR(state->BinaryReader_Type);
Py_CLEAR(state->GCMonitor_Type);
return 0;
}
static void
remote_debugging_free(void *mod)
{
(void)remote_debugging_clear((PyObject *)mod);
}
/* ============================================================================
* BINARY WRITER CLASS
* ============================================================================ */
#define BinaryWriter_CAST(op) ((BinaryWriterObject *)(op))
/*[clinic input]
class _remote_debugging.BinaryWriter "BinaryWriterObject *" "&PyBinaryWriter_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=e948838b90a2003c]*/
/*[clinic input]
@permit_long_docstring_body
_remote_debugging.BinaryWriter.__init__
filename: object
sample_interval_us: unsigned_long_long
start_time_us: unsigned_long_long
*
compression: int = 0
High-performance binary writer for profiling data.
Arguments:
filename: Path to output file
sample_interval_us: Sampling interval in microseconds
start_time_us: Start timestamp in microseconds (from time.monotonic() * 1e6)
compression: 0=none, 1=zstd (default: 0)
Use as a context manager or call finalize() when done.
[clinic start generated code]*/
static int
_remote_debugging_BinaryWriter___init___impl(BinaryWriterObject *self,
PyObject *filename,
unsigned long long sample_interval_us,
unsigned long long start_time_us,
int compression)
/*[clinic end generated code: output=00446656ea2e5986 input=b92f0c77ba4cd274]*/
{
if (self->writer) {
binary_writer_destroy(self->writer);
}
self->writer = binary_writer_create(filename, sample_interval_us, compression, start_time_us);
if (!self->writer) {
return -1;
}
return 0;
}
/*[clinic input]
@permit_long_docstring_body
_remote_debugging.BinaryWriter.write_sample
stack_frames: object
timestamp_us: unsigned_long_long
Write a sample to the binary file.
Arguments:
stack_frames: List of InterpreterInfo objects
timestamp_us: Current timestamp in microseconds (from time.monotonic() * 1e6)
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter_write_sample_impl(BinaryWriterObject *self,
PyObject *stack_frames,
unsigned long long timestamp_us)
/*[clinic end generated code: output=24d5b86679b4128f input=4e6d832d360bea46]*/
{
if (!self->writer) {
PyErr_SetString(PyExc_ValueError, "Writer is closed");
return NULL;
}
if (binary_writer_write_sample(self->writer, stack_frames, timestamp_us) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
/* Finalize the writer, cache total_samples, and destroy it.
*
* The cache assignment must happen AFTER binary_writer_finalize(): finalize
* flushes pending RLE samples via flush_pending_rle(), which increments
* writer->total_samples for each one. Caching before finalize would lose
* those trailing samples. */
static int
binary_writer_finalize_and_cache(BinaryWriterObject *self)
{
if (binary_writer_finalize(self->writer) < 0) {
return -1;
}
self->cached_total_samples = self->writer->total_samples;
binary_writer_destroy(self->writer);
self->writer = NULL;
return 0;
}
/*[clinic input]
_remote_debugging.BinaryWriter.finalize
Finalize and close the binary file.
Writes string/frame tables, footer, and updates header.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter_finalize_impl(BinaryWriterObject *self)
/*[clinic end generated code: output=3534b88c6628de88 input=c02191750682f6a2]*/
{
if (!self->writer) {
PyErr_SetString(PyExc_ValueError, "Writer is already closed");
return NULL;
}
if (binary_writer_finalize_and_cache(self) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
/*[clinic input]
_remote_debugging.BinaryWriter.close
Close the writer without finalizing (discards data).
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter_close_impl(BinaryWriterObject *self)
/*[clinic end generated code: output=9571bb2256fd1fd2 input=6e0da206e60daf16]*/
{
if (self->writer) {
binary_writer_destroy(self->writer);
self->writer = NULL;
}
Py_RETURN_NONE;
}
/*[clinic input]
_remote_debugging.BinaryWriter.__enter__
Enter context manager.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter___enter___impl(BinaryWriterObject *self)
/*[clinic end generated code: output=8eb95f61daf2d120 input=8ef14ee18da561d2]*/
{
Py_INCREF(self);
return (PyObject *)self;
}
/*[clinic input]
_remote_debugging.BinaryWriter.__exit__
exc_type: object = None
exc_val: object = None
exc_tb: object = None
Exit context manager, finalizing the file.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter___exit___impl(BinaryWriterObject *self,
PyObject *exc_type,
PyObject *exc_val,
PyObject *exc_tb)
/*[clinic end generated code: output=61831f47c72a53c6 input=12334ce1009af37f]*/
{
if (self->writer) {
/* Only finalize on normal exit (no exception) */
if (exc_type == Py_None) {
if (binary_writer_finalize_and_cache(self) < 0) {
if (self->writer) {
binary_writer_destroy(self->writer);
self->writer = NULL;
}
return NULL;
}
}
else {
binary_writer_destroy(self->writer);
self->writer = NULL;
}
}
Py_RETURN_FALSE;
}
/*[clinic input]
@permit_long_docstring_body
_remote_debugging.BinaryWriter.get_stats
Get encoding statistics for the writer.
Returns a dict with encoding statistics including repeat/full/suffix/pop-push
record counts, frames written/saved, and compression ratio.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryWriter_get_stats_impl(BinaryWriterObject *self)
/*[clinic end generated code: output=06522cd52544df89 input=837c874ffdebd24c]*/
{
if (!self->writer) {
PyErr_SetString(PyExc_ValueError, "Writer is closed");
return NULL;
}
return binary_writer_get_stats(self->writer);
}
static PyObject *
BinaryWriter_get_total_samples(PyObject *op, void *closure)
{
BinaryWriterObject *self = BinaryWriter_CAST(op);
if (!self->writer) {
/* Use cached value after finalize/close */
return PyLong_FromUnsignedLong(self->cached_total_samples);
}
return PyLong_FromUnsignedLong(self->writer->total_samples);
}
static PyGetSetDef BinaryWriter_getset[] = {
{"total_samples", BinaryWriter_get_total_samples, NULL, "Total samples written", NULL},
{NULL}
};
static PyMethodDef BinaryWriter_methods[] = {
_REMOTE_DEBUGGING_BINARYWRITER_WRITE_SAMPLE_METHODDEF
_REMOTE_DEBUGGING_BINARYWRITER_FINALIZE_METHODDEF
_REMOTE_DEBUGGING_BINARYWRITER_CLOSE_METHODDEF
_REMOTE_DEBUGGING_BINARYWRITER___ENTER___METHODDEF
_REMOTE_DEBUGGING_BINARYWRITER___EXIT___METHODDEF
_REMOTE_DEBUGGING_BINARYWRITER_GET_STATS_METHODDEF
{NULL, NULL, 0, NULL}
};
static void
BinaryWriter_dealloc(PyObject *op)
{
BinaryWriterObject *self = BinaryWriter_CAST(op);
PyTypeObject *tp = Py_TYPE(self);
if (self->writer) {
binary_writer_destroy(self->writer);
}
tp->tp_free(self);
Py_DECREF(tp);
}
static PyType_Slot BinaryWriter_slots[] = {
{Py_tp_getset, BinaryWriter_getset},
{Py_tp_methods, BinaryWriter_methods},
{Py_tp_init, _remote_debugging_BinaryWriter___init__},
{Py_tp_dealloc, BinaryWriter_dealloc},
{0, NULL}
};
static PyType_Spec BinaryWriter_spec = {
.name = "_remote_debugging.BinaryWriter",
.basicsize = sizeof(BinaryWriterObject),
.flags = (
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_IMMUTABLETYPE
),
.slots = BinaryWriter_slots,
};
/* ============================================================================
* BINARY READER CLASS
* ============================================================================ */
#define BinaryReader_CAST(op) ((BinaryReaderObject *)(op))
/*[clinic input]
class _remote_debugging.BinaryReader "BinaryReaderObject *" "&PyBinaryReader_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=36400aaf6f53216d]*/
/*[clinic input]
_remote_debugging.BinaryReader.__init__
filename: object
High-performance binary reader for profiling data.
Arguments:
filename: Path to input file
Use as a context manager or call close() when done.
[clinic start generated code]*/
static int
_remote_debugging_BinaryReader___init___impl(BinaryReaderObject *self,
PyObject *filename)
/*[clinic end generated code: output=f04b33ee5c5e6dbf input=9d7cbe8b4f1a97c9]*/
{
if (self->reader) {
binary_reader_close(self->reader);
}
self->reader = binary_reader_open(filename);
if (!self->reader) {
return -1;
}
return 0;
}
/*[clinic input]
_remote_debugging.BinaryReader.replay
collector: object
progress_callback: object = None
Replay samples through a collector.
Arguments:
collector: Collector object with collect() method
progress_callback: Optional callable(current, total)
Returns:
Number of samples replayed
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader_replay_impl(BinaryReaderObject *self,
PyObject *collector,
PyObject *progress_callback)
/*[clinic end generated code: output=442345562574b61c input=ebb687aed3e0f4f1]*/
{
if (!self->reader) {
PyErr_SetString(PyExc_ValueError, "Reader is closed");
return NULL;
}
Py_ssize_t replayed = binary_reader_replay(self->reader, collector, progress_callback);
if (replayed < 0) {
return NULL;
}
return PyLong_FromSsize_t(replayed);
}
/*[clinic input]
_remote_debugging.BinaryReader.get_info
Get metadata about the binary file.
Returns:
Dict with file metadata
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader_get_info_impl(BinaryReaderObject *self)
/*[clinic end generated code: output=7f641fbd39147391 input=02e75e39c8a6cd1f]*/
{
if (!self->reader) {
PyErr_SetString(PyExc_ValueError, "Reader is closed");
return NULL;
}
return binary_reader_get_info(self->reader);
}
/*[clinic input]
_remote_debugging.BinaryReader.get_stats
Get reconstruction statistics from replay.
Returns a dict with statistics about record types decoded and samples
reconstructed during replay.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader_get_stats_impl(BinaryReaderObject *self)
/*[clinic end generated code: output=628b9ab5e4c4fd36 input=d8dd6654abd6c3c0]*/
{
if (!self->reader) {
PyErr_SetString(PyExc_ValueError, "Reader is closed");
return NULL;
}
return binary_reader_get_stats(self->reader);
}
/*[clinic input]
_remote_debugging.BinaryReader.close
Close the reader and free resources.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader_close_impl(BinaryReaderObject *self)
/*[clinic end generated code: output=ad0238cf5240b4f8 input=b919a66c737712d5]*/
{
if (self->reader) {
binary_reader_close(self->reader);
self->reader = NULL;
}
Py_RETURN_NONE;
}
/*[clinic input]
_remote_debugging.BinaryReader.__enter__
Enter context manager.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader___enter___impl(BinaryReaderObject *self)
/*[clinic end generated code: output=fade133538e93817 input=4794844c9efdc4f6]*/
{
Py_INCREF(self);
return (PyObject *)self;
}
/*[clinic input]
_remote_debugging.BinaryReader.__exit__
exc_type: object = None
exc_val: object = None
exc_tb: object = None
Exit context manager, closing the file.
[clinic start generated code]*/
static PyObject *
_remote_debugging_BinaryReader___exit___impl(BinaryReaderObject *self,
PyObject *exc_type,
PyObject *exc_val,
PyObject *exc_tb)
/*[clinic end generated code: output=2acdd36cfdc14e4a input=87284243d7935835]*/
{
if (self->reader) {
binary_reader_close(self->reader);
self->reader = NULL;
}
Py_RETURN_FALSE;
}
static PyObject *
BinaryReader_get_sample_count(BinaryReaderObject *self, void *closure)
{
if (!self->reader) {
return PyLong_FromLong(0);
}
return PyLong_FromUnsignedLong(self->reader->sample_count);
}
static PyObject *
BinaryReader_get_sample_interval_us(BinaryReaderObject *self, void *closure)
{
if (!self->reader) {
return PyLong_FromLong(0);
}
return PyLong_FromUnsignedLongLong(self->reader->sample_interval_us);
}
static PyGetSetDef BinaryReader_getset[] = {
{"sample_count", (getter)BinaryReader_get_sample_count, NULL, "Number of samples in file", NULL},
{"sample_interval_us", (getter)BinaryReader_get_sample_interval_us, NULL, "Sample interval in microseconds", NULL},
{NULL}
};
static PyMethodDef BinaryReader_methods[] = {
_REMOTE_DEBUGGING_BINARYREADER_REPLAY_METHODDEF
_REMOTE_DEBUGGING_BINARYREADER_GET_INFO_METHODDEF
_REMOTE_DEBUGGING_BINARYREADER_GET_STATS_METHODDEF
_REMOTE_DEBUGGING_BINARYREADER_CLOSE_METHODDEF
_REMOTE_DEBUGGING_BINARYREADER___ENTER___METHODDEF
_REMOTE_DEBUGGING_BINARYREADER___EXIT___METHODDEF
{NULL, NULL, 0, NULL}
};
static void
BinaryReader_dealloc(PyObject *op)
{
BinaryReaderObject *self = BinaryReader_CAST(op);
PyTypeObject *tp = Py_TYPE(self);
if (self->reader) {
binary_reader_close(self->reader);
}
tp->tp_free(self);
Py_DECREF(tp);
}
static PyType_Slot BinaryReader_slots[] = {
{Py_tp_getset, BinaryReader_getset},
{Py_tp_methods, BinaryReader_methods},
{Py_tp_init, _remote_debugging_BinaryReader___init__},
{Py_tp_dealloc, BinaryReader_dealloc},
{0, NULL}
};
static PyType_Spec BinaryReader_spec = {
.name = "_remote_debugging.BinaryReader",
.basicsize = sizeof(BinaryReaderObject),
.flags = (
Py_TPFLAGS_DEFAULT
| Py_TPFLAGS_IMMUTABLETYPE
),
.slots = BinaryReader_slots,
};
/* ============================================================================
* MODULE METHODS
* ============================================================================ */
/*[clinic input]
_remote_debugging.zstd_available
Check if zstd compression is available.
Returns:
True if zstd available, False otherwise
[clinic start generated code]*/
static PyObject *
_remote_debugging_zstd_available_impl(PyObject *module)
/*[clinic end generated code: output=55e35a70ef280cdd input=a1b4d41bc09c7cf9]*/
{
return PyBool_FromLong(binary_io_zstd_available());
}
/* ============================================================================
* MODULE-LEVEL FUNCTIONS
* ============================================================================ */
/*[clinic input]
@permit_long_docstring_body
_remote_debugging.get_child_pids
pid: int
Process ID of the parent process
*
recursive: bool = True
If True, return all descendants (children, grandchildren, etc.).
If False, return only direct children.
Get all child process IDs of the given process.
Returns a list of child process IDs. Returns an empty list if no children
are found.
This function provides a snapshot of child processes at a moment in time.
Child processes may exit or new ones may be created after the list is returned.
Raises:
OSError: If unable to enumerate processes
NotImplementedError: If not supported on this platform
[clinic start generated code]*/
static PyObject *
_remote_debugging_get_child_pids_impl(PyObject *module, int pid,
int recursive)
/*[clinic end generated code: output=1ae2289c6b953e4b input=19d8d5d6e2b59e6e]*/
{
return enumerate_child_pids((pid_t)pid, recursive);
}
/*[clinic input]
_remote_debugging.is_python_process
pid: int
Check if a process is a Python process.
[clinic start generated code]*/
static PyObject *
_remote_debugging_is_python_process_impl(PyObject *module, int pid)
/*[clinic end generated code: output=22947dc8afcac362 input=13488e28c7295d84]*/
{
proc_handle_t handle;
if (_Py_RemoteDebug_InitProcHandle(&handle, pid) < 0) {
PyErr_Clear();
Py_RETURN_FALSE;
}
uintptr_t runtime_start_address = _Py_RemoteDebug_GetPyRuntimeAddress(&handle);
_Py_RemoteDebug_CleanupProcHandle(&handle);
if (runtime_start_address == 0) {
PyErr_Clear();
Py_RETURN_FALSE;
}
Py_RETURN_TRUE;
}
/*[clinic input]
_remote_debugging.get_gc_stats
pid: int
*
all_interpreters: bool = False
If True, return GC statistics from all interpreters.
If False, return only from main interpreter.
Get garbage collector statistics from external Python process.
Returns:
list of GCStatsInfo: A list of stats samples containing:
- gen: GC generation number.
- iid: Interpreter ID.
- ts_start: Raw timestamp at collection start.
- ts_stop: Raw timestamp at collection stop.
- collections: Total number of collections.
- collected: Total number of collected objects.
- uncollectable: Total number of uncollectable objects.
- candidates: Total objects considered and traversed.
- duration: Total collection time, in seconds.
Raises:
RuntimeError: If the target process cannot be inspected or if its
debug offsets or GC stats layout are incompatible.
[clinic start generated code]*/
static PyObject *
_remote_debugging_get_gc_stats_impl(PyObject *module, int pid,
int all_interpreters)
/*[clinic end generated code: output=d9dce5f7add149bb input=a2a08a45a8f0b119]*/
{
RuntimeOffsets offsets;
if (init_runtime_offsets(&offsets, pid, /*debug=*/1) < 0) {
return NULL;
}
RemoteDebuggingState *st = RemoteDebugging_GetState(module);
PyObject *result = get_gc_stats(&offsets, all_interpreters,
st->GCStatsInfo_Type);
cleanup_runtime_offsets(&offsets);
return result;
}
static PyMethodDef remote_debugging_methods[] = {
_REMOTE_DEBUGGING_ZSTD_AVAILABLE_METHODDEF
_REMOTE_DEBUGGING_GET_CHILD_PIDS_METHODDEF
_REMOTE_DEBUGGING_IS_PYTHON_PROCESS_METHODDEF
_REMOTE_DEBUGGING_GET_GC_STATS_METHODDEF
{NULL, NULL, 0, NULL},
};
static PyModuleDef_Slot remote_debugging_slots[] = {
_Py_ABI_SLOT,
{Py_mod_exec, _remote_debugging_exec},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{0, NULL},
};
static struct PyModuleDef remote_debugging_module = {
PyModuleDef_HEAD_INIT,
.m_name = "_remote_debugging",
.m_size = sizeof(RemoteDebuggingState),
.m_methods = remote_debugging_methods,
.m_slots = remote_debugging_slots,
.m_traverse = remote_debugging_traverse,
.m_clear = remote_debugging_clear,
.m_free = remote_debugging_free,
};
PyMODINIT_FUNC
PyInit__remote_debugging(void)
{
return PyModuleDef_Init(&remote_debugging_module);
}