Skip to main content

rs_zero/resil/
cpu.rs

1use std::{
2    sync::{Arc, Mutex},
3    time::{Duration, Instant},
4};
5
6const CPU_MAX_MILLIS: u32 = 1000;
7
8/// Provides CPU usage in millicpu-style units where `1000` means 100%.
9pub trait CpuUsageProvider: Send + Sync + 'static {
10    /// Returns current process or system CPU usage.
11    fn cpu_usage_millis(&self) -> u32;
12}
13
14/// Default CPU provider.
15///
16/// It intentionally reports `0` without an additional platform dependency.
17/// Applications that need CPU-aware shedding can install a provider backed by
18/// their runtime or metrics system.
19#[derive(Debug, Clone, Copy, Default)]
20pub struct NoopCpuUsageProvider;
21
22impl CpuUsageProvider for NoopCpuUsageProvider {
23    fn cpu_usage_millis(&self) -> u32 {
24        0
25    }
26}
27
28/// CPU sampler configuration.
29#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct CpuSamplerConfig {
31    /// Minimum time between refreshes.
32    pub refresh_interval: Duration,
33    /// Exponential moving average beta. `0.95` matches go-zero's smoothing profile.
34    pub beta: f64,
35}
36
37impl Default for CpuSamplerConfig {
38    fn default() -> Self {
39        Self {
40            refresh_interval: Duration::from_millis(250),
41            beta: 0.95,
42        }
43    }
44}
45
46/// Linux CPU provider backed by `/proc/stat`.
47#[derive(Debug)]
48pub struct LinuxCpuUsageProvider {
49    config: CpuSamplerConfig,
50    state: Mutex<LinuxCpuState>,
51}
52
53#[derive(Debug, Clone, Copy)]
54struct LinuxCpuState {
55    previous: Option<CpuTimes>,
56    last_refresh: Option<Instant>,
57    usage_millis: u32,
58}
59
60#[derive(Debug, Clone, Copy)]
61struct CpuTimes {
62    idle: u64,
63    total: u64,
64}
65
66impl LinuxCpuUsageProvider {
67    /// Creates a Linux CPU provider using go-zero style sampling defaults.
68    pub fn new() -> Self {
69        Self::with_config(CpuSamplerConfig::default())
70    }
71
72    /// Creates a Linux CPU provider with explicit sampling configuration.
73    pub fn with_config(config: CpuSamplerConfig) -> Self {
74        Self {
75            config,
76            state: Mutex::new(LinuxCpuState {
77                previous: None,
78                last_refresh: None,
79                usage_millis: 0,
80            }),
81        }
82    }
83
84    fn refresh(&self, now: Instant) -> u32 {
85        let mut state = self.state.lock().expect("linux cpu state lock");
86        if let Some(last_refresh) = state.last_refresh {
87            if now.duration_since(last_refresh) < self.config.refresh_interval {
88                return state.usage_millis;
89            }
90        }
91
92        let Some(current) = read_cpu_times() else {
93            state.last_refresh = Some(now);
94            return state.usage_millis;
95        };
96
97        if let Some(previous) = state.previous {
98            let raw = usage_between(previous, current);
99            let beta = self.config.beta.clamp(0.0, 1.0);
100            state.usage_millis = ((state.usage_millis as f64 * beta) + (raw as f64 * (1.0 - beta)))
101                .round()
102                .clamp(0.0, CPU_MAX_MILLIS as f64) as u32;
103        }
104        state.previous = Some(current);
105        state.last_refresh = Some(now);
106        state.usage_millis
107    }
108}
109
110impl Default for LinuxCpuUsageProvider {
111    fn default() -> Self {
112        Self::new()
113    }
114}
115
116impl CpuUsageProvider for LinuxCpuUsageProvider {
117    fn cpu_usage_millis(&self) -> u32 {
118        self.refresh(Instant::now())
119    }
120}
121
122/// Shared CPU provider handle.
123pub type SharedCpuUsageProvider = Arc<dyn CpuUsageProvider>;
124
125/// Creates the default CPU provider.
126pub fn default_cpu_provider() -> SharedCpuUsageProvider {
127    real_cpu_provider().unwrap_or_else(|| Arc::new(NoopCpuUsageProvider))
128}
129
130/// Creates a platform CPU provider when rs-zero can sample CPU without extra setup.
131pub fn real_cpu_provider() -> Option<SharedCpuUsageProvider> {
132    #[cfg(target_os = "linux")]
133    {
134        return Some(Arc::new(LinuxCpuUsageProvider::new()));
135    }
136
137    #[cfg(not(target_os = "linux"))]
138    {
139        None
140    }
141}
142
143fn read_cpu_times() -> Option<CpuTimes> {
144    #[cfg(target_os = "linux")]
145    {
146        read_linux_proc_stat()
147    }
148
149    #[cfg(not(target_os = "linux"))]
150    {
151        None
152    }
153}
154
155#[cfg(target_os = "linux")]
156fn read_linux_proc_stat() -> Option<CpuTimes> {
157    let stat = std::fs::read_to_string("/proc/stat").ok()?;
158    parse_proc_stat(&stat)
159}
160
161#[cfg(target_os = "linux")]
162fn parse_proc_stat(input: &str) -> Option<CpuTimes> {
163    let line = input.lines().find(|line| line.starts_with("cpu "))?;
164    parse_proc_stat_line(line)
165}
166
167#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
168fn parse_proc_stat_line(line: &str) -> Option<CpuTimes> {
169    let mut parts = line.split_whitespace();
170    if parts.next()? != "cpu" {
171        return None;
172    }
173
174    let values = parts
175        .take(10)
176        .map(str::parse::<u64>)
177        .collect::<Result<Vec<_>, _>>()
178        .ok()?;
179    if values.len() < 4 {
180        return None;
181    }
182
183    let idle = values[3] + values.get(4).copied().unwrap_or(0);
184    let total = values.iter().copied().sum();
185    Some(CpuTimes { idle, total })
186}
187
188fn usage_between(previous: CpuTimes, current: CpuTimes) -> u32 {
189    let total_delta = current.total.saturating_sub(previous.total);
190    if total_delta == 0 {
191        return 0;
192    }
193
194    let idle_delta = current.idle.saturating_sub(previous.idle);
195    let busy_delta = total_delta.saturating_sub(idle_delta);
196    ((busy_delta.saturating_mul(CPU_MAX_MILLIS as u64)) / total_delta).min(CPU_MAX_MILLIS as u64)
197        as u32
198}
199
200#[cfg(test)]
201mod tests {
202    use super::{CpuTimes, parse_proc_stat_line, usage_between};
203
204    #[test]
205    fn parses_linux_proc_stat_line() {
206        let times = parse_proc_stat_line("cpu  10 20 30 40 5 6 7 8 9 10").expect("times");
207
208        assert_eq!(times.idle, 45);
209        assert_eq!(times.total, 145);
210    }
211
212    #[test]
213    fn calculates_busy_cpu_millis() {
214        let previous = CpuTimes {
215            idle: 100,
216            total: 200,
217        };
218        let current = CpuTimes {
219            idle: 125,
220            total: 300,
221        };
222
223        assert_eq!(usage_between(previous, current), 750);
224    }
225}