1use std::collections::HashMap;
18use std::io;
19use std::path::{Path, PathBuf};
20use std::sync::{Arc, Mutex};
21use std::time::Duration;
22
23use tokio::sync::watch;
24
25use crate::api;
26use crate::store::{SampleRecord, Store};
27use crate::sync::lock;
28use crate::time;
29
30pub const SAMPLE_INTERVAL: Duration = Duration::from_secs(api::SAMPLE_INTERVAL);
32
33pub const HOLDERS_INTERVAL: Duration = Duration::from_secs(30);
36
37pub const HOLDERS: usize = 3;
39
40pub const HOLDER_FLOOR: u64 = 512 << 20;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Paths {
46 pub cgroup: PathBuf,
48 pub proc: PathBuf,
50}
51
52impl Default for Paths {
53 fn default() -> Paths {
54 Paths {
55 cgroup: PathBuf::from("/sys/fs/cgroup"),
56 proc: PathBuf::from("/proc"),
57 }
58 }
59}
60
61impl Paths {
62 pub fn slice(&self, slice: &str) -> PathBuf {
64 let name = slice.strip_suffix(".slice").unwrap_or(slice);
65 let mut dir = self.cgroup.clone();
66 let mut prefix = String::new();
67 for part in name.split('-') {
68 if !prefix.is_empty() {
69 prefix.push('-');
70 }
71 prefix.push_str(part);
72 dir.push(format!("{prefix}.slice"));
73 }
74 dir
75 }
76
77 pub fn container(&self, slice: &str, container: &str) -> PathBuf {
79 self.slice(slice).join(format!("docker-{container}.scope"))
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Usage {
86 pub cpu_usage: u64,
88 pub memory: u64,
90 pub memory_with_active_file: u64,
92}
93
94#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
97pub struct Events {
98 pub oom: Option<u64>,
102 pub local_oom: Option<u64>,
105 pub oom_kill: Option<u64>,
107 pub sock_throttled: Option<u64>,
111}
112
113fn read(path: &Path) -> io::Result<String> {
114 std::fs::read_to_string(path)
115}
116
117fn field(text: &str, key: &str) -> Option<u64> {
118 text.lines().find_map(|line| {
119 let (name, value) = line.split_once(' ')?;
120 (name == key).then(|| value.trim().parse().ok()).flatten()
121 })
122}
123
124fn invalid(path: &Path) -> io::Error {
125 io::Error::new(
126 io::ErrorKind::InvalidData,
127 format!("{} has an unexpected format", path.display()),
128 )
129}
130
131pub fn read_usage(cgroup: &Path) -> io::Result<Usage> {
133 let current_path = cgroup.join("memory.current");
134 let current: u64 = read(¤t_path)?
135 .trim()
136 .parse()
137 .map_err(|_| invalid(¤t_path))?;
138 let stat_path = cgroup.join("memory.stat");
139 let stat = read(&stat_path)?;
140 let (Some(active_file), Some(inactive_file)) =
141 (field(&stat, "active_file"), field(&stat, "inactive_file"))
142 else {
143 return Err(invalid(&stat_path));
144 };
145 let cpu_path = cgroup.join("cpu.stat");
146 let cpu_usage = field(&read(&cpu_path)?, "usage_usec").ok_or_else(|| invalid(&cpu_path))?;
147 Ok(Usage {
148 cpu_usage,
149 memory: current.saturating_sub(active_file + inactive_file),
150 memory_with_active_file: current.saturating_sub(inactive_file),
151 })
152}
153
154pub fn read_unavailable(cgroup: &Path) -> u64 {
157 let slab = read(&cgroup.join("memory.stat"))
158 .ok()
159 .and_then(|stat| field(&stat, "slab_reclaimable"))
160 .unwrap_or(0);
161 read_usage(cgroup).map_or(0, |usage| usage.memory.saturating_sub(slab))
162}
163
164pub fn read_events(cgroup: &Path) -> Events {
166 let local_oom = read(&cgroup.join("memory.events.local"))
167 .ok()
168 .and_then(|local| field(&local, "oom"));
169 let Ok(events) = read(&cgroup.join("memory.events")) else {
170 return Events {
171 local_oom,
172 ..Events::default()
173 };
174 };
175 Events {
176 oom: field(&events, "oom"),
177 local_oom,
178 oom_kill: field(&events, "oom_kill"),
179 sock_throttled: field(&events, "sock_throttled"),
180 }
181}
182
183pub fn read_host_memory(paths: &Paths, slice: u64, at: u64) -> Option<api::HostMemory> {
187 let meminfo = read(&paths.proc.join("meminfo")).ok()?;
188 let kib = |key: &str| {
189 meminfo.lines().find_map(|line| {
190 let value = line.strip_prefix(key)?.strip_prefix(':')?;
191 value.trim().strip_suffix("kB")?.trim().parse::<u64>().ok()
192 })
193 };
194 let pressure = read(&paths.proc.join("pressure/memory")).unwrap_or_default();
195 let line = |kind: &str| {
196 let fields = pressure
197 .lines()
198 .find_map(|line| line.strip_prefix(kind)?.strip_prefix(' '))?;
199 let avg = |key: &str| {
200 fields.split_whitespace().find_map(|field| {
201 let value = field.strip_prefix(key)?.strip_prefix('=')?;
202 let (whole, fraction) = value.split_once('.').unwrap_or((value, "0"));
203 let hundredths: u32 = format!("{fraction:0<2}").get(..2)?.parse().ok()?;
204 Some(
205 whole
206 .parse::<u32>()
207 .ok()?
208 .saturating_mul(100)
209 .saturating_add(hundredths),
210 )
211 })
212 };
213 Some(api::Pressure {
214 avg10: avg("avg10")?,
215 avg60: avg("avg60")?,
216 })
217 };
218 Some(api::HostMemory {
219 at,
220 total: kib("MemTotal")?.saturating_mul(1024),
221 available: kib("MemAvailable")?.saturating_mul(1024),
222 slice,
223 some: line("some"),
224 full: line("full"),
225 holders: Vec::new(),
226 })
227}
228
229pub fn read_holders(paths: &Paths, slice: &str) -> Vec<api::Holder> {
236 if !paths.cgroup.join("cgroup.controllers").is_file() {
237 return Vec::new();
238 }
239 let slice = paths.slice(slice);
240 let slice = slice.strip_prefix(&paths.cgroup).unwrap_or(&slice);
241 let Ok(entries) = std::fs::read_dir(&paths.proc) else {
242 return Vec::new();
243 };
244 let mut held: HashMap<String, u64> = HashMap::new();
245 for entry in entries.flatten() {
246 let name = entry.file_name();
247 if !name
248 .to_str()
249 .is_some_and(|name| name.bytes().all(|byte| byte.is_ascii_digit()))
250 {
251 continue;
252 }
253 let dir = entry.path();
254 let Ok(cgroup) = read(&dir.join("cgroup")) else {
255 continue;
256 };
257 let Some(cgroup) = cgroup.lines().find_map(|line| line.strip_prefix("0::")) else {
258 continue;
259 };
260 if Path::new(cgroup.trim_start_matches('/')).starts_with(slice) {
261 continue;
262 }
263 let Ok(status) = std::fs::read(dir.join("status")) else {
265 continue;
266 };
267 let status = String::from_utf8_lossy(&status);
268 let line = |key: &str| {
269 status
270 .lines()
271 .find_map(|line| line.strip_prefix(key)?.strip_prefix(':'))
272 };
273 let kib = |key: &str| {
274 line(key)
275 .and_then(|value| value.trim().strip_suffix("kB")?.trim().parse::<u64>().ok())
276 .unwrap_or(0)
277 };
278 let memory = kib("RssAnon")
279 .saturating_add(kib("RssShmem"))
280 .saturating_mul(1024);
281 let Some(name) = line("Name") else {
282 continue;
283 };
284 let name: String = name.trim().chars().filter(|c| !c.is_control()).collect();
285 if memory > 0 {
286 let total = held.entry(name).or_default();
287 *total = total.saturating_add(memory);
288 }
289 }
290 let mut holders: Vec<api::Holder> = held
291 .into_iter()
292 .filter(|(_, memory)| *memory >= HOLDER_FLOOR)
293 .map(|(name, memory)| api::Holder { name, memory })
294 .collect();
295 holders.sort_by(|a, b| b.memory.cmp(&a.memory).then_with(|| a.name.cmp(&b.name)));
296 holders.truncate(HOLDERS);
297 holders
298}
299
300pub fn read_network(paths: &Paths, pid: u32) -> io::Result<(u64, u64)> {
303 let path = paths.proc.join(pid.to_string()).join("net/dev");
304 let text = read(&path)?;
305 let mut totals = (0, 0);
306 for line in text.lines().skip(2) {
307 let Some((name, counters)) = line.split_once(':') else {
308 return Err(invalid(&path));
309 };
310 if !name.trim().starts_with("eth") {
311 continue;
312 }
313 let counters: Vec<u64> = counters
314 .split_whitespace()
315 .map(str::parse)
316 .collect::<Result<_, _>>()
317 .map_err(|_| invalid(&path))?;
318 let (Some(received), Some(sent)) = (counters.first(), counters.get(8)) else {
320 return Err(invalid(&path));
321 };
322 totals.0 += received;
323 totals.1 += sent;
324 }
325 Ok(totals)
326}
327
328#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct Tracked {
331 pub runner: String,
333 pub container: String,
335 pub pid: u32,
337}
338
339#[derive(Debug)]
341pub struct Sampler {
342 paths: Paths,
343 slice: String,
344 previous: HashMap<String, (u64, u64)>,
345 previous_host: Option<(u64, u64)>,
346 holders: Option<(u64, Vec<api::Holder>)>,
348}
349
350fn millicores(previous: Option<(u64, u64)>, at: u64, cpu_usage: u64) -> u64 {
352 previous.map_or(0, |(then, usage)| {
353 cpu_usage
354 .saturating_sub(usage)
355 .checked_div(at.saturating_sub(then))
356 .unwrap_or(0)
357 })
358}
359
360impl Sampler {
361 pub fn new(paths: Paths, slice: &str) -> Sampler {
363 Sampler {
364 paths,
365 slice: slice.to_owned(),
366 previous: HashMap::new(),
367 previous_host: None,
368 holders: None,
369 }
370 }
371
372 pub fn host_memory(&mut self, at: u64) -> Option<api::HostMemory> {
375 let slice = read_unavailable(&self.paths.slice(&self.slice));
376 let mut memory = read_host_memory(&self.paths, slice, at / 1000)?;
377 let every = u64::try_from(HOLDERS_INTERVAL.as_millis()).unwrap_or(u64::MAX);
378 let holders = match self.holders.take() {
379 Some((then, holders)) if at.saturating_sub(then) < every => (then, holders),
380 _ => (at, read_holders(&self.paths, &self.slice)),
381 };
382 memory.holders.clone_from(&holders.1);
383 self.holders = Some(holders);
384 Some(memory)
385 }
386
387 pub fn sample(
390 &mut self,
391 tracked: &[Tracked],
392 at: u64,
393 ) -> (Vec<SampleRecord>, Option<api::HostSample>) {
394 let mut samples = Vec::new();
395 let mut seen = HashMap::new();
396 for container in tracked {
397 let cgroup = self.paths.container(&self.slice, &container.container);
398 let (Ok(usage), Ok((received, sent))) = (
399 read_usage(&cgroup),
400 read_network(&self.paths, container.pid),
401 ) else {
402 continue;
403 };
404 let cpu = millicores(
405 self.previous.get(&container.runner).copied(),
406 at,
407 usage.cpu_usage,
408 );
409 seen.insert(container.runner.clone(), (at, usage.cpu_usage));
410 samples.push(SampleRecord {
411 runner: container.runner.clone(),
412 at,
413 cpu,
414 cpu_usage: usage.cpu_usage,
415 memory: usage.memory,
416 network_received: received,
417 network_sent: sent,
418 events: read_events(&cgroup),
419 });
420 }
421 self.previous = seen;
422 let host = read_usage(&self.paths.slice(&self.slice))
423 .ok()
424 .map(|usage| {
425 let cpu = millicores(self.previous_host, at, usage.cpu_usage);
426 self.previous_host = Some((at, usage.cpu_usage));
427 api::HostSample {
428 at: at / 1000,
429 cpu,
430 memory: usage.memory_with_active_file,
431 }
432 });
433 (samples, host)
434 }
435}
436
437pub async fn run(state: Sampler, tracked: watch::Receiver<Vec<Tracked>>, store: Store) {
439 let state = Arc::new(Mutex::new(state));
440 let mut interval = tokio::time::interval(SAMPLE_INTERVAL);
441 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
442 loop {
443 interval.tick().await;
444 if tracked.has_changed().is_err() {
445 return;
446 }
447 let containers = tracked.borrow().clone();
448 let state = state.clone();
449 let Ok(((samples, host), memory)) = tokio::task::spawn_blocking(move || {
451 let mut state = lock(&state);
454 let at = time::now_ms();
455 (state.sample(&containers, at), state.host_memory(at))
456 })
457 .await
458 else {
459 continue;
460 };
461 if let Some(memory) = memory {
462 store.host_memory(memory);
463 }
464 if !samples.is_empty() {
465 store.samples(samples);
466 }
467 if let Some(host) = host {
468 store.host_sample(host);
469 }
470 }
471}