/******************************************************************************
 * 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: 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=9d25ae328d62626d]*/
{
    // 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, &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
@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=c22bfee0612e0b69]*/
{
    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, 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
_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=03da0b2d3282ae1b]*/
{
    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

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=2e3f298c69fc7666]*/
{
    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]
_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 (binary_writer_write_sample(self->writer, stack_frames, timestamp_us) < 0) {
        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 (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]
_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 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_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: 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=c6437b52e2fdd880]*/
{
    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);
}
