poolsim_core/
sensitivity.rs1use crate::{
11 distribution::LatencyDistribution,
12 erlang,
13 error::PoolsimError,
14 monte_carlo,
15 types::{
16 DistributionModel, PoolConfig, QueueModel, RiskLevel, SensitivityRow, SimulationOptions, WorkloadConfig,
17 },
18};
19
20pub fn sweep(workload: &WorkloadConfig, pool: &PoolConfig) -> Result<Vec<SensitivityRow>, PoolsimError> {
26 sweep_with_options(workload, pool, &SimulationOptions::default())
27}
28
29pub fn sweep_with_target(
35 workload: &WorkloadConfig,
36 pool: &PoolConfig,
37 target_wait_p99_ms: f64,
38) -> Result<Vec<SensitivityRow>, PoolsimError> {
39 let opts = SimulationOptions {
40 target_wait_p99_ms,
41 ..SimulationOptions::default()
42 };
43 sweep_with_options(workload, pool, &opts)
44}
45
46pub fn sweep_with_target_and_model(
52 workload: &WorkloadConfig,
53 pool: &PoolConfig,
54 target_wait_p99_ms: f64,
55 queue_model: QueueModel,
56) -> Result<Vec<SensitivityRow>, PoolsimError> {
57 let opts = SimulationOptions {
58 queue_model,
59 target_wait_p99_ms,
60 distribution: DistributionModel::LogNormal,
61 ..SimulationOptions::default()
62 };
63 sweep_with_options(workload, pool, &opts)
64}
65
66pub fn sweep_with_options(
72 workload: &WorkloadConfig,
73 pool: &PoolConfig,
74 opts: &SimulationOptions,
75) -> Result<Vec<SensitivityRow>, PoolsimError> {
76 let dist = LatencyDistribution::fit(workload, opts.distribution)?;
77 let mu = 1_000.0 / (dist.mean_ms() + pool.connection_overhead_ms);
78 let lambda = workload.requests_per_second;
79 let target_wait_p99_ms = opts.target_wait_p99_ms;
80
81 let mut rows = Vec::with_capacity((pool.max_pool_size - pool.min_pool_size + 1) as usize);
82
83 for size in pool.min_pool_size..=pool.max_pool_size {
84 let rho = erlang::utilisation(lambda, mu, size);
85
86 let (mean_wait, p99_wait, risk) = if rho >= 1.0 {
87 (f64::MAX, f64::MAX, RiskLevel::Critical)
88 } else {
89 let (mean, p99) = match opts.queue_model {
90 QueueModel::MMC => (
91 erlang::mean_queue_wait_ms(lambda, mu, size)?,
92 erlang::queue_wait_percentile_ms(lambda, mu, size, 0.99)?,
93 ),
94 QueueModel::MDC => {
95 let probe_opts = mdc_probe_options(opts, size);
96 let probe = monte_carlo::run_with_overhead(
97 workload,
98 size,
99 pool.connection_overhead_ms,
100 &dist,
101 &probe_opts,
102 )?;
103 (probe.mean, probe.p99)
104 }
105 };
106 let risk = classify_risk(rho, p99, target_wait_p99_ms);
107 (mean, p99, risk)
108 };
109
110 rows.push(SensitivityRow {
111 pool_size: size,
112 utilisation_rho: rho,
113 mean_queue_wait_ms: mean_wait,
114 p99_queue_wait_ms: p99_wait,
115 risk,
116 });
117 }
118
119 Ok(rows)
120}
121
122fn mdc_probe_options(opts: &SimulationOptions, size: u32) -> SimulationOptions {
123 let mut probe_opts = opts.clone();
124 probe_opts.iterations = (opts.iterations / 4).clamp(400, 2_000);
125 if let Some(seed) = opts.seed {
126 probe_opts.seed = Some(seed ^ ((size as u64 + 1).wrapping_mul(0x517C_C1B7_2722_0A95)));
127 }
128 probe_opts
129}
130
131fn classify_risk(rho: f64, p99_wait_ms: f64, target_wait_p99_ms: f64) -> RiskLevel {
132 if rho >= 0.90 {
133 return RiskLevel::Critical;
134 }
135 if rho >= 0.80 {
136 return RiskLevel::High;
137 }
138 if rho < 0.70 && p99_wait_ms < target_wait_p99_ms / 2.0 {
139 return RiskLevel::Low;
140 }
141 if rho < 0.80 || p99_wait_ms < target_wait_p99_ms {
142 return RiskLevel::Medium;
143 }
144 RiskLevel::High
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn classify_risk_falls_back_to_high_for_nan_inputs() {
153 let risk = classify_risk(f64::NAN, f64::NAN, 50.0);
154 assert_eq!(risk, RiskLevel::High);
155 }
156}