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(
59 "MDC mode uses Monte Carlo probe estimates for candidate search".to_string(),
60 );
61 }
62
63 for size in pool.min_pool_size..=pool.max_pool_size {
64 let rho = erlang::utilisation(lambda, mu, size);
65 if rho >= 1.0 {
66 continue;
67 }
68
69 let p99 = match opts.queue_model {
70 QueueModel::MMC => erlang::queue_wait_percentile_ms(lambda, mu, size, 0.99)?,
71 QueueModel::MDC => mdc_probe_p99(workload, pool, dist, opts, size)?,
72 };
73 if rho < opts.max_acceptable_rho && p99 <= opts.target_wait_p99_ms {
74 candidate = Some(size);
75 break;
76 }
77 }
78
79 let chosen = candidate.unwrap_or(pool.max_pool_size);
80 if candidate.is_none() {
81 warnings.push(
82 "No candidate pool size met target constraints; using max_pool_size fallback".to_string(),
83 );
84 }
85
86 let mc = 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 =
110 monte_carlo::run_with_overhead(workload, size, pool.connection_overhead_ms, dist, &probe_opts)?;
111 Ok(probe.p99)
112}
113
114fn mdc_probe_options(opts: &SimulationOptions, size: u32) -> SimulationOptions {
115 let mut probe_opts = opts.clone();
116 probe_opts.iterations = (opts.iterations / 4).clamp(400, 2_500);
117 if let Some(seed) = opts.seed {
118 probe_opts.seed = Some(seed ^ ((size as u64 + 1).wrapping_mul(0x9E37_79B9_7F4A_7C15)));
119 }
120 probe_opts
121}
122
123fn bootstrap_ci(chosen: u32, pool: &PoolConfig, wait_times: &[f64], target_wait_p99_ms: f64) -> (u32, u32) {
124 if wait_times.is_empty() {
125 return (chosen, chosen);
126 }
127
128 let mean = wait_times.iter().sum::<f64>() / wait_times.len() as f64;
129 let variance = wait_times
130 .iter()
131 .map(|v| {
132 let d = v - mean;
133 d * d
134 })
135 .sum::<f64>()
136 / wait_times.len() as f64;
137
138 let stddev = variance.sqrt();
139 let mut width = (stddev / target_wait_p99_ms).ceil() as u32;
140 width = width.clamp(1, 5);
141
142 (
143 chosen.saturating_sub(width).max(pool.min_pool_size),
144 chosen.saturating_add(width).min(pool.max_pool_size),
145 )
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn bootstrap_ci_returns_degenerate_interval_for_empty_waits() {
154 let pool = PoolConfig {
155 max_server_connections: 100,
156 connection_overhead_ms: 2.0,
157 idle_timeout_ms: None,
158 min_pool_size: 2,
159 max_pool_size: 20,
160 };
161 assert_eq!(bootstrap_ci(7, &pool, &[], 40.0), (7, 7));
162 }
163}