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