use std::collections::BTreeMap;
use std::sync::OnceLock;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct HostContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cpu_vendor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub total_memory_kib: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_total: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_free: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hugepages_size_kib: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thp_enabled: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thp_defrag: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sched_tunables: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub online_cpus: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub numa_nodes: Option<usize>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub cpufreq_governor: BTreeMap<usize, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_release: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kernel_cmdline: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub heap_state: Option<crate::host_heap::HostHeapState>,
}
pub fn parse_bracketed_active_policy(s: &str) -> Option<&str> {
let open = s.find('[')?;
let rest = &s[open + 1..];
let close = rest.find(']')?;
Some(&rest[..close])
}
impl HostContext {
pub fn test_fixture() -> HostContext {
let mut sched_tunables = BTreeMap::new();
sched_tunables.insert("sched_migration_cost_ns".to_string(), "500000".to_string());
sched_tunables.insert("sched_latency_ns".to_string(), "24000000".to_string());
HostContext {
cpu_model: Some("Intel(R) Xeon(R) Test CPU".to_string()),
cpu_vendor: Some("GenuineIntel".to_string()),
total_memory_kib: Some(64 * 1024 * 1024),
hugepages_total: Some(0),
hugepages_free: Some(0),
hugepages_size_kib: Some(2048),
thp_enabled: Some("always [madvise] never".to_string()),
thp_defrag: Some("always defer defer+madvise [madvise] never".to_string()),
sched_tunables: Some(sched_tunables),
online_cpus: Some(16),
numa_nodes: Some(2),
cpufreq_governor: {
let mut m = BTreeMap::new();
for cpu in 0..16 {
m.insert(cpu, "performance".to_string());
}
m
},
kernel_name: Some("Linux".to_string()),
kernel_release: Some("6.16.0-test".to_string()),
arch: Some("x86_64".to_string()),
kernel_cmdline: Some("BOOT_IMAGE=/boot/vmlinuz-test root=/dev/sda1".to_string()),
heap_state: Some(crate::host_heap::HostHeapState::test_fixture()),
}
}
pub fn format_human(&self) -> String {
use std::fmt::Write;
let HostContext {
cpu_model,
cpu_vendor,
total_memory_kib,
hugepages_total,
hugepages_free,
hugepages_size_kib,
thp_enabled,
thp_defrag,
sched_tunables,
online_cpus,
numa_nodes,
cpufreq_governor,
kernel_name,
kernel_release,
arch,
kernel_cmdline,
heap_state,
} = self;
fn row<T: std::fmt::Display>(out: &mut String, key: &str, value: Option<&T>) {
match value {
Some(v) => {
let _ = writeln!(out, "{key}: {v}");
}
None => {
let _ = writeln!(out, "{key}: (unknown)");
}
}
}
let mut out = String::new();
row(&mut out, "kernel_name", kernel_name.as_ref());
row(&mut out, "kernel_release", kernel_release.as_ref());
row(&mut out, "arch", arch.as_ref());
row(&mut out, "cpu_model", cpu_model.as_ref());
row(&mut out, "cpu_vendor", cpu_vendor.as_ref());
row(&mut out, "total_memory_kib", total_memory_kib.as_ref());
row(&mut out, "hugepages_total", hugepages_total.as_ref());
row(&mut out, "hugepages_free", hugepages_free.as_ref());
row(&mut out, "hugepages_size_kib", hugepages_size_kib.as_ref());
row(&mut out, "online_cpus", online_cpus.as_ref());
row(&mut out, "numa_nodes", numa_nodes.as_ref());
row(&mut out, "thp_enabled", thp_enabled.as_ref());
row(&mut out, "thp_defrag", thp_defrag.as_ref());
row(&mut out, "kernel_cmdline", kernel_cmdline.as_ref());
if cpufreq_governor.is_empty() {
out.push_str("cpufreq_governor: (empty)\n");
} else {
out.push_str("cpufreq_governor:\n");
for (cpu, gov) in cpufreq_governor {
let _ = writeln!(&mut out, " cpu{cpu} = {gov}");
}
}
match sched_tunables {
Some(map) if !map.is_empty() => {
out.push_str("sched_tunables:\n");
for (k, v) in map {
let _ = writeln!(&mut out, " {k} = {v}");
}
}
Some(_) => out.push_str("sched_tunables: (empty)\n"),
None => out.push_str("sched_tunables: (unknown)\n"),
}
match heap_state {
Some(h) => {
out.push_str("heap_state:\n");
for line in h.format_human().lines() {
let _ = writeln!(&mut out, " {line}");
}
}
None => out.push_str("heap_state: (unknown)\n"),
}
out
}
pub fn thp_enabled_active(&self) -> Option<&str> {
self.thp_enabled
.as_deref()
.and_then(parse_bracketed_active_policy)
}
pub fn thp_defrag_active(&self) -> Option<&str> {
self.thp_defrag
.as_deref()
.and_then(parse_bracketed_active_policy)
}
pub fn diff(&self, other: &HostContext) -> String {
use std::collections::BTreeMap;
use std::fmt::Write;
let HostContext {
cpu_model: a_cpu_model,
cpu_vendor: a_cpu_vendor,
total_memory_kib: a_total_memory_kib,
hugepages_total: a_hugepages_total,
hugepages_free: a_hugepages_free,
hugepages_size_kib: a_hugepages_size_kib,
thp_enabled: a_thp_enabled,
thp_defrag: a_thp_defrag,
sched_tunables: a_sched_tunables,
online_cpus: a_online_cpus,
numa_nodes: a_numa_nodes,
cpufreq_governor: a_cpufreq_governor,
kernel_name: a_kernel_name,
kernel_release: a_kernel_release,
arch: a_arch,
kernel_cmdline: a_kernel_cmdline,
heap_state: a_heap_state,
} = self;
let HostContext {
cpu_model: b_cpu_model,
cpu_vendor: b_cpu_vendor,
total_memory_kib: b_total_memory_kib,
hugepages_total: b_hugepages_total,
hugepages_free: b_hugepages_free,
hugepages_size_kib: b_hugepages_size_kib,
thp_enabled: b_thp_enabled,
thp_defrag: b_thp_defrag,
sched_tunables: b_sched_tunables,
online_cpus: b_online_cpus,
numa_nodes: b_numa_nodes,
cpufreq_governor: b_cpufreq_governor,
kernel_name: b_kernel_name,
kernel_release: b_kernel_release,
arch: b_arch,
kernel_cmdline: b_kernel_cmdline,
heap_state: b_heap_state,
} = other;
fn fmt_opt<T: std::fmt::Display>(v: Option<&T>) -> String {
match v {
Some(v) => v.to_string(),
None => "(unknown)".to_string(),
}
}
fn row<T: std::fmt::Display + PartialEq>(
out: &mut String,
key: &str,
a: Option<&T>,
b: Option<&T>,
) {
if a == b {
return;
}
let _ = writeln!(out, " {key}: {} → {}", fmt_opt(a), fmt_opt(b));
}
fn summarize_tunables(m: Option<&BTreeMap<String, String>>) -> String {
match m {
None => "(unknown)".to_string(),
Some(map) if map.is_empty() => "(empty)".to_string(),
Some(map) if map.len() == 1 => "(1 entry)".to_string(),
Some(map) => format!("({} entries)", map.len()),
}
}
let mut out = String::new();
row(
&mut out,
"kernel_name",
a_kernel_name.as_ref(),
b_kernel_name.as_ref(),
);
row(
&mut out,
"kernel_release",
a_kernel_release.as_ref(),
b_kernel_release.as_ref(),
);
row(&mut out, "arch", a_arch.as_ref(), b_arch.as_ref());
row(
&mut out,
"cpu_model",
a_cpu_model.as_ref(),
b_cpu_model.as_ref(),
);
row(
&mut out,
"cpu_vendor",
a_cpu_vendor.as_ref(),
b_cpu_vendor.as_ref(),
);
row(
&mut out,
"total_memory_kib",
a_total_memory_kib.as_ref(),
b_total_memory_kib.as_ref(),
);
row(
&mut out,
"hugepages_total",
a_hugepages_total.as_ref(),
b_hugepages_total.as_ref(),
);
row(
&mut out,
"hugepages_free",
a_hugepages_free.as_ref(),
b_hugepages_free.as_ref(),
);
row(
&mut out,
"hugepages_size_kib",
a_hugepages_size_kib.as_ref(),
b_hugepages_size_kib.as_ref(),
);
row(
&mut out,
"online_cpus",
a_online_cpus.as_ref(),
b_online_cpus.as_ref(),
);
row(
&mut out,
"numa_nodes",
a_numa_nodes.as_ref(),
b_numa_nodes.as_ref(),
);
row(
&mut out,
"thp_enabled",
a_thp_enabled.as_ref(),
b_thp_enabled.as_ref(),
);
row(
&mut out,
"thp_defrag",
a_thp_defrag.as_ref(),
b_thp_defrag.as_ref(),
);
row(
&mut out,
"kernel_cmdline",
a_kernel_cmdline.as_ref(),
b_kernel_cmdline.as_ref(),
);
{
let mut cpus: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
cpus.extend(a_cpufreq_governor.keys().copied());
cpus.extend(b_cpufreq_governor.keys().copied());
for cpu in cpus {
let av = a_cpufreq_governor.get(&cpu);
let bv = b_cpufreq_governor.get(&cpu);
if av != bv {
let _ = writeln!(
&mut out,
" cpufreq_governor.cpu{cpu}: {} → {}",
av.map(String::as_str).unwrap_or("(absent)"),
bv.map(String::as_str).unwrap_or("(absent)"),
);
}
}
}
match (a_sched_tunables.as_ref(), b_sched_tunables.as_ref()) {
(Some(am), Some(bm)) => {
let mut keys: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
keys.extend(am.keys().map(String::as_str));
keys.extend(bm.keys().map(String::as_str));
for k in keys {
let av = am.get(k);
let bv = bm.get(k);
if av != bv {
let _ = writeln!(
&mut out,
" sched_tunables.{k}: {} → {}",
av.map(String::as_str).unwrap_or("(absent)"),
bv.map(String::as_str).unwrap_or("(absent)"),
);
}
}
}
(am, bm) if am != bm => {
let _ = writeln!(
&mut out,
" sched_tunables: {} → {}",
summarize_tunables(am),
summarize_tunables(bm),
);
}
_ => {}
}
match (a_heap_state.as_ref(), b_heap_state.as_ref()) {
(Some(ah), Some(bh)) => {
let inner = ah.diff(bh);
if !inner.is_empty() {
out.push_str(" heap_state:\n");
for line in inner.lines() {
let _ = writeln!(&mut out, " {line}");
}
}
}
(a, b) if a != b => {
let _ = writeln!(
&mut out,
" heap_state: {} → {}",
if a.is_some() {
"(present)"
} else {
"(unknown)"
},
if b.is_some() {
"(present)"
} else {
"(unknown)"
},
);
}
_ => {}
}
out
}
}
#[derive(Clone)]
struct StaticHostInfo {
cpu_model: Option<String>,
cpu_vendor: Option<String>,
total_memory_kib: Option<u64>,
hugepages_size_kib: Option<u64>,
online_cpus: Option<usize>,
numa_nodes: Option<usize>,
kernel_name: Option<String>,
kernel_release: Option<String>,
arch: Option<String>,
}
static STATIC_HOST_INFO: OnceLock<StaticHostInfo> = OnceLock::new();
static CPUFREQ_GOVERNORS: OnceLock<BTreeMap<usize, String>> = OnceLock::new();
#[cfg(test)]
static STATIC_INIT_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
static MEMINFO_READ_CALLS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
static CPUFREQ_GOVERNORS_READ_CALLS: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
pub fn collect_host_context() -> HostContext {
let meminfo = read_meminfo();
let static_info = STATIC_HOST_INFO
.get_or_init(|| compute_static_host_info(&meminfo))
.clone();
HostContext {
cpu_model: static_info.cpu_model,
cpu_vendor: static_info.cpu_vendor,
total_memory_kib: static_info.total_memory_kib,
hugepages_total: meminfo.hugepages_total,
hugepages_free: meminfo.hugepages_free,
hugepages_size_kib: static_info.hugepages_size_kib,
thp_enabled: read_trimmed_sysfs("/sys/kernel/mm/transparent_hugepage/enabled"),
thp_defrag: read_trimmed_sysfs("/sys/kernel/mm/transparent_hugepage/defrag"),
sched_tunables: read_sched_tunables(),
online_cpus: static_info.online_cpus,
numa_nodes: static_info.numa_nodes,
cpufreq_governor: cached_cpufreq_governors(),
kernel_name: static_info.kernel_name,
kernel_release: static_info.kernel_release,
arch: static_info.arch,
kernel_cmdline: read_trimmed_sysfs("/proc/cmdline"),
heap_state: {
let h = crate::host_heap::collect();
if h.allocated_bytes == Some(0) && h.active_bytes == Some(0) {
None
} else {
Some(h)
}
},
}
}
pub fn collect_host_context_pre_run() -> HostContext {
collect_host_context()
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub struct HostContextSnapshots {
pub pre: HostContext,
pub post: HostContext,
}
impl HostContextSnapshots {
pub fn new(pre: HostContext, post: HostContext) -> Self {
Self { pre, post }
}
#[cfg(test)]
pub fn capture_same_instant() -> Self {
let snap = collect_host_context();
Self {
pre: snap.clone(),
post: snap,
}
}
}
fn cached_cpufreq_governors() -> BTreeMap<usize, String> {
CPUFREQ_GOVERNORS
.get_or_init(read_cpufreq_governors)
.clone()
}
fn read_cpufreq_governors() -> BTreeMap<usize, String> {
#[cfg(test)]
CPUFREQ_GOVERNORS_READ_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Ok(online_raw) = std::fs::read_to_string("/sys/devices/system/cpu/online") else {
return BTreeMap::new();
};
let Ok(cpus) = crate::topology::parse_cpu_list(&online_raw) else {
return BTreeMap::new();
};
let mut out = BTreeMap::new();
for cpu in cpus {
let path = format!("/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor");
if let Some(gov) = read_trimmed_sysfs(&path) {
out.insert(cpu, gov);
}
}
out
}
fn compute_static_host_info(meminfo: &MeminfoFields) -> StaticHostInfo {
#[cfg(test)]
STATIC_INIT_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let (cpu_model, cpu_vendor) = read_cpuinfo_identity();
let u = rustix::system::uname();
let (online_cpus, numa_nodes) = probe_host_topology_counts();
StaticHostInfo {
cpu_model,
cpu_vendor,
total_memory_kib: meminfo.mem_total_kib,
hugepages_size_kib: meminfo.hugepages_size_kib,
online_cpus,
numa_nodes,
kernel_name: u.sysname().to_str().ok().map(|s| s.to_string()),
kernel_release: u.release().to_str().ok().map(|s| s.to_string()),
arch: u.machine().to_str().ok().map(|s| s.to_string()),
}
}
fn probe_host_topology_counts() -> (Option<usize>, Option<usize>) {
match crate::vmm::host_topology::HostTopology::from_sysfs() {
Ok(topo) => (
Some(topo.online_cpus.len()),
Some(count_numa_nodes_in_topology(&topo)),
),
Err(_) => (None, None),
}
}
fn read_cpuinfo_identity() -> (Option<String>, Option<String>) {
let Ok(text) = std::fs::read_to_string("/proc/cpuinfo") else {
return (None, None);
};
parse_cpuinfo_identity(&text)
}
fn parse_cpuinfo_identity(text: &str) -> (Option<String>, Option<String>) {
let mut model: Option<String> = None;
let mut vendor: Option<String> = None;
for line in text.lines() {
if line.is_empty() {
break;
}
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
if value.is_empty() {
continue;
}
match key {
"model name" if model.is_none() => model = Some(value.to_string()),
"vendor_id" if vendor.is_none() => vendor = Some(value.to_string()),
_ => {}
}
}
}
(model, vendor)
}
#[derive(Default)]
struct MeminfoFields {
mem_total_kib: Option<u64>,
hugepages_total: Option<u64>,
hugepages_free: Option<u64>,
hugepages_size_kib: Option<u64>,
}
fn read_meminfo() -> MeminfoFields {
#[cfg(test)]
MEMINFO_READ_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let Ok(text) = std::fs::read_to_string("/proc/meminfo") else {
return MeminfoFields::default();
};
parse_meminfo(&text)
}
fn parse_meminfo(text: &str) -> MeminfoFields {
let mut out = MeminfoFields::default();
for line in text.lines() {
let Some((key, rest)) = line.split_once(':') else {
continue;
};
let key = key.trim();
let token = rest.split_whitespace().next().unwrap_or("");
let Ok(n) = token.parse::<u64>() else {
continue;
};
match key {
"MemTotal" => out.mem_total_kib = Some(n),
"HugePages_Total" => out.hugepages_total = Some(n),
"HugePages_Free" => out.hugepages_free = Some(n),
"Hugepagesize" => out.hugepages_size_kib = Some(n),
_ => {}
}
}
out
}
fn read_trimmed_sysfs(path: impl AsRef<std::path::Path>) -> Option<String> {
std::fs::read_to_string(path.as_ref())
.ok()
.and_then(|s| parse_trimmed(&s))
}
fn parse_trimmed(text: &str) -> Option<String> {
let trimmed = text.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn read_sched_tunables() -> Option<BTreeMap<String, String>> {
read_sched_tunables_from(std::path::Path::new("/proc/sys/kernel"))
}
fn read_sched_tunables_from(dir: &std::path::Path) -> Option<BTreeMap<String, String>> {
let entries = std::fs::read_dir(dir).ok()?;
let mut out = BTreeMap::new();
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else { continue };
if !name.starts_with("sched_") {
continue;
}
let path = entry.path();
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_file() {
continue;
}
if let Some(content) = read_trimmed_sysfs(&path) {
out.insert(name.to_string(), content);
}
}
Some(out)
}
pub(crate) fn count_numa_nodes_in_topology(
topo: &crate::vmm::host_topology::HostTopology,
) -> usize {
topo.cpu_to_node
.values()
.copied()
.collect::<std::collections::BTreeSet<usize>>()
.len()
.max(1)
}
#[cfg(test)]
#[path = "host_context_tests.rs"]
mod tests;