http_handle/
runtime_autotune.rs1use std::num::NonZeroUsize;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct HostResourceProfile {
23 pub cpu_cores: usize,
25 pub memory_mib: usize,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct RuntimeTuneRecommendation {
44 pub max_inflight: usize,
46 pub max_queue: usize,
48 pub sendfile_threshold_bytes: u64,
50}
51
52impl RuntimeTuneRecommendation {
53 pub fn from_profile(profile: HostResourceProfile) -> Self {
67 let cores = profile.cpu_cores.max(1);
68 let mem = profile.memory_mib.max(256);
69 let max_inflight = (cores * 128).clamp(64, 4096);
70 let max_queue = (cores * 512).clamp(256, 16384);
71 let sendfile_threshold_bytes =
72 if mem < 1024 { 256 * 1024 } else { 64 * 1024 };
73 Self {
74 max_inflight,
75 max_queue,
76 sendfile_threshold_bytes,
77 }
78 }
79}
80
81#[cfg(feature = "high-perf")]
82impl RuntimeTuneRecommendation {
83 pub fn into_perf_limits(self) -> crate::perf_server::PerfLimits {
101 crate::perf_server::PerfLimits {
102 max_inflight: self.max_inflight,
103 max_queue: self.max_queue,
104 sendfile_threshold_bytes: self.sendfile_threshold_bytes,
105 }
106 }
107}
108
109pub fn detect_host_profile() -> HostResourceProfile {
123 let cpu_cores = std::thread::available_parallelism()
124 .unwrap_or_else(|_| NonZeroUsize::new(1).expect("non-zero"))
125 .get();
126 let memory_mib = detect_memory_mib().unwrap_or(2048);
127 HostResourceProfile {
128 cpu_cores,
129 memory_mib,
130 }
131}
132
133fn detect_memory_mib() -> Option<usize> {
134 if let Ok(val) = std::env::var("HTTP_HANDLE_MEMORY_MIB")
135 && let Ok(parsed) = val.parse::<usize>()
136 {
137 return Some(parsed);
138 }
139 #[cfg(target_os = "linux")]
140 {
141 if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo")
142 && let Some(line) =
143 meminfo.lines().find(|l| l.starts_with("MemTotal:"))
144 {
145 let kb = line
146 .split_whitespace()
147 .nth(1)
148 .and_then(|v| v.parse::<usize>().ok())?;
149 return Some(kb / 1024);
150 }
151 }
152 None
153}
154
155#[cfg(test)]
156#[allow(unsafe_code)]
160mod tests {
161 use super::*;
162 use std::sync::{Mutex, OnceLock};
163
164 fn env_lock() -> &'static Mutex<()> {
165 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
166 LOCK.get_or_init(|| Mutex::new(()))
167 }
168
169 #[test]
170 fn recommendation_scales_with_profile() {
171 let small = RuntimeTuneRecommendation::from_profile(
172 HostResourceProfile {
173 cpu_cores: 2,
174 memory_mib: 512,
175 },
176 );
177 let large = RuntimeTuneRecommendation::from_profile(
178 HostResourceProfile {
179 cpu_cores: 16,
180 memory_mib: 16384,
181 },
182 );
183 assert!(large.max_inflight > small.max_inflight);
184 assert!(large.max_queue > small.max_queue);
185 assert!(
186 small.sendfile_threshold_bytes
187 > large.sendfile_threshold_bytes
188 );
189 }
190
191 #[test]
192 fn detect_profile_has_sane_minimums() {
193 let profile = detect_host_profile();
194 assert!(profile.cpu_cores >= 1);
195 assert!(profile.memory_mib >= 1);
196 }
197
198 fn restore_memory_mib_env(previous: Option<String>) {
204 if let Some(old) = previous {
205 unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", old) };
207 } else {
208 unsafe { std::env::remove_var("HTTP_HANDLE_MEMORY_MIB") };
210 }
211 }
212
213 #[test]
214 fn detect_memory_uses_env_hint_when_valid() {
215 let _guard = env_lock().lock().expect("env lock");
216 unsafe {
220 std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "preseed");
221 };
222 let previous = std::env::var("HTTP_HANDLE_MEMORY_MIB").ok();
223 unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "3072") };
225 let got = detect_memory_mib();
226 restore_memory_mib_env(previous);
227 unsafe { std::env::remove_var("HTTP_HANDLE_MEMORY_MIB") };
230 assert_eq!(got, Some(3072));
231 }
232
233 #[test]
234 fn detect_memory_ignores_invalid_env_hint() {
235 let _guard = env_lock().lock().expect("env lock");
236 unsafe { std::env::remove_var("HTTP_HANDLE_MEMORY_MIB") };
240 let previous = std::env::var("HTTP_HANDLE_MEMORY_MIB").ok();
241 assert!(previous.is_none());
242 unsafe {
244 std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "not-a-number")
245 };
246 let got = detect_memory_mib();
247 restore_memory_mib_env(previous);
248 assert!(got.is_none() || got.expect("value") >= 1);
249 }
250
251 #[cfg(feature = "high-perf")]
252 #[test]
253 fn recommendation_maps_to_perf_limits() {
254 let rec = RuntimeTuneRecommendation {
255 max_inflight: 123,
256 max_queue: 456,
257 sendfile_threshold_bytes: 789,
258 };
259 let limits = rec.into_perf_limits();
260 assert_eq!(limits.max_inflight, 123);
261 assert_eq!(limits.max_queue, 456);
262 assert_eq!(limits.sendfile_threshold_bytes, 789);
263 }
264}