Skip to main content

http_handle/
runtime_autotune.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2023 - 2026 HTTP Handle
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)]
156// Test-only env-var mutations (`std::env::set_var` / `remove_var`) need
157// `unsafe` under Rust 2024. Each call site is a paired write + cleanup
158// inside a single test scope and is documented at the use site.
159#[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    /// Both `detect_memory_*` tests share the same env restoration
199    /// pattern. Hoisting it into a helper means both arms (Some + None)
200    /// live on the same source lines and get coverage across the two
201    /// tests instead of duplicating uncovered branches at each call
202    /// site.
203    fn restore_memory_mib_env(previous: Option<String>) {
204        if let Some(old) = previous {
205            // Safety: restoring process env key snapshot.
206            unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", old) };
207        } else {
208            // Safety: paired cleanup for key introduced in this test.
209            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        // Pre-seed so `previous = Some(...)` — covers the Some arm of
217        // restore_memory_mib_env.
218        // Safety: test-only env mutation; serialised by env_lock.
219        unsafe {
220            std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "preseed");
221        };
222        let previous = std::env::var("HTTP_HANDLE_MEMORY_MIB").ok();
223        // Safety: test-only env mutation in a bounded scope.
224        unsafe { std::env::set_var("HTTP_HANDLE_MEMORY_MIB", "3072") };
225        let got = detect_memory_mib();
226        restore_memory_mib_env(previous);
227        // Final cleanup: drop the seed so siblings start clean.
228        // Safety: paired cleanup for key introduced in this test.
229        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        // Force `previous = None` — covers the else arm of
237        // restore_memory_mib_env.
238        // Safety: test-only env mutation; serialised by env_lock.
239        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        // Safety: test-only process env mutation in a bounded scope.
243        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}