use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use serde::Serialize;
use super::linux_mm::{
BuddyInfo, CgroupMemoryStats, CollectionLimits, CollectionReport, FromRead, HugePageStats, Iomem, Meminfo, MemoryPressure, MemorySysctls, PageTypeInfo,
ProcessMemoryStats, SharedMemorySegments, SlabInfo, Swaps, VmStat, ZoneInfo, collect_with_limits,
};
#[derive(Clone, Copy, Debug)]
pub struct LinuxMemoryLimits {
pub heap: super::linux_mm::HeapLimits,
pub collection: CollectionLimits,
pub live_snapshots: usize,
pub snapshot_bytes: usize,
pub retained_snapshot_bytes: usize,
}
impl Default for LinuxMemoryLimits {
fn default() -> Self {
Self {
heap: super::linux_mm::HeapLimits::default(),
collection: CollectionLimits::default(),
live_snapshots: 4,
snapshot_bytes: 8 * 1024 * 1024,
retained_snapshot_bytes: 32 * 1024 * 1024,
}
}
}
impl LinuxMemoryLimits {
pub fn validate(&self) -> std::io::Result<()> {
self.heap.validate()?;
self.collection.validate()?;
if self.snapshot_bytes == 0 || self.retained_snapshot_bytes < self.snapshot_bytes {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid memory snapshot byte limits"));
}
if !(3..=1024).contains(&self.live_snapshots) {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "live memory snapshots must be 3..=1024"));
}
Ok(())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct LinuxMemoryStats {
pub(super) diagnostics: Option<Arc<super::LinuxMemoryDiagnostics>>,
#[serde(flatten)]
pub(super) data: Arc<LinuxMemoryData>,
#[serde(skip)]
pub(super) allocation: Option<Arc<super::linux_mm::HeapReservation>>,
}
impl Default for LinuxMemoryStats {
fn default() -> Self {
static EMPTY: LazyLock<Arc<LinuxMemoryData>> = LazyLock::new(|| Arc::new(LinuxMemoryData::default()));
Self {
data: Arc::clone(&EMPTY),
diagnostics: None,
allocation: None,
}
}
}
impl Deref for LinuxMemoryStats {
type Target = LinuxMemoryData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
#[derive(Default, Debug, Serialize)]
pub struct LinuxMemoryData {
#[serde(skip_serializing_if = "Option::is_none")]
pub collection: Option<CollectionReport>,
pub ksm: Option<Box<super::linux_mm::KsmStats>>,
pub compressed: Option<Box<super::linux_mm::CompressedMemoryStats>>,
pub numa: Option<Box<super::linux_mm::NumaStats>>,
pub meminfo: Option<Box<Meminfo>>,
pub vmstat: Option<VmStat>,
pub pressure: Option<MemoryPressure>,
pub shared_memory: Option<SharedMemorySegments>,
pub iomem: Option<Iomem>,
pub buddyinfo: Option<BuddyInfo>,
pub zoneinfo: Option<ZoneInfo>,
pub swaps: Option<Swaps>,
pub slabinfo: Option<SlabInfo>,
pub pagetypeinfo: Option<PageTypeInfo>,
pub sysctls: MemorySysctls,
pub cgroup: Box<CgroupMemoryStats>,
pub hugepages: Option<Box<HugePageStats>>,
pub process: Box<ProcessMemoryStats>,
}
impl LinuxMemoryStats {
pub fn try_new(budget: &super::linux_mm::HeapBudget, build: impl FnOnce() -> std::io::Result<LinuxMemoryData>) -> std::io::Result<Self> {
use super::linux_mm::{HeapKind, HeapReservation, with_heap_budget};
let bytes = std::mem::size_of::<LinuxMemoryData>() + std::mem::size_of::<HeapReservation>() + 4 * std::mem::size_of::<usize>();
let allocation = budget.try_reserve(HeapKind::Retained, bytes, 2)?;
let data = with_heap_budget(budget, build)?;
Ok(Self {
data: Arc::new(data),
diagnostics: None,
allocation: Some(Arc::new(allocation)),
})
}
pub fn try_from_data(data: LinuxMemoryData, budget: &super::linux_mm::HeapBudget) -> std::io::Result<Self> {
Self::try_new(budget, || Ok(data))
}
pub fn try_rebuild(
&self,
budget: &super::linux_mm::HeapBudget,
build: impl FnOnce(&LinuxMemoryData) -> std::io::Result<LinuxMemoryData>,
) -> std::io::Result<Self> {
let mut replacement = Self::try_new(budget, || build(&self.data))?;
replacement.diagnostics = self.diagnostics.clone();
Ok(replacement)
}
pub fn data(&self) -> &LinuxMemoryData {
&self.data
}
pub fn diagnostics(&self) -> Option<&super::LinuxMemoryDiagnostics> {
self.diagnostics.as_deref()
}
pub fn shares_data_with(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.data, &other.data)
}
pub fn heap_usage(&self) -> Option<super::linux_mm::HeapUsage> {
self.allocation.as_ref().map(|allocation| allocation.budget().usage())
}
pub fn current() -> Self {
Self::from_roots("/proc", "/sys")
}
pub fn from_roots(proc_root: impl AsRef<Path>, sys_root: impl AsRef<Path>) -> Self {
Self::collect(
proc_root.as_ref(),
Some(sys_root.as_ref()),
CollectionLimits::default(),
&super::linux_mm::HeapBudget::current(),
)
.unwrap_or_else(|error| {
tracing::debug!(%error, "memory collection failed");
Self::default()
})
}
pub fn from_roots_with_limits(proc_root: impl AsRef<Path>, sys_root: impl AsRef<Path>, limits: LinuxMemoryLimits) -> std::io::Result<Self> {
limits.validate()?;
let budget = super::linux_mm::HeapBudget::new(limits.heap)?;
let sample = Self::collect(proc_root.as_ref(), Some(sys_root.as_ref()), limits.collection, &budget)?;
super::generations::snapshot_size(sample.data(), limits.snapshot_bytes).map_err(std::io::Error::other)?;
Ok(sample)
}
pub fn from_roots_with_budget(
proc_root: impl AsRef<Path>,
sys_root: impl AsRef<Path>,
limits: CollectionLimits,
budget: &super::linux_mm::HeapBudget,
) -> std::io::Result<Self> {
Self::collect(proc_root.as_ref(), Some(sys_root.as_ref()), limits, budget)
}
pub fn from_proc_root(root: impl AsRef<Path>) -> Self {
Self::collect(root.as_ref(), None, CollectionLimits::default(), &super::linux_mm::HeapBudget::current()).unwrap_or_else(|error| {
tracing::debug!(%error, "memory collection failed");
Self::default()
})
}
fn collect(root: &Path, sys: Option<&Path>, limits: CollectionLimits, budget: &super::linux_mm::HeapBudget) -> std::io::Result<Self> {
limits.validate()?;
if root.as_os_str().len() > limits.path_bytes || sys.is_some_and(|sys| sys.as_os_str().len() > limits.path_bytes) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"memory source roots exceed path byte limit",
));
}
Self::try_new(budget, || {
let (mut data, report) = collect_with_limits(limits, || LinuxMemoryData {
meminfo: read(root.join("meminfo")).map(Box::new),
vmstat: read(root.join("vmstat")),
pressure: read(root.join("pressure/memory")),
shared_memory: read(root.join("sysvipc/shm")),
iomem: read(root.join("iomem")),
buddyinfo: read(root.join("buddyinfo")),
zoneinfo: read(root.join("zoneinfo")),
swaps: read(root.join("swaps")),
slabinfo: read(root.join("slabinfo")),
pagetypeinfo: read(root.join("pagetypeinfo")),
sysctls: MemorySysctls::from_proc_root(root),
cgroup: Box::new(CgroupMemoryStats::from_proc_root(root)),
hugepages: sys.map(|sys| Box::new(HugePageStats::from_sys_root(sys))),
numa: sys.map(|sys| Box::new(super::linux_mm::NumaStats::from_sys_root(sys))),
compressed: sys.map(|sys| Box::new(super::linux_mm::CompressedMemoryStats::from_sys_root(sys))),
ksm: sys.map(|sys| Box::new(super::linux_mm::KsmStats::from_roots(sys, root.join("self"), std::process::id()))),
process: Box::new(ProcessMemoryStats::from_process_root(std::process::id(), root.join("self"))),
collection: None,
})?;
data.collection = Some(report);
Ok(data)
})
}
}
fn read<T: FromRead>(path: PathBuf) -> Option<T> {
match super::linux_mm::read_source(&path) {
Ok(snapshot) => Some(snapshot),
Err(error) => {
tracing::debug!(path = %path.display(), %error, "Linux memory statistics source unavailable");
None
}
}
}