taler-rust

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

bench.rs (8355B)


      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 16 hex string generation
     26 pub fn h16() -> InlineStr<32> {
     27     let mut raw = [0u8; 16];
     28     let mut encoded = [0u8; 32];
     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 32 hex string generation
     40 pub fn h32() -> InlineStr<64> {
     41     let mut raw = [0u8; 32];
     42     let mut encoded = [0u8; 64];
     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 /// Fast 64 hex string generation
     54 pub fn h64() -> InlineStr<128> {
     55     let mut raw = [0u8; 64];
     56     let mut encoded = [0u8; 128];
     57 
     58     rand::fill(&mut raw);
     59     for i in 0..raw.len() {
     60         let byte = raw[i];
     61         encoded[i * 2] = HEX_TABLE[(byte >> 4) as usize];
     62         encoded[i * 2 + 1] = HEX_TABLE[(byte & 0x0F) as usize];
     63     }
     64     InlineStr::copy_from_slice(&encoded)
     65 }
     66 
     67 const WARN: Duration = Duration::from_millis(4);
     68 const ERR: Duration = Duration::from_millis(50);
     69 
     70 fn fmt_measures(times: &[u64]) -> Vec<String> {
     71     // Basic stats calculations
     72     let min = *times.iter().min().unwrap_or(&0);
     73     let max = *times.iter().max().unwrap_or(&0);
     74     let mean = times.iter().sum::<u64>() / times.len() as u64;
     75 
     76     let variance = times
     77         .iter()
     78         .map(|&t| (t as f64 - mean as f64).powi(2))
     79         .sum::<f64>()
     80         / times.len() as f64;
     81     let std_var = variance.sqrt() as u64;
     82 
     83     // Map stats to colored strings
     84     [min, mean, max, std_var]
     85         .iter()
     86         .map(|&val| {
     87             let duration = Duration::from_micros(val);
     88             let s = format!("{:?}", duration);
     89             if duration > ERR {
     90                 format!("\x1b[31m{}\x1b[0m", s) // Red
     91             } else if duration > WARN {
     92                 format!("\x1b[33m{}\x1b[0m", s) // Yellow
     93             } else {
     94                 format!("\x1b[32m{}\x1b[0m", s) // Green
     95             }
     96         })
     97         .collect()
     98 }
     99 
    100 pub struct Bench {
    101     db: PgPool,
    102     buf: String,
    103     pub amount: usize,
    104     iter: usize,
    105     measures: Vec<Vec<String>>,
    106     dirty: bool,
    107 }
    108 
    109 impl Bench {
    110     pub fn new(db: &PgPool) -> Self {
    111         let iter: usize = std::env::var("BENCH_ITER")
    112             .ok()
    113             .and_then(|v| v.parse().ok())
    114             .unwrap_or(10);
    115 
    116         let amount: usize = std::env::var("BENCH_AMOUNT")
    117             .ok()
    118             .and_then(|v| v.parse().ok())
    119             .unwrap_or(100);
    120         println!("Bench {iter} times with {amount} rows");
    121         Self {
    122             db: db.clone(),
    123             buf: String::with_capacity(2 * 1024 * 1024),
    124             amount,
    125             iter,
    126             measures: Vec::new(),
    127             dirty: false,
    128         }
    129     }
    130 }
    131 
    132 impl Bench {
    133     pub async fn table(
    134         &mut self,
    135         table: &str,
    136         mut generator: impl FnMut(&mut String, usize) -> std::fmt::Result,
    137     ) {
    138         println!("Gen rows for {table}");
    139         let mut db = self.db.acquire().await.unwrap();
    140         let mut stream = db
    141             .copy_in_raw(&format!("COPY {table} FROM STDIN"))
    142             .await
    143             .unwrap();
    144         for i in 0..self.amount {
    145             generator(&mut self.buf, i + 1).unwrap();
    146             if self.buf.len() > 1024 * 1024 {
    147                 stream.send(self.buf.as_bytes()).await.unwrap();
    148                 self.buf.clear();
    149             }
    150         }
    151         stream.send(self.buf.as_bytes()).await.unwrap();
    152         self.buf.clear();
    153         stream.finish().await.unwrap();
    154         self.dirty = true;
    155     }
    156 
    157     pub async fn measure<R>(
    158         &mut self,
    159         name: &'static str,
    160         mut lambda: impl AsyncFnMut(usize) -> R,
    161     ) -> Vec<R> {
    162         if self.dirty {
    163             // Update database statistics for better perf
    164             self.db.execute("VACUUM FULL ANALYZE").await.unwrap();
    165             self.dirty = false;
    166         }
    167 
    168         println!("Measure action {}", name);
    169 
    170         let mut results = Vec::with_capacity(self.iter);
    171         let mut times = Vec::with_capacity(self.iter);
    172 
    173         for idx in 0..self.iter {
    174             let start = Instant::now();
    175             let result = lambda(idx).await;
    176             let elapsed = start.elapsed().as_micros() as u64;
    177             results.push(result);
    178             times.push(elapsed);
    179         }
    180 
    181         let mut row = vec![format!("\x1b[35m{}\x1b[0m", name)];
    182         row.extend(fmt_measures(&times));
    183 
    184         self.measures.push(row);
    185 
    186         results
    187     }
    188 }
    189 
    190 impl Drop for Bench {
    191     fn drop(&mut self) {
    192         print_table(
    193             &["benchmark", "min", "mean", "max", "std"],
    194             self.measures.as_slice(),
    195             ' ',
    196             &[
    197                 ColumnStyle::default(),
    198                 ColumnStyle { align_left: false },
    199                 ColumnStyle { align_left: false },
    200                 ColumnStyle { align_left: false },
    201                 ColumnStyle { align_left: false },
    202             ],
    203         );
    204     }
    205 }
    206 
    207 #[derive(Debug, Clone, Copy)]
    208 pub struct ColumnStyle {
    209     pub align_left: bool,
    210 }
    211 impl Default for ColumnStyle {
    212     fn default() -> Self {
    213         Self { align_left: true }
    214     }
    215 }
    216 
    217 /// Helper to calculate visible length of a string (ignoring ANSI escape codes)
    218 fn display_length(s: &str) -> usize {
    219     // Basic regex-free approach to strip ANSI sequences for length calculation
    220     let mut len = 0;
    221     let mut in_esc = false;
    222     for c in s.chars() {
    223         if c == '\x1b' {
    224             in_esc = true;
    225             continue;
    226         }
    227         if in_esc {
    228             if (0x40..=0x7e).contains(&(c as u8)) {
    229                 in_esc = false;
    230             }
    231             continue;
    232         }
    233         len += 1;
    234     }
    235     len
    236 }
    237 
    238 fn print_table(columns: &[&str], rows: &[Vec<String>], separator: char, col_style: &[ColumnStyle]) {
    239     // 1. Calculate column widths (Name vs Max Row Content)
    240     let col_meta: Vec<(&str, usize)> = columns
    241         .iter()
    242         .enumerate()
    243         .map(|(i, &name)| {
    244             let max_row = rows
    245                 .iter()
    246                 .map(|row| display_length(&row[i]))
    247                 .max()
    248                 .unwrap_or(0);
    249             (name, display_length(name).max(max_row))
    250         })
    251         .collect();
    252 
    253     let mut table = String::new();
    254 
    255     let pad = |buf: &mut String, len: usize| {
    256         for _ in 0..len {
    257             buf.push(' ');
    258         }
    259     };
    260 
    261     for (i, (name, len)) in col_meta.iter().enumerate() {
    262         if i > 0 {
    263             table.push(separator);
    264         }
    265 
    266         let pad_len = len - display_length(name);
    267         pad(&mut table, pad_len / 2);
    268         table.push_str(name);
    269         pad(&mut table, pad_len / 2 + pad_len % 2);
    270     }
    271     table.push('\n');
    272 
    273     for row in rows {
    274         for (i, (str_val, &(_, len))) in row.iter().zip(col_meta.iter()).enumerate() {
    275             if i > 0 {
    276                 table.push(separator);
    277             }
    278 
    279             let style = col_style.get(i).cloned().unwrap_or_default();
    280             let pad_len = len - display_length(str_val);
    281 
    282             if style.align_left {
    283                 table.push_str(str_val);
    284                 pad(&mut table, pad_len);
    285             } else {
    286                 pad(&mut table, pad_len);
    287                 table.push_str(str_val);
    288             }
    289         }
    290         table.push('\n');
    291     }
    292 
    293     print!("{}", table);
    294 }