mirror of
https://github.com/python/cpython.git
synced 2026-10-29 10:55:16 +08:00
+2








4ceb3faf2c
* [3.15] gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) (#158813) gh-153364: Make frame, coroutine, and task-waiter chain walks iterative and bounded (GH-153365) * let me declare single limit * use our new limit in process_frame_chain() * add it in parse_async_frame_chain() * parse_coro_chain() * NEWS * async in the message? * test * no race * process_task_awaited_by * process_task_awaited_by limit test * NEWS * MAX_TASK_WAITER_CHAIN_DEPTH * TASK_WAITER_CHAIN_DEPTH in test * TASK_WAITER_CHAIN_DEPTH 256 * prevent the drift with the comment * better naming, better style * MAX_TASK_WAITER_CHAIN_DEPTH comment * task-waiter iterative bfs walk * iterative coro-walk * nicer news * 1 << 14 * comment * unused read_Py_ssize_t * fix tombstones * simplify * correct msg * better test * news for tombstones * left-over from when testing buggy version * redundant new line (cherry picked from commite0861c6ae7) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commit9e401cf7f5) * [3.15] gh-155811: Add a seqcount to `gc_stats` to prevent torn reads (GH-155828) (#158829) * update_seq * no need for XCHGL, MOVL is enough? * gh-155811: Retry an inconsistent GC snapshot once --------- (cherry picked from commit5fecd448bb) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit028124039c) * [3.15] gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) (#158830) gh-151292: `_remote_debugging`: Do not corrupt the binary file when hitting `OverflowError` (GH-152892) * the kolektor * test * better test * news * =Q, move const to the base, not self.running * gh-151292: Track binary writer finalization state --------- (cherry picked from commitf839c061d3) Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit8a7c23f316) * [3.15] gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (#158691) gh-158583: Fix uninitialized memory read in bytes.fromhex() (GH-158584) (cherry picked from commit9d22a5334b) Co-authored-by: Victor Stinner <vstinner@python.org> (cherry picked from commit4f7af46e25) * [3.15] gh-154194: Degrade frames in Tachyon instead of failing the sample (GH-154195) (#158831) * gh-154194: Degrade frames in Tachyon instead of failing the sample (#154195) * degrade gracefully * news * better NEWS wording * do not raise on MAX_REMOTE_STR_READ * bye MAX_REMOTE_STR_READ * fix -m asyncio ps|pstree * test truncation and linetable sentinel * simpler * simpler * redundant now * respect #157790 in the news --------- Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit7d25916b41) * Preserve the stable ABI when creating fallback frame names --------- Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> (cherry picked from commitc27f494061) * [3.15] Add MSan to CI (GH-158625) (#158832) Run a job that the test suite with MSan to the CI (#158625) * Run the test suite with MSan in CI * Additional fixes * Add `_Py_MSAN_UNPOISON_STRING` * Apply Victor's suggestions * Apply Victor's suggestions --------- (cherry picked from commitb93fb19a6e) Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Victor Stinner <victor.stinner@gmail.com> (cherry picked from commit1bc78dea18) * [3.15] gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (#156814) gh-156810: Write the profiler's collapsed-stack export as UTF-8 (GH-156811) (cherry picked from commitc3706f4c68) Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> (cherry picked from commitfce28da9b7) * [3.15] gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) (#158845) gh-158552: Wait for Windows threads to suspend before blocking sampling (GH-158802) * gh-158552: Wait for Windows threads to suspend before blocking sampling * Use a named Windows thread enumeration status constant (cherry picked from commit1643525f0f) Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> (cherry picked from commit31288da21d) * [3.15] gh-152721: Fix quadratic RLE replay time in the profiling binary reader (GH-152722) (#158850) Backport of GH-152722. Co-authored-by: tonghuaroot <tonghuaroot@gmail.com> (cherry picked from commitebaca2ac2d) * [3.15] gh-156545: Fix flamegraph export RecursionError on deeply recursive programs (GH-156546) (#158851) Backport of GH-156546. Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> (cherry picked from commit0ac7217040) * [3.15] gh-158540: Add the profiled script's directory to sys.path (GH-158548) (#158844) gh-158540: Add the profiled script's directory to sys.path (GH-158548) * gh-158540: Add the profiled script's directory to sys.path When a script is profiled with ``python -m profiling.sampling run script.py`` from another directory, the script cannot import modules placed next to it, because ``_sync_coordinator._execute_script()`` executes it with the working directory (added by ``_setup_environment()`` for the module case) as ``sys.path[0]`` instead of the script's own directory. Make the script's directory importable in ``_execute_script()``, matching the behavior of ``python script.py``. Add a regression test that runs the coordinator on a script importing a sibling module. * Update _sync_coordinator.py Comment simplified * gh-158540: Resolve symlinks when adding the script directory to sys.path ``python script.py`` resolves symlinks when computing ``sys.path[0]``, so a script reached through a symlink (``link.py -> sub/where.py``) imports modules from the real script's directory. Apply ``os.path.realpath()`` before taking the directory name, and make sure the result is placed at the front of ``sys.path`` even if it was already listed. Add a regression test for a symlinked script. --------- (cherry picked from commit3f02aab9fa) Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com> Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com> (cherry picked from commitf52d831397) * [3.15] gh-153838: Skip non-regular source files in the heatmap exporter (GH-153839) (#158853) Backport of GH-153839. Co-authored-by: tonghuaroot <tonghuaroot@gmail.com> (cherry picked from commit48998df251) * [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) (#158852) * [3.15] gh-158539: Fix exception mode missing handlers in generators/coroutines (GH-158581) Backport of GH-158581. Co-authored-by: LucasZhou <donghao.zhou@outlook.com> * [3.15] gh-158539: Use portable static assertion messages * [3.15] gh-158539: Keep layout assertions with debug-offset validation * [3.15] gh-158539: Use the platform guard for in-process inspection tests --------- Co-authored-by: LucasZhou <donghao.zhou@outlook.com> (cherry picked from commitd625ecbb8d) * [3.15] gh-158522: Fix truncated stack for a task whose coroutine recurses (GH-158526) (#158870) Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> (cherry picked from commita904b397ed) * gh-156545, gh-158539: Fix deep flamegraph export on small C stacks and macOS runtime lookup (#158874) (cherry picked from commit114de198c0) Include the C-stack test helper from main, introduced byce5ae29ef9, which the regression test requires but 3.15 does not yet provide. --------- Co-authored-by: Miss Islington (bot) <31488909+miss-islington@users.noreply.github.com> Co-authored-by: Maurycy Pawłowski-Wieroński <maurycy@maurycy.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Victor Stinner <vstinner@python.org> Co-authored-by: Stan Ulbrych <stan@python.org> Co-authored-by: Victor Stinner <victor.stinner@gmail.com> Co-authored-by: tonghuaroot (童话) <tonghuaroot@gmail.com> Co-authored-by: he_tao <53343436+hetaozdh@users.noreply.github.com> Co-authored-by: Eduardo Villalpando Mello <eduardo.villalpando.mello@gmail.com> Co-authored-by: LucasZhou <donghao.zhou@outlook.com> Co-authored-by: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com>
755 lines
25 KiB
C
755 lines
25 KiB
C
/******************************************************************************
|
|
* 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
|
|
|
|
/* Sentinel values for optional frame fields */
|
|
#define OPCODE_NONE 255 /* No opcode captured (u8 sentinel) */
|
|
#define LOCATION_NOT_AVAILABLE (-1) /* lineno/column not available (zigzag sentinel) */
|
|
|
|
/* 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 8
|
|
#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 HDR_OFF_CONFIG (HDR_OFF_COMPRESSION + HDR_SIZE_COMPRESSION)
|
|
#define HDR_SIZE_CONFIG 4
|
|
#define FILE_HEADER_SIZE (HDR_OFF_CONFIG + HDR_SIZE_CONFIG)
|
|
#define FILE_HEADER_PLACEHOLDER_SIZE 64
|
|
|
|
static_assert(FILE_HEADER_SIZE <= FILE_HEADER_PLACEHOLDER_SIZE,
|
|
"FILE_HEADER_SIZE exceeds FILE_HEADER_PLACEHOLDER_SIZE");
|
|
|
|
/* Sample header field offsets and sizes */
|
|
#define SMP_OFF_THREAD_ID 0
|
|
#define SMP_SIZE_THREAD_ID sizeof(uint64_t)
|
|
#define SMP_OFF_INTERPRETER_ID (SMP_OFF_THREAD_ID + SMP_SIZE_THREAD_ID)
|
|
#define SMP_SIZE_INTERPRETER_ID sizeof(uint32_t)
|
|
#define SMP_OFF_ENCODING (SMP_OFF_INTERPRETER_ID + SMP_SIZE_INTERPRETER_ID)
|
|
#define SMP_SIZE_ENCODING sizeof(uint8_t)
|
|
#define SAMPLE_HEADER_FIXED_SIZE (SMP_OFF_ENCODING + SMP_SIZE_ENCODING)
|
|
|
|
static_assert(SAMPLE_HEADER_FIXED_SIZE == 13,
|
|
"SAMPLE_HEADER_FIXED_SIZE must remain 13");
|
|
|
|
/* Footer field offsets and sizes */
|
|
#define FTR_OFF_STRINGS 0
|
|
#define FTR_SIZE_STRINGS sizeof(uint32_t)
|
|
#define FTR_OFF_FRAMES (FTR_OFF_STRINGS + FTR_SIZE_STRINGS)
|
|
#define FTR_SIZE_FRAMES sizeof(uint32_t)
|
|
#define FTR_OFF_FILE_SIZE (FTR_OFF_FRAMES + FTR_SIZE_FRAMES)
|
|
#define FTR_SIZE_FILE_SIZE sizeof(uint64_t)
|
|
#define FTR_OFF_CHECKSUM (FTR_OFF_FILE_SIZE + FTR_SIZE_FILE_SIZE)
|
|
#define FTR_SIZE_CHECKSUM (2 * sizeof(uint64_t))
|
|
#define FILE_FOOTER_SIZE (FTR_OFF_CHECKSUM + FTR_SIZE_CHECKSUM)
|
|
|
|
static_assert(FILE_FOOTER_SIZE == 32,
|
|
"FILE_FOOTER_SIZE must remain 32");
|
|
|
|
/* Optional profiling statistics immediately precede the footer. The
|
|
* signature and size live at the end so readers can discover extensions
|
|
* without changing the fixed header or moving streamed sample data. */
|
|
#define PROFILE_STATS_MAGIC "TACHSTAT"
|
|
#define PROFILE_STATS_MAGIC_SIZE 8
|
|
#define PROFILE_STATS_VERSION 1
|
|
#define PST_OFF_DURATION 0
|
|
#define PST_SIZE_DURATION 8
|
|
#define PST_OFF_SAMPLE_RATE (PST_OFF_DURATION + PST_SIZE_DURATION)
|
|
#define PST_SIZE_SAMPLE_RATE 8
|
|
#define PST_OFF_ERROR_RATE (PST_OFF_SAMPLE_RATE + PST_SIZE_SAMPLE_RATE)
|
|
#define PST_SIZE_ERROR_RATE 8
|
|
#define PST_OFF_MISSED_SAMPLES (PST_OFF_ERROR_RATE + PST_SIZE_ERROR_RATE)
|
|
#define PST_SIZE_MISSED_SAMPLES 8
|
|
#define PST_OFF_PRESENT (PST_OFF_MISSED_SAMPLES + PST_SIZE_MISSED_SAMPLES)
|
|
#define PST_SIZE_PRESENT 4
|
|
#define PST_OFF_RESERVED (PST_OFF_PRESENT + PST_SIZE_PRESENT)
|
|
#define PST_SIZE_RESERVED 4
|
|
#define PST_OFF_MAGIC (PST_OFF_RESERVED + PST_SIZE_RESERVED)
|
|
#define PST_OFF_VERSION (PST_OFF_MAGIC + PROFILE_STATS_MAGIC_SIZE)
|
|
#define PST_SIZE_VERSION 4
|
|
#define PST_OFF_SIZE (PST_OFF_VERSION + PST_SIZE_VERSION)
|
|
#define PST_SIZE_SIZE 4
|
|
#define PROFILE_STATS_SIZE (PST_OFF_SIZE + PST_SIZE_SIZE)
|
|
#define PROFILE_STATS_V1_SIZE 32
|
|
#define PROFILE_STATS_ERROR_RATE 0x01
|
|
#define PROFILE_STATS_MISSED 0x02
|
|
|
|
static_assert(PROFILE_STATS_SIZE == 56,
|
|
"PROFILE_STATS_SIZE must remain 56");
|
|
|
|
/* Minimum on-disk bytes of a string (1) and frame (7) table entry. */
|
|
#define MIN_STRING_ENTRY_SIZE 1
|
|
#define MIN_FRAME_ENTRY_SIZE 7
|
|
|
|
/* 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
|
|
|
|
/* Profiling configuration. The mode occupies the low three bits and is
|
|
* stored plus one so zero remains compatible with files written before this
|
|
* field was defined. Capture features are valid when bit 3 is set. */
|
|
#define PROFILING_CONFIG_MODE_MASK 0x7U
|
|
#define PROFILING_CONFIG_FEATURES_KNOWN (1U << 3)
|
|
#define PROFILING_CONFIG_FEATURES_SHIFT 4
|
|
#define PROFILING_FEATURE_ALL_THREADS (1U << 0)
|
|
#define PROFILING_FEATURE_NATIVE (1U << 1)
|
|
#define PROFILING_FEATURE_GC (1U << 2)
|
|
#define PROFILING_FEATURE_OPCODES (1U << 3)
|
|
#define PROFILING_FEATURE_BLOCKING (1U << 4)
|
|
#define PROFILING_FEATURE_MASK 0x1FU
|
|
|
|
/* 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 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.
|
|
* Stores full source position (line, end_line, column, end_column) and opcode.
|
|
* Delta values are computed during serialization for efficiency. */
|
|
typedef struct {
|
|
uint32_t filename_idx;
|
|
uint32_t funcname_idx;
|
|
int32_t lineno; /* Start line number (-1 for synthetic frames) */
|
|
int32_t end_lineno; /* End line number (-1 if not available) */
|
|
int32_t column; /* Start column in UTF-8 bytes (-1 if not available) */
|
|
int32_t end_column; /* End column in UTF-8 bytes (-1 if not available) */
|
|
uint8_t opcode; /* Python opcode (0-254) or OPCODE_NONE (255) */
|
|
} FrameEntry;
|
|
|
|
/* Frame key for hash table lookup - includes all fields for proper deduplication */
|
|
typedef struct {
|
|
uint32_t filename_idx;
|
|
uint32_t funcname_idx;
|
|
int32_t lineno;
|
|
int32_t end_lineno;
|
|
int32_t column;
|
|
int32_t end_column;
|
|
uint8_t opcode;
|
|
} FrameKey;
|
|
|
|
/* 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 */
|
|
uint8_t *pending_rle;
|
|
size_t pending_rle_bytes;
|
|
size_t pending_rle_samples;
|
|
} ThreadEntry;
|
|
|
|
/* Limit errors occur before emitting an incomplete sample. Other write
|
|
* failures may leave partial records and must prevent finalization. */
|
|
typedef enum {
|
|
BINARY_WRITER_OPEN,
|
|
BINARY_WRITER_LIMIT_REACHED,
|
|
BINARY_WRITER_BROKEN,
|
|
} BinaryWriterState;
|
|
|
|
/* Main binary writer structure */
|
|
typedef struct {
|
|
FILE *fp;
|
|
BinaryWriterState state;
|
|
|
|
/* 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;
|
|
uint64_t total_samples;
|
|
double duration_sec;
|
|
double sample_rate;
|
|
double error_rate;
|
|
double missed_samples;
|
|
uint32_t profile_stats_present;
|
|
int has_profile_stats;
|
|
int profiling_mode;
|
|
int capture_features;
|
|
|
|
/* 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 {
|
|
#if USE_MMAP
|
|
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;
|
|
uint64_t sample_count;
|
|
uint32_t thread_count;
|
|
double duration_sec;
|
|
double sample_rate;
|
|
double error_rate;
|
|
double missed_samples;
|
|
uint32_t profile_stats_present;
|
|
int has_profile_stats;
|
|
uint64_t string_table_offset;
|
|
uint64_t frame_table_offset;
|
|
int profiling_mode;
|
|
int capture_features;
|
|
|
|
/* Parsed string table: array of Python string objects */
|
|
PyObject **strings;
|
|
uint32_t strings_count;
|
|
|
|
/* Parsed frame table: array of FrameEntry structures */
|
|
FrameEntry *frames;
|
|
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 (*offset == saved_offset) {
|
|
return 0; /* decode_varint_u64 already set PyErr */
|
|
}
|
|
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)
|
|
{
|
|
size_t saved_offset = *offset;
|
|
uint32_t zigzag = decode_varint_u32(data, offset, max_size);
|
|
if (*offset == saved_offset) {
|
|
return 0; /* decode_varint_u32 already set PyErr */
|
|
}
|
|
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:
|
|
* path: 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)
|
|
* profiling_mode: PROFILING_MODE_* value, or -1 if unknown
|
|
* capture_features: PROFILING_FEATURE_* bit mask, or -1 if unknown
|
|
*
|
|
* Returns:
|
|
* New BinaryWriter* on success, NULL on failure (PyErr set)
|
|
*/
|
|
BinaryWriter *binary_writer_create(
|
|
PyObject *path,
|
|
uint64_t sample_interval_us,
|
|
int compression_type,
|
|
uint64_t start_time_us,
|
|
int profiling_mode,
|
|
int capture_features
|
|
);
|
|
|
|
/*
|
|
* 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);
|
|
|
|
/* Store measured statistics to write during finalization. */
|
|
int binary_writer_set_stats(
|
|
BinaryWriter *writer,
|
|
double duration_sec,
|
|
double sample_rate,
|
|
double error_rate,
|
|
double missed_samples,
|
|
uint32_t present
|
|
);
|
|
|
|
/*
|
|
* 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:
|
|
* path: Path to input file
|
|
*
|
|
* Returns:
|
|
* New BinaryReader* on success, NULL on failure (PyErr set)
|
|
*/
|
|
BinaryReader *binary_reader_open(PyObject *path);
|
|
|
|
/*
|
|
* 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 */
|