#![doc = include_str!("../docs/api.md")]
#![cfg(target_os = "linux")]
#![warn(missing_docs)]
#![warn(rustdoc::broken_intra_doc_links)]
#[cfg(any(test, feature = "bench-support"))]
#[doc(hidden)]
pub mod bench_support;
pub mod children;
pub mod docs;
mod elf;
pub mod error;
pub mod identity;
mod linux;
mod module_base;
mod native_module;
mod proc_maps;
pub mod profile;
pub mod spool;
pub mod state;
mod stats;
pub mod symbolize;
mod symbols;
#[cfg(test)]
mod test_support;
pub use error::{Error, ErrorKind, Result};
pub use identity::{Pid, Tid};
pub use linux::{process, AttachMode, Recorder, RecorderOptions, RecordingSummary, SampleRate};
pub use spool::{Replay, Snapshot, Tail};
pub use symbolize::{StackCache, Symbolizer, SymbolizerBuilder};
const _: fn() = || {
fn assert_send<T: Send>() {}
fn assert_send_sync<T: Send + Sync>() {}
assert_send::<Recorder>();
assert_send_sync::<Replay>();
assert_send_sync::<Snapshot>();
assert_send_sync::<Tail>();
assert_send_sync::<process::RunningProcess>();
assert_send_sync::<process::SuspendedLaunchedProcess>();
};
pub mod record {
use std::io;
pub use crate::linux::perf_event::{PerfFrequencyLimit, MAX_SAMPLE_USER_STACK};
pub use crate::linux::{
AttachMode, AttachOutcome, PollSummary, Recorder, RecorderOptions, RecordingSummary,
RefreshOutcome, SampleRate,
};
pub use crate::stats::{SampleErrorKind, SampleErrorStats};
#[must_use]
pub fn max_sample_rate() -> Option<u64> {
read_max_sample_rate().ok()
}
pub(crate) fn read_max_sample_rate() -> io::Result<u64> {
const PATH: &str = "/proc/sys/kernel/perf_event_max_sample_rate";
let data = std::fs::read_to_string(PATH).map_err(|source| {
let kind = source.kind();
io::Error::new(kind, MaxSampleRateError::Read { source })
})?;
data.trim().parse().map_err(|source| {
io::Error::new(
io::ErrorKind::InvalidData,
MaxSampleRateError::Parse { source },
)
})
}
#[derive(Debug, thiserror::Error)]
enum MaxSampleRateError {
#[error("failed to read /proc/sys/kernel/perf_event_max_sample_rate: {source}")]
Read {
#[source]
source: io::Error,
},
#[error("/proc/sys/kernel/perf_event_max_sample_rate is not an integer: {source}")]
Parse {
#[source]
source: std::num::ParseIntError,
},
}
}
#[must_use]
pub(crate) fn path_name(path: &std::path::Path) -> &str {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or_else(|| path.to_str().unwrap_or("<unknown>"))
}
#[must_use]
pub(crate) fn is_python_module(name: &str) -> bool {
is_python_executable_name(name) || lib_name_matches_libpython(name)
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
pub struct ReadmeDoctests;
pub(crate) fn is_python_runtime_module_path(module_path: impl AsRef<std::path::Path>) -> bool {
let module_path = module_path.as_ref();
module_path
.file_name()
.unwrap_or(module_path.as_os_str())
.to_str()
.is_some_and(is_python_module)
}
#[inline]
fn strip_ascii_prefix<'a>(value: &'a str, prefix: &str) -> Option<&'a str> {
let head = value.get(..prefix.len())?;
if head.eq_ignore_ascii_case(prefix) {
value.get(prefix.len()..)
} else {
None
}
}
#[inline]
fn is_dotted_numeric(value: &str) -> bool {
!value.is_empty()
&& value
.split('.')
.all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
}
#[inline]
fn is_supported_python_abi_tag(b: u8) -> bool {
matches!(b, b'd' | b't')
}
#[inline]
fn is_python_version_with_optional_abi_suffix(value: &str) -> bool {
let version_end = value
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(value.len());
let (version, abi_suffix) = value.split_at(version_end);
let version_ok = if version.contains('.') {
is_dotted_numeric(version)
} else {
version.len() == 1 && version.bytes().all(|b| b.is_ascii_digit())
};
version_ok && abi_suffix.bytes().all(is_supported_python_abi_tag)
}
#[inline]
fn is_python_executable_name(basename: &str) -> bool {
match strip_ascii_prefix(basename, "python") {
Some(rest) => rest.is_empty() || is_python_version_with_optional_abi_suffix(rest),
None => false,
}
}
fn lib_name_matches_libpython(lib: &str) -> bool {
let Some(rest) = strip_ascii_prefix(lib, "libpython") else {
return false;
};
if let Some(pos) = rest.find(".so") {
let version = &rest[..pos];
let tail = &rest[pos + 3..];
if is_python_version_with_optional_abi_suffix(version)
&& (tail.is_empty() || tail.starts_with('.'))
{
return true;
}
}
if let Some(pos) = rest.find(".dylib") {
let version = &rest[..pos];
let tail = &rest[pos + ".dylib".len()..];
if is_python_version_with_optional_abi_suffix(version) && tail.is_empty() {
return true;
}
}
false
}