1use std::collections::HashMap;
16use std::io;
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use tokio::sync::watch;
22
23use crate::api;
24use crate::store::{SampleRecord, Store};
25use crate::sync::lock;
26use crate::time;
27
28pub const SAMPLE_INTERVAL: Duration = Duration::from_secs(2);
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Paths {
34 pub cgroup: PathBuf,
36 pub proc: PathBuf,
38}
39
40impl Default for Paths {
41 fn default() -> Paths {
42 Paths {
43 cgroup: PathBuf::from("/sys/fs/cgroup"),
44 proc: PathBuf::from("/proc"),
45 }
46 }
47}
48
49impl Paths {
50 pub fn slice(&self, slice: &str) -> PathBuf {
52 let name = slice.strip_suffix(".slice").unwrap_or(slice);
53 let mut dir = self.cgroup.clone();
54 let mut prefix = String::new();
55 for part in name.split('-') {
56 if !prefix.is_empty() {
57 prefix.push('-');
58 }
59 prefix.push_str(part);
60 dir.push(format!("{prefix}.slice"));
61 }
62 dir
63 }
64
65 pub fn container(&self, slice: &str, container: &str) -> PathBuf {
67 self.slice(slice).join(format!("docker-{container}.scope"))
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct Usage {
74 pub cpu_usage: u64,
76 pub memory: u64,
78 pub memory_with_active_file: u64,
80}
81
82#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub struct Events {
86 pub oom: Option<u64>,
90 pub oom_kill: Option<u64>,
92 pub sock_throttled: Option<u64>,
96}
97
98fn read(path: &Path) -> io::Result<String> {
99 std::fs::read_to_string(path)
100}
101
102fn field(text: &str, key: &str) -> Option<u64> {
103 text.lines().find_map(|line| {
104 let (name, value) = line.split_once(' ')?;
105 (name == key).then(|| value.trim().parse().ok()).flatten()
106 })
107}
108
109fn invalid(path: &Path) -> io::Error {
110 io::Error::new(
111 io::ErrorKind::InvalidData,
112 format!("{} has an unexpected format", path.display()),
113 )
114}
115
116pub fn read_usage(cgroup: &Path) -> io::Result<Usage> {
118 let current_path = cgroup.join("memory.current");
119 let current: u64 = read(¤t_path)?
120 .trim()
121 .parse()
122 .map_err(|_| invalid(¤t_path))?;
123 let stat_path = cgroup.join("memory.stat");
124 let stat = read(&stat_path)?;
125 let (Some(active_file), Some(inactive_file)) =
126 (field(&stat, "active_file"), field(&stat, "inactive_file"))
127 else {
128 return Err(invalid(&stat_path));
129 };
130 let cpu_path = cgroup.join("cpu.stat");
131 let cpu_usage = field(&read(&cpu_path)?, "usage_usec").ok_or_else(|| invalid(&cpu_path))?;
132 Ok(Usage {
133 cpu_usage,
134 memory: current.saturating_sub(active_file + inactive_file),
135 memory_with_active_file: current.saturating_sub(inactive_file),
136 })
137}
138
139pub fn read_unavailable(cgroup: &Path) -> u64 {
142 let slab = read(&cgroup.join("memory.stat"))
143 .ok()
144 .and_then(|stat| field(&stat, "slab_reclaimable"))
145 .unwrap_or(0);
146 read_usage(cgroup).map_or(0, |usage| usage.memory.saturating_sub(slab))
147}
148
149pub fn read_events(cgroup: &Path) -> Events {
151 let Ok(events) = read(&cgroup.join("memory.events")) else {
152 return Events::default();
153 };
154 Events {
155 oom: field(&events, "oom"),
156 oom_kill: field(&events, "oom_kill"),
157 sock_throttled: field(&events, "sock_throttled"),
158 }
159}
160
161pub fn read_host_memory(paths: &Paths, slice: u64, at: u64) -> Option<api::HostMemory> {
165 let meminfo = read(&paths.proc.join("meminfo")).ok()?;
166 let kib = |key: &str| {
167 meminfo.lines().find_map(|line| {
168 let value = line.strip_prefix(key)?.strip_prefix(':')?;
169 value.trim().strip_suffix("kB")?.trim().parse::<u64>().ok()
170 })
171 };
172 let pressure = read(&paths.proc.join("pressure/memory")).unwrap_or_default();
173 let line = |kind: &str| {
174 let fields = pressure
175 .lines()
176 .find_map(|line| line.strip_prefix(kind)?.strip_prefix(' '))?;
177 let avg = |key: &str| {
178 fields.split_whitespace().find_map(|field| {
179 let value = field.strip_prefix(key)?.strip_prefix('=')?;
180 let (whole, fraction) = value.split_once('.').unwrap_or((value, "0"));
181 let hundredths: u32 = format!("{fraction:0<2}").get(..2)?.parse().ok()?;
182 Some(
183 whole
184 .parse::<u32>()
185 .ok()?
186 .saturating_mul(100)
187 .saturating_add(hundredths),
188 )
189 })
190 };
191 Some(api::Pressure {
192 avg10: avg("avg10")?,
193 avg60: avg("avg60")?,
194 })
195 };
196 Some(api::HostMemory {
197 at,
198 total: kib("MemTotal")?.saturating_mul(1024),
199 available: kib("MemAvailable")?.saturating_mul(1024),
200 slice,
201 some: line("some"),
202 full: line("full"),
203 })
204}
205
206pub fn read_network(paths: &Paths, pid: u32) -> io::Result<(u64, u64)> {
209 let path = paths.proc.join(pid.to_string()).join("net/dev");
210 let text = read(&path)?;
211 let mut totals = (0, 0);
212 for line in text.lines().skip(2) {
213 let Some((name, counters)) = line.split_once(':') else {
214 return Err(invalid(&path));
215 };
216 if !name.trim().starts_with("eth") {
217 continue;
218 }
219 let counters: Vec<u64> = counters
220 .split_whitespace()
221 .map(str::parse)
222 .collect::<Result<_, _>>()
223 .map_err(|_| invalid(&path))?;
224 let (Some(received), Some(sent)) = (counters.first(), counters.get(8)) else {
226 return Err(invalid(&path));
227 };
228 totals.0 += received;
229 totals.1 += sent;
230 }
231 Ok(totals)
232}
233
234#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct Tracked {
237 pub runner: String,
239 pub container: String,
241 pub pid: u32,
243}
244
245#[derive(Debug)]
247pub struct Sampler {
248 paths: Paths,
249 slice: String,
250 previous: HashMap<String, (u64, u64)>,
251 previous_host: Option<(u64, u64)>,
252}
253
254fn millicores(previous: Option<(u64, u64)>, at: u64, cpu_usage: u64) -> u64 {
256 previous.map_or(0, |(then, usage)| {
257 cpu_usage
258 .saturating_sub(usage)
259 .checked_div(at.saturating_sub(then))
260 .unwrap_or(0)
261 })
262}
263
264impl Sampler {
265 pub fn new(paths: Paths, slice: &str) -> Sampler {
267 Sampler {
268 paths,
269 slice: slice.to_owned(),
270 previous: HashMap::new(),
271 previous_host: None,
272 }
273 }
274
275 pub fn host_memory(&self, at: u64) -> Option<api::HostMemory> {
278 let slice = read_unavailable(&self.paths.slice(&self.slice));
279 read_host_memory(&self.paths, slice, at / 1000)
280 }
281
282 pub fn sample(
285 &mut self,
286 tracked: &[Tracked],
287 at: u64,
288 ) -> (Vec<SampleRecord>, Option<api::HostSample>) {
289 let mut samples = Vec::new();
290 let mut seen = HashMap::new();
291 for container in tracked {
292 let cgroup = self.paths.container(&self.slice, &container.container);
293 let (Ok(usage), Ok((received, sent))) = (
294 read_usage(&cgroup),
295 read_network(&self.paths, container.pid),
296 ) else {
297 continue;
298 };
299 let cpu = millicores(
300 self.previous.get(&container.runner).copied(),
301 at,
302 usage.cpu_usage,
303 );
304 seen.insert(container.runner.clone(), (at, usage.cpu_usage));
305 samples.push(SampleRecord {
306 runner: container.runner.clone(),
307 at,
308 cpu,
309 cpu_usage: usage.cpu_usage,
310 memory: usage.memory,
311 network_received: received,
312 network_sent: sent,
313 events: read_events(&cgroup),
314 });
315 }
316 self.previous = seen;
317 let host = read_usage(&self.paths.slice(&self.slice))
318 .ok()
319 .map(|usage| {
320 let cpu = millicores(self.previous_host, at, usage.cpu_usage);
321 self.previous_host = Some((at, usage.cpu_usage));
322 api::HostSample {
323 at: at / 1000,
324 cpu,
325 memory: usage.memory_with_active_file,
326 }
327 });
328 (samples, host)
329 }
330}
331
332pub async fn run(state: Sampler, tracked: watch::Receiver<Vec<Tracked>>, store: Store) {
334 let state = Arc::new(Mutex::new(state));
335 let mut interval = tokio::time::interval(SAMPLE_INTERVAL);
336 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
337 loop {
338 interval.tick().await;
339 if tracked.has_changed().is_err() {
340 return;
341 }
342 let containers = tracked.borrow().clone();
343 let state = state.clone();
344 let Ok(((samples, host), memory)) = tokio::task::spawn_blocking(move || {
346 let mut state = lock(&state);
349 let at = time::now_ms();
350 (state.sample(&containers, at), state.host_memory(at))
351 })
352 .await
353 else {
354 continue;
355 };
356 if let Some(memory) = memory {
357 store.host_memory(memory);
358 }
359 if !samples.is_empty() {
360 store.samples(samples);
361 }
362 if let Some(host) = host {
363 store.host_sample(host);
364 }
365 }
366}