rsprof_trace/
lib.rs

1//! Self-instrumentation library for rsprof.
2//!
3//! This crate provides CPU and heap profiling through self-instrumentation:
4//! - **CPU profiling**: Timer-based sampling using SIGPROF
5//! - **Heap profiling**: Custom allocator that tracks allocations
6//!
7//! # Usage
8//!
9//! Add to your `Cargo.toml`:
10//! ```toml
11//! [dependencies]
12//! rsprof-trace = { version = "0.1", features = ["profiling"] }
13//! ```
14//!
15//! Enable profiling with the `profiler!` macro:
16//! ```rust,ignore
17//! rsprof_trace::profiler!();  // CPU at 99Hz + heap profiling
18//! ```
19//!
20//! Or customize the CPU sampling frequency:
21//! ```rust,ignore
22//! rsprof_trace::profiler!(cpu = 199);  // CPU at 199Hz + heap profiling
23//! ```
24//!
25//! Build with frame pointers for accurate stack traces:
26//! ```bash
27//! RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release --features profiling
28//! ```
29//!
30//! When the `profiling` feature is disabled, the macro expands to a no-op
31//! allocator passthrough with zero overhead.
32
33#![no_std]
34
35extern crate alloc;
36
37// Include profiling module when any profiling feature is enabled
38#[cfg(any(feature = "heap", feature = "cpu"))]
39mod profiling;
40
41// Re-export CPU profiling functions
42#[cfg(feature = "cpu")]
43pub use profiling::{start_cpu_profiling, stop_cpu_profiling};
44
45// Stubs when CPU feature is disabled
46#[cfg(not(feature = "cpu"))]
47#[inline]
48pub fn start_cpu_profiling(_freq_hz: u32) {}
49
50#[cfg(not(feature = "cpu"))]
51#[inline]
52pub fn stop_cpu_profiling() {}
53
54/// A profiling allocator that wraps the system allocator.
55///
56/// The const generic `CPU_FREQ` specifies the CPU sampling frequency in Hz.
57/// Set to 0 to disable CPU profiling.
58///
59/// When the `heap` feature is enabled, this allocator captures
60/// allocation and deallocation events along with stack traces.
61/// CPU profiling (if enabled) starts automatically on the first allocation.
62///
63/// When profiling features are disabled, it's a zero-cost passthrough.
64pub struct ProfilingAllocator<const CPU_FREQ: u32 = 99>;
65
66impl<const CPU_FREQ: u32> ProfilingAllocator<CPU_FREQ> {
67    pub const fn new() -> Self {
68        Self
69    }
70}
71
72impl<const CPU_FREQ: u32> Default for ProfilingAllocator<CPU_FREQ> {
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78// Legacy alias for backwards compatibility
79pub type HeapProfiler = ProfilingAllocator<99>;
80
81#[cfg(not(feature = "heap"))]
82mod disabled {
83    use super::ProfilingAllocator;
84    use core::alloc::{GlobalAlloc, Layout};
85
86    unsafe impl<const CPU_FREQ: u32> GlobalAlloc for ProfilingAllocator<CPU_FREQ> {
87        #[inline]
88        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
89            unsafe { libc::malloc(layout.size()) as *mut u8 }
90        }
91
92        #[inline]
93        unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
94            unsafe { libc::free(ptr as *mut libc::c_void) }
95        }
96
97        #[inline]
98        unsafe fn realloc(&self, ptr: *mut u8, _layout: Layout, new_size: usize) -> *mut u8 {
99            unsafe { libc::realloc(ptr as *mut libc::c_void, new_size) as *mut u8 }
100        }
101
102        #[inline]
103        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
104            unsafe { libc::calloc(1, layout.size()) as *mut u8 }
105        }
106    }
107}
108
109#[cfg(feature = "heap")]
110mod enabled {
111    use super::ProfilingAllocator;
112    #[cfg(feature = "cpu")]
113    use super::profiling::start_cpu_profiling;
114    use super::profiling::{record_alloc, record_dealloc};
115    use core::alloc::{GlobalAlloc, Layout};
116    use core::sync::atomic::{AtomicBool, Ordering};
117
118    static CPU_INITIALIZED: AtomicBool = AtomicBool::new(false);
119
120    #[inline]
121    fn maybe_init_cpu<const FREQ: u32>() {
122        #[cfg(feature = "cpu")]
123        {
124            if FREQ > 0 && !CPU_INITIALIZED.swap(true, Ordering::SeqCst) {
125                start_cpu_profiling(FREQ);
126            }
127        }
128    }
129
130    unsafe impl<const CPU_FREQ: u32> GlobalAlloc for ProfilingAllocator<CPU_FREQ> {
131        // IMPORTANT: These must NOT be inlined!
132        // If inlined into libstd (which has no frame pointers), stack capture breaks.
133        #[inline(never)]
134        unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
135            maybe_init_cpu::<CPU_FREQ>();
136            let ptr = unsafe { libc::malloc(layout.size()) as *mut u8 };
137            if !ptr.is_null() {
138                record_alloc(ptr, layout.size());
139            }
140            ptr
141        }
142
143        #[inline(never)]
144        unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
145            record_dealloc(ptr, layout.size());
146            unsafe { libc::free(ptr as *mut libc::c_void) }
147        }
148
149        #[inline(never)]
150        unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
151            record_dealloc(ptr, layout.size());
152            let new_ptr = unsafe { libc::realloc(ptr as *mut libc::c_void, new_size) as *mut u8 };
153            if !new_ptr.is_null() {
154                record_alloc(new_ptr, new_size);
155            }
156            new_ptr
157        }
158
159        #[inline(never)]
160        unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
161            maybe_init_cpu::<CPU_FREQ>();
162            let ptr = unsafe { libc::calloc(1, layout.size()) as *mut u8 };
163            if !ptr.is_null() {
164                record_alloc(ptr, layout.size());
165            }
166            ptr
167        }
168    }
169}
170
171/// Enable profiling for your application.
172///
173/// This macro sets up both CPU and heap profiling with sensible defaults.
174/// CPU profiling starts automatically on the first allocation.
175/// When the `profiling` feature is disabled, it expands to a zero-cost no-op.
176///
177/// # Examples
178///
179/// ```rust,ignore
180/// // Default: CPU at 99Hz + heap profiling
181/// rsprof_trace::profiler!();
182///
183/// // Custom CPU frequency
184/// rsprof_trace::profiler!(cpu = 199);
185/// ```
186///
187/// # Build
188///
189/// Enable profiling at build time:
190/// ```bash
191/// RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release --features profiling
192/// ```
193#[macro_export]
194#[cfg(feature = "heap")]
195macro_rules! profiler {
196    () => {
197        $crate::profiler!(cpu = 99);
198    };
199    (cpu = $freq:expr) => {
200        #[global_allocator]
201        static __RSPROF_ALLOC: $crate::ProfilingAllocator<$freq> =
202            $crate::ProfilingAllocator::<$freq>::new();
203    };
204}
205
206/// No-op when heap feature is disabled (CPU-only not supported with this macro)
207#[macro_export]
208#[cfg(not(feature = "heap"))]
209macro_rules! profiler {
210    () => {};
211    (cpu = $freq:expr) => {};
212}