poolsim_core/
optimizer.rs1use crate::{
14 distribution::LatencyDistribution,
15 erlang,
16 error::PoolsimError,
17 monte_carlo,
18 types::{PoolConfig, QueueModel, SimulationOptions, WorkloadConfig},
19};
20
21#[derive(Debug, Clone)]
23pub struct OptimalResult {
24 pub pool_size: u32,
26 pub confidence_interval: (u32, u32),
28 pub utilisation_rho: f64,
30 pub mean_queue_wait_ms: f64,
32 pub p99_queue_wait_ms: f64,
34 pub warnings: Vec<String>,
36}
37
38pub fn find_optimal(
47 workload: &WorkloadConfig,
48 pool: &PoolConfig,
49 dist: &LatencyDistribution,
50 opts: &SimulationOptions,
51) -> Result<OptimalResult, PoolsimError> {
52 let lambda = workload.requests_per_second;
53 let mu = 1_000.0 / (dist.mean_ms() + pool.connection_overhead_ms);
54
55 let mut candidate = None;
56 let mut warnings = Vec::new();
57 if opts.queue_model == QueueModel::MDC {
58 warnings.push("MDC mode uses Monte Carlo probe estimates for candidate search".to_string());
59 }
60
61 for size in pool.min_pool_size..=pool.max_pool_size {
62 let rho = erlang::utilisation(lambda, mu, size);
63 if rho >= 1.0 {
64 continue;
65 }
66
67 let p99 = match opts.queue_model {
68 QueueModel::MMC => erlang::queue_wait_percentile_ms(lambda, mu, size, 0.99)?,
69 QueueModel::MDC => mdc_probe_p99(workload, pool, dist, opts, size)?,
70 };
71 if rho < opts.max_acceptable_rho && p99 <= opts.target_wait_p99_ms {
72 candidate = Some(size);
73 break;
74 }
75 }
76
77 let chosen = candidate.unwrap_or(pool.max_pool_size);
78 if candidate.is_none() {
79 warnings.push(
80 "No candidate pool size met target constraints; using max_pool_size fallback"
81 .to_string(),
82 );
83 }
84
85 let mc =
86 monte_carlo::run_with_overhead(workload, chosen, pool.connection_overhead_ms, dist, opts)?;
87
88 let rho = erlang::utilisation(lambda, mu, chosen);
89 let ci = bootstrap_ci(chosen, pool, &mc.wait_times_ms, opts.target_wait_p99_ms);
90
91 Ok(OptimalResult {
92 pool_size: chosen,
93 confidence_interval: ci,
94 utilisation_rho: rho,
95 mean_queue_wait_ms: mc.mean,
96 p99_queue_wait_ms: mc.p99,
97 warnings,
98 })
99}
100
101fn mdc_probe_p99(
102 workload: &WorkloadConfig,
103 pool: &PoolConfig,
104 dist: &LatencyDistribution,
105 opts: &SimulationOptions,
106 size: u32,
107) -> Result<f64, PoolsimError> {
108 let probe_opts = mdc_probe_options(opts, size);
109 let probe = monte_carlo::run_with_overhead(
110 workload,
111 size,
112 pool.connection_overhead_ms,
113 dist,
114 &probe_opts,
115 )?;
116 Ok(probe.p99)
117}
118
119fn mdc_probe_options(opts: &SimulationOptions, size: u32) -> SimulationOptions {
120 let mut probe_opts = opts.clone();
121 probe_opts.iterations = (opts.iterations / 4).clamp(400, 2_500);
122 if let Some(seed) = opts.seed {
123 probe_opts.seed = Some(seed ^ ((size as u64 + 1).wrapping_mul(0x9E37_79B9_7F4A_7C15)));
124 }
125 probe_opts
126}
127
128fn bootstrap_ci(
129 chosen: u32,
130 pool: &PoolConfig,
131 wait_times: &[f64],
132 target_wait_p99_ms: f64,
133) -> (u32, u32) {
134 if wait_times.is_empty() {
135 return (chosen, chosen);
136 }
137
138 let mean = wait_times.iter().sum::<f64>() / wait_times.len() as f64;
139 let variance = wait_times
140 .iter()
141 .map(|v| {
142 let d = v - mean;
143 d * d
144 })
145 .sum::<f64>()
146 / wait_times.len() as f64;
147
148 let stddev = variance.sqrt();
149 let mut width = (stddev / target_wait_p99_ms).ceil() as u32;
150 width = width.clamp(1, 5);
151
152 (
153 chosen.saturating_sub(width).max(pool.min_pool_size),
154 chosen.saturating_add(width).min(pool.max_pool_size),
155 )
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn bootstrap_ci_returns_degenerate_interval_for_empty_waits() {
164 let pool = PoolConfig {
165 max_server_connections: 100,
166 connection_overhead_ms: 2.0,
167 idle_timeout_ms: None,
168 min_pool_size: 2,
169 max_pool_size: 20,
170 };
171 assert_eq!(bootstrap_ci(7, &pool, &[], 40.0), (7, 7));
172 }
173}