embedded_profiling/
lib.rs

1//! # Embedded-Profiling
2//!
3//! A lightweight framework for profiling functions, geared towards
4//! `no-std` embedded environments. Initialization is very similar
5//! to how the `log` crate is initialized. By default, there is a
6//! no-op profiler that does nothing until you call [`set_profiler`].
7//! Once your profiler has been installed, your profiling
8//! functionality will be in use.
9//!
10//! ## Usage
11//!
12//! You can manually start & end your snapshot:
13//! ```
14//! let start = embedded_profiling::start_snapshot();
15//! // (...) some expensive computation
16//! if let Some(snapshot) = embedded_profiling::end_snapshot(start, "name-of-computation") {
17//!     // Optionally, log it if we didn't overflow
18//!     embedded_profiling::log_snapshot(&snapshot);
19//! }
20//! ```
21//!
22//! Or profile some code in a closure:
23//! ```
24//! embedded_profiling::profile("profile println", || {
25//!     println!("profiling this closure");
26//! });
27//! ```
28//!
29//! ## With a Procedural Macro
30//!
31//! With the `proc-macros` feature enabled, you can simply annotate
32//! the target function with the procedural macro
33//! [`profile_function`](embedded_profiling_proc_macros::profile_function).
34//! Note that you must first set your profiler with the [`set_profiler`]
35//! function.
36//! ```
37//! # #[cfg(feature = "proc-macros")]
38//! #[embedded_profiling::profile_function]
39//! fn my_long_running_function() {
40//!     println!("Hello, world!");
41//! }
42//! ```
43//!
44//! ## Features
45//!
46//! ### `container-u64`
47//!
48//! Use a [`u64`] as the time storage type instead of [`u32`] for longer running profiling.
49//!
50//! ### `proc-macros`
51//!
52//! enables the `proc-macros` feature in [`embedded-profiling`](self). Enables
53//! the [`embedded_profiling::profile_function`](self::profile_function) procedural macro.
54//!
55#![warn(missing_docs)]
56#![cfg_attr(not(test), no_std)]
57
58use core::sync::atomic::{AtomicU8, Ordering};
59
60#[cfg(test)]
61mod mock;
62#[cfg(feature = "proc-macros")]
63pub use embedded_profiling_proc_macros::profile_function;
64
65pub use fugit;
66
67// do the feature gating on a private type so our public documentation is only in one place
68#[cfg(not(feature = "container-u64"))]
69type PrivContainer = u32;
70#[cfg(feature = "container-u64")]
71type PrivContainer = u64;
72
73/// The underlying container of our [`Duration`](fugit::Duration)/[`Instant`](fugit::Instant) types.
74/// Can be either `u32` or `u64`, depending on features (default: `u32`).
75pub type EPContainer = PrivContainer;
76
77/// Our [`Duration`](fugit::Duration) type, representing time elapsed in microseconds.
78pub type EPDuration = fugit::MicrosDuration<EPContainer>;
79
80/// Our [`Instant`](fugit::Instant) type, representing a snapshot in time from
81/// a clock with 1 µs precision (or at least, converted to this representation).
82pub type EPInstant = fugit::Instant<EPContainer, 1, 1_000_000>;
83
84/// An [`Instant`](fugit::Instant) type but with a generic fraction. This needs to be
85/// converted into [`EPInstant`] for use in the [`EmbeddedProfiler::read_clock`] function.
86pub type EPInstantGeneric<const NOM: u32, const DENOM: u32> =
87    fugit::Instant<EPContainer, NOM, DENOM>;
88
89/// A recorded snapshot.
90pub struct EPSnapshot {
91    /// The name of this trace.
92    pub name: &'static str,
93    /// The duration of this trace.
94    pub duration: EPDuration,
95}
96
97impl core::fmt::Display for EPSnapshot {
98    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
99        write!(f, "<EPSS {}: {}>", self.name, self.duration)
100    }
101}
102
103/// The main trait to implement. All that is required is a way to read time and a way
104/// to output our results, if desired. You can also implement functions that
105/// get called when a snapshot starts and ends.
106pub trait EmbeddedProfiler {
107    /// Takes a reading from the clock.
108    ///
109    /// Used by the underlying trait methods [`EmbeddedProfiler::start_snapshot`] and
110    /// [`EmbeddedProfiler::end_snapshot`].
111    fn read_clock(&self) -> EPInstant;
112
113    /// Optionally log the snapshot to some output, like a serial port.
114    fn log_snapshot(&self, _snapshot: &EPSnapshot) {}
115
116    /// Optional function that gets called at the start of the snapshot recording.
117    ///
118    /// If one would want to very simple profiling, they could use `at_start` and `at_end`
119    /// to simply toggle a GPIO.
120    fn at_start(&self) {}
121
122    /// Optional function that gets called at the end of the snapshot recording.
123    fn at_end(&self) {}
124
125    /// takes the starting snapshot of a specific trace.
126    ///
127    /// ```
128    /// # use embedded_profiling::*;
129    /// # struct MyProfiler;
130    /// # impl EmbeddedProfiler for MyProfiler { fn read_clock(&self) -> EPInstant {EPInstant::from_ticks(0)} }
131    /// # let my_profiler = MyProfiler;
132    /// # fn function_to_profile() {}
133    /// let start_time = my_profiler.start_snapshot();
134    /// function_to_profile();
135    /// if let Some(snapshot) = my_profiler.end_snapshot(start_time, "function_to_profile") {
136    ///     my_profiler.log_snapshot(&snapshot);
137    /// }
138    /// ```
139    fn start_snapshot(&self) -> EPInstant {
140        self.at_start();
141        self.read_clock()
142    }
143
144    /// computes the duration of the snapshot given the start time, if there hasn't been overflow.
145    fn end_snapshot(&self, start: EPInstant, name: &'static str) -> Option<EPSnapshot> {
146        self.at_end();
147        let now = self.read_clock();
148        now.checked_duration_since(start)
149            .map(|duration| EPSnapshot { name, duration })
150    }
151}
152
153/// Converts an instant of an unknown fraction `NOM`/`DENOM` to our microsecond representation.
154///
155/// This function is useful when implementing [`EmbeddedProfiler::read_clock`], to convert from
156/// your clocks native resolution to the 1µs resolution it needs.
157/// ```
158/// # use embedded_profiling::*;
159/// let my_clock_instant = EPInstantGeneric::<1, 1_000>::from_ticks(100);
160/// let converted_instant = convert_instant::<1, 1_000>(my_clock_instant);
161/// assert_eq!(100_000, converted_instant.ticks());
162/// ```
163#[inline]
164pub const fn convert_instant<const NOM: u32, const DENOM: u32>(
165    now: EPInstantGeneric<NOM, DENOM>,
166) -> EPInstant {
167    let us: fugit::MicrosDuration<EPContainer> = now.duration_since_epoch().convert();
168    EPInstant::from_ticks(us.ticks())
169}
170
171struct NoopProfiler;
172
173impl EmbeddedProfiler for NoopProfiler {
174    fn read_clock(&self) -> EPInstant {
175        EPInstant::from_ticks(0)
176    }
177
178    fn log_snapshot(&self, _snapshot: &EPSnapshot) {}
179}
180
181static mut PROFILER: &dyn EmbeddedProfiler = &NoopProfiler;
182
183const UNINITIALIZED: u8 = 0;
184const INITIALIZED: u8 = 2;
185
186static STATE: AtomicU8 = AtomicU8::new(UNINITIALIZED);
187
188/// Indicates that setting the profiler has gone awry, probably because the
189/// profiler has already been set.
190#[derive(Debug)]
191pub struct SetProfilerError;
192
193/// Sets the global profiler.
194///
195/// # Safety
196/// Must be completed with no other threads running
197/// or, in an embedded single core environment, with interrupts disabled.
198///
199/// # Errors
200/// returns `Err(SetProfilerError)` when a global profiler has already been configured
201///
202/// ```
203/// # struct MyProfiler;
204/// # impl embedded_profiling::EmbeddedProfiler for MyProfiler { fn read_clock(&self) -> embedded_profiling::EPInstant { embedded_profiling::EPInstant::from_ticks(0) } }
205/// # static MY_PROFILER: MyProfiler = MyProfiler;
206/// let noop_profiler_ref = embedded_profiling::profiler();  // no-op profiler returned because we haven't set one yet
207/// // interrupts should be disabled while this is called with something like `cortex_m::interrupt::free`
208/// unsafe {
209///     embedded_profiling::set_profiler(&MY_PROFILER).unwrap();
210/// }
211/// let my_profiler_ref = embedded_profiling::profiler();  // our profiler now returned
212/// ```
213pub unsafe fn set_profiler(
214    profiler: &'static dyn EmbeddedProfiler,
215) -> Result<(), SetProfilerError> {
216    match STATE.load(Ordering::Acquire) {
217        UNINITIALIZED => {
218            PROFILER = profiler;
219            STATE.store(INITIALIZED, Ordering::Release);
220            Ok(())
221        }
222        INITIALIZED => Err(SetProfilerError),
223        _ => unreachable!(),
224    }
225}
226
227/// Returns a reference to the configured profiler.
228///
229/// If a profiler hasn't yet been set by [`set_profiler`], the no-op profiler
230/// will be returned. Generally, you should use one of the other provided
231/// functions rather than getting a reference through this function.
232///
233/// ```
234/// let start = embedded_profiling::profiler().start_snapshot();
235/// // (...)
236/// let snapshot = embedded_profiling::profiler().end_snapshot(start, "doc-example");
237/// ```
238#[inline]
239pub fn profiler() -> &'static dyn EmbeddedProfiler {
240    if STATE.load(Ordering::Acquire) == INITIALIZED {
241        unsafe { PROFILER }
242    } else {
243        static NOP: NoopProfiler = NoopProfiler;
244        &NOP
245    }
246}
247
248/// takes the starting snapshot of a specific trace.
249///
250/// ```
251/// let start = embedded_profiling::start_snapshot();
252/// // (...)
253/// let snapshot = embedded_profiling::end_snapshot(start, "doc-example");
254/// ```
255#[inline]
256pub fn start_snapshot() -> EPInstant {
257    profiler().start_snapshot()
258}
259
260/// computes the duration of the snapshot given the start time using the
261/// globally configured profiler.
262#[inline]
263pub fn end_snapshot(start: EPInstant, name: &'static str) -> Option<EPSnapshot> {
264    profiler().end_snapshot(start, name)
265}
266
267/// Logs the given snapshot with the globally configured profiler.
268///
269/// ```
270/// let start = embedded_profiling::start_snapshot();
271/// // (...)
272/// if let Some(snapshot) = embedded_profiling::end_snapshot(start, "doc-example") {
273///     embedded_profiling::log_snapshot(&snapshot);
274/// }
275#[inline]
276pub fn log_snapshot(snapshot: &EPSnapshot) {
277    profiler().log_snapshot(snapshot);
278}
279
280/// Profiles the given closure `target` with name `name`.
281///
282/// ```
283/// embedded_profiling::profile("profile println", || {
284///     println!("profiling this closure");
285/// });
286/// ```
287pub fn profile<T, R>(name: &'static str, target: T) -> R
288where
289    T: Fn() -> R,
290{
291    let start = start_snapshot();
292    let ret = target();
293    if let Some(snapshot) = end_snapshot(start, name) {
294        log_snapshot(&snapshot);
295    }
296    ret
297}
298
299#[cfg(test)]
300mod test {
301    use super::mock::StdMockProfiler;
302    use super::*;
303
304    #[cfg(feature = "proc-macros")]
305    use crate as embedded_profiling;
306
307    use std::sync::Once;
308
309    static INIT_PROFILER: Once = Once::new();
310    static mut MOCK_PROFILER: Option<StdMockProfiler> = None;
311
312    fn set_profiler() {
313        INIT_PROFILER.call_once(|| unsafe {
314            if MOCK_PROFILER.is_none() {
315                MOCK_PROFILER = Some(StdMockProfiler::default());
316            }
317            super::set_profiler(MOCK_PROFILER.as_ref().unwrap()).unwrap();
318        });
319    }
320
321    #[test]
322    #[serial_test::serial]
323    fn basic_duration() {
324        let profiler = StdMockProfiler::default();
325
326        let start = profiler.start_snapshot();
327        std::thread::sleep(std::time::Duration::from_millis(25));
328        let end = profiler.end_snapshot(start, "basic_dur").unwrap();
329        profiler.log_snapshot(&end);
330    }
331
332    #[test]
333    #[serial_test::serial]
334    fn basic_duration_and_set_profiler() {
335        // set the profiler, if it hasn't been already
336        set_profiler();
337
338        let start = start_snapshot();
339        std::thread::sleep(std::time::Duration::from_millis(25));
340        let end = end_snapshot(start, "basic_dur").unwrap();
341        log_snapshot(&end);
342    }
343
344    #[test]
345    #[serial_test::serial]
346    fn profile_closure() {
347        // set the profiler, if it hasn't been already
348        set_profiler();
349
350        profile("25ms closure", || {
351            std::thread::sleep(std::time::Duration::from_millis(25));
352        });
353    }
354
355    #[cfg(feature = "proc-macros")]
356    #[test]
357    #[serial_test::serial]
358    fn profile_proc_macro() {
359        #[profile_function]
360        fn delay_25ms() {
361            std::thread::sleep(std::time::Duration::from_millis(25));
362        }
363
364        // set the profiler, if it hasn't been already
365        set_profiler();
366
367        delay_25ms();
368    }
369
370    #[cfg(feature = "proc-macros")]
371    #[test]
372    #[serial_test::serial]
373    fn check_call_and_order() {
374        use Ordering::SeqCst;
375
376        #[profile_function]
377        fn delay_25ms() {
378            std::thread::sleep(std::time::Duration::from_millis(25));
379        }
380
381        // set the profiler, if it hasn't been already
382        set_profiler();
383
384        delay_25ms();
385
386        // check if our functions were called and if the order is right
387        let stats = unsafe { &MOCK_PROFILER.as_ref().unwrap().funcs_called };
388        let at_start_was_called = stats.at_start.called.load(SeqCst);
389        let read_clock_was_called = stats.read_clock.called.load(SeqCst);
390        let at_end_was_called = stats.at_end.called.load(SeqCst);
391        // stats.read_clock (but skipped since we've already called it)
392        let log_snapshot_was_called = stats.log_snapshot.called.load(SeqCst);
393
394        let at_start_at = stats.at_start.at.load(SeqCst);
395        let read_clock_at = stats.read_clock.at.load(SeqCst);
396        let at_end_at = stats.at_end.at.load(SeqCst);
397        let log_snapshot_at = stats.log_snapshot.at.load(SeqCst);
398
399        if at_start_was_called {
400            println!("at_start called #{}", at_start_at);
401        } else {
402            println!("at_start not called");
403        }
404        if read_clock_was_called {
405            println!("read_clock called #{}", read_clock_at);
406        } else {
407            println!("read_clock not called");
408        }
409        if at_end_was_called {
410            println!("at_end called #{}", at_end_at);
411        } else {
412            println!("at_end not called");
413        }
414        if log_snapshot_was_called {
415            println!("log_snapshot called #{}", log_snapshot_at);
416        } else {
417            println!("log_snapshot not called");
418        }
419
420        assert!(at_start_was_called, "'at_start' was never called");
421        assert!(read_clock_was_called, "'read_clock' was never called");
422        assert!(at_end_was_called, "'at_end' was never called");
423        assert!(log_snapshot_was_called, "'log_snapshot' was never called");
424
425        assert_eq!(at_start_at, 0, "'at_start' called at wrong time");
426        assert_eq!(read_clock_at, 1, "'read_clock' called at wrong time");
427        assert_eq!(at_end_at, 2, "'at_end' called at wrong time");
428        assert_eq!(log_snapshot_at, 3, "'log_snapshot' called at wrong time");
429    }
430
431    #[test]
432    const fn check_conversion() {
433        // check to see if the conversion is naive and saturates or not
434        const NOM: u32 = 4;
435        const DENOM: u32 = 4_000_000;
436        const INITIAL_INSTANT: EPInstantGeneric<NOM, DENOM> =
437            EPInstantGeneric::from_ticks(EPContainer::MAX - 10);
438        const RESULT_INSTANT: EPInstant = convert_instant(INITIAL_INSTANT);
439        assert!(RESULT_INSTANT.ticks() == INITIAL_INSTANT.ticks());
440    }
441}