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 oom_kill: Option<u64>,
104 pub sock_throttled: Option<u64>,
108}
109
110fn read(path: &Path) -> io::Result<String> {
111 std::fs::read_to_string(path)
112}
113
114fn field(text: &str, key: &str) -> Option<u64> {
115 text.lines().find_map(|line| {
116 let (name, value) = line.split_once(' ')?;
117 (name == key).then(|| value.trim().parse().ok()).flatten()
118 })
119}
120
121fn invalid(path: &Path) -> io::Error {
122 io::Error::new(
123 io::ErrorKind::InvalidData,
124 format!("{} has an unexpected format", path.display()),
125 )
126}
127
128pub fn read_usage(cgroup: &Path) -> io::Result<Usage> {
130 let current_path = cgroup.join("memory.current");
131 let current: u64 = read(¤t_path)?
132 .trim()
133 .parse()
134 .map_err(|_| invalid(¤t_path))?;
135 let stat_path = cgroup.join("memory.stat");
136 let stat = read(&stat_path)?;
137 let (Some(active_file), Some(inactive_file)) =
138 (field(&stat, "active_file"), field(&stat, "inactive_file"))
139 else {
140 return Err(invalid(&stat_path));
141 };
142 let cpu_path = cgroup.join("cpu.stat");
143 let cpu_usage = field(&read(&cpu_path)?, "usage_usec").ok_or_else(|| invalid(&cpu_path))?;
144 Ok(Usage {
145 cpu_usage,
146 memory: current.saturating_sub(active_file + inactive_file),
147 memory_with_active_file: current.saturating_sub(inactive_file),
148 })
149}
150
151pub fn read_unavailable(cgroup: &Path) -> u64 {
154 let slab = read(&cgroup.join("memory.stat"))
155 .ok()
156 .and_then(|stat| field(&stat, "slab_reclaimable"))
157 .unwrap_or(0);
158 read_usage(cgroup).map_or(0, |usage| usage.memory.saturating_sub(slab))
159}
160
161pub fn read_events(cgroup: &Path) -> Events {
163 let Ok(events) = read(&cgroup.join("memory.events")) else {
164 return Events::default();
165 };
166 Events {
167 oom: field(&events, "oom"),
168 oom_kill: field(&events, "oom_kill"),
169 sock_throttled: field(&events, "sock_throttled"),
170 }
171}
172
173pub fn read_host_memory(paths: &Paths, slice: u64, at: u64) -> Option<api::HostMemory> {
177 let meminfo = read(&paths.proc.join("meminfo")).ok()?;
178 let kib = |key: &str| {
179 meminfo.lines().find_map(|line| {
180 let value = line.strip_prefix(key)?.strip_prefix(':')?;
181 value.trim().strip_suffix("kB")?.trim().parse::<u64>().ok()
182 })
183 };
184 let pressure = read(&paths.proc.join("pressure/memory")).unwrap_or_default();
185 let line = |kind: &str| {
186 let fields = pressure
187 .lines()
188 .find_map(|line| line.strip_prefix(kind)?.strip_prefix(' '))?;
189 let avg = |key: &str| {
190 fields.split_whitespace().find_map(|field| {
191 let value = field.strip_prefix(key)?.strip_prefix('=')?;
192 let (whole, fraction) = value.split_once('.').unwrap_or((value, "0"));
193 let hundredths: u32 = format!("{fraction:0<2}").get(..2)?.parse().ok()?;
194 Some(
195 whole
196 .parse::<u32>()
197 .ok()?
198 .saturating_mul(100)
199 .saturating_add(hundredths),
200 )
201 })
202 };
203 Some(api::Pressure {
204 avg10: avg("avg10")?,
205 avg60: avg("avg60")?,
206 })
207 };
208 Some(api::HostMemory {
209 at,
210 total: kib("MemTotal")?.saturating_mul(1024),
211 available: kib("MemAvailable")?.saturating_mul(1024),
212 slice,
213 some: line("some"),
214 full: line("full"),
215 holders: Vec::new(),
216 })
217}
218
219pub fn read_holders(paths: &Paths, slice: &str) -> Vec<api::Holder> {
226 if !paths.cgroup.join("cgroup.controllers").is_file() {
227 return Vec::new();
228 }
229 let slice = paths.slice(slice);
230 let slice = slice.strip_prefix(&paths.cgroup).unwrap_or(&slice);
231 let Ok(entries) = std::fs::read_dir(&paths.proc) else {
232 return Vec::new();
233 };
234 let mut held: HashMap<String, u64> = HashMap::new();
235 for entry in entries.flatten() {
236 let name = entry.file_name();
237 if !name
238 .to_str()
239 .is_some_and(|name| name.bytes().all(|byte| byte.is_ascii_digit()))
240 {
241 continue;
242 }
243 let dir = entry.path();
244 let Ok(cgroup) = read(&dir.join("cgroup")) else {
245 continue;
246 };
247 let Some(cgroup) = cgroup.lines().find_map(|line| line.strip_prefix("0::")) else {
248 continue;
249 };
250 if Path::new(cgroup.trim_start_matches('/')).starts_with(slice) {
251 continue;
252 }
253 let Ok(status) = std::fs::read(dir.join("status")) else {
255 continue;
256 };
257 let status = String::from_utf8_lossy(&status);
258 let line = |key: &str| {
259 status
260 .lines()
261 .find_map(|line| line.strip_prefix(key)?.strip_prefix(':'))
262 };
263 let kib = |key: &str| {
264 line(key)
265 .and_then(|value| value.trim().strip_suffix("kB")?.trim().parse::<u64>().ok())
266 .unwrap_or(0)
267 };
268 let memory = kib("RssAnon")
269 .saturating_add(kib("RssShmem"))
270 .saturating_mul(1024);
271 let Some(name) = line("Name") else {
272 continue;
273 };
274 let name: String = name.trim().chars().filter(|c| !c.is_control()).collect();
275 if memory > 0 {
276 let total = held.entry(name).or_default();
277 *total = total.saturating_add(memory);
278 }
279 }
280 let mut holders: Vec<api::Holder> = held
281 .into_iter()
282 .filter(|(_, memory)| *memory >= HOLDER_FLOOR)
283 .map(|(name, memory)| api::Holder { name, memory })
284 .collect();
285 holders.sort_by(|a, b| b.memory.cmp(&a.memory).then_with(|| a.name.cmp(&b.name)));
286 holders.truncate(HOLDERS);
287 holders
288}
289
290pub fn read_network(paths: &Paths, pid: u32) -> io::Result<(u64, u64)> {
293 let path = paths.proc.join(pid.to_string()).join("net/dev");
294 let text = read(&path)?;
295 let mut totals = (0, 0);
296 for line in text.lines().skip(2) {
297 let Some((name, counters)) = line.split_once(':') else {
298 return Err(invalid(&path));
299 };
300 if !name.trim().starts_with("eth") {
301 continue;
302 }
303 let counters: Vec<u64> = counters
304 .split_whitespace()
305 .map(str::parse)
306 .collect::<Result<_, _>>()
307 .map_err(|_| invalid(&path))?;
308 let (Some(received), Some(sent)) = (counters.first(), counters.get(8)) else {
310 return Err(invalid(&path));
311 };
312 totals.0 += received;
313 totals.1 += sent;
314 }
315 Ok(totals)
316}
317
318#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct Tracked {
321 pub runner: String,
323 pub container: String,
325 pub pid: u32,
327}
328
329#[derive(Debug)]
331pub struct Sampler {
332 paths: Paths,
333 slice: String,
334 previous: HashMap<String, (u64, u64)>,
335 previous_host: Option<(u64, u64)>,
336 holders: Option<(u64, Vec<api::Holder>)>,
338}
339
340fn millicores(previous: Option<(u64, u64)>, at: u64, cpu_usage: u64) -> u64 {
342 previous.map_or(0, |(then, usage)| {
343 cpu_usage
344 .saturating_sub(usage)
345 .checked_div(at.saturating_sub(then))
346 .unwrap_or(0)
347 })
348}
349
350impl Sampler {
351 pub fn new(paths: Paths, slice: &str) -> Sampler {
353 Sampler {
354 paths,
355 slice: slice.to_owned(),
356 previous: HashMap::new(),
357 previous_host: None,
358 holders: None,
359 }
360 }
361
362 pub fn host_memory(&mut self, at: u64) -> Option<api::HostMemory> {
365 let slice = read_unavailable(&self.paths.slice(&self.slice));
366 let mut memory = read_host_memory(&self.paths, slice, at / 1000)?;
367 let every = u64::try_from(HOLDERS_INTERVAL.as_millis()).unwrap_or(u64::MAX);
368 let holders = match self.holders.take() {
369 Some((then, holders)) if at.saturating_sub(then) < every => (then, holders),
370 _ => (at, read_holders(&self.paths, &self.slice)),
371 };
372 memory.holders.clone_from(&holders.1);
373 self.holders = Some(holders);
374 Some(memory)
375 }
376
377 pub fn sample(
380 &mut self,
381 tracked: &[Tracked],
382 at: u64,
383 ) -> (Vec<SampleRecord>, Option<api::HostSample>) {
384 let mut samples = Vec::new();
385 let mut seen = HashMap::new();
386 for container in tracked {
387 let cgroup = self.paths.container(&self.slice, &container.container);
388 let (Ok(usage), Ok((received, sent))) = (
389 read_usage(&cgroup),
390 read_network(&self.paths, container.pid),
391 ) else {
392 continue;
393 };
394 let cpu = millicores(
395 self.previous.get(&container.runner).copied(),
396 at,
397 usage.cpu_usage,
398 );
399 seen.insert(container.runner.clone(), (at, usage.cpu_usage));
400 samples.push(SampleRecord {
401 runner: container.runner.clone(),
402 at,
403 cpu,
404 cpu_usage: usage.cpu_usage,
405 memory: usage.memory,
406 network_received: received,
407 network_sent: sent,
408 events: read_events(&cgroup),
409 });
410 }
411 self.previous = seen;
412 let host = read_usage(&self.paths.slice(&self.slice))
413 .ok()
414 .map(|usage| {
415 let cpu = millicores(self.previous_host, at, usage.cpu_usage);
416 self.previous_host = Some((at, usage.cpu_usage));
417 api::HostSample {
418 at: at / 1000,
419 cpu,
420 memory: usage.memory_with_active_file,
421 }
422 });
423 (samples, host)
424 }
425}
426
427pub async fn run(state: Sampler, tracked: watch::Receiver<Vec<Tracked>>, store: Store) {
429 let state = Arc::new(Mutex::new(state));
430 let mut interval = tokio::time::interval(SAMPLE_INTERVAL);
431 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
432 loop {
433 interval.tick().await;
434 if tracked.has_changed().is_err() {
435 return;
436 }
437 let containers = tracked.borrow().clone();
438 let state = state.clone();
439 let Ok(((samples, host), memory)) = tokio::task::spawn_blocking(move || {
441 let mut state = lock(&state);
444 let at = time::now_ms();
445 (state.sample(&containers, at), state.host_memory(at))
446 })
447 .await
448 else {
449 continue;
450 };
451 if let Some(memory) = memory {
452 store.host_memory(memory);
453 }
454 if !samples.is_empty() {
455 store.samples(samples);
456 }
457 if let Some(host) = host {
458 store.host_sample(host);
459 }
460 }
461}