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(×)); 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 }