pprof-alloc 0.1.0

Allocation profiling and Linux memory telemetry for Rust services.
Documentation
use prometheus_client::collector::Collector;
use prometheus_client::encoding::DescriptorEncoder;
use prometheus_client::metrics::gauge::ConstGauge;
use serde::Serialize;
use std::fmt::Error;
use std::fs::File;
use std::io::Read;

#[derive(Debug, Default, PartialEq, Clone, Serialize)]
pub struct ProcessStats {
	pub size: u64,
	pub rss: u64,
	pub pss: u64,
	pub pss_dirty: u64,
	pub shared_clean: u64,
	pub shared_dirty: u64,
	pub private_clean: u64,
	pub private_dirty: u64,
	pub referenced: u64,
	pub anonymous: u64,
	pub lazy_free: u64,
	pub anon_huge_pages: u64,
	pub shmem_huge_pages: u64,
	pub shmem_pmd_mapped: u64,
	pub file_pmd_mapped: u64,
	pub shared_hugetlb: u64,
	pub private_hugetlb: u64,
	pub swap: u64,
	pub swap_pss: u64,
	pub locked: u64,
}

pub fn rollup() -> anyhow::Result<ProcessStats> {
	let path = "/proc/self/smaps_rollup";
	let mut file = File::open(path)?;
	let mut input = String::new();
	file.read_to_string(&mut input)?;
	parse_rollup(&input)
}

fn parse_rollup(input: &str) -> anyhow::Result<ProcessStats> {
	let smaps = super::procmaps::from_str(input).expect("library never returns None");
	if smaps.len() != 1 {
		return Err(anyhow::anyhow!(
			"Expected 1 smaps entry, got {}",
			smaps.len()
		));
	}
	let smap = smaps.into_iter().next().unwrap();
	Ok(ProcessStats {
		size: smap.size,
		rss: smap.rss,
		pss: smap.pss,
		pss_dirty: smap.pss_dirty,
		shared_clean: smap.shared_clean,
		shared_dirty: smap.shared_dirty,
		private_clean: smap.private_clean,
		private_dirty: smap.private_dirty,
		referenced: smap.referenced,
		anonymous: smap.anonymous,
		lazy_free: smap.lazy_free,
		anon_huge_pages: smap.anon_huge_pages,
		shmem_huge_pages: smap.shmem_huge_pages,
		shmem_pmd_mapped: smap.shmem_pmd_mapped,
		file_pmd_mapped: smap.file_pmd_mapped,
		shared_hugetlb: smap.shared_hugetlb,
		private_hugetlb: smap.private_hugetlb,
		swap: smap.swap,
		swap_pss: smap.swap_pss,
		locked: smap.locked,
	})
}

#[derive(Debug, Clone)]
pub struct PrometheusCollector {}

impl PrometheusCollector {
	pub fn register(registry: &mut prometheus_client::registry::Registry) {
		registry.register_collector(Box::new(Self {}))
	}
}

impl Collector for PrometheusCollector {
	fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), Error> {
		use prometheus_client::encoding::EncodeMetric;
		let Ok(s) = rollup() else {
			return Ok(());
		};
		let mut encode = |v: u64, n: &'static str, d: &str| {
			let metric = ConstGauge::new(v);
			let metric_encoder = encoder.encode_descriptor(n, d, None, metric.metric_type())?;
			metric.encode(metric_encoder)?;
			Ok(())
		};
		encode(s.size, "process_size", "size memory usage")?;
		encode(s.rss, "process_rss", "rss memory usage")?;
		encode(s.pss, "process_pss", "pss memory usage")?;
		encode(s.pss_dirty, "process_pss_dirty", "pss_dirty memory usage")?;
		encode(
			s.shared_clean,
			"process_shared_clean",
			"shared_clean memory usage",
		)?;
		encode(
			s.shared_dirty,
			"process_shared_dirty",
			"shared_dirty memory usage",
		)?;
		encode(
			s.private_clean,
			"process_private_clean",
			"private_clean memory usage",
		)?;
		encode(
			s.private_dirty,
			"process_private_dirty",
			"private_dirty memory usage",
		)?;
		encode(
			s.referenced,
			"process_referenced",
			"referenced memory usage",
		)?;
		encode(s.anonymous, "process_anonymous", "anonymous memory usage")?;
		encode(s.lazy_free, "process_lazy_free", "lazy free memory usage")?;
		encode(
			s.anon_huge_pages,
			"process_anon_huge_pages",
			"anonymous huge pages usage",
		)?;
		encode(
			s.shmem_huge_pages,
			"process_shmem_huge_pages",
			"shared memory huge pages usage",
		)?;
		encode(
			s.shmem_pmd_mapped,
			"process_shmem_pmd_mapped",
			"shared memory pmd mapped usage",
		)?;
		encode(
			s.file_pmd_mapped,
			"process_file_pmd_mapped",
			"file pmd mapped usage",
		)?;
		encode(
			s.shared_hugetlb,
			"process_shared_hugetlb",
			"shared hugetlb usage",
		)?;
		encode(
			s.private_hugetlb,
			"process_private_hugetlb",
			"private hugetlb usage",
		)?;
		encode(s.swap, "process_swap", "process swap usage")?;
		encode(
			s.swap_pss,
			"process_swap_pss",
			"process proportional swap usage",
		)?;
		encode(s.locked, "process_locked", "process locked memory usage")?;
		Ok(())
	}
}

#[cfg(test)]
mod tests {
	use super::{ProcessStats, parse_rollup};

	#[test]
	fn parse_rollup_includes_non_heap_process_signals() {
		let input = "\
638000000000-638000001000 ---p 00000000 00:00 0                          [rollup]
Rss:                8192 kB
Pss:                6144 kB
Pss_Dirty:          2048 kB
Shared_Clean:       1024 kB
Shared_Dirty:        512 kB
Private_Clean:      1536 kB
Private_Dirty:      5120 kB
Referenced:         7168 kB
Anonymous:          4096 kB
LazyFree:            256 kB
AnonHugePages:      2048 kB
ShmemHugePages:      128 kB
ShmemPmdMapped:      256 kB
FilePmdMapped:       512 kB
Shared_Hugetlb:       64 kB
Private_Hugetlb:      32 kB
Swap:               1024 kB
SwapPss:             768 kB
Locked:               16 kB
Size:              16384 kB
";

		assert_eq!(
			parse_rollup(input).expect("smaps rollup should parse"),
			ProcessStats {
				size: 16384 * 1024,
				rss: 8192 * 1024,
				pss: 6144 * 1024,
				pss_dirty: 2048 * 1024,
				shared_clean: 1024 * 1024,
				shared_dirty: 512 * 1024,
				private_clean: 1536 * 1024,
				private_dirty: 5120 * 1024,
				referenced: 7168 * 1024,
				anonymous: 4096 * 1024,
				lazy_free: 256 * 1024,
				anon_huge_pages: 2048 * 1024,
				shmem_huge_pages: 128 * 1024,
				shmem_pmd_mapped: 256 * 1024,
				file_pmd_mapped: 512 * 1024,
				shared_hugetlb: 64 * 1024,
				private_hugetlb: 32 * 1024,
				swap: 1024 * 1024,
				swap_pss: 768 * 1024,
				locked: 16 * 1024,
			}
		);
	}
}