python

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package python implements injection of CPython 3.12+'s perf trampoline (sys.activate_stack_trampoline) into running processes via ptrace, so that perf-agent can resolve Python JIT frames to qualnames without requiring the target to be launched with `python -X perf`.

Lifecycle: each profiling run activates the trampoline at start and deactivates at end. Activation is idempotent — re-running on the same process is safe and is the supported pattern for continuous profiling. If the target was launched with `python -X perf`, the deactivate-at-end will turn the trampoline off; users who want to keep `-X perf` always-on should not pass --inject-python.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotPython is returned when the target is not a CPython process.
	// Examples: a Go binary, a non-Python executable, or a process whose
	// libpython/exe lacks the interpreter symbols we need.
	ErrNotPython = errors.New("not a python process")

	// ErrPythonTooOld is returned when a Python process is detected but its
	// libpython SONAME indicates a version older than 3.12. The
	// sys.activate_stack_trampoline primitive does not exist on older versions.
	ErrPythonTooOld = errors.New("python version too old (need 3.12+)")

	// ErrNoPerfTrampoline is returned at activation time (not detection) when
	// PyRun_SimpleString returns non-zero — typically because the interpreter
	// was compiled without --enable-perf-trampoline. Distributors strip
	// internal symbols from production libpython, so we cannot detect this
	// from the symbol table; we rely on the runtime return value.
	ErrNoPerfTrampoline = errors.New("python activation refused (likely built without --enable-perf-trampoline)")

	// ErrStaticallyLinkedNoSymbols is returned when neither libpython nor
	// /proc/<pid>/exe exposes the libpython internal symbols needed for
	// remote calls (PyRun_SimpleString, PyGILState_Ensure, PyGILState_Release).
	ErrStaticallyLinkedNoSymbols = errors.New("python interpreter symbols not resolvable")
)

Detection-result sentinels. All are non-fatal: detection returns one of these (wrapped) when a process should be skipped without aborting the run.

Functions

func ActivatePayload

func ActivatePayload() []byte

ActivatePayload returns the byte slice (NUL-terminated) to write into the target's address space before calling PyRun_SimpleString. Caller must NOT mutate the returned slice.

func DeactivatePayload

func DeactivatePayload() []byte

DeactivatePayload returns the byte slice (NUL-terminated) for the shutdown deactivation call. Caller must NOT mutate the returned slice.

Types

type Detector

type Detector interface {
	Detect(pid uint32) (*Target, error)
}

Detector inspects a process and reports whether it is a Python 3.12+ candidate suitable for trampoline injection.

func NewDetector

func NewDetector(procRoot string, log *slog.Logger) Detector

NewDetector builds a /proc-based Detector. procRoot is configurable for testing; production callers pass "/proc". log may be nil (uses slog.Default()).

type LowLevelInjector

type LowLevelInjector interface {
	RemoteActivate(pid uint32, addrs SymbolAddrsForTarget) error
	RemoteDeactivate(pid uint32, addrs SymbolAddrsForTarget) error
}

LowLevelInjector is the contract Manager uses for the ptrace dance. The production implementation wraps inject/ptraceop.Injector; tests can stub it.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager orchestrates Python perf-trampoline injection across a profile run: detection ladder, strict/lenient policy, in-memory tracked-PID set, bounded shutdown deactivation, and idempotent late-arrival activation.

func NewManager

func NewManager(opts Options) *Manager

NewManager constructs a Manager.

Defaults:

  • opts.Logger → slog.Default() if nil
  • opts.DeactivateDeadline → 5 * time.Second if zero
  • opts.Detector → NewDetector("/proc", logger) if nil

opts.Injector has no in-package default — inject/python deliberately does not import inject/ptraceop, so the low-level injector must be supplied by the caller. perfagent.Agent wires it via the ptraceopBridge; tests inject stubs. NewManager panics with a clear message if Injector is nil; surfacing that wiring mistake at construction is friendlier than the deep NPE the first ActivateAll call would otherwise raise.

func (*Manager) ActivateAll

func (m *Manager) ActivateAll(pids []uint32) error

ActivateAll runs detection and activation for the given PIDs. Returns nil in lenient mode (errors are logged + counted); returns the first error in strict mode. Caller blocks on this; it is fine to call once at profile start before BPF attach.

func (*Manager) ActivateLate

func (m *Manager) ActivateLate(pid uint32)

ActivateLate is the mmap-watcher hook for new exec events during -a mode. Always lenient (logs + counts on error). Idempotent: dedupes by tracked and pending sets.

func (*Manager) DeactivateAll

func (m *Manager) DeactivateAll(ctx context.Context)

DeactivateAll runs the bounded shutdown deactivation pass. Tolerates ESRCH (process gone). Honors ctx cancellation AND the configured deactivation deadline (5s default).

func (*Manager) Stats

func (m *Manager) Stats() *Stats

Stats returns a pointer to the in-place atomic counters. Callers should treat it as read-only; mutate via the atomic methods if needed (currently only the package itself mutates them).

type Options

type Options struct {
	// StrictPerPID makes ActivateAll fail-fast on the first error. Used for
	// --pid N --inject-python; lenient (false) for -a --inject-python.
	StrictPerPID bool

	// Logger receives structured log lines for every detect/activate/deactivate
	// event. nil → slog.Default().
	Logger *slog.Logger

	// Injector overrides the default ptraceop-based injector. Tests inject
	// stubs; production passes nil and gets the real ptraceop.Injector.
	// In production this is wired by perfagent.Agent (Task 8) since this
	// package does not import inject/ptraceop directly.
	Injector LowLevelInjector

	// Detector overrides the default /proc-based detector. Tests inject stubs;
	// production passes nil.
	Detector Detector

	// DeactivateDeadline caps the total time spent in DeactivateAll. Default
	// 5 seconds.
	DeactivateDeadline time.Duration
}

Options configures a Manager.

type Stats

type Stats struct {
	Activated        atomic.Uint64
	Deactivated      atomic.Uint64
	SkippedNotPython atomic.Uint64
	SkippedTooOld    atomic.Uint64
	SkippedNoTramp   atomic.Uint64
	SkippedNoSymbols atomic.Uint64
	ActivateFailed   atomic.Uint64
	DeactivateFailed atomic.Uint64
}

Stats holds counters that operators inspect after a run. All counters are safe for concurrent use.

type SymbolAddrsForTarget

type SymbolAddrsForTarget struct {
	PyGILEnsure  uint64
	PyGILRelease uint64
	PyRunString  uint64
}

SymbolAddrsForTarget is the data the LowLevelInjector needs to perform one remote-call sequence — independent of inject/ptraceop's exact struct layout to keep the test boundary clean.

type Target

type Target struct {
	PID              uint32
	LibPythonPath    string // on-disk path used for ELF parsing
	LoadBase         uint64 // address from /proc/<pid>/maps for libpython
	PyGILEnsureAddr  uint64
	PyGILReleaseAddr uint64
	PyRunStringAddr  uint64
	Major, Minor     int // detected libpython version
}

Target describes a Python 3.12+ process that detection has confirmed is suitable for trampoline injection. All address fields are absolute remote addresses (load_base + symbol_offset) ready to be passed to ptraceop.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL