Skip to main content

http_handle/
runtime_autotune.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2026 Sebastien Rousseau
3
4//! Runtime auto-tuning helpers based on detected host resource profile.
5
6use std::num::NonZeroUsize;
7
8/// Host resource profile used for tuning decisions.
9///
10/// # Examples
11///
12/// ```rust
13/// use http_handle::runtime_autotune::HostResourceProfile;
14/// let p = HostResourceProfile { cpu_cores: 4, memory_mib: 4096 };
15/// assert_eq!(p.cpu_cores, 4);
16/// ```
17///
18/// # Panics
19///
20/// This type does not panic.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct HostResourceProfile {
23    /// Detected logical cores.
24    pub cpu_cores: usize,
25    /// Estimated memory in MiB.
26    pub memory_mib: usize,
27}
28
29/// Tuning recommendation independent of server runtime type.
30///
31/// # Examples
32///
33/// ```rust
34/// use http_handle::runtime_autotune::RuntimeTuneRecommendation;
35/// let rec = RuntimeTuneRecommendation { max_inflight: 128, max_queue: 512, sendfile_threshold_bytes: 65536 };
36/// assert_eq!(rec.max_queue, 512);
37/// ```
38///
39/// # Panics
40///
41/// This type does not panic.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct RuntimeTuneRecommendation {
44    /// Max concurrent inflight requests.
45    pub max_inflight: usize,
46    /// Max queued requests.
47    pub max_queue: usize,
48    /// Threshold for sendfile fast-path.
49    pub sendfile_threshold_bytes: u64,
50}
51
52impl RuntimeTuneRecommendation {
53    /// Produces recommendation from host profile.
54    ///
55    /// # Examples
56    ///
57    /// ```rust
58    /// use http_handle::runtime_autotune::{HostResourceProfile, RuntimeTuneRecommendation};
59    /// let rec = RuntimeTuneRecommendation::from_profile(HostResourceProfile { cpu_cores: 8, memory_mib: 8192 });
60    /// assert!(rec.max_inflight >= 64);
61    /// ```
62    ///
63    /// # Panics
64    ///
65    /// This function does not panic.
66    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    /// Converts recommendation into high-performance server limits.
84    ///
85    /// # Examples
86    ///
87    /// ```rust
88    /// use http_handle::runtime_autotune::RuntimeTuneRecommendation;
89    /// let rec = RuntimeTuneRecommendation { max_inflight: 1, max_queue: 2, sendfile_threshold_bytes: 3 };
90    /// #[cfg(feature = "high-perf")]
91    /// {
92    ///     let limits = rec.into_perf_limits();
93    ///     assert_eq!(limits.max_queue, 2);
94    /// }
95    /// ```
96    ///
97    /// # Panics
98    ///
99    /// This function does not panic.
100    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
109/// Detects host profile from runtime and lightweight OS hints.
110///
111/// # Examples
112///
113/// ```rust
114/// use http_handle::runtime_autotune::detect_host_profile;
115/// let p = detect_host_profile();
116/// assert!(p.cpu_cores >= 1);
117/// ```
118///
119/// # Panics
120///
121/// This function does not panic.
122pub 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)]
156mod tests {
157    use super::*;
158    use std::sync::{Mutex, OnceLock};
159
160    fn env_lock() -> &'static Mutex<()> {
161        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
162        LOCK.get_or_init(|| Mutex::new(()))
163    }
164
165    #[test]
166    fn recommendation_scales_with_profile() {
167        let small = RuntimeTuneRecommendation::from_profile(
168            HostResourceProfile {
169                cpu_cores: 2,
170                memory_mib: 512,
171            },
172        );
173        let large = RuntimeTuneRecommendation::from_profile(
174            HostResourceProfile {
175                cpu_cores: 16,
176                memory_mib: 16384,
177            },
178        );
179        assert!(large.max_inflight > small.max_inflight);
180        assert!(large.max_queue > small.max_queue);
181        assert!(
182            small.sendfile_threshold_bytes
183                > large.sendfile_threshold_bytes
184        );
185    }
186
187    #[test]
188    fn detect_profile_has_sane_minimums() {
189        let profile = detect_host_profile();
190        assert!(profile.cpu_cores >= 1);
191        assert!(profile.memory_mib >= 1);
192    }
193
194    #[test]
195    fn detect_memory_uses_env_hint_when_valid() {
196        let _guard = env_lock().lock().expect("env lock");
197        let previous = std::env::var("HTTP_HANDLE_MEMORY_MIB").ok();
198        // Safety: test-only process env mutation in a bounded scope.
199        unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "3072") };
200        let got = detect_memory_mib();
201        if let Some(old) = previous {
202            // Safety: restoring process env key snapshot.
203            unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", old) };
204        } else {
205            // Safety: paired cleanup for key introduced in this test.
206            unsafe { std::env::remove_var("HTTP_HANDLE_MEMORY_MIB") };
207        }
208        assert_eq!(got, Some(3072));
209    }
210
211    #[test]
212    fn detect_memory_ignores_invalid_env_hint() {
213        let _guard = env_lock().lock().expect("env lock");
214        let previous = std::env::var("HTTP_HANDLE_MEMORY_MIB").ok();
215        // Safety: test-only process env mutation in a bounded scope.
216        unsafe {
217            std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "not-a-number")
218        };
219        let got = detect_memory_mib();
220        if let Some(old) = previous {
221            // Safety: restoring process env key snapshot.
222            unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", old) };
223        } else {
224            // Safety: paired cleanup for key introduced in this test.
225            unsafe { std::env::remove_var("HTTP_HANDLE_MEMORY_MIB") };
226        }
227        assert!(got.is_none() || got.expect("value") >= 1);
228    }
229
230    #[cfg(feature = "high-perf")]
231    #[test]
232    fn recommendation_maps_to_perf_limits() {
233        let rec = RuntimeTuneRecommendation {
234            max_inflight: 123,
235            max_queue: 456,
236            sendfile_threshold_bytes: 789,
237        };
238        let limits = rec.into_perf_limits();
239        assert_eq!(limits.max_inflight, 123);
240        assert_eq!(limits.max_queue, 456);
241        assert_eq!(limits.sendfile_threshold_bytes, 789);
242    }
243}