use py_spy::{config::Config, sampler::Sampler};
use pyroscope::{
backend::{
Backend, BackendConfig, BackendImpl, BackendUninitialized, Report, Rule, Ruleset,
StackBuffer, StackFrame, StackTrace,
},
error::{PyroscopeError, Result},
};
use std::{
ops::Deref,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread::JoinHandle,
};
const LOG_TAG: &str = "Pyroscope::Pyspy";
pub fn pyspy_backend(config: PyspyConfig) -> BackendImpl<BackendUninitialized> {
let backend_config = config.backend_config.clone();
BackendImpl::new(Box::new(Pyspy::new(config)), Some(backend_config))
}
#[derive(Debug, Clone)]
pub struct PyspyConfig {
pid: Option<i32>,
sample_rate: u32,
backend_config: BackendConfig,
lock_process: py_spy::config::LockingStrategy,
time_limit: Option<core::time::Duration>,
with_subprocesses: bool,
include_idle: bool,
gil_only: bool,
native: bool,
}
impl Default for PyspyConfig {
fn default() -> Self {
PyspyConfig {
pid: Some(0),
sample_rate: 100,
backend_config: BackendConfig::default(),
lock_process: py_spy::config::LockingStrategy::NonBlocking,
time_limit: None,
with_subprocesses: false,
include_idle: false,
gil_only: false,
native: false,
}
}
}
impl PyspyConfig {
pub fn new(pid: i32) -> Self {
PyspyConfig {
pid: Some(pid),
..Default::default()
}
}
pub fn sample_rate(self, sample_rate: u32) -> Self {
PyspyConfig {
sample_rate,
..self
}
}
pub fn report_pid(self) -> Self {
let backend_config = BackendConfig {
report_pid: true,
..self.backend_config
};
PyspyConfig {
backend_config,
..self
}
}
pub fn report_thread_id(self) -> Self {
let backend_config = BackendConfig {
report_thread_id: true,
..self.backend_config
};
PyspyConfig {
backend_config,
..self
}
}
pub fn report_thread_name(self) -> Self {
let backend_config = BackendConfig {
report_thread_name: true,
..self.backend_config
};
PyspyConfig {
backend_config,
..self
}
}
pub fn lock_process(self, lock_process: bool) -> Self {
PyspyConfig {
lock_process: if lock_process {
py_spy::config::LockingStrategy::Lock
} else {
py_spy::config::LockingStrategy::NonBlocking
},
..self
}
}
pub fn time_limit(self, time_limit: Option<core::time::Duration>) -> Self {
PyspyConfig { time_limit, ..self }
}
pub fn with_subprocesses(self, with_subprocesses: bool) -> Self {
PyspyConfig {
with_subprocesses,
..self
}
}
pub fn include_idle(self, include_idle: bool) -> Self {
PyspyConfig {
include_idle,
..self
}
}
pub fn gil_only(self, gil_only: bool) -> Self {
PyspyConfig { gil_only, ..self }
}
pub fn native(self, native: bool) -> Self {
PyspyConfig { native, ..self }
}
}
#[derive(Default)]
pub struct Pyspy {
buffer: Arc<Mutex<StackBuffer>>,
config: PyspyConfig,
sampler_config: Option<Config>,
sampler_thread: Option<JoinHandle<Result<()>>>,
running: Arc<AtomicBool>,
ruleset: Arc<Mutex<Ruleset>>,
}
impl std::fmt::Debug for Pyspy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Pyspy Backend")
}
}
impl Pyspy {
pub fn new(config: PyspyConfig) -> Self {
Pyspy {
buffer: Arc::new(Mutex::new(StackBuffer::default())),
config,
sampler_config: None,
sampler_thread: None,
running: Arc::new(AtomicBool::new(false)),
ruleset: Arc::new(Mutex::new(Ruleset::default())),
}
}
}
impl Backend for Pyspy {
fn spy_name(&self) -> Result<String> {
Ok("pyspy".to_string())
}
fn spy_extension(&self) -> Result<Option<String>> {
Ok(Some("cpu".to_string()))
}
fn sample_rate(&self) -> Result<u32> {
Ok(self.config.sample_rate)
}
fn set_config(&self, config: BackendConfig) {}
fn get_config(&self) -> Result<BackendConfig> {
Ok(self.config.backend_config)
}
fn add_rule(&self, rule: Rule) -> Result<()> {
self.ruleset.lock()?.add_rule(rule)?;
Ok(())
}
fn remove_rule(&self, rule: Rule) -> Result<()> {
self.ruleset.lock()?.remove_rule(rule)?;
Ok(())
}
fn initialize(&mut self) -> Result<()> {
if self.config.pid.is_none() {
return Err(PyroscopeError::new("Pyspy: No Process ID Specified"));
}
let duration = match self.config.time_limit {
Some(duration) => py_spy::config::RecordDuration::Seconds(duration.as_secs()),
None => py_spy::config::RecordDuration::Unlimited,
};
self.sampler_config = Some(Config {
blocking: self.config.lock_process.clone(),
native: self.config.native,
pid: self.config.pid,
sampling_rate: self.config.sample_rate as u64,
include_idle: self.config.include_idle,
include_thread_ids: true,
subprocesses: self.config.with_subprocesses,
gil_only: self.config.gil_only,
duration,
..Config::default()
});
let running = Arc::clone(&self.running);
running.store(true, Ordering::Relaxed);
let buffer = self.buffer.clone();
let config = self
.sampler_config
.clone()
.ok_or_else(|| PyroscopeError::new("Pyspy: Sampler configuration is not set"))?;
let ruleset = self.ruleset.clone();
let backend_config = self.config.backend_config.clone();
self.sampler_thread = Some(std::thread::spawn(move || {
let pid = config
.pid
.ok_or_else(|| PyroscopeError::new("Pyspy: PID is not set"))?;
let sampler = Sampler::new(pid, &config)
.map_err(|e| PyroscopeError::new(&format!("Pyspy: Sampler Error: {}", e)))?;
let sampler_output = sampler.take_while(|_x| running.load(Ordering::Relaxed));
for sample in sampler_output {
for trace in sample.traces {
if !(config.include_idle || trace.active) {
continue;
}
if config.gil_only && !trace.owns_gil {
continue;
}
let own_trace: StackTrace =
Into::<StackTraceWrapper>::into((trace.clone(), &backend_config)).into();
let stacktrace = own_trace + &ruleset.lock()?.clone();
buffer.lock()?.record(stacktrace)?;
}
}
Ok(())
}));
Ok(())
}
fn shutdown(self: Box<Self>) -> Result<()> {
log::trace!(target: LOG_TAG, "Shutting down sampler thread");
self.running.store(false, Ordering::Relaxed);
self.sampler_thread
.ok_or_else(|| PyroscopeError::new("Pyspy: Failed to unwrap Sampler Thread"))?
.join()
.unwrap_or_else(|_| Err(PyroscopeError::new("Pyspy: Failed to join sampler thread")))?;
Ok(())
}
fn report(&mut self) -> Result<Vec<Report>> {
let report: StackBuffer = self.buffer.lock()?.deref().to_owned();
let reports: Vec<Report> = report.into();
self.buffer.lock()?.clear();
Ok(reports)
}
}
struct StackFrameWrapper(StackFrame);
impl From<StackFrameWrapper> for StackFrame {
fn from(stack_frame: StackFrameWrapper) -> Self {
stack_frame.0
}
}
impl From<py_spy::Frame> for StackFrameWrapper {
fn from(frame: py_spy::Frame) -> Self {
StackFrameWrapper(StackFrame {
module: frame.module.clone(),
name: Some(frame.name.clone()),
filename: frame.short_filename.clone(),
relative_path: None,
absolute_path: Some(frame.filename.clone()),
line: Some(frame.line as u32),
})
}
}
struct StackTraceWrapper(StackTrace);
impl From<StackTraceWrapper> for StackTrace {
fn from(stack_trace: StackTraceWrapper) -> Self {
stack_trace.0
}
}
impl From<(py_spy::StackTrace, &BackendConfig)> for StackTraceWrapper {
fn from(arg: (py_spy::StackTrace, &BackendConfig)) -> Self {
let (stack_trace, config) = arg;
let stacktrace = StackTrace::new(
config,
Some(stack_trace.pid as u32),
Some(stack_trace.thread_id as u64),
stack_trace.thread_name.clone(),
stack_trace
.frames
.iter()
.map(|frame| Into::<StackFrameWrapper>::into(frame.clone()).into())
.collect(),
);
StackTraceWrapper(stacktrace)
}
}