1#![doc = include_str!("../README.md")]
2#![doc(html_root_url = "https://docs.rs/poolsim-core/0.2.0")]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![deny(missing_docs)]
5
6pub mod distribution;
8pub mod erlang;
10pub mod error;
12pub mod monte_carlo;
14pub mod optimizer;
16pub mod sensitivity;
18pub mod telemetry;
20pub mod types;
22
23use distribution::LatencyDistribution;
24use error::PoolsimError;
25use optimizer::find_optimal;
26use types::{
27 EvaluationResult, PoolConfig, SaturationLevel, SensitivityRow, SimulationOptions,
28 SimulationReport, StepLoadResult, WorkloadConfig,
29};
30
31pub use types::DistributionModel;
33pub use types::QueueModel;
35pub use types::RiskLevel;
37
38pub const MIN_FULL_SIMULATION_ITERATIONS: u32 = 10_000;
40pub const PERFORMANCE_CONTRACT_WARNING: &str = "performance contract not met: expected <= 200ms";
42
43pub fn emit_performance_contract_warning(elapsed_ms: u128, threshold_ms: u128) {
45 if elapsed_ms > threshold_ms {
46 eprintln!("{PERFORMANCE_CONTRACT_WARNING}");
47 }
48}
49
50pub fn simulate(
57 workload: &WorkloadConfig,
58 pool: &PoolConfig,
59 opts: &SimulationOptions,
60) -> Result<SimulationReport, PoolsimError> {
61 workload.validate()?;
62 pool.validate()?;
63 opts.validate()?;
64
65 let mut effective_opts = opts.clone();
66 let mut warnings = Vec::new();
67 if effective_opts.iterations < MIN_FULL_SIMULATION_ITERATIONS {
68 effective_opts.iterations = MIN_FULL_SIMULATION_ITERATIONS;
69 warnings.push(format!(
70 "iterations increased to {} for full simulation fidelity",
71 MIN_FULL_SIMULATION_ITERATIONS
72 ));
73 }
74
75 let dist = LatencyDistribution::fit(workload, effective_opts.distribution)?;
76 let optimal = find_optimal(workload, pool, &dist, &effective_opts)?;
77 let sensitivity = sensitivity::sweep_with_options(workload, pool, &effective_opts)?;
78 let cold_start_min_pool_size =
79 recommend_cold_start_pool_size(workload, pool, &dist, &effective_opts, optimal.pool_size);
80
81 let mut step_opts = effective_opts.clone();
82 if workload.step_load_profile.is_some() {
83 let reduced = (effective_opts.iterations / 4).clamp(1_500, 5_000);
84 if reduced < effective_opts.iterations {
85 step_opts.iterations = reduced;
86 warnings.push(format!(
87 "step-load analysis used {} iterations per step for responsiveness",
88 reduced
89 ));
90 }
91 }
92 let step_load_analysis = build_step_load_analysis(workload, optimal.pool_size, &step_opts)?;
93
94 let saturation = SaturationLevel::from_rho(optimal.utilisation_rho);
95 warnings.extend(optimal.warnings);
96 if saturation != SaturationLevel::Ok {
97 warnings.push(format!(
98 "System utilisation is high at the recommended size (rho={:.3})",
99 optimal.utilisation_rho
100 ));
101 }
102
103 Ok(SimulationReport {
104 optimal_pool_size: optimal.pool_size,
105 confidence_interval: optimal.confidence_interval,
106 cold_start_min_pool_size,
107 utilisation_rho: optimal.utilisation_rho,
108 mean_queue_wait_ms: optimal.mean_queue_wait_ms,
109 p99_queue_wait_ms: optimal.p99_queue_wait_ms,
110 saturation,
111 sensitivity,
112 step_load_analysis,
113 warnings,
114 })
115}
116
117pub fn evaluate(
123 workload: &WorkloadConfig,
124 pool_size: u32,
125 opts: &SimulationOptions,
126) -> Result<EvaluationResult, PoolsimError> {
127 workload.validate()?;
128 opts.validate()?;
129
130 if pool_size == 0 {
131 return Err(PoolsimError::invalid_input(
132 "INVALID_POOL_SIZE",
133 "pool_size must be greater than 0",
134 None,
135 ));
136 }
137
138 let dist = LatencyDistribution::fit(workload, opts.distribution)?;
139 let mc = monte_carlo::run(workload, pool_size, &dist, opts)?;
140
141 let lambda = workload.requests_per_second;
142 let mu = 1_000.0 / dist.mean_ms();
143 let rho = erlang::utilisation(lambda, mu, pool_size);
144 let mean_wait = match opts.queue_model {
145 QueueModel::MMC => erlang::mean_queue_wait_ms(lambda, mu, pool_size).unwrap_or(mc.mean),
146 QueueModel::MDC => mc.mean,
147 };
148
149 let saturation = SaturationLevel::from_rho(rho);
150 let mut warnings = Vec::new();
151 if saturation != SaturationLevel::Ok {
152 warnings.push(format!("utilisation is elevated (rho={:.3})", rho));
153 }
154
155 Ok(EvaluationResult {
156 pool_size,
157 utilisation_rho: rho,
158 mean_queue_wait_ms: mean_wait,
159 p99_queue_wait_ms: mc.p99,
160 saturation,
161 warnings,
162 })
163}
164
165pub fn sweep(
171 workload: &WorkloadConfig,
172 pool: &PoolConfig,
173) -> Result<Vec<SensitivityRow>, PoolsimError> {
174 sweep_with_options(workload, pool, &SimulationOptions::default())
175}
176
177pub fn sweep_with_options(
183 workload: &WorkloadConfig,
184 pool: &PoolConfig,
185 opts: &SimulationOptions,
186) -> Result<Vec<SensitivityRow>, PoolsimError> {
187 workload.validate()?;
188 pool.validate()?;
189 opts.validate()?;
190 sensitivity::sweep_with_options(workload, pool, opts)
191}
192
193fn recommend_cold_start_pool_size(
194 workload: &WorkloadConfig,
195 pool: &PoolConfig,
196 dist: &LatencyDistribution,
197 opts: &SimulationOptions,
198 recommended_pool_size: u32,
199) -> u32 {
200 let peak_rps = workload
201 .step_load_profile
202 .as_ref()
203 .and_then(|profile| {
204 profile
205 .iter()
206 .map(|point| point.requests_per_second)
207 .max_by(|a, b| a.total_cmp(b))
208 })
209 .map(|peak| peak.max(workload.requests_per_second))
210 .unwrap_or(workload.requests_per_second);
211
212 let mu = 1_000.0 / (dist.mean_ms() + pool.connection_overhead_ms);
213 if !mu.is_finite() || mu <= 0.0 {
214 return pool.min_pool_size.min(recommended_pool_size);
215 }
216
217 let warm_rho_target = opts.max_acceptable_rho.clamp(0.35, 0.70);
218 let required = (peak_rps / (mu * warm_rho_target)).ceil().max(1.0) as u32;
219 required.max(pool.min_pool_size).min(recommended_pool_size)
220}
221
222fn build_step_load_analysis(
223 workload: &WorkloadConfig,
224 pool_size: u32,
225 opts: &SimulationOptions,
226) -> Result<Vec<StepLoadResult>, PoolsimError> {
227 let Some(profile) = &workload.step_load_profile else {
228 return Ok(Vec::new());
229 };
230
231 let mut rows = Vec::with_capacity(profile.len());
232 for point in profile {
233 let mut step_workload = workload.clone();
234 step_workload.requests_per_second = point.requests_per_second;
235 step_workload.step_load_profile = None;
236
237 let step = evaluate(&step_workload, pool_size, opts)?;
238 rows.push(StepLoadResult {
239 time_s: point.time_s,
240 requests_per_second: point.requests_per_second,
241 utilisation_rho: step.utilisation_rho,
242 p99_queue_wait_ms: step.p99_queue_wait_ms,
243 saturation: step.saturation,
244 });
245 }
246
247 Ok(rows)
248}