commit 6472b10238c25a90ca805c5f94e33747cbd35c67
parent 00a8009bd60e382c0de1cd054e9b21bfc8707001
Author: Antoine A <>
Date: Tue, 21 Apr 2026 09:27:28 +0200
common: add bench utils
Diffstat:
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(×));
+
+ 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;