1use std::collections::HashMap;
7use std::io;
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10use std::time::Duration;
11
12use tokio::sync::watch;
13
14use crate::api;
15use crate::store::{SampleRecord, Store};
16use crate::sync::lock;
17use crate::time;
18
19pub const SAMPLE_INTERVAL: Duration = Duration::from_secs(2);
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Paths {
25 pub cgroup: PathBuf,
27 pub proc: PathBuf,
29}
30
31impl Default for Paths {
32 fn default() -> Paths {
33 Paths {
34 cgroup: PathBuf::from("/sys/fs/cgroup"),
35 proc: PathBuf::from("/proc"),
36 }
37 }
38}
39
40impl Paths {
41 pub fn slice(&self, slice: &str) -> PathBuf {
43 let name = slice.strip_suffix(".slice").unwrap_or(slice);
44 let mut dir = self.cgroup.clone();
45 let mut prefix = String::new();
46 for part in name.split('-') {
47 if !prefix.is_empty() {
48 prefix.push('-');
49 }
50 prefix.push_str(part);
51 dir.push(format!("{prefix}.slice"));
52 }
53 dir
54 }
55
56 pub fn container(&self, slice: &str, container: &str) -> PathBuf {
58 self.slice(slice).join(format!("docker-{container}.scope"))
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Usage {
65 pub cpu_usage: u64,
67 pub memory: u64,
69}
70
71fn read(path: &Path) -> io::Result<String> {
72 std::fs::read_to_string(path)
73}
74
75fn field(text: &str, key: &str) -> Option<u64> {
76 text.lines().find_map(|line| {
77 let (name, value) = line.split_once(' ')?;
78 (name == key).then(|| value.trim().parse().ok()).flatten()
79 })
80}
81
82fn invalid(path: &Path) -> io::Error {
83 io::Error::new(
84 io::ErrorKind::InvalidData,
85 format!("{} has an unexpected format", path.display()),
86 )
87}
88
89pub fn read_usage(cgroup: &Path) -> io::Result<Usage> {
91 let current_path = cgroup.join("memory.current");
92 let current: u64 = read(¤t_path)?
93 .trim()
94 .parse()
95 .map_err(|_| invalid(¤t_path))?;
96 let stat_path = cgroup.join("memory.stat");
97 let inactive_file =
98 field(&read(&stat_path)?, "inactive_file").ok_or_else(|| invalid(&stat_path))?;
99 let cpu_path = cgroup.join("cpu.stat");
100 let cpu_usage = field(&read(&cpu_path)?, "usage_usec").ok_or_else(|| invalid(&cpu_path))?;
101 Ok(Usage {
102 cpu_usage,
103 memory: current.saturating_sub(inactive_file),
104 })
105}
106
107pub fn read_network(paths: &Paths, pid: u32) -> io::Result<(u64, u64)> {
110 let path = paths.proc.join(pid.to_string()).join("net/dev");
111 let text = read(&path)?;
112 let mut totals = (0, 0);
113 for line in text.lines().skip(2) {
114 let Some((name, counters)) = line.split_once(':') else {
115 return Err(invalid(&path));
116 };
117 if !name.trim().starts_with("eth") {
118 continue;
119 }
120 let counters: Vec<u64> = counters
121 .split_whitespace()
122 .map(str::parse)
123 .collect::<Result<_, _>>()
124 .map_err(|_| invalid(&path))?;
125 let (Some(received), Some(sent)) = (counters.first(), counters.get(8)) else {
127 return Err(invalid(&path));
128 };
129 totals.0 += received;
130 totals.1 += sent;
131 }
132 Ok(totals)
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Tracked {
138 pub runner: String,
140 pub container: String,
142 pub pid: u32,
144}
145
146#[derive(Debug)]
148pub struct Sampler {
149 paths: Paths,
150 slice: String,
151 previous: HashMap<String, (u64, u64)>,
152 previous_host: Option<(u64, u64)>,
153}
154
155fn millicores(previous: Option<(u64, u64)>, at: u64, cpu_usage: u64) -> u64 {
157 previous.map_or(0, |(then, usage)| {
158 cpu_usage
159 .saturating_sub(usage)
160 .checked_div(at.saturating_sub(then))
161 .unwrap_or(0)
162 })
163}
164
165impl Sampler {
166 pub fn new(paths: Paths, slice: &str) -> Sampler {
168 Sampler {
169 paths,
170 slice: slice.to_owned(),
171 previous: HashMap::new(),
172 previous_host: None,
173 }
174 }
175
176 pub fn sample(
179 &mut self,
180 tracked: &[Tracked],
181 at: u64,
182 ) -> (Vec<SampleRecord>, Option<api::HostSample>) {
183 let mut samples = Vec::new();
184 let mut seen = HashMap::new();
185 for container in tracked {
186 let cgroup = self.paths.container(&self.slice, &container.container);
187 let (Ok(usage), Ok((received, sent))) = (
188 read_usage(&cgroup),
189 read_network(&self.paths, container.pid),
190 ) else {
191 continue;
192 };
193 let cpu = millicores(
194 self.previous.get(&container.runner).copied(),
195 at,
196 usage.cpu_usage,
197 );
198 seen.insert(container.runner.clone(), (at, usage.cpu_usage));
199 samples.push(SampleRecord {
200 runner: container.runner.clone(),
201 at,
202 cpu,
203 cpu_usage: usage.cpu_usage,
204 memory: usage.memory,
205 network_received: received,
206 network_sent: sent,
207 });
208 }
209 self.previous = seen;
210 let host = read_usage(&self.paths.slice(&self.slice))
211 .ok()
212 .map(|usage| {
213 let cpu = millicores(self.previous_host, at, usage.cpu_usage);
214 self.previous_host = Some((at, usage.cpu_usage));
215 api::HostSample {
216 at: at / 1000,
217 cpu,
218 memory: usage.memory,
219 }
220 });
221 (samples, host)
222 }
223}
224
225pub async fn run(state: Sampler, tracked: watch::Receiver<Vec<Tracked>>, store: Store) {
227 let state = Arc::new(Mutex::new(state));
228 let mut interval = tokio::time::interval(SAMPLE_INTERVAL);
229 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
230 loop {
231 interval.tick().await;
232 if tracked.has_changed().is_err() {
233 return;
234 }
235 let containers = tracked.borrow().clone();
236 let state = state.clone();
237 let Ok((samples, host)) = tokio::task::spawn_blocking(move || {
239 lock(&state).sample(&containers, time::now_ms())
242 })
243 .await
244 else {
245 continue;
246 };
247 if !samples.is_empty() {
248 store.samples(samples);
249 }
250 if let Some(host) = host {
251 store.host_sample(host);
252 }
253 }
254}