mod compare;
mod storage;
pub use compare::{
Comparison, Delta, DeltaStatus, LatencyDeltas, RegressionMetric, ThroughputDeltas, Verdict, compare,
};
pub use storage::{load, load_file, resolve_baseline_dir, save};
use std::{collections::BTreeMap, fmt, str::FromStr};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BaselineName(String);
impl BaselineName {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for BaselineName {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err("baseline name cannot be empty".to_string());
}
if !s
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(format!(
"invalid baseline name '{}': must contain only [a-zA-Z0-9_.-]",
s
));
}
Ok(BaselineName(s.to_string()))
}
}
impl fmt::Display for BaselineName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for BaselineName {
fn as_ref(&self) -> &str {
&self.0
}
}
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub(crate) const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BenchConfig {
pub concurrency: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_secs: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iterations: Option<u64>,
pub warmup: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<u32>,
pub actual_duration_secs: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct BaselineMetadata {
pub name: String,
pub created_at: DateTime<Utc>,
pub rlt_version: String,
pub bench_config: BenchConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Summary {
pub success_ratio: f64,
pub total_time: f64,
pub concurrency: u32,
pub iters: RateSummary,
pub items: RateSummary,
pub bytes: RateSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct RateSummary {
pub total: u64,
pub rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LatencyStats {
pub min: f64,
pub max: f64,
pub mean: f64,
pub median: f64,
pub stdev: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Latency {
pub stats: LatencyStats,
pub percentiles: BTreeMap<String, f64>,
pub histogram: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SerializableReport {
pub summary: Summary,
#[serde(skip_serializing_if = "Option::is_none")]
pub latency: Option<Latency>,
pub status: BTreeMap<String, u64>,
pub errors: BTreeMap<String, u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Baseline {
pub(crate) schema_version: u32,
pub(crate) metadata: BaselineMetadata,
#[serde(flatten)]
pub(crate) report: SerializableReport,
}
impl Baseline {
pub fn validate(&self, cli: &crate::cli::BenchCli) -> anyhow::Result<()> {
let config = &self.metadata.bench_config;
if cli.concurrency.get() != config.concurrency {
anyhow::bail!(
"Concurrency mismatch: current={}, baseline={}. Results are not comparable.",
cli.concurrency.get(),
config.concurrency
);
}
#[cfg(feature = "rate_limit")]
{
let current_rate = cli.rate.map(|r| r.get());
if current_rate != config.rate_limit {
anyhow::bail!(
"Rate limit mismatch: current={:?}, baseline={:?}. Results are not comparable.",
current_rate,
config.rate_limit
);
}
}
Ok(())
}
}