#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CgroupMemory {
pub total_memory: u64,
pub rss: u64,
}
#[cfg(target_os = "linux")]
fn meminfo_bytes(keys: &[&str]) -> Vec<Option<u64>> {
let mut out = vec![None; keys.len()];
let Ok(content) = std::fs::read_to_string("/proc/meminfo") else {
return out;
};
for line in content.lines() {
let Some((key, rest)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let Some(i) = keys.iter().position(|k| *k == key) else {
continue;
};
out[i] = rest
.split_whitespace()
.next()
.and_then(|v| v.parse::<u64>().ok())
.map(|kib| kib.saturating_mul(1024));
}
out
}
pub fn mem_total_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
meminfo_bytes(&["MemTotal"])[0]
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub fn mem_available_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let v = meminfo_bytes(&[
"MemAvailable",
"MemFree",
"Buffers",
"Cached",
"SReclaimable",
"Shmem",
]);
if let Some(avail) = v[0] {
return Some(avail);
}
let free = v[1]?;
Some(
free.saturating_add(v[2].unwrap_or(0))
.saturating_add(v[3].unwrap_or(0))
.saturating_add(v[4].unwrap_or(0))
.saturating_sub(v[5].unwrap_or(0)),
)
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
#[cfg(target_os = "linux")]
fn read_u64_file(path: &std::path::Path) -> Option<u64> {
std::fs::read_to_string(path).ok()?.trim().parse().ok()
}
#[cfg(target_os = "linux")]
fn read_v2_max(path: &std::path::Path) -> u64 {
match std::fs::read_to_string(path) {
Ok(s) if s.trim() != "max" => s.trim().parse().unwrap_or(u64::MAX),
_ => u64::MAX,
}
}
#[cfg(target_os = "linux")]
fn read_stat_key(path: &std::path::Path, want: &str) -> Option<u64> {
let content = std::fs::read_to_string(path).ok()?;
for line in content.lines() {
let mut it = line.split_whitespace();
if it.next() == Some(want) {
return it.next().and_then(|v| v.parse().ok());
}
}
None
}
#[cfg(target_os = "linux")]
fn self_cgroup_path(v2: bool) -> Option<String> {
let content = std::fs::read_to_string("/proc/self/cgroup").ok()?;
for line in content.lines() {
let mut parts = line.splitn(3, ':');
let _id = parts.next()?;
let controllers = parts.next()?;
let path = parts.next()?;
if v2 {
if controllers.is_empty() {
return Some(path.trim_start_matches('/').to_string());
}
} else if controllers.split(',').any(|c| c == "memory") {
return Some(path.trim_start_matches('/').to_string());
}
}
None
}
#[cfg(target_os = "linux")]
fn tightest_limit(
base: &std::path::Path,
root: &std::path::Path,
limit_file: &str,
usage_file: &str,
mem_total: u64,
read_limit: fn(&std::path::Path) -> u64,
) -> Option<u64> {
read_u64_file(&base.join(usage_file))?;
let mut total = mem_total;
for path in base.ancestors() {
let max = read_limit(&path.join(limit_file));
if max <= mem_total {
total = total.min(max);
}
if path == root {
return Some(total);
}
}
None
}
pub fn cgroup_memory() -> Option<CgroupMemory> {
#[cfg(target_os = "linux")]
{
use std::path::Path;
let mem_total = mem_total_bytes()?;
let v2_root = Path::new("/sys/fs/cgroup");
if let Some(rel) = self_cgroup_path(true) {
let base = v2_root.join(&rel);
if let (Some(total_memory), Some(rss)) = (
tightest_limit(
&base,
v2_root,
"memory.max",
"memory.current",
mem_total,
read_v2_max,
),
read_stat_key(&base.join("memory.stat"), "anon"),
) {
return Some(CgroupMemory { total_memory, rss });
}
}
let v1_root = Path::new("/sys/fs/cgroup/memory");
let rel = self_cgroup_path(false)?;
let base = v1_root.join(&rel);
let total_memory = tightest_limit(
&base,
v1_root,
"memory.limit_in_bytes",
"memory.usage_in_bytes",
mem_total,
|p| read_u64_file(p).unwrap_or(u64::MAX),
)?;
let rss = read_stat_key(&base.join("memory.stat"), "total_rss")?;
Some(CgroupMemory { total_memory, rss })
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub fn physical_core_count() -> Option<usize> {
#[cfg(target_os = "linux")]
{
use std::collections::HashSet;
let content = std::fs::read_to_string("/proc/cpuinfo").ok()?;
let mut seen: HashSet<String> = HashSet::new();
let (mut core_id, mut physical_id, mut cpu) = (String::new(), String::new(), String::new());
let mut flush = |core_id: &mut String, physical_id: &mut String, cpu: &mut String| {
if !core_id.is_empty() && !physical_id.is_empty() {
seen.insert(format!("{core_id} {physical_id}"));
} else if !cpu.is_empty() {
seen.insert(cpu.clone());
}
core_id.clear();
physical_id.clear();
cpu.clear();
};
fn after_colon(line: &str) -> &str {
match line.split_once(':') {
Some((_, rest)) => rest.trim(),
None => line.trim(),
}
}
for line in content.lines() {
if line.is_empty() {
flush(&mut core_id, &mut physical_id, &mut cpu);
} else if line.starts_with("processor") {
cpu = after_colon(line).to_string();
} else if line.starts_with("core id") {
core_id = after_colon(line).to_string();
} else if line.starts_with("physical id") {
physical_id = after_colon(line).to_string();
}
}
flush(&mut core_id, &mut physical_id, &mut cpu);
if seen.is_empty() { None } else { Some(seen.len()) }
}
#[cfg(not(target_os = "linux"))]
{
None
}
}
pub fn logical_cpu_count() -> usize {
#[cfg(target_os = "linux")]
if let Ok(content) = std::fs::read_to_string("/proc/cpuinfo") {
let n = content
.lines()
.filter(|l| l.starts_with("processor") && l.contains(':'))
.count();
if n > 0 {
return n;
}
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}