1#[cfg(feature = "async")]
2pub use asynchronous::async_benchmark_fn;
3use metrics::WallClock;
4use num_traits::ToPrimitive;
5use std::{
6 cmp::Ordering,
7 hint::black_box,
8 io,
9 marker::PhantomData,
10 ops::{Deref, RangeInclusive},
11 time::Duration,
12};
13use thiserror::Error;
14
15pub mod child;
16pub mod cli;
17pub mod commpage;
18pub mod platform;
19pub mod protocol;
20pub mod worker;
21
22#[derive(Debug, Error)]
23pub enum Error {
24 #[error("No measurements given")]
25 NoMeasurements,
26
27 #[error("Benchmark is missing")]
28 BenchmarkNotFound,
29
30 #[error("Unknown sampler type. Available options are: flat and linear")]
31 UnknownSamplerType,
32
33 #[error("Invalid test name given")]
34 InvalidTestName,
35
36 #[error("IO Error")]
37 IOError(#[from] io::Error),
38
39 #[error("Panic: {0}")]
40 Panic(String),
41}
42
43#[macro_export]
60macro_rules! tango_benchmarks {
61 ($($func_expr:expr),+) => {
62 fn main() -> $crate::cli::Result<std::process::ExitCode> {
63 let mut benchmarks = vec![];
64 $(benchmarks.extend($crate::IntoBenchmarks::into_benchmarks($func_expr));)*;
65 $crate::cli::run(benchmarks)
66 }
67 };
68}
69
70#[macro_export]
72#[deprecated]
73macro_rules! tango_main {
74 () => {};
75}
76
77pub struct BenchmarkParams {
78 pub seed: u64,
79}
80
81pub struct Bencher<M> {
82 params: BenchmarkParams,
83 metric: PhantomData<M>,
84}
85
86impl<M> Deref for Bencher<M> {
87 type Target = BenchmarkParams;
88
89 fn deref(&self) -> &Self::Target {
90 &self.params
91 }
92}
93
94impl<M: Metric + 'static> Bencher<M> {
95 pub fn metric<T: Metric>(self) -> Bencher<T> {
96 Bencher {
97 params: self.params,
98 metric: PhantomData::<T>,
99 }
100 }
101
102 pub fn iter<O: 'static, F: FnMut() -> O + 'static>(self, func: F) -> Box<dyn ErasedSampler> {
103 Box::new(Sampler::<_, M>::new(func))
104 }
105}
106
107struct Sampler<F, M> {
108 func: F,
109 metric: PhantomData<M>,
110}
111
112impl<F, M> Sampler<F, M> {
113 fn new(func: F) -> Self {
114 Self {
115 func,
116 metric: PhantomData::<M>,
117 }
118 }
119}
120
121pub trait ErasedSampler {
122 fn measure(&mut self, iterations: usize) -> u64;
134
135 fn estimate_iterations(&mut self, time_ms: u32) -> usize {
142 let mut iters = 1;
143 let time_ns = Duration::from_millis(time_ms as u64).as_nanos() as u64;
144
145 for _ in 0..5 {
146 let time = self.measure(iters).max(1_000);
150 let time_per_iteration = (time / iters as u64).max(1);
151 let new_iters = (time_ns / time_per_iteration) as usize;
152
153 if new_iters < 2 * iters {
155 return new_iters;
156 }
157
158 iters = new_iters;
159 }
160
161 iters
162 }
163}
164
165impl<O, F: FnMut() -> O, M: Metric> ErasedSampler for Sampler<F, M> {
166 fn measure(&mut self, iterations: usize) -> u64 {
167 M::measure_fn(|| {
168 for _ in 0..iterations {
169 black_box((self.func)());
170 }
171 })
172 }
173}
174
175pub struct Benchmark {
176 name: String,
177 sampler_factory: Box<dyn SamplerFactory>,
178}
179
180pub fn benchmark_fn<F: FnMut(Bencher<WallClock>) -> Box<dyn ErasedSampler> + 'static>(
181 name: impl Into<String>,
182 sampler_factory: F,
183) -> Benchmark {
184 let name = name.into();
185 assert!(!name.is_empty());
186 Benchmark {
187 name,
188 sampler_factory: Box::new(SyncSampleFactory(sampler_factory)),
189 }
190}
191
192pub trait SamplerFactory {
193 fn create_sampler(&mut self, params: BenchmarkParams) -> Box<dyn ErasedSampler>;
194}
195
196struct SyncSampleFactory<F>(F);
197
198impl<F: FnMut(Bencher<WallClock>) -> Box<dyn ErasedSampler>> SamplerFactory
199 for SyncSampleFactory<F>
200{
201 fn create_sampler(&mut self, params: BenchmarkParams) -> Box<dyn ErasedSampler> {
202 (self.0)(Bencher {
203 params,
204 metric: PhantomData::<WallClock>,
205 })
206 }
207}
208
209impl Benchmark {
210 pub fn prepare_state(&mut self, seed: u64) -> Box<dyn ErasedSampler> {
212 self.sampler_factory
213 .create_sampler(BenchmarkParams { seed })
214 }
215
216 pub fn name(&self) -> &str {
218 self.name.as_str()
219 }
220}
221
222pub trait IntoBenchmarks {
224 fn into_benchmarks(self) -> Vec<Benchmark>;
225}
226
227impl<const N: usize> IntoBenchmarks for [Benchmark; N] {
228 fn into_benchmarks(self) -> Vec<Benchmark> {
229 self.into_iter().collect()
230 }
231}
232
233impl IntoBenchmarks for Vec<Benchmark> {
234 fn into_benchmarks(self) -> Vec<Benchmark> {
235 self
236 }
237}
238
239pub(crate) fn calculate_run_result(
243 b_c_samples: &[(u64, u64)],
244 iterations_per_sample: usize,
245 filter_outliers: bool,
246 noise_threshold: f64,
247) -> Option<RunResult> {
248 let mut diff = b_c_samples
249 .iter()
250 .map(|&(b, c)| c as f64 - b as f64)
252 .map(|diff| diff / iterations_per_sample as f64)
254 .collect::<Vec<_>>();
255
256 let n = diff.len();
258
259 let mut baseline = b_c_samples
261 .iter()
262 .map(|&(b, _)| (b as f64) / (iterations_per_sample as f64))
263 .collect::<Vec<_>>();
264 let mut candidate = b_c_samples
265 .iter()
266 .map(|&(_, c)| (c as f64) / (iterations_per_sample as f64))
267 .collect::<Vec<_>>();
268
269 let range = if filter_outliers {
271 iqr_variance_thresholds(diff.to_vec())
272 } else {
273 None
274 };
275
276 if let Some(range) = range {
278 assert_eq!(diff.len(), baseline.len());
282 assert_eq!(diff.len(), candidate.len());
283
284 let mut i = 0;
285 while i < diff.len() {
286 if range.contains(&diff[i]) {
287 i += 1;
288 } else {
289 diff.swap_remove(i);
290 baseline.swap_remove(i);
291 candidate.swap_remove(i);
292 }
293 }
294 };
295
296 let diff_summary = Summary::from(&diff)?;
297 let baseline_summary = Summary::from(&baseline)?;
298 let candidate_summary = Summary::from(&candidate)?;
299
300 let diff_estimate = DiffEstimate::build(&baseline_summary, &diff_summary, noise_threshold);
301
302 Some(RunResult {
303 baseline: baseline_summary,
304 candidate: candidate_summary,
305 diff: diff_summary,
306 diff_estimate,
307 outliers: n - diff_summary.n,
308 })
309}
310
311pub(crate) struct DiffEstimate {
313 pct: f64,
317
318 significant: bool,
320}
321
322impl DiffEstimate {
323 fn build(baseline: &Summary<f64>, diff: &Summary<f64>, noise_threshold: f64) -> Self {
331 let std_dev = diff.variance.sqrt();
332 let std_err = std_dev / (diff.n as f64).sqrt();
333 let z_score = diff.mean / std_err;
334
335 let pct = diff.mean / baseline.mean * 100.0;
336
337 let significant = z_score.abs() >= 2.6 && pct.abs() > noise_threshold;
340
341 Self { pct, significant }
342 }
343}
344
345pub(crate) struct RunResult {
347 baseline: Summary<f64>,
349
350 candidate: Summary<f64>,
352
353 diff: Summary<f64>,
355
356 diff_estimate: DiffEstimate,
357
358 outliers: usize,
360}
361
362#[derive(Clone, Copy)]
367pub struct Summary<T> {
368 pub n: usize,
369 pub min: T,
370 pub max: T,
371 pub mean: f64,
372 pub variance: f64,
373}
374
375impl<T: PartialOrd> Summary<T> {
376 pub fn from<'a, C>(values: C) -> Option<Self>
377 where
378 C: IntoIterator<Item = &'a T>,
379 T: ToPrimitive + Copy + Default + 'a,
380 {
381 Self::running(values.into_iter().copied()).last()
382 }
383
384 pub fn running<I>(iter: I) -> impl Iterator<Item = Summary<T>>
385 where
386 T: ToPrimitive + Copy + Default,
387 I: Iterator<Item = T>,
388 {
389 RunningSummary {
390 iter,
391 n: 0,
392 min: T::default(),
393 max: T::default(),
394 mean: 0.,
395 s: 0.,
396 }
397 }
398}
399
400struct RunningSummary<T, I> {
401 iter: I,
402 n: usize,
403 min: T,
404 max: T,
405 mean: f64,
406 s: f64,
407}
408
409impl<T, I> Iterator for RunningSummary<T, I>
410where
411 T: Copy + PartialOrd,
412 I: Iterator<Item = T>,
413 T: ToPrimitive,
414{
415 type Item = Summary<T>;
416
417 fn next(&mut self) -> Option<Self::Item> {
418 let value = self.iter.next()?;
419 let fvalue = value.to_f64().expect("f64 overflow detected");
420
421 if self.n == 0 {
422 self.min = value;
423 self.max = value;
424 }
425
426 if let Some(Ordering::Less) = value.partial_cmp(&self.min) {
427 self.min = value;
428 }
429 if let Some(Ordering::Greater) = value.partial_cmp(&self.max) {
430 self.max = value;
431 }
432
433 self.n += 1;
434 let mean_p = self.mean;
435 self.mean += (fvalue - self.mean) / self.n as f64;
436 self.s += (fvalue - mean_p) * (fvalue - self.mean);
437 let variance = if self.n > 1 {
438 self.s / (self.n - 1) as f64
439 } else {
440 0.
441 };
442
443 Some(Summary {
444 n: self.n,
445 min: self.min,
446 max: self.max,
447 mean: self.mean,
448 variance,
449 })
450 }
451}
452
453pub fn iqr_variance_thresholds(mut input: Vec<f64>) -> Option<RangeInclusive<f64>> {
458 const MINIMUM_IQR: f64 = 1.;
459
460 input.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
461 let (q1, q3) = (input.len() / 4, input.len() * 3 / 4 - 1);
462 if q1 >= q3 || q3 >= input.len() {
463 return None;
464 }
465 let iqr = (input[q3] - input[q1]).max(MINIMUM_IQR);
468
469 let low_threshold = input[q1] - iqr * 1.5;
470 let high_threshold = input[q3] + iqr * 1.5;
471
472 let low_threshold_idx =
474 match input[0..q1].binary_search_by(|probe| probe.total_cmp(&low_threshold)) {
475 Ok(idx) => idx,
476 Err(idx) => idx,
477 };
478
479 let high_threshold_idx =
480 match input[q3..].binary_search_by(|probe| probe.total_cmp(&high_threshold)) {
481 Ok(idx) => idx,
482 Err(idx) => idx,
483 };
484
485 if low_threshold_idx == 0 || high_threshold_idx >= input.len() {
486 return None;
487 }
488
489 let outliers_cnt = low_threshold_idx.min(input.len() - high_threshold_idx);
491
492 Some(input[outliers_cnt]..=(input[input.len() - outliers_cnt - 1]))
493}
494
495pub trait Metric {
497 fn measure_fn(f: impl FnMut()) -> u64;
499}
500
501pub struct AuxMetricEntry {
503 pub id: &'static str,
504 pub start: fn() -> u64,
505 pub finish: fn(u64) -> u64,
506}
507
508pub fn available_aux_metrics() -> Vec<AuxMetricEntry> {
510 vec![aux::system_time(), aux::user_time()]
511}
512
513pub mod aux {
514 use super::AuxMetricEntry;
515 use crate::platform;
516
517 pub const CPU_SYSTEM_TIME_ID: &str = "cpu_sys";
518 pub const CPU_USER_TIME_ID: &str = "cpu_usr";
519
520 pub fn system_time() -> AuxMetricEntry {
522 fn start() -> u64 {
523 let r = platform::rusage();
524 r.system_time.as_nanos() as u64
525 }
526 fn finish(start: u64) -> u64 {
527 let r = platform::rusage();
528 let end = r.system_time.as_nanos() as u64;
529 end - start
530 }
531 AuxMetricEntry {
532 id: CPU_SYSTEM_TIME_ID,
533 start,
534 finish,
535 }
536 }
537
538 pub fn user_time() -> AuxMetricEntry {
540 fn start() -> u64 {
541 let r = platform::rusage();
542 r.user_time.as_nanos() as u64
543 }
544 fn finish(start: u64) -> u64 {
545 let r = platform::rusage();
546 let end = r.user_time.as_nanos() as u64;
547 end - start
548 }
549 AuxMetricEntry {
550 id: CPU_USER_TIME_ID,
551 start,
552 finish,
553 }
554 }
555}
556
557pub mod metrics {
558 use crate::Metric;
559
560 pub struct WallClock;
561
562 impl Metric for WallClock {
563 #[cfg(not(all(feature = "hw-timer", target_arch = "x86_64")))]
565 fn measure_fn(mut f: impl FnMut()) -> u64 {
566 use std::time::Instant;
567 let start = Instant::now();
568 f();
569 start.elapsed().as_nanos() as u64
570 }
571
572 #[cfg(all(feature = "hw-timer", target_arch = "x86_64"))]
574 fn measure_fn(mut f: impl FnMut()) -> u64 {
575 use std::arch::x86_64::{__rdtscp, _mm_mfence};
576 let start = unsafe {
577 _mm_mfence();
578 __rdtscp(&mut 0)
579 };
580 f();
581 unsafe {
582 let end = __rdtscp(&mut 0);
583 _mm_mfence();
584 end - start
585 }
586 }
587 }
588
589 pub struct CpuTime;
594
595 impl Metric for CpuTime {
596 #[cfg(target_family = "unix")]
597 fn measure_fn(mut f: impl FnMut()) -> u64 {
598 use std::mem::MaybeUninit;
599
600 let mut start_ts = unsafe { MaybeUninit::<libc::timespec>::zeroed().assume_init() };
601 let mut end_ts = unsafe { MaybeUninit::<libc::timespec>::zeroed().assume_init() };
602
603 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut start_ts) };
604 f();
605 unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut end_ts) };
606
607 let start_nanos = start_ts.tv_sec as u64 * 1_000_000_000 + start_ts.tv_nsec as u64;
608 let end_nanos = end_ts.tv_sec as u64 * 1_000_000_000 + end_ts.tv_nsec as u64;
609 end_nanos - start_nanos
610 }
611
612 #[cfg(target_os = "windows")]
613 fn measure_fn(mut f: impl FnMut()) -> u64 {
614 use windows::Win32::{
615 Foundation::FILETIME,
616 System::Threading::{GetCurrentThread, GetThreadTimes},
617 };
618
619 fn filetime_to_nanos(ft: &FILETIME) -> u64 {
620 ((ft.dwHighDateTime as u64) << 32 | ft.dwLowDateTime as u64) * 100
621 }
622
623 let thread = unsafe { GetCurrentThread() };
624
625 let mut dummy = FILETIME::default();
626 let mut start_kernel = FILETIME::default();
627 let mut start_user = FILETIME::default();
628 let mut end_kernel = FILETIME::default();
629 let mut end_user = FILETIME::default();
630
631 unsafe {
632 GetThreadTimes(
633 thread,
634 &mut dummy,
635 &mut dummy,
636 &mut start_kernel,
637 &mut start_user,
638 )
639 }
640 .unwrap();
641 f();
642 unsafe {
643 GetThreadTimes(
644 thread,
645 &mut dummy,
646 &mut dummy,
647 &mut end_kernel,
648 &mut end_user,
649 )
650 }
651 .unwrap();
652
653 let start = filetime_to_nanos(&start_kernel) + filetime_to_nanos(&start_user);
654 let end = filetime_to_nanos(&end_kernel) + filetime_to_nanos(&end_user);
655 end - start
656 }
657 }
658}
659
660#[cfg(feature = "async")]
661pub mod asynchronous {
662 use crate::metrics::WallClock;
663
664 use super::{Benchmark, BenchmarkParams, ErasedSampler, Sampler, SamplerFactory};
665 use std::{future::Future, ops::Deref};
666
667 pub fn async_benchmark_fn<R, F>(
668 name: impl Into<String>,
669 runtime: R,
670 sampler_factory: F,
671 ) -> Benchmark
672 where
673 R: AsyncRuntime + 'static,
674 F: FnMut(AsyncBencher<R>) -> Box<dyn ErasedSampler> + 'static,
675 {
676 let name = name.into();
677 assert!(!name.is_empty());
678 Benchmark {
679 name,
680 sampler_factory: Box::new(AsyncSampleFactory(sampler_factory, runtime)),
681 }
682 }
683
684 pub struct AsyncSampleFactory<F, R>(pub F, pub R);
685
686 impl<R: AsyncRuntime, F: FnMut(AsyncBencher<R>) -> Box<dyn ErasedSampler>> SamplerFactory
687 for AsyncSampleFactory<F, R>
688 {
689 fn create_sampler(&mut self, params: BenchmarkParams) -> Box<dyn ErasedSampler> {
690 (self.0)(AsyncBencher {
691 params,
692 runtime: self.1,
693 })
694 }
695 }
696
697 pub struct AsyncBencher<R> {
698 params: BenchmarkParams,
699 runtime: R,
700 }
701
702 impl<R: AsyncRuntime + 'static> AsyncBencher<R> {
703 pub fn iter<O, Fut, F>(self, func: F) -> Box<dyn ErasedSampler>
704 where
705 O: 'static,
706 Fut: Future<Output = O>,
707 F: FnMut() -> Fut + Copy + 'static,
708 {
709 Box::new(Sampler::<_, WallClock>::new(move || {
710 self.runtime.block_on(func)
711 }))
712 }
713 }
714
715 impl<R> Deref for AsyncBencher<R> {
716 type Target = BenchmarkParams;
717
718 fn deref(&self) -> &Self::Target {
719 &self.params
720 }
721 }
722
723 pub trait AsyncRuntime: Copy {
724 fn block_on<O, Fut: Future<Output = O>, F: FnMut() -> Fut>(&self, f: F) -> O;
725 }
726
727 #[cfg(feature = "async-tokio")]
728 pub mod tokio {
729 use super::*;
730 use ::tokio::runtime::Builder;
731
732 #[derive(Copy, Clone, Default)]
733 pub struct TokioRuntime;
734
735 impl AsyncRuntime for TokioRuntime {
736 fn block_on<O, Fut: Future<Output = O>, F: FnMut() -> Fut>(&self, mut f: F) -> O {
737 let mut builder = Builder::new_current_thread();
738 #[cfg(feature = "async-tokio-all-drivers")]
739 builder.enable_all();
740 let runtime = builder.build().unwrap();
741 runtime.block_on(f())
742 }
743 }
744 }
745}
746
747#[cfg(test)]
748mod tests {
749 use super::*;
750 use rand::{rngs::SmallRng, Rng, RngCore, SeedableRng};
751 use std::{
752 iter::Sum,
753 ops::{Add, Div},
754 thread,
755 time::Duration,
756 };
757
758 #[test]
759 fn check_iqr_variance_thresholds() {
760 let mut rng = SmallRng::from_entropy();
761
762 let mut values = vec![];
766 values.extend((0..20).map(|_| rng.gen_range(-50.0..=50.)));
767 values.extend((0..10).map(|_| rng.gen_range(-1000.0..=-200.0)));
768 values.extend((0..10).map(|_| rng.gen_range(200.0..=1000.0)));
769
770 let thresholds = iqr_variance_thresholds(values).unwrap();
771
772 assert!(
773 -50. <= *thresholds.start() && *thresholds.end() <= 50.,
774 "Invalid range: {:?}",
775 thresholds
776 );
777 }
778
779 #[test]
781 fn check_outliers_zero_iqr() {
782 let mut rng = SmallRng::from_entropy();
783
784 let mut values = vec![];
785 values.extend([0.; 20]);
786 values.extend((0..10).map(|_| rng.gen_range(-1000.0..=-200.0)));
787 values.extend((0..10).map(|_| rng.gen_range(200.0..=1000.0)));
788
789 let thresholds = iqr_variance_thresholds(values).unwrap();
790
791 assert!(
792 0. <= *thresholds.start() && *thresholds.end() <= 0.,
793 "Invalid range: {:?}",
794 thresholds
795 );
796 }
797
798 #[test]
799 fn check_summary_statistics() {
800 for i in 2u32..100 {
801 let range = 1..=i;
802 let values = range.collect::<Vec<_>>();
803 let stat = Summary::from(&values).unwrap();
804
805 let sum = (i * (i + 1)) as f64 / 2.;
806 let expected_mean = sum / i as f64;
807 let expected_variance = naive_variance(values.as_slice());
808
809 assert_eq!(stat.min, 1);
810 assert_eq!(stat.n, i as usize);
811 assert_eq!(stat.max, i);
812 assert!(
813 (stat.mean - expected_mean).abs() < 1e-5,
814 "Expected close to: {}, given: {}",
815 expected_mean,
816 stat.mean
817 );
818 assert!(
819 (stat.variance - expected_variance).abs() < 1e-5,
820 "Expected close to: {}, given: {}",
821 expected_variance,
822 stat.variance
823 );
824 }
825 }
826
827 #[test]
828 fn check_summary_statistics_types() {
829 Summary::from(<&[i64]>::default());
830 Summary::from(<&[u32]>::default());
831 Summary::from(&Vec::<i64>::default());
832 }
833
834 #[test]
835 fn check_naive_variance() {
836 assert_eq!(naive_variance(&[1, 2, 3]), 1.0);
837 assert_eq!(naive_variance(&[1, 2, 3, 4, 5]), 2.5);
838 }
839
840 #[test]
841 fn check_running_variance() {
842 let input = [1i64, 2, 3, 4, 5, 6, 7];
843 let variances = Summary::running(input.into_iter())
844 .map(|s| s.variance)
845 .collect::<Vec<_>>();
846 let expected = &[0., 0.5, 1., 1.6666, 2.5, 3.5, 4.6666];
847
848 assert_eq!(variances.len(), expected.len());
849
850 for (value, expected_value) in variances.iter().zip(expected) {
851 assert!(
852 (value - expected_value).abs() < 1e-3,
853 "Expected close to: {}, given: {}",
854 expected_value,
855 value
856 );
857 }
858 }
859
860 #[test]
861 fn check_running_variance_stress_test() {
862 let rng = RngIterator(SmallRng::seed_from_u64(0)).map(|i| i as i64);
863 let mut variances = Summary::running(rng).map(|s| s.variance);
864
865 assert!(variances.nth(1_000_000).unwrap() > 0.)
866 }
867
868 #[test]
875 fn check_measure_time() {
876 let expected_delay = 100;
877 let mut target = benchmark_fn("foo", move |b| {
878 b.metric::<WallClock>()
879 .iter(move || thread::sleep(Duration::from_millis(expected_delay)))
880 });
881 target.prepare_state(0);
882
883 let median = median_execution_time(&mut target, 10).as_millis() as u64;
884 assert!(median < expected_delay * 10, "Median {median} is too large");
885 }
886
887 #[test]
889 fn check_cpu_time_metric() {
890 use metrics::CpuTime;
891
892 let mut target = benchmark_fn("cpu_work", |b| {
893 b.metric::<CpuTime>().iter(|| {
894 let mut sum = 0u64;
895 for i in 0..100_000 {
896 sum = sum.wrapping_add(i);
897 }
898 sum
899 })
900 });
901 target.prepare_state(0);
902
903 let median = median_execution_time(&mut target, 10).as_nanos() as u64;
904 assert!(
905 median > 0,
906 "CpuTime should report non-zero for CPU-bound work"
907 );
908 }
909
910 #[test]
912 fn cpu_time_excludes_sleep() {
913 use metrics::CpuTime;
914
915 let cpu_nanos = CpuTime::measure_fn(|| {
916 thread::sleep(Duration::from_millis(50));
917 });
918
919 assert!(
921 cpu_nanos < 1_000_000,
922 "CpuTime during 50ms sleep should be < 1ms, got {cpu_nanos} ns"
923 );
924 }
925
926 #[test]
928 #[cfg(unix)]
930 fn cpu_time_tracks_wall_clock_during_busy_work() {
931 use metrics::CpuTime;
932 use std::time::Instant;
933
934 for _ in 0..20 {
936 let target_duration = Duration::from_millis(50);
937 let deadline = Instant::now() + target_duration;
938
939 let cpu_nanos = CpuTime::measure_fn(|| {
940 while Instant::now() < deadline {
941 black_box(0u64);
942 }
943 });
944
945 let wall_nanos = target_duration.as_nanos() as f64;
946 let cpu = cpu_nanos as f64;
947 let error = ((cpu - wall_nanos) / wall_nanos).abs();
948 if error >= 0.01 {
949 eprintln!("CPU time ({cpu_nanos} ns) should be within 1% of wall time ({wall_nanos} ns), error: {error:.4}")
950 } else {
951 return;
952 }
953 }
954 panic!("No more attempts left");
955 }
956
957 struct RngIterator<T>(T);
958
959 impl<T: RngCore> Iterator for RngIterator<T> {
960 type Item = u32;
961
962 fn next(&mut self) -> Option<Self::Item> {
963 Some(self.0.next_u32())
964 }
965 }
966
967 fn naive_variance<T>(values: &[T]) -> f64
968 where
969 T: Sum + Copy,
970 f64: From<T>,
971 {
972 let n = values.len() as f64;
973 let mean = f64::from(values.iter().copied().sum::<T>()) / n;
974 let mut sum_of_squares = 0.;
975 for value in values.iter().copied() {
976 sum_of_squares += (f64::from(value) - mean).powi(2);
977 }
978 sum_of_squares / (n - 1.)
979 }
980
981 fn median_execution_time(target: &mut Benchmark, iterations: u32) -> Duration {
982 assert!(iterations >= 1);
983 let mut state = target.prepare_state(0);
984 let measures: Vec<_> = (0..iterations).map(|_| state.measure(1)).collect();
985 let time = median(measures).max(1);
986 Duration::from_nanos(time)
987 }
988
989 fn median<T: Copy + Ord + Add<Output = T> + Div<Output = T>>(mut measures: Vec<T>) -> T {
990 assert!(!measures.is_empty(), "Vec is empty");
991 measures.sort_unstable();
992 measures[measures.len() / 2]
993 }
994
995 #[test]
996 fn check_aux_metric_user_cpu_time() {
997 let metric = aux::user_time();
998
999 let start = (metric.start)();
1000 let mut sum = 0u64;
1002 for i in 0..black_box(1_000_000) {
1004 sum = sum.wrapping_add(i);
1005 }
1006 black_box(sum);
1007 let elapsed = (metric.finish)(start);
1008
1009 assert!(
1010 elapsed > 0,
1011 "RUsageCpuTime should report non-zero for CPU-bound work, got {elapsed}"
1012 );
1013 }
1014}