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