1use std::{
2 sync::{Arc, Mutex},
3 time::{Duration, Instant},
4};
5
6const CPU_MAX_MILLIS: u32 = 1000;
7
8pub trait CpuUsageProvider: Send + Sync + 'static {
10 fn cpu_usage_millis(&self) -> u32;
12}
13
14#[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#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct CpuSamplerConfig {
31 pub refresh_interval: Duration,
33 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#[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 pub fn new() -> Self {
69 Self::with_config(CpuSamplerConfig::default())
70 }
71
72 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
122pub type SharedCpuUsageProvider = Arc<dyn CpuUsageProvider>;
124
125pub fn default_cpu_provider() -> SharedCpuUsageProvider {
127 real_cpu_provider().unwrap_or_else(|| Arc::new(NoopCpuUsageProvider))
128}
129
130pub 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}