#![doc = include_str!("../README.md")]
#![doc(html_root_url = "https://docs.rs/poolsim-core/0.1.0")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
pub mod distribution;
pub mod erlang;
pub mod error;
pub mod monte_carlo;
pub mod optimizer;
pub mod sensitivity;
pub mod types;
use distribution::LatencyDistribution;
use error::PoolsimError;
use optimizer::find_optimal;
use types::{
EvaluationResult, PoolConfig, SaturationLevel, SensitivityRow, SimulationOptions, SimulationReport,
StepLoadResult, WorkloadConfig,
};
pub use types::DistributionModel;
pub use types::QueueModel;
pub use types::RiskLevel;
pub const MIN_FULL_SIMULATION_ITERATIONS: u32 = 10_000;
pub const PERFORMANCE_CONTRACT_WARNING: &str = "performance contract not met: expected <= 200ms";
pub fn emit_performance_contract_warning(elapsed_ms: u128, threshold_ms: u128) {
if elapsed_ms > threshold_ms {
eprintln!("{PERFORMANCE_CONTRACT_WARNING}");
}
}
pub fn simulate(
workload: &WorkloadConfig,
pool: &PoolConfig,
opts: &SimulationOptions,
) -> Result<SimulationReport, PoolsimError> {
workload.validate()?;
pool.validate()?;
opts.validate()?;
let mut effective_opts = opts.clone();
let mut warnings = Vec::new();
if effective_opts.iterations < MIN_FULL_SIMULATION_ITERATIONS {
effective_opts.iterations = MIN_FULL_SIMULATION_ITERATIONS;
warnings.push(format!(
"iterations increased to {} for full simulation fidelity",
MIN_FULL_SIMULATION_ITERATIONS
));
}
let dist = LatencyDistribution::fit(workload, effective_opts.distribution)?;
let optimal = find_optimal(workload, pool, &dist, &effective_opts)?;
let sensitivity = sensitivity::sweep_with_options(workload, pool, &effective_opts)?;
let cold_start_min_pool_size =
recommend_cold_start_pool_size(workload, pool, &dist, &effective_opts, optimal.pool_size);
let mut step_opts = effective_opts.clone();
if workload.step_load_profile.is_some() {
let reduced = (effective_opts.iterations / 4).clamp(1_500, 5_000);
if reduced < effective_opts.iterations {
step_opts.iterations = reduced;
warnings.push(format!(
"step-load analysis used {} iterations per step for responsiveness",
reduced
));
}
}
let step_load_analysis = build_step_load_analysis(workload, optimal.pool_size, &step_opts)?;
let saturation = SaturationLevel::from_rho(optimal.utilisation_rho);
warnings.extend(optimal.warnings);
if saturation != SaturationLevel::Ok {
warnings.push(format!(
"System utilisation is high at the recommended size (rho={:.3})",
optimal.utilisation_rho
));
}
Ok(SimulationReport {
optimal_pool_size: optimal.pool_size,
confidence_interval: optimal.confidence_interval,
cold_start_min_pool_size,
utilisation_rho: optimal.utilisation_rho,
mean_queue_wait_ms: optimal.mean_queue_wait_ms,
p99_queue_wait_ms: optimal.p99_queue_wait_ms,
saturation,
sensitivity,
step_load_analysis,
warnings,
})
}
pub fn evaluate(
workload: &WorkloadConfig,
pool_size: u32,
opts: &SimulationOptions,
) -> Result<EvaluationResult, PoolsimError> {
workload.validate()?;
opts.validate()?;
if pool_size == 0 {
return Err(PoolsimError::invalid_input(
"INVALID_POOL_SIZE",
"pool_size must be greater than 0",
None,
));
}
let dist = LatencyDistribution::fit(workload, opts.distribution)?;
let mc = monte_carlo::run(workload, pool_size, &dist, opts)?;
let lambda = workload.requests_per_second;
let mu = 1_000.0 / dist.mean_ms();
let rho = erlang::utilisation(lambda, mu, pool_size);
let mean_wait = match opts.queue_model {
QueueModel::MMC => erlang::mean_queue_wait_ms(lambda, mu, pool_size).unwrap_or(mc.mean),
QueueModel::MDC => mc.mean,
};
let saturation = SaturationLevel::from_rho(rho);
let mut warnings = Vec::new();
if saturation != SaturationLevel::Ok {
warnings.push(format!("utilisation is elevated (rho={:.3})", rho));
}
Ok(EvaluationResult {
pool_size,
utilisation_rho: rho,
mean_queue_wait_ms: mean_wait,
p99_queue_wait_ms: mc.p99,
saturation,
warnings,
})
}
pub fn sweep(
workload: &WorkloadConfig,
pool: &PoolConfig,
) -> Result<Vec<SensitivityRow>, PoolsimError> {
sweep_with_options(workload, pool, &SimulationOptions::default())
}
pub fn sweep_with_options(
workload: &WorkloadConfig,
pool: &PoolConfig,
opts: &SimulationOptions,
) -> Result<Vec<SensitivityRow>, PoolsimError> {
workload.validate()?;
pool.validate()?;
opts.validate()?;
sensitivity::sweep_with_options(workload, pool, opts)
}
fn recommend_cold_start_pool_size(
workload: &WorkloadConfig,
pool: &PoolConfig,
dist: &LatencyDistribution,
opts: &SimulationOptions,
recommended_pool_size: u32,
) -> u32 {
let peak_rps = workload
.step_load_profile
.as_ref()
.and_then(|profile| {
profile
.iter()
.map(|point| point.requests_per_second)
.max_by(|a, b| a.total_cmp(b))
})
.map(|peak| peak.max(workload.requests_per_second))
.unwrap_or(workload.requests_per_second);
let mu = 1_000.0 / (dist.mean_ms() + pool.connection_overhead_ms);
if !mu.is_finite() || mu <= 0.0 {
return pool.min_pool_size.min(recommended_pool_size);
}
let warm_rho_target = opts.max_acceptable_rho.min(0.70).max(0.35);
let required = (peak_rps / (mu * warm_rho_target)).ceil().max(1.0) as u32;
required
.max(pool.min_pool_size)
.min(recommended_pool_size)
}
fn build_step_load_analysis(
workload: &WorkloadConfig,
pool_size: u32,
opts: &SimulationOptions,
) -> Result<Vec<StepLoadResult>, PoolsimError> {
let Some(profile) = &workload.step_load_profile else {
return Ok(Vec::new());
};
let mut rows = Vec::with_capacity(profile.len());
for point in profile {
let mut step_workload = workload.clone();
step_workload.requests_per_second = point.requests_per_second;
step_workload.step_load_profile = None;
let step = evaluate(&step_workload, pool_size, opts)?;
rows.push(StepLoadResult {
time_s: point.time_s,
requests_per_second: point.requests_per_second,
utilisation_rho: step.utilisation_rho,
p99_queue_wait_ms: step.p99_queue_wait_ms,
saturation: step.saturation,
});
}
Ok(rows)
}