taler-rust

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

commit 6472b10238c25a90ca805c5f94e33747cbd35c67
parent 00a8009bd60e382c0de1cd054e9b21bfc8707001
Author: Antoine A <>
Date:   Tue, 21 Apr 2026 09:27:28 +0200

common: add bench utils

Diffstat:
Mcommon/taler-api/tests/security.rs | 2+-
Acommon/taler-common/src/bench.rs | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/taler-common/src/config.rs | 2+-
Mcommon/taler-common/src/lib.rs | 1+
Mcommon/taler-common/src/types/utils.rs | 20+++++++++++++-------
Mtaler-cyclos/src/api.rs | 5+----
6 files changed, 288 insertions(+), 13 deletions(-)

diff --git a/common/taler-api/tests/security.rs b/common/taler-api/tests/security.rs @@ -36,7 +36,7 @@ async fn body_parsing() { let (server, _) = setup().await; let normal_body = TransferRequest { request_uid: Base32::rand(), - amount: Amount::zero(&eur), + amount: Amount::zero(&Currency::EUR), exchange_base_url: url("https://test.com"), wtid: Base32::rand(), credit_account: payto("payto:://test?receiver-name=lol"), diff --git a/common/taler-common/src/bench.rs b/common/taler-common/src/bench.rs @@ -0,0 +1,271 @@ +/* + This file is part of TALER + Copyright (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::time::{Duration, Instant}; + +use sqlx::{Executor, PgPool}; + +use crate::types::utils::InlineStr; + +const HEX_TABLE: &[u8; 16] = b"0123456789abcdef"; + +/// Fast 32 hex string generation +pub fn h32() -> InlineStr<64> { + let mut raw = [0u8; 32]; + let mut encoded = [0u8; 64]; + + rand::fill(&mut raw); + for i in 0..raw.len() { + let byte = raw[i]; + encoded[i * 2] = HEX_TABLE[(byte >> 4) as usize]; + encoded[i * 2 + 1] = HEX_TABLE[(byte & 0x0F) as usize]; + } + InlineStr::copy_from_slice(&encoded) +} + +/// Fast 64 hex string generation +pub fn h64() -> InlineStr<128> { + let mut raw = [0u8; 64]; + let mut encoded = [0u8; 128]; + + rand::fill(&mut raw); + for i in 0..raw.len() { + let byte = raw[i]; + encoded[i * 2] = HEX_TABLE[(byte >> 4) as usize]; + encoded[i * 2 + 1] = HEX_TABLE[(byte & 0x0F) as usize]; + } + InlineStr::copy_from_slice(&encoded) +} + +const WARN: Duration = Duration::from_millis(4); +const ERR: Duration = Duration::from_millis(50); + +fn fmt_measures(times: &[u64]) -> Vec<String> { + // Basic stats calculations + let min = *times.iter().min().unwrap_or(&0); + let max = *times.iter().max().unwrap_or(&0); + let mean = times.iter().sum::<u64>() / times.len() as u64; + + let variance = times + .iter() + .map(|&t| (t as f64 - mean as f64).powi(2)) + .sum::<f64>() + / times.len() as f64; + let std_var = variance.sqrt() as u64; + + // Map stats to colored strings + [min, mean, max, std_var] + .iter() + .map(|&val| { + let duration = Duration::from_micros(val); + let s = format!("{:?}", duration); + if duration > ERR { + format!("\x1b[31m{}\x1b[0m", s) // Red + } else if duration > WARN { + format!("\x1b[33m{}\x1b[0m", s) // Yellow + } else { + format!("\x1b[32m{}\x1b[0m", s) // Green + } + }) + .collect() +} + +pub struct Bench { + db: PgPool, + buf: String, + amount: usize, + iter: usize, + measures: Vec<Vec<String>>, + dirty: bool, +} + +impl Bench { + pub fn new(db: &PgPool, iter: usize, amount: usize) -> Self { + println!("Bench {iter} times with {amount} rows"); + Self { + db: db.clone(), + buf: String::with_capacity(2 * 1024 * 1024), + amount, + iter, + measures: Vec::new(), + dirty: false, + } + } +} + +impl Bench { + pub async fn table( + &mut self, + table: &str, + mut generator: impl FnMut(&mut String, usize) -> std::fmt::Result, + ) { + println!("Gen rows for {table}"); + let mut db = self.db.acquire().await.unwrap(); + let mut stream = db + .copy_in_raw(&format!("COPY {table} FROM STDIN")) + .await + .unwrap(); + for i in 0..self.amount { + generator(&mut self.buf, i + 1).unwrap(); + if self.buf.len() > 1024 * 1024 { + stream.send(self.buf.as_bytes()).await.unwrap(); + self.buf.clear(); + } + } + stream.send(self.buf.as_bytes()).await.unwrap(); + self.buf.clear(); + stream.finish().await.unwrap(); + self.dirty = true; + } + + pub async fn measure<R>( + &mut self, + name: &'static str, + lambda: impl AsyncFn(usize) -> R, + ) -> Vec<R> { + if self.dirty { + // Update database statistics for better perf + self.db.execute("VACUUM FULL ANALYZE").await.unwrap(); + self.dirty = false; + } + + println!("Measure action {}", name); + + let mut results = Vec::with_capacity(self.iter); + let mut times = Vec::with_capacity(self.iter); + + for idx in 0..self.iter { + let start = Instant::now(); + let result = lambda(idx).await; + let elapsed = start.elapsed().as_micros() as u64; + results.push(result); + times.push(elapsed); + } + + let mut row = vec![format!("\x1b[35m{}\x1b[0m", name)]; + row.extend(fmt_measures(&times)); + + self.measures.push(row); + + results + } +} + +impl Drop for Bench { + fn drop(&mut self) { + print_table( + &["benchmark", "min", "mean", "max", "std"], + self.measures.as_slice(), + ' ', + &[ + ColumnStyle::default(), + ColumnStyle { align_left: false }, + ColumnStyle { align_left: false }, + ColumnStyle { align_left: false }, + ColumnStyle { align_left: false }, + ], + ); + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ColumnStyle { + pub align_left: bool, +} +impl Default for ColumnStyle { + fn default() -> Self { + Self { align_left: true } + } +} + +/// Helper to calculate visible length of a string (ignoring ANSI escape codes) +fn display_length(s: &str) -> usize { + // Basic regex-free approach to strip ANSI sequences for length calculation + let mut len = 0; + let mut in_esc = false; + for c in s.chars() { + if c == '\x1b' { + in_esc = true; + continue; + } + if in_esc { + if (0x40..=0x7e).contains(&(c as u8)) { + in_esc = false; + } + continue; + } + len += 1; + } + len +} + +fn print_table(columns: &[&str], rows: &[Vec<String>], separator: char, col_style: &[ColumnStyle]) { + // 1. Calculate column widths (Name vs Max Row Content) + let col_meta: Vec<(&str, usize)> = columns + .iter() + .enumerate() + .map(|(i, &name)| { + let max_row = rows + .iter() + .map(|row| display_length(&row[i])) + .max() + .unwrap_or(0); + (name, display_length(name).max(max_row)) + }) + .collect(); + + let mut table = String::new(); + + let padd = |buf: &mut String, len: usize| { + for _ in 0..len { + buf.push(' '); + } + }; + + for (i, (name, len)) in col_meta.iter().enumerate() { + if i > 0 { + table.push(separator); + } + + let pad = len - display_length(name); + padd(&mut table, pad / 2); + table.push_str(name); + padd(&mut table, pad / 2 + pad % 2); + } + table.push('\n'); + + for row in rows { + for (i, (str_val, &(_, len))) in row.iter().zip(col_meta.iter()).enumerate() { + if i > 0 { + table.push(separator); + } + + let style = col_style.get(i).cloned().unwrap_or_default(); + let pad = len - display_length(str_val); + + if style.align_left { + table.push_str(str_val); + padd(&mut table, pad); + } else { + padd(&mut table, pad); + table.push_str(str_val); + } + } + table.push('\n'); + } + + print!("{}", table); +} diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -958,7 +958,7 @@ mod test { use std::{ fmt::{Debug, Display}, fs::{File, Permissions}, - os::unix::fs::PermissionsExt + os::unix::fs::PermissionsExt, }; use tracing::error; diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -27,6 +27,7 @@ pub mod api_params; pub mod api_revenue; pub mod api_transfer; pub mod api_wire; +pub mod bench; pub mod cli; pub mod config; pub mod db; diff --git a/common/taler-common/src/types/utils.rs b/common/taler-common/src/types/utils.rs @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Debug, ops::Deref}; +use std::ops::Deref; use jiff::{ Timestamp, @@ -85,12 +85,6 @@ impl<const LEN: usize> AsRef<str> for InlineStr<LEN> { } } -impl<const LEN: usize> Debug for InlineStr<LEN> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self.as_ref(), f) - } -} - impl<const LEN: usize> Deref for InlineStr<LEN> { type Target = [u8]; @@ -101,6 +95,18 @@ impl<const LEN: usize> Deref for InlineStr<LEN> { } } +impl<const LEN: usize> std::fmt::Display for InlineStr<LEN> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +impl<const LEN: usize> std::fmt::Debug for InlineStr<LEN> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + /** Convert a date to a UTC timestamp */ pub fn date_to_utc_ts(date: &Date) -> Timestamp { date.to_zoned(TimeZone::UTC).unwrap().timestamp() diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs @@ -330,10 +330,7 @@ impl PreparedTransfer for CyclosApi { #[cfg(test)] mod test { - use std::{ - str::FromStr as _, - sync::{Arc, LazyLock}, - }; + use std::sync::{Arc, LazyLock}; use compact_str::CompactString; use jiff::Timestamp;