| /****************************************************************************** |
| * Python Remote Debugging Module - Binary I/O Header |
| * |
| * This header provides declarations for high-performance binary file I/O |
| * for profiling data with optional zstd streaming compression. |
| ******************************************************************************/ |
| |
| #ifndef Py_BINARY_IO_H |
| #define Py_BINARY_IO_H |
| |
| #ifdef __cplusplus |
| extern "C" { |
| #endif |
| |
| #include "Python.h" |
| #include "pycore_hashtable.h" |
| #include <stdint.h> |
| #include <stdio.h> |
| |
| /* ============================================================================ |
| * BINARY FORMAT CONSTANTS |
| * ============================================================================ */ |
| |
| #define BINARY_FORMAT_MAGIC 0x54414348 /* "TACH" (Tachyon) in native byte order */ |
| #define BINARY_FORMAT_MAGIC_SWAPPED 0x48434154 /* Byte-swapped magic for endianness detection */ |
| #define BINARY_FORMAT_VERSION 1 |
| |
| /* Conditional byte-swap macros for cross-endian file reading. |
| * Uses Python's optimized byte-swap functions from pycore_bitutils.h */ |
| #define SWAP16_IF(swap, x) ((swap) ? _Py_bswap16(x) : (x)) |
| #define SWAP32_IF(swap, x) ((swap) ? _Py_bswap32(x) : (x)) |
| #define SWAP64_IF(swap, x) ((swap) ? _Py_bswap64(x) : (x)) |
| |
| /* Header field offsets and sizes */ |
| #define HDR_OFF_MAGIC 0 |
| #define HDR_SIZE_MAGIC 4 |
| #define HDR_OFF_VERSION (HDR_OFF_MAGIC + HDR_SIZE_MAGIC) |
| #define HDR_SIZE_VERSION 4 |
| #define HDR_OFF_PY_VERSION (HDR_OFF_VERSION + HDR_SIZE_VERSION) |
| #define HDR_SIZE_PY_VERSION 4 /* 3 bytes: major, minor, micro + 1 reserved */ |
| #define HDR_OFF_PY_MAJOR HDR_OFF_PY_VERSION |
| #define HDR_OFF_PY_MINOR (HDR_OFF_PY_VERSION + 1) |
| #define HDR_OFF_PY_MICRO (HDR_OFF_PY_VERSION + 2) |
| #define HDR_OFF_START_TIME (HDR_OFF_PY_VERSION + HDR_SIZE_PY_VERSION) |
| #define HDR_SIZE_START_TIME 8 |
| #define HDR_OFF_INTERVAL (HDR_OFF_START_TIME + HDR_SIZE_START_TIME) |
| #define HDR_SIZE_INTERVAL 8 |
| #define HDR_OFF_SAMPLES (HDR_OFF_INTERVAL + HDR_SIZE_INTERVAL) |
| #define HDR_SIZE_SAMPLES 4 |
| #define HDR_OFF_THREADS (HDR_OFF_SAMPLES + HDR_SIZE_SAMPLES) |
| #define HDR_SIZE_THREADS 4 |
| #define HDR_OFF_STR_TABLE (HDR_OFF_THREADS + HDR_SIZE_THREADS) |
| #define HDR_SIZE_STR_TABLE 8 |
| #define HDR_OFF_FRAME_TABLE (HDR_OFF_STR_TABLE + HDR_SIZE_STR_TABLE) |
| #define HDR_SIZE_FRAME_TABLE 8 |
| #define HDR_OFF_COMPRESSION (HDR_OFF_FRAME_TABLE + HDR_SIZE_FRAME_TABLE) |
| #define HDR_SIZE_COMPRESSION 4 |
| #define FILE_HEADER_SIZE (HDR_OFF_COMPRESSION + HDR_SIZE_COMPRESSION) |
| #define FILE_HEADER_PLACEHOLDER_SIZE 64 |
| |
| static_assert(FILE_HEADER_SIZE <= FILE_HEADER_PLACEHOLDER_SIZE, |
| "FILE_HEADER_SIZE exceeds FILE_HEADER_PLACEHOLDER_SIZE"); |
| |
| /* Buffer sizes: 512KB balances syscall amortization against memory use, |
| * and aligns well with filesystem block sizes and zstd dictionary windows */ |
| #define WRITE_BUFFER_SIZE (512 * 1024) |
| #define COMPRESSED_BUFFER_SIZE (512 * 1024) |
| |
| /* Compression types */ |
| #define COMPRESSION_NONE 0 |
| #define COMPRESSION_ZSTD 1 |
| |
| /* Stack encoding types for delta compression */ |
| #define STACK_REPEAT 0x00 /* RLE: identical to previous, with count */ |
| #define STACK_FULL 0x01 /* Full stack (first sample or no match) */ |
| #define STACK_SUFFIX 0x02 /* Shares N frames from bottom */ |
| #define STACK_POP_PUSH 0x03 /* Remove M frames, add N frames */ |
| |
| /* Maximum stack depth we'll buffer for delta encoding */ |
| #define MAX_STACK_DEPTH 256 |
| |
| /* Initial capacity for RLE pending buffer */ |
| #define INITIAL_RLE_CAPACITY 64 |
| |
| /* Initial capacities for dynamic arrays - sized to reduce reallocations */ |
| #define INITIAL_STRING_CAPACITY 4096 |
| #define INITIAL_FRAME_CAPACITY 4096 |
| #define INITIAL_THREAD_CAPACITY 256 |
| |
| /* ============================================================================ |
| * STATISTICS STRUCTURES |
| * ============================================================================ */ |
| |
| /* Writer statistics - tracks encoding efficiency */ |
| typedef struct { |
| uint64_t repeat_records; /* Number of RLE repeat records written */ |
| uint64_t repeat_samples; /* Total samples encoded via RLE */ |
| uint64_t full_records; /* Number of full stack records */ |
| uint64_t suffix_records; /* Number of suffix match records */ |
| uint64_t pop_push_records; /* Number of pop-push records */ |
| uint64_t total_frames_written;/* Total frame indices written */ |
| uint64_t frames_saved; /* Frames avoided due to delta encoding */ |
| uint64_t bytes_written; /* Total bytes written (before compression) */ |
| } BinaryWriterStats; |
| |
| /* Reader statistics - tracks reconstruction performance */ |
| typedef struct { |
| uint64_t repeat_records; /* RLE records decoded */ |
| uint64_t repeat_samples; /* Samples decoded from RLE */ |
| uint64_t full_records; /* Full stack records decoded */ |
| uint64_t suffix_records; /* Suffix match records decoded */ |
| uint64_t pop_push_records; /* Pop-push records decoded */ |
| uint64_t total_samples; /* Total samples reconstructed */ |
| uint64_t stack_reconstructions; /* Number of stack array reconstructions */ |
| } BinaryReaderStats; |
| |
| /* ============================================================================ |
| * PLATFORM ABSTRACTION |
| * ============================================================================ */ |
| |
| #if defined(__linux__) || defined(__APPLE__) |
| #include <sys/mman.h> |
| #include <unistd.h> |
| #include <sys/stat.h> |
| #include <fcntl.h> |
| #define USE_MMAP 1 |
| #else |
| #define USE_MMAP 0 |
| #endif |
| |
| /* 64-bit file position support for files larger than 2GB. |
| * On POSIX: use ftello/fseeko with off_t (already 64-bit on 64-bit systems) |
| * On Windows: use _ftelli64/_fseeki64 with __int64 */ |
| #if defined(_WIN32) || defined(_WIN64) |
| #include <io.h> |
| typedef __int64 file_offset_t; |
| #define FTELL64(fp) _ftelli64(fp) |
| #define FSEEK64(fp, offset, whence) _fseeki64(fp, offset, whence) |
| #else |
| /* POSIX - off_t is 64-bit on 64-bit systems, ftello/fseeko handle large files */ |
| typedef off_t file_offset_t; |
| #define FTELL64(fp) ftello(fp) |
| #define FSEEK64(fp, offset, whence) fseeko(fp, offset, whence) |
| #endif |
| |
| /* Forward declare zstd types if available */ |
| #ifdef HAVE_ZSTD |
| #include <zstd.h> |
| #endif |
| |
| /* Branch prediction hints - same as Objects/obmalloc.c */ |
| #if (defined(__clang__) || (defined(__GNUC__) && (__GNUC__ > 2))) && defined(__OPTIMIZE__) |
| # define UNLIKELY(value) __builtin_expect((value), 0) |
| # define LIKELY(value) __builtin_expect((value), 1) |
| #else |
| # define UNLIKELY(value) (value) |
| # define LIKELY(value) (value) |
| #endif |
| |
| /* ============================================================================ |
| * BINARY WRITER STRUCTURES |
| * ============================================================================ */ |
| |
| /* zstd compression state (only used if HAVE_ZSTD defined) */ |
| typedef struct { |
| #ifdef HAVE_ZSTD |
| ZSTD_CCtx *cctx; /* Modern API: CCtx and CStream are the same since v1.3.0 */ |
| #else |
| void *cctx; /* Placeholder */ |
| #endif |
| uint8_t *compressed_buffer; |
| size_t compressed_buffer_size; |
| } ZstdCompressor; |
| |
| /* Frame entry - combines all frame data for better cache locality */ |
| typedef struct { |
| uint32_t filename_idx; |
| uint32_t funcname_idx; |
| int32_t lineno; |
| } FrameEntry; |
| |
| /* Frame key for hash table lookup */ |
| typedef struct { |
| uint32_t filename_idx; |
| uint32_t funcname_idx; |
| int32_t lineno; |
| } FrameKey; |
| |
| /* Pending RLE sample - buffered for run-length encoding */ |
| typedef struct { |
| uint64_t timestamp_delta; |
| uint8_t status; |
| } PendingRLESample; |
| |
| /* Thread entry - tracks per-thread state for delta encoding */ |
| typedef struct { |
| uint64_t thread_id; |
| uint64_t prev_timestamp; |
| uint32_t interpreter_id; |
| |
| /* Previous stack for delta encoding (frame indices, innermost first) */ |
| uint32_t *prev_stack; |
| size_t prev_stack_depth; |
| size_t prev_stack_capacity; |
| |
| /* RLE pending buffer - samples waiting to be written as a repeat group */ |
| PendingRLESample *pending_rle; |
| size_t pending_rle_count; |
| size_t pending_rle_capacity; |
| int has_pending_rle; /* Flag: do we have buffered repeats? */ |
| } ThreadEntry; |
| |
| /* Main binary writer structure */ |
| typedef struct { |
| FILE *fp; |
| char *filename; |
| |
| /* Write buffer for batched I/O */ |
| uint8_t *write_buffer; |
| size_t buffer_pos; |
| size_t buffer_size; |
| |
| /* Compression */ |
| int compression_type; |
| ZstdCompressor zstd; |
| |
| /* Metadata */ |
| uint64_t start_time_us; |
| uint64_t sample_interval_us; |
| uint32_t total_samples; |
| |
| /* String hash table: PyObject* -> uint32_t index */ |
| _Py_hashtable_t *string_hash; |
| /* String storage: array of UTF-8 encoded strings */ |
| char **strings; |
| size_t *string_lengths; |
| size_t string_count; |
| size_t string_capacity; |
| |
| /* Frame hash table: FrameKey* -> uint32_t index */ |
| _Py_hashtable_t *frame_hash; |
| /* Frame storage: combined struct for better cache locality */ |
| FrameEntry *frame_entries; |
| size_t frame_count; |
| size_t frame_capacity; |
| |
| /* Thread timestamp tracking for delta encoding - combined for cache locality */ |
| ThreadEntry *thread_entries; |
| size_t thread_count; |
| size_t thread_capacity; |
| |
| /* Statistics */ |
| BinaryWriterStats stats; |
| } BinaryWriter; |
| |
| /* ============================================================================ |
| * BINARY READER STRUCTURES |
| * ============================================================================ */ |
| |
| /* Per-thread state for stack reconstruction during replay */ |
| typedef struct { |
| uint64_t thread_id; |
| uint32_t interpreter_id; |
| uint64_t prev_timestamp; |
| |
| /* Reconstructed stack buffer (frame indices, innermost first) */ |
| uint32_t *current_stack; |
| size_t current_stack_depth; |
| size_t current_stack_capacity; |
| } ReaderThreadState; |
| |
| /* Main binary reader structure */ |
| typedef struct { |
| char *filename; |
| |
| #if USE_MMAP |
| int fd; |
| uint8_t *mapped_data; |
| size_t mapped_size; |
| #else |
| FILE *fp; |
| uint8_t *file_data; |
| size_t file_size; |
| #endif |
| |
| /* Decompression state */ |
| int compression_type; |
| /* Note: ZSTD_DCtx is not stored - created/freed during decompression */ |
| uint8_t *decompressed_data; |
| size_t decompressed_size; |
| |
| /* Header metadata */ |
| uint8_t py_major; |
| uint8_t py_minor; |
| uint8_t py_micro; |
| int needs_swap; /* Non-zero if file was written on different-endian system */ |
| uint64_t start_time_us; |
| uint64_t sample_interval_us; |
| uint32_t sample_count; |
| uint32_t thread_count; |
| uint64_t string_table_offset; |
| uint64_t frame_table_offset; |
| |
| /* Parsed string table: array of Python string objects */ |
| PyObject **strings; |
| uint32_t strings_count; |
| |
| /* Parsed frame table: packed as [filename_idx, funcname_idx, lineno] */ |
| uint32_t *frame_data; |
| uint32_t frames_count; |
| |
| /* Sample data region */ |
| uint8_t *sample_data; |
| size_t sample_data_size; |
| |
| /* Per-thread state for stack reconstruction (used during replay) */ |
| ReaderThreadState *thread_states; |
| size_t thread_state_count; |
| size_t thread_state_capacity; |
| |
| /* Statistics */ |
| BinaryReaderStats stats; |
| } BinaryReader; |
| |
| /* ============================================================================ |
| * VARINT ENCODING/DECODING (INLINE FOR PERFORMANCE) |
| * ============================================================================ */ |
| |
| /* Encode unsigned 64-bit varint (LEB128). Returns bytes written. */ |
| static inline size_t |
| encode_varint_u64(uint8_t *buf, uint64_t value) |
| { |
| /* Fast path for single-byte values (0-127) - very common case */ |
| if (value < 0x80) { |
| buf[0] = (uint8_t)value; |
| return 1; |
| } |
| |
| size_t i = 0; |
| while (value >= 0x80) { |
| buf[i++] = (uint8_t)((value & 0x7F) | 0x80); |
| value >>= 7; |
| } |
| buf[i++] = (uint8_t)(value & 0x7F); |
| return i; |
| } |
| |
| /* Encode unsigned 32-bit varint. Returns bytes written. */ |
| static inline size_t |
| encode_varint_u32(uint8_t *buf, uint32_t value) |
| { |
| return encode_varint_u64(buf, value); |
| } |
| |
| /* Encode signed 32-bit varint (zigzag encoding). Returns bytes written. */ |
| static inline size_t |
| encode_varint_i32(uint8_t *buf, int32_t value) |
| { |
| /* Zigzag encode: map signed to unsigned */ |
| uint32_t zigzag = ((uint32_t)value << 1) ^ (uint32_t)(value >> 31); |
| return encode_varint_u32(buf, zigzag); |
| } |
| |
| /* Decode unsigned 64-bit varint (LEB128). Updates offset only on success. |
| * On error (overflow or incomplete), offset is NOT updated, allowing callers |
| * to detect errors via (offset == prev_offset) check. Sets PyErr on error. */ |
| static inline uint64_t |
| decode_varint_u64(const uint8_t *data, size_t *offset, size_t max_size) |
| { |
| size_t pos = *offset; |
| uint64_t result = 0; |
| int shift = 0; |
| |
| /* Fast path for single-byte varints (0-127) - most common case */ |
| if (LIKELY(pos < max_size && (data[pos] & 0x80) == 0)) { |
| *offset = pos + 1; |
| return data[pos]; |
| } |
| |
| while (pos < max_size) { |
| uint8_t byte = data[pos++]; |
| result |= (uint64_t)(byte & 0x7F) << shift; |
| if ((byte & 0x80) == 0) { |
| *offset = pos; |
| return result; |
| } |
| shift += 7; |
| if (UNLIKELY(shift >= 64)) { |
| PyErr_SetString(PyExc_ValueError, "Invalid or incomplete varint in binary data"); |
| return 0; |
| } |
| } |
| |
| PyErr_SetString(PyExc_ValueError, "Invalid or incomplete varint in binary data"); |
| return 0; |
| } |
| |
| /* Decode unsigned 32-bit varint. If value exceeds UINT32_MAX, treats as error. */ |
| static inline uint32_t |
| decode_varint_u32(const uint8_t *data, size_t *offset, size_t max_size) |
| { |
| size_t saved_offset = *offset; |
| uint64_t value = decode_varint_u64(data, offset, max_size); |
| if (PyErr_Occurred()) { |
| return 0; |
| } |
| if (UNLIKELY(value > UINT32_MAX)) { |
| *offset = saved_offset; |
| PyErr_SetString(PyExc_ValueError, "Invalid or incomplete varint in binary data"); |
| return 0; |
| } |
| return (uint32_t)value; |
| } |
| |
| /* Decode signed 32-bit varint (zigzag encoding). */ |
| static inline int32_t |
| decode_varint_i32(const uint8_t *data, size_t *offset, size_t max_size) |
| { |
| uint32_t zigzag = decode_varint_u32(data, offset, max_size); |
| if (PyErr_Occurred()) { |
| return 0; |
| } |
| return (int32_t)((zigzag >> 1) ^ -(int32_t)(zigzag & 1)); |
| } |
| |
| /* ============================================================================ |
| * SHARED UTILITY FUNCTIONS |
| * ============================================================================ */ |
| |
| /* Generic array growth - returns new pointer or NULL (sets PyErr_NoMemory) |
| * Includes overflow checking for capacity doubling and allocation size. */ |
| static inline void * |
| grow_array(void *ptr, size_t *capacity, size_t elem_size) |
| { |
| size_t old_cap = *capacity; |
| |
| /* Check for overflow when doubling capacity */ |
| if (old_cap > SIZE_MAX / 2) { |
| PyErr_SetString(PyExc_OverflowError, "Array capacity overflow"); |
| return NULL; |
| } |
| size_t new_cap = old_cap * 2; |
| |
| /* Check for overflow when calculating allocation size */ |
| if (new_cap > SIZE_MAX / elem_size) { |
| PyErr_SetString(PyExc_OverflowError, "Array allocation size overflow"); |
| return NULL; |
| } |
| |
| void *new_ptr = PyMem_Realloc(ptr, new_cap * elem_size); |
| if (new_ptr) { |
| *capacity = new_cap; |
| } else { |
| PyErr_NoMemory(); |
| } |
| return new_ptr; |
| } |
| |
| static inline int |
| grow_array_inplace(void **ptr_addr, size_t count, size_t *capacity, size_t elem_size) |
| { |
| if (count < *capacity) { |
| return 0; |
| } |
| void *tmp = grow_array(*ptr_addr, capacity, elem_size); |
| if (tmp == NULL) { |
| return -1; |
| } |
| *ptr_addr = tmp; |
| return 0; |
| } |
| |
| #define GROW_ARRAY(ptr, count, cap, type) \ |
| grow_array_inplace((void**)&(ptr), (count), &(cap), sizeof(type)) |
| |
| /* ============================================================================ |
| * BINARY WRITER API |
| * ============================================================================ */ |
| |
| /* |
| * Create a new binary writer. |
| * |
| * Arguments: |
| * filename: Path to output file |
| * sample_interval_us: Sampling interval in microseconds |
| * compression_type: COMPRESSION_NONE or COMPRESSION_ZSTD |
| * start_time_us: Start timestamp in microseconds (from time.monotonic() * 1e6) |
| * |
| * Returns: |
| * New BinaryWriter* on success, NULL on failure (PyErr set) |
| */ |
| BinaryWriter *binary_writer_create( |
| const char *filename, |
| uint64_t sample_interval_us, |
| int compression_type, |
| uint64_t start_time_us |
| ); |
| |
| /* |
| * Write a sample to the binary file. |
| * |
| * Arguments: |
| * writer: Writer from binary_writer_create |
| * stack_frames: List of InterpreterInfo struct sequences |
| * timestamp_us: Current timestamp in microseconds (from time.monotonic() * 1e6) |
| * |
| * Returns: |
| * 0 on success, -1 on failure (PyErr set) |
| */ |
| int binary_writer_write_sample( |
| BinaryWriter *writer, |
| PyObject *stack_frames, |
| uint64_t timestamp_us |
| ); |
| |
| /* |
| * Finalize and close the binary file. |
| * Writes string/frame tables, footer, and updates header. |
| * |
| * Arguments: |
| * writer: Writer to finalize |
| * |
| * Returns: |
| * 0 on success, -1 on failure (PyErr set) |
| */ |
| int binary_writer_finalize(BinaryWriter *writer); |
| |
| /* |
| * Destroy a binary writer and free all resources. |
| * Safe to call even if writer is partially initialized. |
| * |
| * Arguments: |
| * writer: Writer to destroy (may be NULL) |
| */ |
| void binary_writer_destroy(BinaryWriter *writer); |
| |
| /* ============================================================================ |
| * BINARY READER API |
| * ============================================================================ */ |
| |
| /* |
| * Open a binary file for reading. |
| * |
| * Arguments: |
| * filename: Path to input file |
| * |
| * Returns: |
| * New BinaryReader* on success, NULL on failure (PyErr set) |
| */ |
| BinaryReader *binary_reader_open(const char *filename); |
| |
| /* |
| * Replay samples from binary file through a collector. |
| * |
| * Arguments: |
| * reader: Reader from binary_reader_open |
| * collector: Python collector with collect() method |
| * progress_callback: Optional callable(current, total) or NULL |
| * |
| * Returns: |
| * Number of samples replayed on success, -1 on failure (PyErr set) |
| */ |
| Py_ssize_t binary_reader_replay( |
| BinaryReader *reader, |
| PyObject *collector, |
| PyObject *progress_callback |
| ); |
| |
| /* |
| * Get metadata about the binary file. |
| * |
| * Arguments: |
| * reader: Reader from binary_reader_open |
| * |
| * Returns: |
| * Dict with file metadata on success, NULL on failure (PyErr set) |
| */ |
| PyObject *binary_reader_get_info(BinaryReader *reader); |
| |
| /* |
| * Close a binary reader and free all resources. |
| * |
| * Arguments: |
| * reader: Reader to close (may be NULL) |
| */ |
| void binary_reader_close(BinaryReader *reader); |
| |
| /* ============================================================================ |
| * STATISTICS FUNCTIONS |
| * ============================================================================ */ |
| |
| /* |
| * Get writer statistics as a Python dict. |
| * |
| * Arguments: |
| * writer: Writer to get stats from |
| * |
| * Returns: |
| * Dict with statistics on success, NULL on failure (PyErr set) |
| */ |
| PyObject *binary_writer_get_stats(BinaryWriter *writer); |
| |
| /* |
| * Get reader statistics as a Python dict. |
| * |
| * Arguments: |
| * reader: Reader to get stats from |
| * |
| * Returns: |
| * Dict with statistics on success, NULL on failure (PyErr set) |
| */ |
| PyObject *binary_reader_get_stats(BinaryReader *reader); |
| |
| /* ============================================================================ |
| * UTILITY FUNCTIONS |
| * ============================================================================ */ |
| |
| /* |
| * Check if zstd compression is available. |
| * |
| * Returns: |
| * 1 if zstd available, 0 otherwise |
| */ |
| int binary_io_zstd_available(void); |
| |
| /* |
| * Get the best available compression type. |
| * |
| * Returns: |
| * COMPRESSION_ZSTD if available, COMPRESSION_NONE otherwise |
| */ |
| int binary_io_get_best_compression(void); |
| |
| #ifdef __cplusplus |
| } |
| #endif |
| |
| #endif /* Py_BINARY_IO_H */ |