use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Instant;
use thiserror::Error;
pub mod cache;
pub mod database;
pub mod runner;
pub mod transaction;
#[derive(Debug, Error)]
pub enum PerfTestError {
#[error("Test error: {0}")]
TestError(String),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Measurement error: {0}")]
MeasurementError(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Bitcoin error: {0}")]
BitcoinError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, PerfTestError>;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum MetricType {
TPS,
LatencyMs,
MemoryMB,
CpuPercent,
DbOpsPerSecond,
CacheHitRate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestResult {
pub name: String,
pub timestamp: String,
pub duration_ms: u64,
pub metrics: HashMap<String, f64>,
pub metric_types: HashMap<String, MetricType>,
pub parameters: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestConfig {
pub name: String,
pub iterations: usize,
pub warmup_iterations: usize,
pub duration_limit_secs: u64,
pub parameters: HashMap<String, String>,
}
pub trait PerformanceTestable {
fn run_test(&self, config: &TestConfig) -> Result<TestResult>;
fn name(&self) -> &str;
}
pub struct PerformanceTestRunner {
configs: Vec<TestConfig>,
components: Vec<Box<dyn PerformanceTestable>>,
results: Vec<TestResult>,
}
impl Default for PerformanceTestRunner {
fn default() -> Self {
Self::new()
}
}
impl PerformanceTestRunner {
pub fn new() -> Self {
Self {
configs: Vec::new(),
components: Vec::new(),
results: Vec::new(),
}
}
pub fn add_config(&mut self, config: TestConfig) {
self.configs.push(config);
}
pub fn add_component(&mut self, component: Box<dyn PerformanceTestable>) {
self.components.push(component);
}
pub fn run_all_tests(&mut self) -> Result<()> {
for config in &self.configs {
for component in &self.components {
if component.name() == config.name || config.name == "all" {
println!(
"Running test: {} on component: {}",
config.name,
component.name()
);
let result = component.run_test(config)?;
self.results.push(result);
}
}
}
Ok(())
}
pub fn run_test(&mut self, test_name: &str) -> Result<()> {
let config = self
.configs
.iter()
.find(|c| c.name == test_name)
.ok_or_else(|| {
PerfTestError::ConfigurationError(format!(
"Test configuration not found: {test_name}"
))
})?;
for component in &self.components {
if component.name() == config.name || config.name == "all" {
println!(
"Running test: {} on component: {}",
config.name,
component.name()
);
let result = component.run_test(config)?;
self.results.push(result);
}
}
Ok(())
}
pub fn get_results(&self) -> &[TestResult] {
&self.results
}
pub fn generate_report_markdown(&self) -> String {
let mut markdown = String::new();
markdown.push_str("# Performance Test Results\n\n");
markdown.push_str(&format!(
"- **Date:** {}\n",
chrono::Utc::now().to_rfc3339()
));
markdown.push_str(&format!("- **Total Tests:** {}\n\n", self.results.len()));
for result in &self.results {
markdown.push_str(&format!("## Test: {}\n\n", result.name));
markdown.push_str(&format!("- **Duration:** {} ms\n", result.duration_ms));
markdown.push_str("- **Metrics:**\n");
for (name, value) in &result.metrics {
let type_str = match result.metric_types.get(name) {
Some(MetricType::TPS) => "TPS",
Some(MetricType::LatencyMs) => "ms",
Some(MetricType::MemoryMB) => "MB",
Some(MetricType::CpuPercent) => "%",
Some(MetricType::DbOpsPerSecond) => "ops/s",
Some(MetricType::CacheHitRate) => "%",
None => "",
};
markdown.push_str(&format!(" - **{name}:** {value:.2} {type_str}\n"));
}
markdown.push_str("- **Parameters:**\n");
for (name, value) in &result.parameters {
markdown.push_str(&format!(" - **{name}:** {value}\n"));
}
markdown.push('\n');
}
markdown
}
}
pub struct Timer {
start: Option<Instant>,
end: Option<Instant>,
}
impl Default for Timer {
fn default() -> Self {
Self::new()
}
}
impl Timer {
pub fn new() -> Self {
Self {
start: None,
end: None,
}
}
pub fn start(&mut self) {
self.start = Some(Instant::now());
self.end = None;
}
pub fn stop(&mut self) {
self.end = Some(Instant::now());
}
pub fn elapsed_ms(&self) -> Result<u64> {
match (self.start, self.end) {
(Some(start), Some(end)) => Ok(end.duration_since(start).as_millis() as u64),
(Some(start), None) => Ok(Instant::now().duration_since(start).as_millis() as u64),
_ => Err(PerfTestError::MeasurementError(
"Timer not started".to_string(),
)),
}
}
pub fn elapsed_secs(&self) -> Result<f64> {
Ok(self.elapsed_ms()? as f64 / 1000.0)
}
}