Skip to main content

span_timing/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4#[cfg(all(
5    not(feature = "std"),
6    not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"))
7))]
8compile_error!(
9    "span-timing without the `std` feature supports only x86, x86_64, and aarch64 targets"
10);
11
12/// Receives measurements collected by [`timed_span!`].
13///
14/// The macro calls [`Self::increment_count`] when a timed scope begins and
15/// [`Self::add_elapsed_ticks`] when it ends. Implementations may store those
16/// values directly, aggregate them differently, or update additional metrics.
17/// [`timing_entries!`] uses [`Self::INITIAL`] to create each element of a
18/// generated static counter array.
19///
20/// # Example
21///
22/// This counter computes a running average on demand from its sample count and
23/// total ticks:
24///
25/// ```
26/// use std::sync::atomic::{AtomicU64, Ordering};
27/// use span_timing::TimingCounter;
28///
29/// struct AverageCounter {
30///     samples: AtomicU64,
31///     total_ticks: AtomicU64,
32/// }
33///
34/// impl AverageCounter {
35///     fn average_ticks(&self) -> Option<u64> {
36///         let samples = self.samples.load(Ordering::Relaxed);
37///         (samples != 0).then(|| self.total_ticks.load(Ordering::Relaxed) / samples)
38///     }
39/// }
40///
41/// impl TimingCounter for AverageCounter {
42///     const INITIAL: Self = Self {
43///         samples: AtomicU64::new(0),
44///         total_ticks: AtomicU64::new(0),
45///     };
46///
47///     fn increment_count(&self) {
48///         self.samples.fetch_add(1, Ordering::Relaxed);
49///     }
50///
51///     fn add_elapsed_ticks(&self, ticks: u64) {
52///         self.total_ticks.fetch_add(ticks, Ordering::Relaxed);
53///     }
54/// }
55/// ```
56pub trait TimingCounter {
57    /// The const value used to initialize each element of a static counter collection.
58    const INITIAL: Self;
59
60    /// Records one invocation of the timed operation, before the timed scope runs.
61    fn increment_count(&self);
62
63    /// Records the elapsed processor-counter ticks or nanoseconds when the scope ends.
64    fn add_elapsed_ticks(&self, elapsed_ticks: u64);
65}
66
67/// The standard atomic counter implementation for [`timed_span!`].
68///
69/// `count` records the number of timed spans, while `ticks` accumulates their
70/// elapsed processor-counter ticks (or nanoseconds on unsupported architectures).
71/// Use this type when a total, a count, and their derived average are sufficient.
72#[cfg(target_has_atomic = "64")]
73#[derive(Debug, Default)]
74pub struct Counter {
75    pub count: core::sync::atomic::AtomicU64,
76    pub ticks: core::sync::atomic::AtomicU64,
77}
78
79#[cfg(target_has_atomic = "64")]
80impl Counter {
81    /// Creates a counter with both measurements set to zero.
82    pub const fn new() -> Self {
83        Self {
84            count: core::sync::atomic::AtomicU64::new(0),
85            ticks: core::sync::atomic::AtomicU64::new(0),
86        }
87    }
88
89    /// Resets both measurements to zero.
90    pub fn reset(&self) {
91        self.count.store(0, core::sync::atomic::Ordering::Relaxed);
92        self.ticks.store(0, core::sync::atomic::Ordering::Relaxed);
93    }
94}
95
96#[cfg(target_has_atomic = "64")]
97impl TimingCounter for Counter {
98    const INITIAL: Self = Self::new();
99
100    fn increment_count(&self) {
101        self.count
102            .fetch_add(1, core::sync::atomic::Ordering::Relaxed);
103    }
104
105    fn add_elapsed_ticks(&self, ticks: u64) {
106        self.ticks
107            .fetch_add(ticks, core::sync::atomic::Ordering::Relaxed);
108    }
109}
110
111/// Declares an enum and optionally a static counter collection for timing entries.
112///
113/// The generated enum has `ALL`, `COUNT`, and `to_str` associated items. Adding
114/// `static COUNTERS: [Counter];` after the enum generates a `[Counter; COUNT]` static,
115/// initialized from [`TimingCounter::INITIAL`]. This keeps entry names, array length,
116/// and reporting order in one declaration.
117///
118/// Without the `static` declaration, the macro only generates the enum and its helpers.
119#[macro_export]
120macro_rules! timing_entries {
121    (
122        $visibility:vis enum $name:ident {
123            $($entry:ident $(= $value:expr)?),*
124            $(,)?
125        }
126        $counter_visibility:vis static $counters:ident: [$counter_type:ty];
127    ) => {
128        $crate::timing_entries! {
129            @entries
130            $visibility enum $name {
131                $($entry $(= $value)?),*
132            }
133        }
134
135        $counter_visibility static $counters: [$counter_type; $name::COUNT] =
136            [const { <$counter_type as $crate::TimingCounter>::INITIAL }; $name::COUNT];
137    };
138    (
139        $visibility:vis enum $name:ident {
140            $($entry:ident $(= $value:expr)?),*
141            $(,)?
142        }
143    ) => {
144        $crate::timing_entries! {
145            @entries
146            $visibility enum $name {
147                $($entry $(= $value)?),*
148            }
149        }
150    };
151    (
152        @entries
153        $visibility:vis enum $name:ident {
154            $($entry:ident $(= $value:expr)?),*
155            $(,)?
156        }
157    ) => {
158        #[derive(Clone, Copy)]
159        $visibility enum $name {
160            $($entry $(= $value)?),*
161        }
162
163        impl $name {
164            pub const ALL: &[$name] = &[$($name::$entry),*];
165            pub const COUNT: usize = $name::ALL.len();
166
167            pub const fn to_str(&self) -> &'static str {
168                match self {
169                    $(
170                        $name::$entry => stringify!($entry),
171                    )*
172                }
173            }
174        }
175    };
176}
177
178/// Starts a timed span and returns its guard.
179///
180/// The first argument is an enum variant that can be cast to an index. The second is an
181/// indexable collection whose entries implement [`TimingCounter`]. Bind the returned
182/// guard for the scope to measure. When dropped, it adds the elapsed measurement, even
183/// if the scope returns early or unwinds. On x86, x86_64, and aarch64 the elapsed value
184/// is a processor-counter tick count; on other architectures it is elapsed nanoseconds.
185#[macro_export]
186macro_rules! timed_span {
187    ($entry:expr, $counters:expr $(,)?) => {{
188        let counter = &($counters)[($entry) as usize];
189        $crate::TimingCounter::increment_count(counter);
190        $crate::TimedSpanGuard::new(counter)
191    }};
192}
193
194#[cfg(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"))]
195mod clock {
196    use super::TimingCounter;
197    use core::arch::asm;
198
199    #[cfg(target_arch = "aarch64")]
200    #[inline]
201    fn read_counter() -> u64 {
202        let value: u64;
203        unsafe {
204            asm!("mrs {}, CNTVCT_EL0", out(reg) value, options(nostack, nomem));
205        }
206        value
207    }
208
209    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
210    #[inline]
211    fn read_counter() -> u64 {
212        let low: u32;
213        let high: u32;
214        unsafe {
215            asm!(
216                "rdtsc",
217                out("eax") low,
218                out("edx") high,
219                options(nostack, nomem)
220            );
221        }
222        ((high as u64) << 32) | low as u64
223    }
224
225    /// A scope guard that adds elapsed processor-counter ticks to an atomic counter.
226    pub struct TimedSpanGuard<'a, C: TimingCounter + ?Sized> {
227        start: u64,
228        counter: &'a C,
229    }
230
231    impl<'a, C: TimingCounter + ?Sized> TimedSpanGuard<'a, C> {
232        /// Starts timing and records elapsed ticks in `counter` when dropped.
233        pub fn new(counter: &'a C) -> Self {
234            Self {
235                start: read_counter(),
236                counter,
237            }
238        }
239    }
240
241    impl<C: TimingCounter + ?Sized> Drop for TimedSpanGuard<'_, C> {
242        fn drop(&mut self) {
243            self.counter.add_elapsed_ticks(read_counter() - self.start);
244        }
245    }
246}
247
248#[cfg(all(
249    feature = "std",
250    not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"))
251))]
252mod clock {
253    use super::TimingCounter;
254    use std::time::Instant;
255
256    /// A scope guard that adds elapsed nanoseconds to an atomic counter.
257    pub struct TimedSpanGuard<'a, C: TimingCounter + ?Sized> {
258        start: Instant,
259        counter: &'a C,
260    }
261
262    impl<'a, C: TimingCounter + ?Sized> TimedSpanGuard<'a, C> {
263        /// Starts timing and records elapsed nanoseconds in `counter` when dropped.
264        pub fn new(counter: &'a C) -> Self {
265            Self {
266                start: Instant::now(),
267                counter,
268            }
269        }
270    }
271
272    impl<C: TimingCounter + ?Sized> Drop for TimedSpanGuard<'_, C> {
273        fn drop(&mut self) {
274            self.counter
275                .add_elapsed_ticks(self.start.elapsed().as_nanos() as u64);
276        }
277    }
278}
279
280pub use clock::TimedSpanGuard;
281
282#[cfg(all(test, target_has_atomic = "64"))]
283mod tests {
284    use crate::{Counter as StandardCounter, TimingCounter};
285    use core::sync::atomic::{AtomicU64, Ordering};
286
287    timing_entries! {
288        pub enum Entry {
289            First,
290            Second,
291        }
292        static COUNTERS: [Counter];
293    }
294
295    struct Counter {
296        invocations: AtomicU64,
297        elapsed: AtomicU64,
298    }
299
300    impl TimingCounter for Counter {
301        const INITIAL: Self = Self {
302            invocations: AtomicU64::new(0),
303            elapsed: AtomicU64::new(0),
304        };
305
306        fn increment_count(&self) {
307            self.invocations.fetch_add(1, Ordering::Relaxed);
308        }
309
310        fn add_elapsed_ticks(&self, elapsed_ticks: u64) {
311            self.elapsed.fetch_add(elapsed_ticks, Ordering::Relaxed);
312        }
313    }
314
315    #[test]
316    fn standard_counter_records_and_resets_measurements() {
317        let counter = StandardCounter::default();
318        counter.increment_count();
319        counter.add_elapsed_ticks(42);
320
321        assert_eq!(counter.count.load(Ordering::Relaxed), 1);
322        assert_eq!(counter.ticks.load(Ordering::Relaxed), 42);
323
324        counter.reset();
325        assert_eq!(counter.count.load(Ordering::Relaxed), 0);
326        assert_eq!(counter.ticks.load(Ordering::Relaxed), 0);
327    }
328
329    #[test]
330    fn declares_entries_and_records_a_span() {
331        assert_eq!(Entry::ALL.len(), 2);
332        assert_eq!(Entry::Second.to_str(), "Second");
333
334        {
335            let _timed_span_guard = timed_span!(Entry::First, COUNTERS);
336            for value in 0..100_000 {
337                core::hint::black_box(value);
338            }
339        }
340        assert_eq!(
341            COUNTERS[Entry::First as usize]
342                .invocations
343                .load(Ordering::Relaxed),
344            1
345        );
346        assert_ne!(
347            COUNTERS[Entry::First as usize]
348                .elapsed
349                .load(Ordering::Relaxed),
350            0
351        );
352    }
353}