#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
pub mod attribute;
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
pub mod modules;
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
pub mod unwind;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(windows)]
mod windows;
pub mod stream;
use std::time::Duration;
pub const MAX_STACK_BYTES: usize = 256 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CaptureKind {
RawContext,
}
#[derive(Clone, Debug)]
pub struct ThreadSample {
pub os_tid: u64,
pub stack_pointer: u64,
pub instruction_pointer: u64,
pub frame_pointer: u64,
pub link_register: Option<u64>,
pub stack_bytes: Vec<u8>,
pub truncated: bool,
pub kind: CaptureKind,
pub frames: Vec<u64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SnapshotStats {
pub threads_total: u32,
pub threads_captured: u32,
pub threads_dropped: u32,
pub pause_nanos: u64,
}
#[derive(Clone, Debug, Default)]
pub struct Snapshot {
pub threads: Vec<ThreadSample>,
pub stats: SnapshotStats,
pub frames_resolved: bool,
}
impl Snapshot {
pub fn is_complete(&self) -> bool {
self.stats.threads_dropped == 0
}
pub fn pause(&self) -> Duration {
Duration::from_nanos(self.stats.pause_nanos)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SnapshotConfig {
pub max_stack_bytes: usize,
}
impl Default for SnapshotConfig {
fn default() -> Self {
Self {
max_stack_bytes: MAX_STACK_BYTES,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SnapshotError {
#[error("cooperative snapshot is not implemented for this platform yet")]
Unsupported,
#[error("snapshot failed: {0}")]
Os(#[from] std::io::Error),
#[error("Mach operation {operation} failed with kernel code {code}")]
Mach {
operation: &'static str,
code: i32,
},
}
pub fn capture_all_threads(config: &SnapshotConfig) -> Result<Snapshot, SnapshotError> {
#[cfg(windows)]
{
windows::capture(config)
}
#[cfg(target_os = "linux")]
{
linux::capture(config)
}
#[cfg(target_os = "macos")]
{
macos::capture(config)
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
{
let _ = config;
Err(SnapshotError::Unsupported)
}
}
pub fn capture_and_resolve(config: &SnapshotConfig) -> Result<Snapshot, SnapshotError> {
#[cfg(windows)]
{
let mut snapshot = capture_all_threads(config)?;
let modules = modules::enumerate_modules()?;
unwind::resolve_frames(&mut snapshot, &modules);
Ok(snapshot)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let mut snapshot = capture_all_threads(config)?;
unwind::resolve_frames_for_current_process(&mut snapshot)?;
Ok(snapshot)
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
{
let _ = config;
Err(SnapshotError::Unsupported)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_uses_the_documented_cap() {
assert_eq!(SnapshotConfig::default().max_stack_bytes, MAX_STACK_BYTES);
}
#[cfg(windows)]
#[test]
fn capture_and_resolve_produces_resolved_frames() {
let snapshot = capture_and_resolve(&SnapshotConfig::default()).expect("capture");
assert!(
snapshot.frames_resolved,
"the combined path must run the unwinder"
);
assert!(
snapshot.threads.iter().any(|t| !t.frames.is_empty()),
"at least one captured thread should yield frames"
);
}
#[test]
fn a_snapshot_with_drops_is_not_complete() {
let mut snap = Snapshot::default();
assert!(snap.is_complete());
snap.stats.threads_dropped = 1;
assert!(
!snap.is_complete(),
"a dropped thread must make the snapshot partial"
);
}
#[test]
fn raw_captures_report_frames_unresolved() {
let snap = Snapshot::default();
assert!(!snap.frames_resolved);
}
#[test]
fn pause_is_reported_in_wall_clock_terms() {
let snap = Snapshot {
stats: SnapshotStats {
pause_nanos: 1_500_000,
..Default::default()
},
..Default::default()
};
assert_eq!(snap.pause(), Duration::from_micros(1500));
}
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
#[test]
fn known_blocked_stack_contains_marker_frame() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
#[cfg(windows)]
#[allow(unsafe_code)]
fn current_tid() -> u64 {
u64::from(unsafe { winapi::um::processthreadsapi::GetCurrentThreadId() })
}
#[cfg(target_os = "linux")]
#[allow(unsafe_code)]
fn current_tid() -> u64 {
unsafe { libc::syscall(libc::SYS_gettid) as u64 }
}
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn current_tid() -> u64 {
let mut tid = 0u64;
let result = unsafe { libc::pthread_threadid_np(0, &mut tid) };
assert_eq!(result, 0, "pthread_threadid_np");
tid
}
#[cfg(not(target_arch = "x86_64"))]
#[inline(never)]
fn blocked_leaf(ready: &AtomicBool, stop: &AtomicBool) -> bool {
ready.store(true, Ordering::Release);
while !stop.load(Ordering::Acquire) {
std::hint::spin_loop();
std::hint::black_box(());
}
stop.load(Ordering::Acquire)
}
#[cfg(all(target_arch = "x86_64", windows))]
#[inline(never)]
#[allow(unsafe_code)]
fn blocked_leaf(ready: &AtomicBool, stop: &AtomicBool) -> bool {
ready.store(true, Ordering::Release);
let observed: u8;
unsafe {
std::arch::asm!(
"2:",
"mov {observed}, byte ptr [{stop}]",
"test {observed}, {observed}",
"je 2b",
stop = in(reg) stop.as_ptr(),
observed = out(reg_byte) observed,
options(nostack),
);
}
observed != 0
}
#[cfg(all(target_arch = "x86_64", not(windows)))]
#[unsafe(naked)]
#[allow(unsafe_code)]
extern "C" fn blocked_leaf(_ready: &AtomicBool, _stop: &AtomicBool) -> bool {
std::arch::naked_asm!(
".cfi_startproc",
"push rbp",
".cfi_def_cfa_offset 16",
".cfi_offset rbp, -16",
"mov rbp, rsp",
".cfi_def_cfa_register rbp",
"mov byte ptr [rdi], 1",
"2:",
"mov al, byte ptr [rsi]",
"test al, al",
"je 2b",
"pop rbp",
".cfi_def_cfa rsp, 8",
"ret",
".cfi_endproc",
);
}
#[inline(never)]
fn blocked_marker(ready: &AtomicBool, stop: &AtomicBool) {
let observed = blocked_leaf(ready, stop);
ready.store(observed, Ordering::Release);
}
let ready = Arc::new(AtomicBool::new(false));
let stop = Arc::new(AtomicBool::new(false));
let (tx, rx) = mpsc::sync_channel(1);
let worker = {
let ready = Arc::clone(&ready);
let stop = Arc::clone(&stop);
std::thread::Builder::new()
.name("blocked_marker".into())
.spawn(move || {
tx.send(current_tid()).unwrap();
blocked_marker(&ready, &stop);
})
.unwrap()
};
let tid = rx.recv().unwrap();
while !ready.load(Ordering::Acquire) {
std::thread::yield_now();
}
let snapshot = capture_and_resolve(&SnapshotConfig::default()).expect("capture + unwind");
stop.store(true, Ordering::Release);
worker.join().unwrap();
let sample = snapshot
.threads
.iter()
.find(|sample| sample.os_tid == tid)
.unwrap_or_else(|| panic!("named worker {tid} absent from snapshot"));
let marker = blocked_marker as *const () as usize as u64;
assert!(
sample
.frames
.iter()
.skip(1)
.any(|frame| frame.abs_diff(marker) < 4096),
"unwound caller frames did not contain blocked_marker near {marker:#x} \
(captured ip={:#x}): {:?}",
sample.instruction_pointer,
sample.frames,
);
}
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
#[test]
fn unimplemented_platforms_report_unsupported_not_empty() {
assert!(matches!(
capture_all_threads(&SnapshotConfig::default()),
Err(SnapshotError::Unsupported)
));
}
}