bma_benchmark/
lib.rs

1#![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "README.md" ) ) ]
2#[macro_use]
3extern crate lazy_static;
4#[macro_use]
5extern crate prettytable;
6
7pub use bma_benchmark_proc::benchmark_stage;
8use colored::Colorize;
9use num_format::{Locale, ToFormattedString};
10use prettytable::Table;
11use std::collections::BTreeMap;
12use std::fmt;
13use std::sync::Mutex;
14use std::time::Duration;
15use std::time::Instant;
16use terminal_size::{terminal_size, Height, Width};
17
18lazy_static! {
19    pub static ref DEFAULT_BENCHMARK: Mutex<Benchmark> = Mutex::new(Benchmark::new0());
20    pub static ref DEFAULT_STAGED_BENCHMARK: Mutex<StagedBenchmark> =
21        Mutex::new(StagedBenchmark::new());
22}
23
24macro_rules! result_separator {
25    () => {
26        separator("--- Benchmark results ")
27    };
28}
29
30macro_rules! format_number {
31    ($n: expr) => {
32        $n.to_formatted_string(&Locale::en).replace(',', "_")
33    };
34}
35
36#[macro_export]
37/// run a stage of staged bechmark
38macro_rules! staged_benchmark {
39    ($name: expr, $iterations: expr, $code: block) => {
40        $crate::staged_benchmark_start!($name);
41        black_box(move || {
42        for _iteration in 0..$iterations
43            $code
44        })();
45        $crate::staged_benchmark_finish!($name, $iterations);
46    };
47}
48
49#[macro_export]
50/// run a stage of staged bechmark and check the result for each iteration
51///
52/// The statement MUST return true for ok and false for errors
53macro_rules! staged_benchmark_check {
54    ($name: expr, $iterations: expr, $code: block) => {
55        let mut bma_benchmark_errors = 0;
56        $crate::staged_benchmark_start!($name);
57        black_box(move || {
58            for _iteration in 0..$iterations {
59                if !$code {
60                    bma_benchmark_errors += 1;
61                }
62            }
63        })();
64        $crate::staged_benchmark_finish!($name, $iterations, bma_benchmark_errors);
65    };
66}
67
68#[macro_export]
69/// run a benchmark
70macro_rules! benchmark {
71    ($iterations: expr, $code: block) => {
72        $crate::benchmark_start!();
73        black_box(move || {
74        for _iteration in 0..$iterations
75            $code
76        })();
77        $crate::benchmark_print!($iterations);
78    };
79}
80
81#[macro_export]
82/// run a benchmark and check the result for each iteration
83///
84/// The statement MUST return true for ok and false for errors
85macro_rules! benchmark_check {
86    ($iterations: expr, $code: block) => {
87        let mut bma_benchmark_errors = 0;
88        $crate::benchmark_start!();
89        black_box(move || {
90            for _iteration in 0..$iterations {
91                if !$code {
92                    bma_benchmark_errors += 1;
93                }
94            }
95        })();
96        $crate::benchmark_print!($iterations, bma_benchmark_errors);
97    };
98}
99
100/// Start the default stared benchmark stage
101#[macro_export]
102macro_rules! staged_benchmark_start {
103    ($name: expr) => {
104        $crate::DEFAULT_STAGED_BENCHMARK
105            .lock()
106            .unwrap()
107            .start($name);
108    };
109}
110
111/// Finish the default staged benchmark stage
112#[macro_export]
113macro_rules! staged_benchmark_finish {
114    ($name: expr, $iterations: expr) => {
115        $crate::DEFAULT_STAGED_BENCHMARK
116            .lock()
117            .unwrap()
118            .finish($name, $iterations, 0);
119    };
120    ($name: expr, $iterations: expr, $errors: expr) => {
121        $crate::DEFAULT_STAGED_BENCHMARK
122            .lock()
123            .unwrap()
124            .finish($name, $iterations, $errors);
125    };
126}
127
128/// Finish the default staged benchmark current (last started) stage
129#[macro_export]
130macro_rules! staged_benchmark_finish_current {
131    ($iterations: expr) => {
132        $crate::DEFAULT_STAGED_BENCHMARK
133            .lock()
134            .unwrap()
135            .finish_current($iterations, 0);
136    };
137    ($iterations: expr, $errors: expr) => {
138        $crate::DEFAULT_STAGED_BENCHMARK
139            .lock()
140            .unwrap()
141            .finish_current($iterations, $errors);
142    };
143}
144
145/// Reset the default staged benchmark
146#[macro_export]
147macro_rules! staged_benchmark_reset {
148    () => {
149        $crate::DEFAULT_STAGED_BENCHMARK.lock().unwrap().reset();
150    };
151}
152
153/// Print staged benchmark result
154#[macro_export]
155macro_rules! staged_benchmark_print {
156    () => {
157        $crate::DEFAULT_STAGED_BENCHMARK.lock().unwrap().print();
158    };
159}
160
161/// Print staged benchmark result, specifying the reference stage
162#[macro_export]
163macro_rules! staged_benchmark_print_for {
164    ($eta: expr) => {
165        $crate::DEFAULT_STAGED_BENCHMARK
166            .lock()
167            .unwrap()
168            .print_for($eta);
169    };
170}
171
172/// Start a simple benchmark
173#[macro_export]
174macro_rules! benchmark_start {
175    () => {
176        $crate::DEFAULT_BENCHMARK.lock().unwrap().reset();
177    };
178}
179
180/// Finish a simple benchmark and print results
181#[macro_export]
182macro_rules! benchmark_print {
183    ($iterations: expr) => {
184        $crate::DEFAULT_BENCHMARK
185            .lock()
186            .unwrap()
187            .print(Some($iterations), None);
188    };
189    ($iterations: expr, $errors: expr) => {
190        $crate::DEFAULT_BENCHMARK
191            .lock()
192            .unwrap()
193            .print(Some($iterations), Some($errors));
194    };
195}
196
197#[derive(Default)]
198pub struct LatencyBenchmark {
199    latencies: Vec<Duration>,
200    op: Option<Instant>,
201}
202
203impl LatencyBenchmark {
204    #[inline]
205    pub fn new() -> Self {
206        Self::default()
207    }
208    pub fn clear(&mut self) {
209        self.latencies.clear();
210        self.op.take();
211    }
212    #[inline]
213    pub fn op_start(&mut self) {
214        self.op.replace(Instant::now());
215    }
216    /// # Panics
217    ///
218    /// Will panic if op is not started
219    #[inline]
220    pub fn op_finish(&mut self) {
221        self.latencies.push(self.op.take().unwrap().elapsed());
222    }
223    #[inline]
224    pub fn push(&mut self, latency: Duration) {
225        self.latencies.push(latency);
226    }
227    #[allow(clippy::cast_possible_truncation)]
228    pub fn avg(&self) -> Duration {
229        self.latencies.iter().sum::<Duration>() / self.latencies.len() as u32
230    }
231    pub fn min(&self) -> Duration {
232        self.latencies.iter().min().copied().unwrap_or_default()
233    }
234    pub fn max(&self) -> Duration {
235        self.latencies.iter().max().copied().unwrap_or_default()
236    }
237    pub fn print(&self) {
238        let avg = format_number!(self.avg().as_micros()).yellow();
239        let min = format_number!(self.min().as_micros()).green();
240        let max = format_number!(self.max().as_micros()).red();
241        println!("latency (μs) avg: {}, min: {}, max: {}", avg, min, max);
242    }
243}
244
245/// Benchmark results for a simple benchmark or a stage
246pub struct BenchmarkResult {
247    pub elapsed: Duration,
248    pub iterations: u32,
249    pub errors: u32,
250    pub speed: u32,
251}
252
253/// Staged benchmark
254pub struct StagedBenchmark {
255    benchmarks: BTreeMap<String, Benchmark>,
256    current_stage: Option<String>,
257}
258
259impl Default for StagedBenchmark {
260    fn default() -> Self {
261        Self::new()
262    }
263}
264
265impl StagedBenchmark {
266    pub fn new() -> Self {
267        Self {
268            benchmarks: BTreeMap::new(),
269            current_stage: None,
270        }
271    }
272
273    /// Start benchmark stage
274    ///
275    /// # Panics
276    ///
277    /// Will panic if a stage with the same name already exists
278    pub fn start(&mut self, name: &str) {
279        self.current_stage = Some(name.to_owned());
280        println!("{}", format!("!!! stage started: {} ", name).black());
281        let benchmark = Benchmark::new0();
282        assert!(
283            self.benchmarks.insert(name.to_owned(), benchmark).is_none(),
284            "Benchmark stage {} already exists",
285            name
286        );
287    }
288
289    /// Finish benchmark stage
290    ///
291    /// # Panics
292    ///
293    /// Will panic if a specified stage was not started
294    pub fn finish(&mut self, name: &str, iterations: u32, errors: u32) {
295        let benchmark = self
296            .benchmarks
297            .get_mut(name)
298            .unwrap_or_else(|| panic!("Benchmark stage {} not found", name));
299        benchmark.finish(Some(iterations), Some(errors));
300        println!(
301            "{}",
302            format!(
303                "*** stage completed: {} ({} iters, {:.3} secs)",
304                name,
305                format_number!(iterations),
306                benchmark.elapsed.unwrap().as_secs_f64()
307            )
308            .black()
309        );
310    }
311
312    /// Finish current (last started) benchmark stage
313    /// # Panics
314    ///
315    /// Will panic if no active benchmark stage
316    pub fn finish_current(&mut self, iterations: u32, errors: u32) {
317        let current_stage = self
318            .current_stage
319            .take()
320            .expect("No active benchmark stage");
321        self.finish(&current_stage, iterations, errors);
322    }
323
324    /// Reset staged benchmark
325    pub fn reset(&mut self) {
326        self.benchmarks.clear();
327    }
328
329    fn _result_table_for(&self, eta: Option<&str>) -> Table {
330        let mut have_errs = false;
331        let mut results: Vec<(String, BenchmarkResult)> = Vec::new();
332        for (stage, benchmark) in &self.benchmarks {
333            let result = benchmark.result0();
334            if result.errors > 0 {
335                have_errs = true;
336            }
337            results.push((stage.clone(), result));
338        }
339        let mut header = vec!["stage", "iters"];
340        if have_errs {
341            header.extend(["succs", "errs", "err.rate"]);
342        }
343        header.extend(["secs", "msecs", "iters/s"]);
344        let eta_speed = eta.map(|v| {
345            header.push("diff.s");
346            self.benchmarks.get(v).unwrap().result0().speed
347        });
348        let mut table = ctable(Some(header), false);
349        for (stage, benchmark) in &self.benchmarks {
350            let result = benchmark.result0();
351            let elapsed = result.elapsed.as_secs_f64();
352            let mut cells = vec![
353                cell!(stage),
354                cell!(format_number!(result.iterations).magenta()),
355            ];
356            if have_errs {
357                let success = result.iterations - result.errors;
358                cells.extend([
359                    cell!(if success > 0 {
360                        format_number!(success).green()
361                    } else {
362                        <_>::default()
363                    }),
364                    cell!(if result.errors > 0 {
365                        format_number!(result.errors).red()
366                    } else {
367                        <_>::default()
368                    }),
369                    cell!(if result.errors > 0 {
370                        format!(
371                            "{:.2} %",
372                            (f64::from(result.errors) / f64::from(result.iterations) * 100.0)
373                        )
374                        .red()
375                    } else {
376                        "".normal()
377                    }),
378                ]);
379            }
380            cells.extend([
381                cell!(format!("{:.3}", elapsed).blue()),
382                cell!(format!("{:.3}", elapsed * 1000.0).cyan()),
383                cell!(format_number!(result.speed).yellow()),
384            ]);
385            if let Some(r) = eta_speed {
386                if result.speed != r {
387                    let diff = f64::from(result.speed) / f64::from(r);
388                    if !(0.9999..=1.0001).contains(&diff) {
389                        cells.push(cell!(if diff > 1.0 {
390                            format!("+{:.2} %", ((diff - 1.0) * 100.0)).green()
391                        } else {
392                            format!("-{:.2} %", ((1.0 - diff) * 100.0)).red()
393                        }));
394                    }
395                }
396            };
397            table.add_row(prettytable::Row::new(cells));
398        }
399        table
400    }
401
402    /// Get the result table for staged benchmark
403    pub fn result_table(&self) -> Table {
404        self._result_table_for(None)
405    }
406
407    /// Get the result table for staged benchmark, specifying the reference stage
408    pub fn result_table_for(&self, eta: &str) -> Table {
409        self._result_table_for(Some(eta))
410    }
411
412    /// Print the result table
413    pub fn print(&self) {
414        println!("{}", result_separator!());
415        self.result_table().printstd();
416    }
417
418    /// Print the result table, specifying the reference stage
419    pub fn print_for(&self, eta: &str) {
420        println!("{}", result_separator!());
421        self.result_table_for(eta).printstd();
422    }
423}
424
425/// Simple benchmark or a stage
426pub struct Benchmark {
427    started: Instant,
428    iterations: u32,
429    set_iterations: u32,
430    errors: u32,
431    elapsed: Option<Duration>,
432}
433
434impl Default for Benchmark {
435    fn default() -> Self {
436        Self::new0()
437    }
438}
439
440impl fmt::Display for Benchmark {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        write!(
443            f,
444            "{}",
445            self.to_string_for(Some(self.iterations), Some(self.errors))
446        )
447    }
448}
449
450impl Benchmark {
451    /// Create simple benchmark with unknown number of iterations
452    pub fn new0() -> Self {
453        Self {
454            started: Instant::now(),
455            iterations: 0,
456            set_iterations: 0,
457            errors: 0,
458            elapsed: None,
459        }
460    }
461
462    /// Create simple benchmark with pre-defined number of iterations
463    pub fn new(iterations: u32) -> Self {
464        Self {
465            started: Instant::now(),
466            iterations,
467            set_iterations: iterations,
468            errors: 0,
469            elapsed: None,
470        }
471    }
472
473    /// Reset the benchmark timer
474    pub fn reset(&mut self) {
475        self.started = Instant::now();
476        self.iterations = self.set_iterations;
477        self.errors = 0;
478    }
479
480    /// Finish a simple benchmark
481    pub fn finish0(&mut self) {
482        self.elapsed = Some(self.started.elapsed());
483    }
484
485    /// Finish a simple benchmark, specifying number of iterations made
486    pub fn finish(&mut self, iterations: Option<u32>, errors: Option<u32>) {
487        self.elapsed = Some(self.started.elapsed());
488        if let Some(i) = iterations {
489            self.iterations = i;
490        }
491        if let Some(e) = errors {
492            self.errors = e;
493        }
494    }
495
496    /// Print a simple benchmark result
497    pub fn print0(&self) {
498        self.print(Some(self.iterations), Some(self.errors));
499    }
500
501    /// Print a simple benchmark result, specifying number of iterations made
502    pub fn print(&self, iterations: Option<u32>, errors: Option<u32>) {
503        println!("{}", self.to_string_for(iterations, errors));
504    }
505
506    #[allow(clippy::cast_sign_loss)]
507    #[allow(clippy::cast_possible_truncation)]
508    /// Get a benchmark result
509    pub fn result0(&self) -> BenchmarkResult {
510        self.result(Some(self.iterations), Some(self.errors))
511    }
512
513    #[allow(clippy::cast_sign_loss)]
514    #[allow(clippy::cast_possible_truncation)]
515    /// Get a benchmark result, specifying number of iterations made
516    pub fn result(&self, iterations: Option<u32>, errors: Option<u32>) -> BenchmarkResult {
517        let elapsed = self.elapsed.unwrap_or_else(|| self.started.elapsed());
518        let it = iterations.unwrap_or(self.iterations);
519        let errs = errors.unwrap_or(self.errors);
520        BenchmarkResult {
521            elapsed,
522            iterations: it,
523            errors: errs,
524            speed: (f64::from(it - errs) / elapsed.as_secs_f64()) as u32,
525        }
526    }
527
528    fn to_string_for(&self, iterations: Option<u32>, errors: Option<u32>) -> String {
529        let result = self.result(iterations, errors);
530        let elapsed = result.elapsed.as_secs_f64();
531        format!(
532            "{}\nIterations: {}, success: {}, errors: {}{}\n\
533            Elapsed:\n {} secs ({} msecs)\n {} iters/s\n {} ns per iter",
534            result_separator!(),
535            format_number!(result.iterations).magenta(),
536            format_number!(result.iterations - result.errors).green(),
537            if result.errors > 0 {
538                format_number!(result.errors).red()
539            } else {
540                "None".normal()
541            },
542            if result.errors > 0 {
543                format!(
544                    ", error rate: {}",
545                    format!(
546                        "{:.2} %",
547                        (f64::from(result.errors) / f64::from(result.iterations) * 100.0)
548                    )
549                    .red()
550                )
551            } else {
552                String::new()
553            },
554            format!("{:.3}", elapsed).blue(),
555            format!("{:.3}", elapsed * 1000.0).cyan(),
556            format_number!(result.speed).yellow(),
557            format_number!(1_000_000_000 / result.speed).magenta()
558        )
559    }
560
561    /// Increment iterations inside benchmark
562    ///
563    /// Not required to use if the number of iterations is specified at benchmark creation or
564    /// finish / print
565    pub fn increment(&mut self) {
566        self.iterations += 1;
567    }
568
569    /// Increment errors inside benchmark
570    ///
571    /// Not required to use if the number of errors is specified at benchmark finish / print
572    pub fn increment_errors(&mut self) {
573        self.errors += 1;
574    }
575}
576
577fn ctable(titles: Option<Vec<&str>>, raw: bool) -> prettytable::Table {
578    let mut table = prettytable::Table::new();
579    let format = prettytable::format::FormatBuilder::new()
580        .column_separator(' ')
581        .borders(' ')
582        .separators(
583            &[prettytable::format::LinePosition::Title],
584            prettytable::format::LineSeparator::new('-', '-', '-', '-'),
585        )
586        .padding(0, 1)
587        .build();
588    table.set_format(format);
589    if let Some(tt) = titles {
590        let mut titlevec: Vec<prettytable::Cell> = Vec::new();
591        for t in tt {
592            if raw {
593                titlevec.push(prettytable::Cell::new(t));
594            } else {
595                titlevec.push(prettytable::Cell::new(t).style_spec("Fb"));
596            }
597        }
598        table.set_titles(prettytable::Row::new(titlevec));
599    };
600    table
601}
602
603#[allow(clippy::cast_possible_truncation)]
604fn separator(title: &str) -> colored::ColoredString {
605    let size = terminal_size();
606    let width = if let Some((Width(w), Height(_))) = size {
607        w
608    } else {
609        40
610    };
611    (title.to_owned()
612        + &(0..width - title.len() as u16)
613            .map(|_| "-")
614            .collect::<String>())
615        .black()
616}
617
618pub struct Perf {
619    start: Instant,
620    iterations: usize,
621    checkpoints: Vec<&'static str>,
622    measurements: BTreeMap<&'static str, Vec<Duration>>,
623}
624
625impl Default for Perf {
626    fn default() -> Self {
627        Self::new()
628    }
629}
630
631impl Perf {
632    pub fn new() -> Self {
633        Self {
634            start: Instant::now(),
635            iterations: 0,
636            checkpoints: Vec::new(),
637            measurements: BTreeMap::new(),
638        }
639    }
640    pub fn reset(&mut self) {
641        self.iterations = 0;
642        self.checkpoints.clear();
643        self.measurements.clear();
644    }
645    pub fn start(&mut self) {
646        self.iterations += 1;
647        self.start = Instant::now();
648    }
649    pub fn checkpoint(&mut self, name: &'static str) {
650        if self.iterations == 1 {
651            self.checkpoints.push(name);
652        }
653        self.measurements
654            .entry(name)
655            .or_default()
656            .push(self.start.elapsed());
657        self.start = Instant::now();
658    }
659    /// # Panics
660    ///
661    /// Will panic if the number of iterations is less than 1 or greater than u32::MAX
662    pub fn print(&self) {
663        println!("Iterations: {}", self.iterations.to_string().magenta());
664        println!();
665        let header = vec!["checkpoint", "min", "max", "avg"];
666        let mut table = ctable(Some(header), false);
667        for name in &self.checkpoints {
668            let durations = self.measurements.get(name).unwrap();
669            let min = durations.iter().min().unwrap().as_micros();
670            let max = durations.iter().max().unwrap().as_micros();
671            let avg = (durations.iter().sum::<Duration>()
672                / u32::try_from(durations.len()).unwrap())
673            .as_micros();
674            table.add_row(prettytable::Row::new(vec![
675                cell!(name),
676                cell!(format_number!(min).blue().bold()),
677                cell!(format_number!(max).yellow()),
678                cell!(format_number!(avg).green().bold()),
679            ]));
680        }
681        let mut totals: Vec<Duration> = Vec::with_capacity(self.iterations);
682        for i in 0..self.iterations {
683            let mut t = Duration::default();
684            for name in &self.checkpoints {
685                t += self.measurements.get(name).unwrap()[i];
686            }
687            totals.push(t);
688        }
689        let min = totals.iter().min().unwrap().as_micros();
690        let max = totals.iter().max().unwrap().as_micros();
691        let avg =
692            (totals.iter().sum::<Duration>() / u32::try_from(totals.len()).unwrap()).as_micros();
693        table.add_row(row!["-----".black()]);
694        table.add_row(prettytable::Row::new(vec![
695            cell!("TOTAL".yellow().bold()),
696            cell!(format_number!(min).blue().bold()),
697            cell!(format_number!(max).yellow()),
698            cell!(format_number!(avg).green().bold()),
699        ]));
700        table.printstd();
701        println!();
702        println!("{}", "(the durations are provided in microseconds)".black());
703    }
704}
705
706const WARMUP_DURATION: Duration = Duration::from_secs(5);
707
708/// recommended to call this function before running speed race benchmarks
709pub fn warmup() {
710    println!("{}", "warming up".black());
711    std::hint::black_box(move || {
712        let start = Instant::now();
713        while start.elapsed() < WARMUP_DURATION {
714            std::thread::yield_now();
715        }
716    })();
717    println!("{}", "CPU has been warmed up".black());
718}
719
720/// a shortcut to bma_benchmark::warmup() in case all the macros are imported
721#[macro_export]
722macro_rules! warmup {
723    () => {
724        $crate::warmup();
725    };
726}