mirror of
https://github.com/python/cpython.git
synced 2026-10-29 10:55:16 +08:00
* the kolektor * test * better test * news * =Q, move const to the base, not self.running * gh-151292: Track binary writer finalization state --------- Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
160 lines
5.1 KiB
Python
160 lines
5.1 KiB
Python
"""Thin Python wrapper around C binary writer for profiling data."""
|
|
|
|
import sys
|
|
import time
|
|
|
|
import _remote_debugging
|
|
|
|
from .collector import Collector
|
|
|
|
# Compression type constants (must match binary_io.h)
|
|
COMPRESSION_NONE = 0
|
|
COMPRESSION_ZSTD = 1
|
|
|
|
CAPTURE_FEATURES = {
|
|
"all_threads": 1 << 0,
|
|
"native": 1 << 1,
|
|
"gc": 1 << 2,
|
|
"opcodes": 1 << 3,
|
|
"blocking": 1 << 4,
|
|
}
|
|
|
|
|
|
def encode_capture_config(capture_config):
|
|
if capture_config is None:
|
|
return -1
|
|
return sum(
|
|
bit for name, bit in CAPTURE_FEATURES.items()
|
|
if capture_config.get(name, False)
|
|
)
|
|
|
|
|
|
def _resolve_compression(compression):
|
|
"""Resolve compression type from string or int.
|
|
|
|
Args:
|
|
compression: 'auto', 'zstd', 'none', or int (0/1)
|
|
|
|
Returns:
|
|
int: Compression type constant
|
|
"""
|
|
if isinstance(compression, int):
|
|
return compression
|
|
|
|
compression = compression.lower()
|
|
if compression == 'none':
|
|
return COMPRESSION_NONE
|
|
elif compression == 'zstd':
|
|
return COMPRESSION_ZSTD
|
|
elif compression == 'auto':
|
|
# Auto: use zstd if available, otherwise none
|
|
if _remote_debugging.zstd_available():
|
|
return COMPRESSION_ZSTD
|
|
return COMPRESSION_NONE
|
|
else:
|
|
raise ValueError(f"Unknown compression type: {compression}")
|
|
|
|
|
|
class BinaryCollector(Collector):
|
|
"""High-performance binary collector using C implementation.
|
|
|
|
This collector writes profiling data directly to a binary file format
|
|
with optional zstd compression. All I/O is performed in C for maximum
|
|
throughput.
|
|
|
|
The binary format uses string/frame deduplication and varint encoding
|
|
for efficient storage.
|
|
"""
|
|
|
|
def __init__(self, filename, sample_interval_usec, *, skip_idle=False,
|
|
compression='auto', mode=None, capture_config=None):
|
|
"""Create a new binary collector.
|
|
|
|
Args:
|
|
filename: Path to output binary file
|
|
sample_interval_usec: Sampling interval in microseconds
|
|
skip_idle: If True, skip idle threads (not used in binary format)
|
|
compression: 'auto', 'zstd', 'none', or int (0=none, 1=zstd)
|
|
mode: Profiling mode, or None if unknown
|
|
capture_config: Mapping of capture feature names to booleans, or
|
|
None if the capture configuration is unknown
|
|
"""
|
|
self.filename = filename
|
|
self.sample_interval_usec = sample_interval_usec
|
|
self.skip_idle = skip_idle
|
|
self.running = True
|
|
|
|
compression_type = _resolve_compression(compression)
|
|
start_time_us = int(time.monotonic() * 1_000_000)
|
|
self._writer = _remote_debugging.BinaryWriter(
|
|
filename, sample_interval_usec, start_time_us,
|
|
compression=compression_type,
|
|
mode=-1 if mode is None else mode,
|
|
capture_features=encode_capture_config(capture_config),
|
|
)
|
|
|
|
def collect(self, stack_frames, timestamp_us=None):
|
|
"""Collect profiling data from stack frames.
|
|
|
|
This passes stack_frames directly to the C writer which handles
|
|
all encoding and buffering.
|
|
|
|
Args:
|
|
stack_frames: List of InterpreterInfo objects from _remote_debugging
|
|
timestamp_us: Optional timestamp in microseconds. If not provided,
|
|
uses time.monotonic() to generate one.
|
|
"""
|
|
if not self.running:
|
|
return
|
|
if timestamp_us is None:
|
|
timestamp_us = int(time.monotonic() * 1_000_000)
|
|
try:
|
|
self._writer.write_sample(stack_frames, timestamp_us)
|
|
except OverflowError as e:
|
|
if not self._writer.limit_reached:
|
|
raise
|
|
self.running = False
|
|
print(f"Warning: {e}; stopping early and keeping the data "
|
|
"collected so far.",
|
|
file=sys.stderr)
|
|
|
|
def collect_failed_sample(self):
|
|
"""Record a failed sample attempt (no-op for binary format)."""
|
|
pass
|
|
|
|
def set_stats(self, sample_interval_usec, duration_sec, sample_rate,
|
|
error_rate=None, missed_samples=None, **kwargs):
|
|
"""Persist measured statistics for later replay."""
|
|
self._writer.set_stats(
|
|
duration_sec, sample_rate, error_rate, missed_samples
|
|
)
|
|
|
|
def export(self, filename=None):
|
|
"""Finalize and close the binary file.
|
|
|
|
Args:
|
|
filename: Ignored (binary files are written incrementally)
|
|
"""
|
|
self._writer.finalize()
|
|
return True
|
|
|
|
@property
|
|
def total_samples(self):
|
|
return self._writer.total_samples
|
|
|
|
def get_stats(self):
|
|
"""Get encoding statistics.
|
|
|
|
Returns:
|
|
Dict with encoding statistics including repeat/full/suffix/pop-push
|
|
record counts, frames written/saved, and compression ratio.
|
|
"""
|
|
return self._writer.get_stats()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
"""Finalize if the writer can still produce a valid file."""
|
|
return self._writer.__exit__(exc_type, exc_val, exc_tb)
|