Skip to main content

pprof_alloc/stats/
cgroups.rs

1use anyhow::anyhow;
2use prometheus_client::collector::Collector;
3use prometheus_client::encoding::DescriptorEncoder;
4use prometheus_client::metrics::counter::ConstCounter;
5use prometheus_client::metrics::gauge::ConstGauge;
6use regex::Regex;
7use serde::Serialize;
8use std::fmt::Error;
9
10lazy_static::lazy_static! {
11		static ref CGROUP_V2_PATH_RE: Regex = Regex::new(r#"(?m)^0::(/.*)$"#).unwrap();
12		static ref MEMORY_CURRENT_PATH: String = get_memory_current_path().unwrap_or_else(|_| String::new());
13		static ref MEMORY_STAT_PATH: String = get_memory_stat_path().unwrap_or_else(|_| String::new());
14}
15
16fn get_cgroupv2_path() -> anyhow::Result<String> {
17	let cgroup_path = String::from_utf8(std::fs::read("/proc/self/cgroup")?)?;
18
19	CGROUP_V2_PATH_RE
20		.captures(&cgroup_path)
21		.map(|x| format!("/sys/fs/cgroup{}", x.get(1).unwrap().as_str()))
22		.ok_or_else(|| anyhow::anyhow!("Failed to parse cgroup path"))
23}
24
25fn get_memory_current_path() -> anyhow::Result<String> {
26	let path = get_cgroupv2_path()?;
27	Ok(format!("{}/memory.current", path))
28}
29
30fn get_memory_stat_path() -> anyhow::Result<String> {
31	let path = get_cgroupv2_path()?;
32	Ok(format!("{}/memory.stat", path))
33}
34
35pub fn get_memory() -> anyhow::Result<u64> {
36	let content = std::fs::read_to_string(&*MEMORY_CURRENT_PATH)?;
37	content
38		.trim()
39		.parse::<u64>()
40		.map_err(|e| anyhow::anyhow!("Failed to parse memory value: {}", e))
41}
42
43pub fn get_stats() -> anyhow::Result<MemoryStat> {
44	let content = std::fs::read_to_string(&*MEMORY_STAT_PATH)?;
45	let mut r = MemoryStat::parse(content.trim())
46		.map_err(|e| anyhow::anyhow!("Failed to parse memory stat: {}", e))?;
47	r.usage = get_memory()?;
48	r.working_set = r.usage.saturating_sub(r.inactive_file);
49	Ok(r)
50}
51
52/// A few interesting values from memory.stat
53#[derive(Default, Debug, Clone, Serialize)]
54pub struct MemoryStat {
55	pub usage: u64,
56	// https://github.com/google/cadvisor/blob/5adb1c3bb38b4c5d50b31f39faf3214a44ae479b/container/libcontainer/handler.go#L847
57	pub working_set: u64,
58	/// Anonymous memory, inclusive of swap.
59	pub anon: u64,
60	pub inactive_anon: u64,
61	pub active_anon: u64,
62	/// File-backed memory.
63	pub file: u64,
64	pub file_mapped: u64,
65	pub active_file: u64,
66	pub inactive_file: u64,
67	pub shmem: u64,
68	/// Kernel memory.
69	pub kernel: u64,
70	pub kernel_stack: u64,
71	pub pagetables: u64,
72	pub percpu: u64,
73	pub sock: u64,
74	pub slab: u64,
75	pub slab_reclaimable: u64,
76	pub slab_unreclaimable: u64,
77	pub pgfault: u64,
78	pub pgmajfault: u64,
79	pub workingset_refault_anon: u64,
80	pub workingset_refault_file: u64,
81	pub workingset_activate_anon: u64,
82	pub workingset_activate_file: u64,
83	pub workingset_restore_anon: u64,
84	pub workingset_restore_file: u64,
85}
86
87impl MemoryStat {
88	fn parse(content: &str) -> anyhow::Result<Self> {
89		let mut res = MemoryStat::default();
90
91		for line in content.lines() {
92			let mut parts = line.split_whitespace();
93			let key = parts
94				.next()
95				.ok_or_else(|| anyhow!("Invalid line: '{}' (no key)", line))?;
96			let value = parts
97				.next()
98				.ok_or_else(|| anyhow!("Invalid line: '{}' (no value)", line))?
99				.parse::<u64>()
100				.map_err(|_| anyhow!("Invalid line: '{}' (invalid value)", line))?;
101			if parts.next().is_some() {
102				return Err(anyhow!("Invalid line: '{}' (too many parts)", line));
103			}
104
105			match key {
106				"anon" => res.anon = value,
107				"inactive_anon" => res.inactive_anon = value,
108				"active_anon" => res.active_anon = value,
109				"file" => res.file = value,
110				"file_mapped" => res.file_mapped = value,
111				"active_file" => res.active_file = value,
112				"inactive_file" => res.inactive_file = value,
113				"shmem" => res.shmem = value,
114				"kernel" => res.kernel = value,
115				"kernel_stack" => res.kernel_stack = value,
116				"pagetables" => res.pagetables = value,
117				"percpu" => res.percpu = value,
118				"sock" => res.sock = value,
119				"slab" => res.slab = value,
120				"slab_reclaimable" => res.slab_reclaimable = value,
121				"slab_unreclaimable" => res.slab_unreclaimable = value,
122				"pgfault" => res.pgfault = value,
123				"pgmajfault" => res.pgmajfault = value,
124				"workingset_refault_anon" => res.workingset_refault_anon = value,
125				"workingset_refault_file" => res.workingset_refault_file = value,
126				"workingset_activate_anon" => res.workingset_activate_anon = value,
127				"workingset_activate_file" => res.workingset_activate_file = value,
128				"workingset_restore_anon" => res.workingset_restore_anon = value,
129				"workingset_restore_file" => res.workingset_restore_file = value,
130				// Ignore other keys
131				_ => {},
132			}
133		}
134
135		Ok(res)
136	}
137}
138
139#[derive(Debug, Clone)]
140pub struct PrometheusCollector {}
141
142impl PrometheusCollector {
143	pub fn register(registry: &mut prometheus_client::registry::Registry) {
144		registry.register_collector(Box::new(Self {}))
145	}
146}
147
148impl Collector for PrometheusCollector {
149	fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), Error> {
150		use prometheus_client::encoding::EncodeMetric;
151		let Ok(s) = get_stats() else {
152			return Ok(());
153		};
154
155		fn encode_gauge(
156			encoder: &mut DescriptorEncoder,
157			value: u64,
158			name: &'static str,
159			help: &str,
160		) -> Result<(), Error> {
161			let metric = ConstGauge::new(value);
162			let metric_encoder = encoder.encode_descriptor(name, help, None, metric.metric_type())?;
163			metric.encode(metric_encoder)?;
164			Ok(())
165		}
166
167		fn encode_counter(
168			encoder: &mut DescriptorEncoder,
169			value: u64,
170			name: &'static str,
171			help: &str,
172		) -> Result<(), Error> {
173			let metric = ConstCounter::new(value);
174			let metric_encoder = encoder.encode_descriptor(name, help, None, metric.metric_type())?;
175			metric.encode(metric_encoder)?;
176			Ok(())
177		}
178
179		encode_gauge(
180			&mut encoder,
181			s.usage,
182			"cgroup_usage",
183			"current memory usage",
184		)?;
185		encode_gauge(
186			&mut encoder,
187			s.working_set,
188			"cgroup_working_set",
189			"current working set",
190		)?;
191		encode_gauge(
192			&mut encoder,
193			s.anon,
194			"cgroup_anon",
195			"current anonymous usage",
196		)?;
197		encode_gauge(
198			&mut encoder,
199			s.inactive_anon,
200			"cgroup_inactive_anon",
201			"current inactive anonymous usage",
202		)?;
203		encode_gauge(
204			&mut encoder,
205			s.active_anon,
206			"cgroup_active_anon",
207			"current active anonymous usage",
208		)?;
209		encode_gauge(&mut encoder, s.file, "cgroup_file", "current file usage")?;
210		encode_gauge(
211			&mut encoder,
212			s.file_mapped,
213			"cgroup_file_mapped",
214			"current mapped file usage",
215		)?;
216		encode_gauge(
217			&mut encoder,
218			s.active_file,
219			"cgroup_active_file",
220			"current active file usage",
221		)?;
222		encode_gauge(
223			&mut encoder,
224			s.inactive_file,
225			"cgroup_inactive_file",
226			"current inactive file usage",
227		)?;
228		encode_gauge(&mut encoder, s.shmem, "cgroup_shmem", "current shmem usage")?;
229		encode_gauge(
230			&mut encoder,
231			s.kernel,
232			"cgroup_kernel",
233			"current kernel usage",
234		)?;
235		encode_gauge(
236			&mut encoder,
237			s.kernel_stack,
238			"cgroup_kernel_stack",
239			"current kernel stack usage",
240		)?;
241		encode_gauge(
242			&mut encoder,
243			s.pagetables,
244			"cgroup_pagetables",
245			"current pagetables usage",
246		)?;
247		encode_gauge(
248			&mut encoder,
249			s.percpu,
250			"cgroup_percpu",
251			"current percpu usage",
252		)?;
253		encode_gauge(
254			&mut encoder,
255			s.sock,
256			"cgroup_sock",
257			"current socket memory usage",
258		)?;
259		encode_gauge(&mut encoder, s.slab, "cgroup_slab", "current slab usage")?;
260		encode_gauge(
261			&mut encoder,
262			s.slab_reclaimable,
263			"cgroup_slab_reclaimable",
264			"current reclaimable slab usage",
265		)?;
266		encode_gauge(
267			&mut encoder,
268			s.slab_unreclaimable,
269			"cgroup_slab_unreclaimable",
270			"current unreclaimable slab usage",
271		)?;
272		encode_counter(
273			&mut encoder,
274			s.pgfault,
275			"cgroup_pgfault_total",
276			"cgroup page faults",
277		)?;
278		encode_counter(
279			&mut encoder,
280			s.pgmajfault,
281			"cgroup_pgmajfault_total",
282			"cgroup major page faults",
283		)?;
284		encode_counter(
285			&mut encoder,
286			s.workingset_refault_anon,
287			"cgroup_workingset_refault_anon_total",
288			"anonymous workingset refaults",
289		)?;
290		encode_counter(
291			&mut encoder,
292			s.workingset_refault_file,
293			"cgroup_workingset_refault_file_total",
294			"file workingset refaults",
295		)?;
296		encode_counter(
297			&mut encoder,
298			s.workingset_activate_anon,
299			"cgroup_workingset_activate_anon_total",
300			"anonymous workingset activations",
301		)?;
302		encode_counter(
303			&mut encoder,
304			s.workingset_activate_file,
305			"cgroup_workingset_activate_file_total",
306			"file workingset activations",
307		)?;
308		encode_counter(
309			&mut encoder,
310			s.workingset_restore_anon,
311			"cgroup_workingset_restore_anon_total",
312			"anonymous workingset restores",
313		)?;
314		encode_counter(
315			&mut encoder,
316			s.workingset_restore_file,
317			"cgroup_workingset_restore_file_total",
318			"file workingset restores",
319		)?;
320		Ok(())
321	}
322}
323
324#[cfg(test)]
325mod tests {
326	use super::MemoryStat;
327
328	#[test]
329	fn parse_memory_stat_includes_non_heap_signals() {
330		let input = "\
331anon 4096
332inactive_anon 1024
333active_anon 3072
334file 8192
335file_mapped 2048
336active_file 4096
337inactive_file 4096
338shmem 512
339kernel 1024
340kernel_stack 128
341pagetables 256
342percpu 64
343sock 32
344slab 768
345slab_reclaimable 512
346slab_unreclaimable 256
347pgfault 100
348pgmajfault 3
349workingset_refault_anon 7
350workingset_refault_file 11
351workingset_activate_anon 5
352workingset_activate_file 13
353workingset_restore_anon 2
354workingset_restore_file 17";
355
356		let stats = MemoryStat::parse(input).expect("memory.stat should parse");
357		assert_eq!(stats.anon, 4096);
358		assert_eq!(stats.file_mapped, 2048);
359		assert_eq!(stats.shmem, 512);
360		assert_eq!(stats.slab_reclaimable, 512);
361		assert_eq!(stats.slab_unreclaimable, 256);
362		assert_eq!(stats.pgfault, 100);
363		assert_eq!(stats.pgmajfault, 3);
364		assert_eq!(stats.workingset_refault_file, 11);
365		assert_eq!(stats.workingset_restore_file, 17);
366	}
367}