use super::{Host, HostStats, O11yError, ProcessStats, RuntimeEnvironment};
use serde::Serialize;
use std::collections::BTreeSet;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum LinuxMemoryCollector {
ProcessMappings,
Slub,
KernelAllocations,
Fragmentation,
LruGenerations,
PhysicalPages,
PageOwner,
KernelLeaks,
MemoryHardware,
}
#[derive(Clone, Debug)]
pub struct LinuxMemoryOptions {
pub provenance_byte_limit: u64,
pub physical_pages: Option<super::linux_mm::PageRange>,
pub virtual_pages: Option<super::linux_mm::PageRange>,
pub collectors: BTreeSet<LinuxMemoryCollector>,
pub interval: Duration,
pub proc_root: PathBuf,
pub sys_root: PathBuf,
}
impl Default for LinuxMemoryOptions {
fn default() -> Self {
Self {
provenance_byte_limit: 4 * 1024 * 1024,
physical_pages: None,
virtual_pages: None,
collectors: BTreeSet::new(),
interval: Duration::from_secs(60),
proc_root: "/proc".into(),
sys_root: "/sys".into(),
}
}
}
impl LinuxMemoryOptions {
pub fn all_snapshots() -> Self {
[
LinuxMemoryCollector::ProcessMappings,
LinuxMemoryCollector::Slub,
LinuxMemoryCollector::KernelAllocations,
LinuxMemoryCollector::Fragmentation,
LinuxMemoryCollector::LruGenerations,
LinuxMemoryCollector::PageOwner,
LinuxMemoryCollector::KernelLeaks,
LinuxMemoryCollector::MemoryHardware,
]
.into_iter()
.fold(Self::new(), Self::enable)
}
pub fn with_physical_page_range(mut self, range: super::linux_mm::PageRange) -> Self {
self.physical_pages = Some(range);
self.collectors.insert(LinuxMemoryCollector::PhysicalPages);
self
}
pub fn with_virtual_page_range(mut self, range: super::linux_mm::PageRange) -> Self {
self.virtual_pages = Some(range);
self.collectors.insert(LinuxMemoryCollector::PhysicalPages);
self
}
pub fn new() -> Self {
Self::default()
}
pub fn enable(mut self, collector: LinuxMemoryCollector) -> Self {
self.collectors.insert(collector);
self
}
pub fn with_interval(mut self, interval: Duration) -> Self {
self.interval = interval;
self
}
pub fn with_roots(mut self, proc_root: impl Into<PathBuf>, sys_root: impl Into<PathBuf>) -> Self {
self.proc_root = proc_root.into();
self.sys_root = sys_root.into();
self
}
}
#[derive(Clone, Debug, Serialize)]
pub struct LinuxMemoryDiagnostics {
pub hardware: Option<Box<super::linux_mm::MemoryHardwareStats>>,
pub page_owner: Option<Box<super::linux_mm::PageOwnerStats>>,
pub kernel_leaks: Option<Box<super::linux_mm::KernelLeakStats>>,
pub physical_pages: Option<Box<super::linux_mm::PhysicalPageStats>>,
pub page_map: Option<Box<super::linux_mm::ProcessPageMap>>,
pub lru_generations: Option<Box<super::linux_mm::LruStats>>,
pub fragmentation: Option<Box<super::linux_mm::FragmentationStats>>,
pub kernel_allocations: Option<Box<super::linux_mm::KernelAllocationStats>>,
pub slub: Option<Box<super::linux_mm::SlubStats>>,
pub sampled_at: chrono::DateTime<chrono::Utc>,
pub process_mappings: Option<Box<super::linux_mm::ProcessMemoryDetails>>,
}
#[derive(Clone, Debug)]
pub struct LinuxMemoryEnvironment<R> {
inner: R,
options: LinuxMemoryOptions,
cache: Arc<Mutex<Option<(Instant, LinuxMemoryDiagnostics)>>>,
}
impl<R> LinuxMemoryEnvironment<R> {
pub fn new(inner: R, options: LinuxMemoryOptions) -> Result<Self, O11yError> {
if !(1..=64 * 1024 * 1024).contains(&options.provenance_byte_limit) {
return Err(O11yError::RuntimeEnvironment("provenance byte limit must be 1..=64 MiB".into()));
}
if options.collectors.contains(&LinuxMemoryCollector::PhysicalPages) {
if options.physical_pages.is_none() && options.virtual_pages.is_none() {
return Err(O11yError::RuntimeEnvironment(
"PhysicalPages requires an explicit PFN or virtual-page range".into(),
));
}
for range in [options.physical_pages, options.virtual_pages].into_iter().flatten() {
range.validate().map_err(|e| O11yError::RuntimeEnvironment(e.to_string()))?;
}
}
if options.interval.is_zero() {
return Err(O11yError::RuntimeEnvironment("Linux memory diagnostic interval must be nonzero".into()));
}
Ok(Self {
inner,
options,
cache: Arc::new(Mutex::new(None)),
})
}
fn decorate(&self, stats: &mut HostStats) -> Result<(), O11yError> {
if self.options.collectors.is_empty() {
return Ok(());
}
let mut cache = self
.cache
.lock()
.map_err(|_| O11yError::RuntimeEnvironment("Linux memory diagnostic cache poisoned".into()))?;
if cache.as_ref().is_none_or(|(time, _)| time.elapsed() >= self.options.interval) {
let selected = |kind| self.options.collectors.contains(&kind);
let data = LinuxMemoryDiagnostics {
hardware: selected(LinuxMemoryCollector::MemoryHardware)
.then(|| Box::new(super::linux_mm::MemoryHardwareStats::from_sys_root(&self.options.sys_root))),
page_owner: selected(LinuxMemoryCollector::PageOwner).then(|| {
Box::new(super::linux_mm::PageOwnerStats::from_debug_root_with_limit(
self.options.sys_root.join("kernel/debug"),
self.options.provenance_byte_limit,
))
}),
kernel_leaks: selected(LinuxMemoryCollector::KernelLeaks).then(|| {
Box::new(super::linux_mm::KernelLeakStats::from_debug_root_with_limit(
self.options.sys_root.join("kernel/debug"),
self.options.provenance_byte_limit,
))
}),
physical_pages: selected(LinuxMemoryCollector::PhysicalPages)
.then_some(self.options.physical_pages)
.flatten()
.map(|range| Box::new(super::linux_mm::PhysicalPageStats::from_proc_root(&self.options.proc_root, range))),
page_map: selected(LinuxMemoryCollector::PhysicalPages)
.then_some(self.options.virtual_pages)
.flatten()
.map(|range| {
Box::new(super::linux_mm::ProcessPageMap::from_process_root(
std::process::id(),
self.options.proc_root.join("self"),
range,
))
}),
lru_generations: selected(LinuxMemoryCollector::LruGenerations)
.then(|| Box::new(super::linux_mm::LruStats::from_sys_root(&self.options.sys_root))),
fragmentation: selected(LinuxMemoryCollector::Fragmentation)
.then(|| Box::new(super::linux_mm::FragmentationStats::from_debug_root(self.options.sys_root.join("kernel/debug")))),
kernel_allocations: selected(LinuxMemoryCollector::KernelAllocations)
.then(|| Box::new(super::linux_mm::KernelAllocationStats::from_proc_root(&self.options.proc_root))),
slub: selected(LinuxMemoryCollector::Slub).then(|| Box::new(super::linux_mm::SlubStats::from_sys_root(&self.options.sys_root))),
sampled_at: chrono::Utc::now(),
process_mappings: selected(LinuxMemoryCollector::ProcessMappings).then(|| {
Box::new(super::linux_mm::ProcessMemoryDetails::from_process_root(
std::process::id(),
self.options.proc_root.join("self"),
))
}),
};
*cache = Some((Instant::now(), data));
}
stats.linux_mm.diagnostics = cache.as_ref().map(|(_, data)| Box::new(data.clone()));
Ok(())
}
}
impl<R: RuntimeEnvironment> RuntimeEnvironment for LinuxMemoryEnvironment<R> {
fn initial_host(&self) -> Result<Host, O11yError> {
let mut host = self.inner.initial_host()?;
self.decorate(&mut host.stats)?;
Ok(host)
}
fn initial_process_stats(&self) -> Option<ProcessStats> {
self.inner.initial_process_stats()
}
fn refresh_host_stats(&self) -> Result<Option<HostStats>, O11yError> {
let mut stats = self.inner.refresh_host_stats()?;
if let Some(stats) = stats.as_mut() {
self.decorate(stats)?;
}
Ok(stats)
}
fn refresh_process_stats(&self) -> Option<ProcessStats> {
self.inner.refresh_process_stats()
}
}