use serde::Serialize; use crate::storage::RocksDbMemorySnapshot; #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct MallocTrimProbe { pub supported: bool, pub return_value: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct ProcessMemorySnapshot { pub label: String, pub vm_rss_kb: Option, pub vm_size_kb: Option, pub vm_data_kb: Option, pub vm_swap_kb: Option, pub rss_anon_kb: Option, pub rss_file_kb: Option, pub rss_shmem_kb: Option, pub threads: Option, pub fd_count: Option, pub smaps_rollup: Option, pub smaps_mapping_summary: Option, pub errors: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct SmapsRollupSnapshot { pub rss_kb: Option, pub pss_kb: Option, pub shared_clean_kb: Option, pub shared_dirty_kb: Option, pub private_clean_kb: Option, pub private_dirty_kb: Option, pub anonymous_kb: Option, pub swap_kb: Option, pub swap_pss_kb: Option, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct SmapsMappingSummary { pub heap: SmapsMappingCategory, pub anonymous_mmap: SmapsMappingCategory, pub file_backed: SmapsMappingCategory, pub stack: SmapsMappingCategory, pub special: SmapsMappingCategory, pub total: SmapsMappingCategory, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct SmapsMappingCategory { pub mappings: u64, pub size_kb: u64, pub rss_kb: u64, pub pss_kb: u64, pub private_clean_kb: u64, pub private_dirty_kb: u64, pub anonymous_kb: u64, pub largest_mapping_rss_kb: u64, pub large_mapping_count_64m: u64, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct MemoryTelemetryCheckpoint { pub label: String, pub elapsed_ms: u64, pub process: ProcessMemorySnapshot, pub rocksdb: RocksDbMemorySnapshot, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct MemoryTelemetrySummary { pub checkpoints: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub object_graph: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub malloc_trim_probes: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct ObjectGraphMemorySummary { pub captured_at_label: String, pub total_estimated_bytes: u64, pub sections: Vec, pub notes: Vec, } #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] pub struct ObjectGraphMemorySection { pub name: String, pub item_count: u64, pub shallow_bytes: u64, pub heap_bytes: u64, pub estimated_bytes: u64, pub string_count: u64, pub string_bytes: u64, pub string_capacity_bytes: u64, pub vec_count: u64, pub vec_heap_bytes: u64, pub vec_capacity_bytes: u64, pub details: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct ObjectGraphMemoryMetric { pub name: String, pub value: u64, } pub fn process_memory_snapshot(label: impl Into) -> ProcessMemorySnapshot { let label = label.into(); let mut snapshot = ProcessMemorySnapshot { label, vm_rss_kb: None, vm_size_kb: None, vm_data_kb: None, vm_swap_kb: None, rss_anon_kb: None, rss_file_kb: None, rss_shmem_kb: None, threads: None, fd_count: current_fd_count(), smaps_rollup: None, smaps_mapping_summary: None, errors: Vec::new(), }; match std::fs::read_to_string("/proc/self/status") { Ok(status) => parse_status(&status, &mut snapshot), Err(err) => snapshot .errors .push(format!("read /proc/self/status failed: {err}")), } match std::fs::read_to_string("/proc/self/smaps_rollup") { Ok(smaps) => snapshot.smaps_rollup = Some(parse_smaps_rollup(&smaps)), Err(err) => snapshot .errors .push(format!("read /proc/self/smaps_rollup failed: {err}")), } match std::fs::read_to_string("/proc/self/smaps") { Ok(smaps) => snapshot.smaps_mapping_summary = Some(parse_smaps_mapping_summary(&smaps)), Err(err) => snapshot .errors .push(format!("read /proc/self/smaps failed: {err}")), } snapshot } pub fn malloc_trim_probe() -> MallocTrimProbe { #[cfg(all(target_os = "linux", target_env = "gnu"))] { MallocTrimProbe { supported: true, return_value: Some(unsafe { malloc_trim(0) }), } } #[cfg(not(all(target_os = "linux", target_env = "gnu")))] { MallocTrimProbe { supported: false, return_value: None, } } } #[cfg(all(target_os = "linux", target_env = "gnu"))] unsafe extern "C" { fn malloc_trim(pad: usize) -> i32; } fn current_fd_count() -> Option { std::fs::read_dir("/proc/self/fd") .ok() .map(|entries| entries.filter_map(Result::ok).count() as u64) } fn parse_status(status: &str, snapshot: &mut ProcessMemorySnapshot) { for line in status.lines() { let Some((key, value)) = line.split_once(':') else { continue; }; let parsed = parse_kb_or_plain_u64(value); match key { "VmRSS" => snapshot.vm_rss_kb = parsed, "VmSize" => snapshot.vm_size_kb = parsed, "VmData" => snapshot.vm_data_kb = parsed, "VmSwap" => snapshot.vm_swap_kb = parsed, "RssAnon" => snapshot.rss_anon_kb = parsed, "RssFile" => snapshot.rss_file_kb = parsed, "RssShmem" => snapshot.rss_shmem_kb = parsed, "Threads" => snapshot.threads = parsed, _ => {} } } } fn parse_smaps_rollup(smaps: &str) -> SmapsRollupSnapshot { let mut snapshot = SmapsRollupSnapshot::default(); for line in smaps.lines() { let Some((key, value)) = line.split_once(':') else { continue; }; let parsed = parse_kb_or_plain_u64(value); match key { "Rss" => snapshot.rss_kb = parsed, "Pss" => snapshot.pss_kb = parsed, "Shared_Clean" => snapshot.shared_clean_kb = parsed, "Shared_Dirty" => snapshot.shared_dirty_kb = parsed, "Private_Clean" => snapshot.private_clean_kb = parsed, "Private_Dirty" => snapshot.private_dirty_kb = parsed, "Anonymous" => snapshot.anonymous_kb = parsed, "Swap" => snapshot.swap_kb = parsed, "SwapPss" => snapshot.swap_pss_kb = parsed, _ => {} } } snapshot } fn parse_smaps_mapping_summary(smaps: &str) -> SmapsMappingSummary { let mut summary = SmapsMappingSummary::default(); let mut current_path = String::new(); let mut current = SmapsMappingCategory::default(); let mut have_mapping = false; for line in smaps.lines() { if is_smaps_mapping_header(line) { if have_mapping { add_mapping(&mut summary, ¤t_path, ¤t); } current_path = smaps_header_path(line); current = SmapsMappingCategory { mappings: 1, ..SmapsMappingCategory::default() }; have_mapping = true; continue; } if !have_mapping { continue; } let Some((key, value)) = line.split_once(':') else { continue; }; let parsed = parse_kb_or_plain_u64(value).unwrap_or(0); match key { "Size" => current.size_kb = parsed, "Rss" => current.rss_kb = parsed, "Pss" => current.pss_kb = parsed, "Private_Clean" => current.private_clean_kb = parsed, "Private_Dirty" => current.private_dirty_kb = parsed, "Anonymous" => current.anonymous_kb = parsed, _ => {} } } if have_mapping { add_mapping(&mut summary, ¤t_path, ¤t); } summary } fn is_smaps_mapping_header(line: &str) -> bool { let mut parts = line.split_whitespace(); let Some(range) = parts.next() else { return false; }; let Some(perms) = parts.next() else { return false; }; let Some((start, end)) = range.split_once('-') else { return false; }; !start.is_empty() && !end.is_empty() && start.as_bytes().iter().all(u8::is_ascii_hexdigit) && end.as_bytes().iter().all(u8::is_ascii_hexdigit) && perms.len() == 4 && perms .as_bytes() .iter() .all(|b| matches!(b, b'r' | b'w' | b'x' | b's' | b'p' | b'-')) } fn smaps_header_path(line: &str) -> String { line.split_whitespace() .skip(5) .collect::>() .join(" ") } fn add_mapping(summary: &mut SmapsMappingSummary, path: &str, mapping: &SmapsMappingCategory) { add_category(&mut summary.total, mapping); match mapping_category(path) { MappingCategory::Heap => add_category(&mut summary.heap, mapping), MappingCategory::AnonymousMmap => add_category(&mut summary.anonymous_mmap, mapping), MappingCategory::FileBacked => add_category(&mut summary.file_backed, mapping), MappingCategory::Stack => add_category(&mut summary.stack, mapping), MappingCategory::Special => add_category(&mut summary.special, mapping), } } fn add_category(target: &mut SmapsMappingCategory, source: &SmapsMappingCategory) { target.mappings += source.mappings; target.size_kb += source.size_kb; target.rss_kb += source.rss_kb; target.pss_kb += source.pss_kb; target.private_clean_kb += source.private_clean_kb; target.private_dirty_kb += source.private_dirty_kb; target.anonymous_kb += source.anonymous_kb; target.largest_mapping_rss_kb = target.largest_mapping_rss_kb.max(source.rss_kb); if source.rss_kb >= 64 * 1024 { target.large_mapping_count_64m += source.mappings; } } enum MappingCategory { Heap, AnonymousMmap, FileBacked, Stack, Special, } fn mapping_category(path: &str) -> MappingCategory { if path == "[heap]" { MappingCategory::Heap } else if path.starts_with("[stack") { MappingCategory::Stack } else if path.is_empty() { MappingCategory::AnonymousMmap } else if path.starts_with('/') { MappingCategory::FileBacked } else { MappingCategory::Special } } fn parse_kb_or_plain_u64(value: &str) -> Option { value.split_whitespace().next()?.parse::().ok() }