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