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
12pub trait TimingCounter {
57 const INITIAL: Self;
59
60 fn increment_count(&self);
62
63 fn add_elapsed_ticks(&self, elapsed_ticks: u64);
65}
66
67#[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 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 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#[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#[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 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 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 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 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}