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]
37macro_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]
50macro_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]
69macro_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]
82macro_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#[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#[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#[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#[macro_export]
147macro_rules! staged_benchmark_reset {
148 () => {
149 $crate::DEFAULT_STAGED_BENCHMARK.lock().unwrap().reset();
150 };
151}
152
153#[macro_export]
155macro_rules! staged_benchmark_print {
156 () => {
157 $crate::DEFAULT_STAGED_BENCHMARK.lock().unwrap().print();
158 };
159}
160
161#[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#[macro_export]
174macro_rules! benchmark_start {
175 () => {
176 $crate::DEFAULT_BENCHMARK.lock().unwrap().reset();
177 };
178}
179
180#[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 #[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
245pub struct BenchmarkResult {
247 pub elapsed: Duration,
248 pub iterations: u32,
249 pub errors: u32,
250 pub speed: u32,
251}
252
253pub 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 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 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 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(¤t_stage, iterations, errors);
322 }
323
324 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 pub fn result_table(&self) -> Table {
404 self._result_table_for(None)
405 }
406
407 pub fn result_table_for(&self, eta: &str) -> Table {
409 self._result_table_for(Some(eta))
410 }
411
412 pub fn print(&self) {
414 println!("{}", result_separator!());
415 self.result_table().printstd();
416 }
417
418 pub fn print_for(&self, eta: &str) {
420 println!("{}", result_separator!());
421 self.result_table_for(eta).printstd();
422 }
423}
424
425pub 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 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 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 pub fn reset(&mut self) {
475 self.started = Instant::now();
476 self.iterations = self.set_iterations;
477 self.errors = 0;
478 }
479
480 pub fn finish0(&mut self) {
482 self.elapsed = Some(self.started.elapsed());
483 }
484
485 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 pub fn print0(&self) {
498 self.print(Some(self.iterations), Some(self.errors));
499 }
500
501 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 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 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 pub fn increment(&mut self) {
566 self.iterations += 1;
567 }
568
569 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 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
708pub 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#[macro_export]
722macro_rules! warmup {
723 () => {
724 $crate::warmup();
725 };
726}