| /****************************************************************************** | |
| * 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; | |
| uint64_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 | |
| _remote_debugging.RemoteUnwinder.__init__ | |
| pid: pid_t | |
| * | |
| 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, | |
| pid_t 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=acfe554c8a92cf6b input=3b5a5ad153709125]*/ | |
| { | |
| // 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) { | |
| if (_Py_RemoteDebug_HasPermissionError()) { | |
| return -1; | |
| } | |
| 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 profiler anchors from previous profilers. This prevents us | |
| // from stopping frame walking early due to stale frame pointers. | |
| 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] | |
| @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=86a992b853f48aa9]*/ | |
| { | |
| 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, ¤t_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_docstring_body | |
| @critical_section | |
| _remote_debugging.RemoteUnwinder.get_all_awaited_by | |
| Get awaited_by relationships for tasks in the remote process. | |
| Returns: | |
| A list of AwaitedInfo objects, where each object contains: | |
| - thread_id (int): Identifier of the thread, or 0 for tasks in the | |
| interpreter's fallback task list. | |
| - awaited_by (list[TaskInfo]): Tasks registered with this thread. | |
| Each TaskInfo contains: | |
| - task_id (int): Identifier of the task. | |
| - task_name (str): Name of the task. | |
| - coroutine_stack (list[CoroInfo]): Stack of coroutine frames. | |
| - awaited_by (list[CoroInfo]): Coroutine information for tasks or futures | |
| awaiting this task. | |
| Each CoroInfo contains: | |
| - call_stack (list[FrameInfo]): Call stack frames for the coroutine. | |
| - task_name (int): Identifier of the task or future. | |
| 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: | |
| [ | |
| AwaitedInfo( | |
| thread_id=12345, | |
| awaited_by=[ | |
| TaskInfo( | |
| task_id=1, | |
| task_name="Task-1", | |
| coroutine_stack=[...], | |
| awaited_by=[] | |
| ) | |
| ] | |
| ) | |
| ] | |
| [clinic start generated code]*/ | |
| static PyObject * | |
| _remote_debugging_RemoteUnwinder_get_all_awaited_by_impl(RemoteUnwinderObject *self) | |
| /*[clinic end generated code: output=6a49cd345e8aec53 input=79e556b3973d21a8]*/ | |
| { | |
| 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 | |
| @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=bc802e4221c99399]*/ | |
| { | |
| 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] | |
| @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=87905c65038fb06e]*/ | |
| { | |
| 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] | |
| @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=b91dde5517c9dde2]*/ | |
| { | |
| #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] | |
| @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=130758d55d46897a]*/ | |
| { | |
| #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, pid_t 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 | |
| _remote_debugging.GCMonitor.__init__ | |
| pid: pid_t | |
| * | |
| 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, pid_t pid, | |
| int debug) | |
| /*[clinic end generated code: output=03b4c92bef0673ad input=dcc6ee2ec5a16fa1]*/ | |
| { | |
| 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=ec016bc4be6dd003]*/ | |
| { | |
| 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; | |
| } | |
| 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] | |
| _remote_debugging.BinaryWriter.__init__ | |
| filename: object | |
| sample_interval_us: unsigned_long_long | |
| start_time_us: unsigned_long_long | |
| * | |
| compression: int = 0 | |
| mode: int = -1 | |
| capture_features: int = -1 | |
| 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) | |
| mode: Profiling mode, or -1 if unknown (default: -1) | |
| capture_features: Capture feature bit mask, or -1 if unknown | |
| (default: -1) | |
| 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, int mode, | |
| int capture_features) | |
| /*[clinic end generated code: output=3c1c9576795658ce input=98add735b20403ad]*/ | |
| { | |
| if (mode < -1 || mode > PROFILING_MODE_EXCEPTION) { | |
| PyErr_SetString(PyExc_ValueError, "invalid profiling mode"); | |
| return -1; | |
| } | |
| if (capture_features < -1 || | |
| capture_features > (int)PROFILING_FEATURE_MASK) { | |
| PyErr_SetString(PyExc_ValueError, "invalid capture features"); | |
| return -1; | |
| } | |
| if (self->writer) { | |
| binary_writer_destroy(self->writer); | |
| } | |
| self->writer = binary_writer_create( | |
| filename, sample_interval_us, compression, start_time_us, mode, | |
| capture_features); | |
| if (!self->writer) { | |
| return -1; | |
| } | |
| return 0; | |
| } | |
| /*[clinic input] | |
| _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=5033f1ae7fa135f1]*/ | |
| { | |
| if (!self->writer) { | |
| PyErr_SetString(PyExc_ValueError, "Writer is closed"); | |
| return NULL; | |
| } | |
| if (self->writer->state == BINARY_WRITER_BROKEN) { | |
| PyErr_SetString(PyExc_ValueError, "Writer is broken"); | |
| return NULL; | |
| } | |
| self->writer->state = BINARY_WRITER_OPEN; | |
| if (binary_writer_write_sample(self->writer, stack_frames, timestamp_us) < 0) { | |
| if (self->writer->state != BINARY_WRITER_LIMIT_REACHED) { | |
| self->writer->state = BINARY_WRITER_BROKEN; | |
| } | |
| return NULL; | |
| } | |
| Py_RETURN_NONE; | |
| } | |
| /*[clinic input] | |
| _remote_debugging.BinaryWriter.set_stats | |
| duration_sec: double | |
| sample_rate: double | |
| error_rate: object = None | |
| missed_samples: object = None | |
| Store measured profile statistics in the binary file. | |
| [clinic start generated code]*/ | |
| static PyObject * | |
| _remote_debugging_BinaryWriter_set_stats_impl(BinaryWriterObject *self, | |
| double duration_sec, | |
| double sample_rate, | |
| PyObject *error_rate, | |
| PyObject *missed_samples) | |
| /*[clinic end generated code: output=28ab1bdd7c631a97 input=1646e7182f4c2259]*/ | |
| { | |
| if (!self->writer) { | |
| PyErr_SetString(PyExc_ValueError, "Writer is closed"); | |
| return NULL; | |
| } | |
| uint32_t present = 0; | |
| double error_rate_value = 0.0; | |
| double missed_samples_value = 0.0; | |
| if (error_rate != Py_None) { | |
| error_rate_value = PyFloat_AsDouble(error_rate); | |
| if (error_rate_value == -1.0 && PyErr_Occurred()) { | |
| return NULL; | |
| } | |
| present |= PROFILE_STATS_ERROR_RATE; | |
| } | |
| if (missed_samples != Py_None) { | |
| missed_samples_value = PyFloat_AsDouble(missed_samples); | |
| if (missed_samples_value == -1.0 && PyErr_Occurred()) { | |
| return NULL; | |
| } | |
| present |= PROFILE_STATS_MISSED; | |
| } | |
| if (binary_writer_set_stats(self->writer, duration_sec, sample_rate, | |
| error_rate_value, missed_samples_value, | |
| present) < 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 records and updates the file header before the writer is | |
| * destroyed. Caching before finalize would preserve a count for a file that | |
| * might still fail to finish. */ | |
| static int | |
| binary_writer_finalize_and_cache(BinaryWriterObject *self) | |
| { | |
| if (self->writer->state == BINARY_WRITER_BROKEN) { | |
| PyErr_SetString(PyExc_ValueError, "Writer is broken"); | |
| return -1; | |
| } | |
| if (binary_writer_finalize(self->writer) < 0) { | |
| self->writer->state = BINARY_WRITER_BROKEN; | |
| 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) { | |
| if (self->writer->state != BINARY_WRITER_BROKEN) { | |
| 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] | |
| _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=a8bb8c8682ccd34b]*/ | |
| { | |
| 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_FromUnsignedLongLong(self->cached_total_samples); | |
| } | |
| return PyLong_FromUnsignedLongLong(self->writer->total_samples); | |
| } | |
| static PyObject * | |
| BinaryWriter_get_limit_reached(PyObject *op, void *closure) | |
| { | |
| BinaryWriter *writer = BinaryWriter_CAST(op)->writer; | |
| return PyBool_FromLong(writer && writer->state == BINARY_WRITER_LIMIT_REACHED); | |
| } | |
| static PyGetSetDef BinaryWriter_getset[] = { | |
| {"total_samples", BinaryWriter_get_total_samples, NULL, "Total samples written", NULL}, | |
| {"limit_reached", BinaryWriter_get_limit_reached, NULL, | |
| "A format limit was reached; the collected samples can still be finalized", NULL}, | |
| {NULL} | |
| }; | |
| static PyMethodDef BinaryWriter_methods[] = { | |
| _REMOTE_DEBUGGING_BINARYWRITER_WRITE_SAMPLE_METHODDEF | |
| _REMOTE_DEBUGGING_BINARYWRITER_SET_STATS_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=15b8d8f89ccf3726]*/ | |
| { | |
| 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_FromUnsignedLongLong(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] | |
| _remote_debugging.get_child_pids | |
| pid: pid_t | |
| 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, pid_t pid, | |
| int recursive) | |
| /*[clinic end generated code: output=fa3dfd1b02eed29b input=3325d95e9f39d75d]*/ | |
| { | |
| return enumerate_child_pids(pid, recursive); | |
| } | |
| /*[clinic input] | |
| _remote_debugging.is_python_process | |
| pid: pid_t | |
| Check if a process is a Python process. | |
| [clinic start generated code]*/ | |
| static PyObject * | |
| _remote_debugging_is_python_process_impl(PyObject *module, pid_t pid) | |
| /*[clinic end generated code: output=63541478c889e536 input=ff998fef4aeef433]*/ | |
| { | |
| 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: pid_t | |
| * | |
| 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, pid_t pid, | |
| int all_interpreters) | |
| /*[clinic end generated code: output=dd33199ccb6a56e9 input=41399e77788aa369]*/ | |
| { | |
| 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); | |
| } |