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] = &{
165 let declared: [$name; 0 $(+ { let _ = $name::$entry; 1 })*] =
166 [$($name::$entry),*];
167 let mut ordered = declared;
168 let mut index = 0;
169
170 while index < declared.len() {
171 let entry = declared[index];
172 ordered[entry as usize] = entry;
173 index += 1;
174 }
175
176 ordered
177 };
178 pub const COUNT: usize = $name::ALL.len();
179
180 pub const fn to_str(&self) -> &'static str {
181 match self {
182 $(
183 $name::$entry => stringify!($entry),
184 )*
185 }
186 }
187 }
188
189 $(
190 const _: $name = $name::ALL[$name::$entry as usize];
191 )*
192 };
193}
194
195#[macro_export]
203macro_rules! timed_span {
204 ($entry:expr, $counters:expr $(,)?) => {{
205 let counter = &($counters)[($entry) as usize];
206 $crate::TimingCounter::increment_count(counter);
207 $crate::TimedSpanGuard::new(counter)
208 }};
209}
210
211#[cfg(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"))]
212mod clock {
213 use super::TimingCounter;
214 use core::arch::asm;
215
216 #[cfg(target_arch = "aarch64")]
217 #[inline]
218 fn read_counter() -> u64 {
219 let value: u64;
220 unsafe {
221 asm!("mrs {}, CNTVCT_EL0", out(reg) value, options(nostack, nomem));
222 }
223 value
224 }
225
226 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
227 #[inline]
228 fn read_counter() -> u64 {
229 let low: u32;
230 let high: u32;
231 unsafe {
232 asm!(
233 "rdtsc",
234 out("eax") low,
235 out("edx") high,
236 options(nostack, nomem)
237 );
238 }
239 ((high as u64) << 32) | low as u64
240 }
241
242 pub struct TimedSpanGuard<'a, C: TimingCounter + ?Sized> {
244 start: u64,
245 counter: &'a C,
246 }
247
248 impl<'a, C: TimingCounter + ?Sized> TimedSpanGuard<'a, C> {
249 pub fn new(counter: &'a C) -> Self {
251 Self {
252 start: read_counter(),
253 counter,
254 }
255 }
256 }
257
258 impl<C: TimingCounter + ?Sized> Drop for TimedSpanGuard<'_, C> {
259 fn drop(&mut self) {
260 self.counter.add_elapsed_ticks(read_counter() - self.start);
261 }
262 }
263}
264
265#[cfg(all(
266 feature = "std",
267 not(any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"))
268))]
269mod clock {
270 use super::TimingCounter;
271 use std::time::Instant;
272
273 pub struct TimedSpanGuard<'a, C: TimingCounter + ?Sized> {
275 start: Instant,
276 counter: &'a C,
277 }
278
279 impl<'a, C: TimingCounter + ?Sized> TimedSpanGuard<'a, C> {
280 pub fn new(counter: &'a C) -> Self {
282 Self {
283 start: Instant::now(),
284 counter,
285 }
286 }
287 }
288
289 impl<C: TimingCounter + ?Sized> Drop for TimedSpanGuard<'_, C> {
290 fn drop(&mut self) {
291 self.counter
292 .add_elapsed_ticks(self.start.elapsed().as_nanos() as u64);
293 }
294 }
295}
296
297pub use clock::TimedSpanGuard;
298
299#[cfg(all(test, target_has_atomic = "64"))]
300mod tests {
301 use crate::{Counter as StandardCounter, TimingCounter};
302 use core::sync::atomic::{AtomicU64, Ordering};
303
304 timing_entries! {
305 pub enum Entry {
306 First,
307 Second,
308 }
309 static COUNTERS: [Counter];
310 }
311
312 struct Counter {
313 invocations: AtomicU64,
314 elapsed: AtomicU64,
315 }
316
317 impl TimingCounter for Counter {
318 const INITIAL: Self = Self {
319 invocations: AtomicU64::new(0),
320 elapsed: AtomicU64::new(0),
321 };
322
323 fn increment_count(&self) {
324 self.invocations.fetch_add(1, Ordering::Relaxed);
325 }
326
327 fn add_elapsed_ticks(&self, elapsed_ticks: u64) {
328 self.elapsed.fetch_add(elapsed_ticks, Ordering::Relaxed);
329 }
330 }
331
332 #[test]
333 fn standard_counter_records_and_resets_measurements() {
334 let counter = StandardCounter::default();
335 counter.increment_count();
336 counter.add_elapsed_ticks(42);
337
338 assert_eq!(counter.count.load(Ordering::Relaxed), 1);
339 assert_eq!(counter.ticks.load(Ordering::Relaxed), 42);
340
341 counter.reset();
342 assert_eq!(counter.count.load(Ordering::Relaxed), 0);
343 assert_eq!(counter.ticks.load(Ordering::Relaxed), 0);
344 }
345
346 #[test]
347 fn declares_entries_and_records_a_span() {
348 assert_eq!(Entry::ALL.len(), 2);
349 assert_eq!(Entry::Second.to_str(), "Second");
350
351 {
352 let _timed_span_guard = timed_span!(Entry::First, COUNTERS);
353 for value in 0..100_000 {
354 core::hint::black_box(value);
355 }
356 }
357 assert_eq!(
358 COUNTERS[Entry::First as usize]
359 .invocations
360 .load(Ordering::Relaxed),
361 1
362 );
363 assert_ne!(
364 COUNTERS[Entry::First as usize]
365 .elapsed
366 .load(Ordering::Relaxed),
367 0
368 );
369 }
370
371 #[test]
372 fn declares_entries_with_in_bounds_explicit_discriminants() {
373 timing_entries! {
374 enum ExplicitEntry {
375 Second = 1,
376 First = 0,
377 }
378 }
379
380 assert_eq!(ExplicitEntry::COUNT, 2);
381 assert_eq!(ExplicitEntry::First as usize, 0);
382 assert_eq!(ExplicitEntry::Second as usize, 1);
383 assert_eq!(
384 ExplicitEntry::ALL[ExplicitEntry::First as usize].to_str(),
385 "First"
386 );
387 assert_eq!(
388 ExplicitEntry::ALL[ExplicitEntry::Second as usize].to_str(),
389 "Second"
390 );
391 }
392}
393
394#[cfg(all(test, feature = "std"))]
398mod compile_fail_tests {
399 use std::{
400 env, fs,
401 process::{self, Command},
402 time::{SystemTime, UNIX_EPOCH},
403 };
404
405 #[test]
406 fn rejects_entries_with_out_of_bounds_discriminants() {
407 let unique = SystemTime::now()
408 .duration_since(UNIX_EPOCH)
409 .expect("system clock is before the Unix epoch")
410 .as_nanos();
411 let test_dir = env::temp_dir().join(format!("span-timing-oob-{unique}-{}", process::id()));
412 let manifest_dir =
413 env::var("CARGO_MANIFEST_DIR").expect("Cargo did not set CARGO_MANIFEST_DIR");
414
415 fs::create_dir_all(test_dir.join("src")).expect("failed to create temporary test crate");
416 fs::write(
417 test_dir.join("Cargo.toml"),
418 format!(
419 "[package]\nname = \"timing-entries-oob\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[dependencies]\nspan-timing = {{ path = {manifest_dir:?} }}\n"
420 ),
421 )
422 .expect("failed to write temporary manifest");
423 fs::write(
424 test_dir.join("src/main.rs"),
425 "use span_timing::timing_entries;\n\ntiming_entries! {\n enum Oob {\n Foo = 42,\n }\n}\n\nfn main() {}\n",
426 )
427 .expect("failed to write temporary source");
428
429 let output = Command::new(env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
430 .args(["check", "--offline"])
431 .current_dir(&test_dir)
432 .output()
433 .expect("failed to run cargo check");
434 let _ = fs::remove_dir_all(&test_dir);
435
436 assert!(
437 !output.status.success(),
438 "out-of-bounds discriminant unexpectedly compiled:\n{}",
439 String::from_utf8_lossy(&output.stdout)
440 );
441 }
442}