taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

bench.rs (7677B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::time::{Duration, Instant};
     18 
     19 use sqlx::{Executor, PgPool};
     20 
     21 use crate::types::utils::InlineStr;
     22 
     23 const HEX_TABLE: &[u8; 16] = b"0123456789abcdef";
     24 
     25 /// Fast 32 hex string generation
     26 pub fn h32() -> InlineStr<64> {
     27     let mut raw = [0u8; 32];
     28     let mut encoded = [0u8; 64];
     29 
     30     rand::fill(&mut raw);
     31     for i in 0..raw.len() {
     32         let byte = raw[i];
     33         encoded[i * 2] = HEX_TABLE[(byte >> 4) as usize];
     34         encoded[i * 2 + 1] = HEX_TABLE[(byte & 0x0F) as usize];
     35     }
     36     InlineStr::copy_from_slice(&encoded)
     37 }
     38 
     39 /// Fast 64 hex string generation
     40 pub fn h64() -> InlineStr<128> {
     41     let mut raw = [0u8; 64];
     42     let mut encoded = [0u8; 128];
     43 
     44     rand::fill(&mut raw);
     45     for i in 0..raw.len() {
     46         let byte = raw[i];
     47         encoded[i * 2] = HEX_TABLE[(byte >> 4) as usize];
     48         encoded[i * 2 + 1] = HEX_TABLE[(byte & 0x0F) as usize];
     49     }
     50     InlineStr::copy_from_slice(&encoded)
     51 }
     52 
     53 const WARN: Duration = Duration::from_millis(4);
     54 const ERR: Duration = Duration::from_millis(50);
     55 
     56 fn fmt_measures(times: &[u64]) -> Vec<String> {
     57     // Basic stats calculations
     58     let min = *times.iter().min().unwrap_or(&0);
     59     let max = *times.iter().max().unwrap_or(&0);
     60     let mean = times.iter().sum::<u64>() / times.len() as u64;
     61 
     62     let variance = times
     63         .iter()
     64         .map(|&t| (t as f64 - mean as f64).powi(2))
     65         .sum::<f64>()
     66         / times.len() as f64;
     67     let std_var = variance.sqrt() as u64;
     68 
     69     // Map stats to colored strings
     70     [min, mean, max, std_var]
     71         .iter()
     72         .map(|&val| {
     73             let duration = Duration::from_micros(val);
     74             let s = format!("{:?}", duration);
     75             if duration > ERR {
     76                 format!("\x1b[31m{}\x1b[0m", s) // Red
     77             } else if duration > WARN {
     78                 format!("\x1b[33m{}\x1b[0m", s) // Yellow
     79             } else {
     80                 format!("\x1b[32m{}\x1b[0m", s) // Green
     81             }
     82         })
     83         .collect()
     84 }
     85 
     86 pub struct Bench {
     87     db: PgPool,
     88     buf: String,
     89     amount: usize,
     90     iter: usize,
     91     measures: Vec<Vec<String>>,
     92     dirty: bool,
     93 }
     94 
     95 impl Bench {
     96     pub fn new(db: &PgPool, iter: usize, amount: usize) -> Self {
     97         println!("Bench {iter} times with {amount} rows");
     98         Self {
     99             db: db.clone(),
    100             buf: String::with_capacity(2 * 1024 * 1024),
    101             amount,
    102             iter,
    103             measures: Vec::new(),
    104             dirty: false,
    105         }
    106     }
    107 }
    108 
    109 impl Bench {
    110     pub async fn table(
    111         &mut self,
    112         table: &str,
    113         mut generator: impl FnMut(&mut String, usize) -> std::fmt::Result,
    114     ) {
    115         println!("Gen rows for {table}");
    116         let mut db = self.db.acquire().await.unwrap();
    117         let mut stream = db
    118             .copy_in_raw(&format!("COPY {table} FROM STDIN"))
    119             .await
    120             .unwrap();
    121         for i in 0..self.amount {
    122             generator(&mut self.buf, i + 1).unwrap();
    123             if self.buf.len() > 1024 * 1024 {
    124                 stream.send(self.buf.as_bytes()).await.unwrap();
    125                 self.buf.clear();
    126             }
    127         }
    128         stream.send(self.buf.as_bytes()).await.unwrap();
    129         self.buf.clear();
    130         stream.finish().await.unwrap();
    131         self.dirty = true;
    132     }
    133 
    134     pub async fn measure<R>(
    135         &mut self,
    136         name: &'static str,
    137         lambda: impl AsyncFn(usize) -> R,
    138     ) -> Vec<R> {
    139         if self.dirty {
    140             // Update database statistics for better perf
    141             self.db.execute("VACUUM FULL ANALYZE").await.unwrap();
    142             self.dirty = false;
    143         }
    144 
    145         println!("Measure action {}", name);
    146 
    147         let mut results = Vec::with_capacity(self.iter);
    148         let mut times = Vec::with_capacity(self.iter);
    149 
    150         for idx in 0..self.iter {
    151             let start = Instant::now();
    152             let result = lambda(idx).await;
    153             let elapsed = start.elapsed().as_micros() as u64;
    154             results.push(result);
    155             times.push(elapsed);
    156         }
    157 
    158         let mut row = vec![format!("\x1b[35m{}\x1b[0m", name)];
    159         row.extend(fmt_measures(&times));
    160 
    161         self.measures.push(row);
    162 
    163         results
    164     }
    165 }
    166 
    167 impl Drop for Bench {
    168     fn drop(&mut self) {
    169         print_table(
    170             &["benchmark", "min", "mean", "max", "std"],
    171             self.measures.as_slice(),
    172             ' ',
    173             &[
    174                 ColumnStyle::default(),
    175                 ColumnStyle { align_left: false },
    176                 ColumnStyle { align_left: false },
    177                 ColumnStyle { align_left: false },
    178                 ColumnStyle { align_left: false },
    179             ],
    180         );
    181     }
    182 }
    183 
    184 #[derive(Debug, Clone, Copy)]
    185 pub struct ColumnStyle {
    186     pub align_left: bool,
    187 }
    188 impl Default for ColumnStyle {
    189     fn default() -> Self {
    190         Self { align_left: true }
    191     }
    192 }
    193 
    194 /// Helper to calculate visible length of a string (ignoring ANSI escape codes)
    195 fn display_length(s: &str) -> usize {
    196     // Basic regex-free approach to strip ANSI sequences for length calculation
    197     let mut len = 0;
    198     let mut in_esc = false;
    199     for c in s.chars() {
    200         if c == '\x1b' {
    201             in_esc = true;
    202             continue;
    203         }
    204         if in_esc {
    205             if (0x40..=0x7e).contains(&(c as u8)) {
    206                 in_esc = false;
    207             }
    208             continue;
    209         }
    210         len += 1;
    211     }
    212     len
    213 }
    214 
    215 fn print_table(columns: &[&str], rows: &[Vec<String>], separator: char, col_style: &[ColumnStyle]) {
    216     // 1. Calculate column widths (Name vs Max Row Content)
    217     let col_meta: Vec<(&str, usize)> = columns
    218         .iter()
    219         .enumerate()
    220         .map(|(i, &name)| {
    221             let max_row = rows
    222                 .iter()
    223                 .map(|row| display_length(&row[i]))
    224                 .max()
    225                 .unwrap_or(0);
    226             (name, display_length(name).max(max_row))
    227         })
    228         .collect();
    229 
    230     let mut table = String::new();
    231 
    232     let padd = |buf: &mut String, len: usize| {
    233         for _ in 0..len {
    234             buf.push(' ');
    235         }
    236     };
    237 
    238     for (i, (name, len)) in col_meta.iter().enumerate() {
    239         if i > 0 {
    240             table.push(separator);
    241         }
    242 
    243         let pad = len - display_length(name);
    244         padd(&mut table, pad / 2);
    245         table.push_str(name);
    246         padd(&mut table, pad / 2 + pad % 2);
    247     }
    248     table.push('\n');
    249 
    250     for row in rows {
    251         for (i, (str_val, &(_, len))) in row.iter().zip(col_meta.iter()).enumerate() {
    252             if i > 0 {
    253                 table.push(separator);
    254             }
    255 
    256             let style = col_style.get(i).cloned().unwrap_or_default();
    257             let pad = len - display_length(str_val);
    258 
    259             if style.align_left {
    260                 table.push_str(str_val);
    261                 padd(&mut table, pad);
    262             } else {
    263                 padd(&mut table, pad);
    264                 table.push_str(str_val);
    265             }
    266         }
    267         table.push('\n');
    268     }
    269 
    270     print!("{}", table);
    271 }