Skip to main content

tango_bench/
lib.rs

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/// Registers benchmark in the system
44///
45/// Macros accepts a list of functions that produce any [`IntoBenchmarks`] type. All of the benchmarks
46/// created by those functions are registered in the harness.
47///
48/// ## Example
49/// ```rust
50/// use std::time::Instant;
51/// use tango_bench::{benchmark_fn, IntoBenchmarks, tango_benchmarks};
52///
53/// fn time_benchmarks() -> impl IntoBenchmarks {
54///     [benchmark_fn("current_time", |b| b.iter(|| Instant::now()))]
55/// }
56///
57/// tango_benchmarks!(time_benchmarks());
58/// ```
59#[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/// Main entrypoint for benchmarks. Deprecated. Not needed anymore. Still present for backward compatibility
71#[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    /// Measures the performance if the function
123    ///
124    /// Returns the cumulative execution time (all iterations) with nanoseconds precision,
125    /// but not necessarily accuracy. Usually this time is get by `clock_gettime()` call or some other
126    /// platform-specific call.
127    ///
128    /// This method should use the same arguments for measuring the test function unless [`prepare_state()`]
129    /// method is called. Only then new set of input arguments should be generated. It is NOT allowed
130    /// to call this method without first calling [`prepare_state()`].
131    ///
132    /// [`prepare_state()`]: Benchmark::prepare_state()
133    fn measure(&mut self, iterations: usize) -> u64;
134
135    /// Estimates the number of iterations achievable within given time.
136    ///
137    /// Time span is given in milliseconds (`time_ms`). Estimate can be an approximation and it is important
138    /// for implementation to be fast (in the order of 10 ms).
139    /// If possible the same input arguments should be used when building the estimate.
140    /// If the single call of a function is longer than provided timespan the implementation should return 0.
141    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            // Never believe short measurements because they are very unreliable. Pretending that
147            // measurement at least took 1us guarantees that we won't end up with an unreasonably large number
148            // of iterations
149            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            // Do early stop if new estimate has the same order of magnitude. It is good enough.
154            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    /// Generates next sampler for the measurement
211    pub fn prepare_state(&mut self, seed: u64) -> Box<dyn ErasedSampler> {
212        self.sampler_factory
213            .create_sampler(BenchmarkParams { seed })
214    }
215
216    /// Name of the benchmark
217    pub fn name(&self) -> &str {
218        self.name.as_str()
219    }
220}
221
222/// Converts the implementing type into a vector of [`Benchmark`].
223pub 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
239/// Calculates the result of the benchmarking run
240///
241/// Return None if no measurements were made
242pub(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        // Calculating difference between candidate and baseline
251        .map(|&(b, c)| c as f64 - b as f64)
252        // Normalizing difference to iterations count
253        .map(|diff| diff / iterations_per_sample as f64)
254        .collect::<Vec<_>>();
255
256    // need to save number of original samples to calculate number of outliers correctly
257    let n = diff.len();
258
259    // Normalizing measurements to iterations count
260    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    // Calculating measurements range. All measurements outside this interval considered outliers
270    let range = if filter_outliers {
271        iqr_variance_thresholds(diff.to_vec())
272    } else {
273        None
274    };
275
276    // Cleaning measurements from outliers if needed
277    if let Some(range) = range {
278        // We filtering outliers to build statistical Summary and the order of elements in arrays
279        // doesn't matter, therefore swap_remove() is used. But we need to make sure that all arrays
280        // has the same length
281        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
311/// Contains the estimation of how much faster or slower is candidate function compared to baseline
312pub(crate) struct DiffEstimate {
313    // Percentage of difference between candidate and baseline
314    //
315    // Negative value means that candidate is faster than baseline, positive - slower.
316    pct: f64,
317
318    // Is the difference statistically significant
319    significant: bool,
320}
321
322impl DiffEstimate {
323    /// Builds [`DiffEstimate`] from flat sampling
324    ///
325    /// Flat sampling is a sampling where each measurement is normalized by the number of iterations.
326    /// This is needed to make measurements comparable between each other. Linear sampling is more
327    /// robust to outliers, but it is requiring more iterations.
328    ///
329    /// It is assumed that baseline and candidate are already normalized by iterations count.
330    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        // significant result is far away from 0 and exceeds the noise threshold
338        // z_score = 2.6 corresponds to 99% significance level
339        let significant = z_score.abs() >= 2.6 && pct.abs() > noise_threshold;
340
341        Self { pct, significant }
342    }
343}
344
345/// Describes the results of a single benchmark run
346pub(crate) struct RunResult {
347    /// statistical summary of baseline function measurements
348    baseline: Summary<f64>,
349
350    /// statistical summary of candidate function measurements
351    candidate: Summary<f64>,
352
353    /// individual measurements of a benchmark (candidate - baseline)
354    diff: Summary<f64>,
355
356    diff_estimate: DiffEstimate,
357
358    /// Numbers of detected and filtered outliers
359    outliers: usize,
360}
361
362/// Statistical summary for a given iterator of numbers.
363///
364/// Calculates all the information using single pass over the data. Mean and variance are calculated using
365/// streaming algorithm described in _Art of Computer Programming, Vol 2, page 232_.
366#[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
453/// Outlier detection algorithm based on interquartile range
454///
455/// Observations that are 1.5 IQR away from the corresponding quartile are consideted as outliers
456/// as described in original Tukey's paper.
457pub 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    // In case q1 and q3 are equal, we need to make sure that IQR is not 0
466    // In the future it would be nice to measure system timer precision empirically.
467    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    // Calculating the indices of the thresholds in an dataset
473    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    // Calculating the equal number of observations which should be removed from each "side" of observations
490    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
495/// This trait allows to define strategy for measuring metric of interest about the code
496pub trait Metric {
497    /// Measures current metric on a given code
498    fn measure_fn(f: impl FnMut()) -> u64;
499}
500
501/// Dynamic dispatch entry for collecting a metric
502pub struct AuxMetricEntry {
503    pub id: &'static str,
504    pub start: fn() -> u64,
505    pub finish: fn(u64) -> u64,
506}
507
508/// Returns all auxiliary metrics available in this build.
509pub 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    /// Measures system CPU time via `getrusage()` / `GetProcessTimes()`.
521    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    /// Measures system CPU time via `getrusage()` / `GetProcessTimes()`.
539    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        /// Implementation of wall clock timer that uses standard OS time source
564        #[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        /// Implementation of wall clock timer that uses rdtscp on x86
573        #[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    /// Metric that measures CPU time consumed by the current thread.
590    ///
591    /// Unlike [`WallClock`], this metric excludes time spent sleeping, blocking on I/O,
592    /// or waiting for locks. It measures only the time the thread was actively running on a CPU.
593    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        // Generate 20 random values in range [-50, 50]
763        // and add 10 outliers in each of two ranges [-1000, -200] and [200, 1000]
764        // This way IQR is no more than 100 and thresholds should be within [-50, 50] range
765        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    /// This tests checks that the algorithm is stable in case of zero difference between 25 and 75 percentiles
780    #[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    /// Basic check of measurement code
869    ///
870    /// This test is quite brittle. There is no guarantee the OS scheduler will wake up the thread
871    /// soon enough to meet measurement target. We try to mitigate this possibility using several strategies:
872    /// 1. repeating test several times and taking median as target measurement.
873    /// 2. using more liberal checking condition (allowing 1 order of magnitude error in measurement)
874    #[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    /// Verify CpuTime returns non-zero for CPU-bound work
888    #[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    /// CpuTime should report near-zero during sleep (only wall time passes, not CPU time).
911    #[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        // 50ms of sleep should consume very little CPU time — well under 1ms.
920        assert!(
921            cpu_nanos < 1_000_000,
922            "CpuTime during 50ms sleep should be < 1ms, got {cpu_nanos} ns"
923        );
924    }
925
926    /// CpuTime should track wall-clock time closely during a pure CPU-bound loop.
927    #[test]
928    /// Tests predictably fails on Windows, need to research
929    #[cfg(unix)]
930    fn cpu_time_tracks_wall_clock_during_busy_work() {
931        use metrics::CpuTime;
932        use std::time::Instant;
933
934        // This test is flaky by design especially in CI, doing multiple attempts
935        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        // Do some CPU work
1001        let mut sum = 0u64;
1002        // black_box prevents constant folding optimization
1003        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}