pub(crate) struct TopoOverride {
pub numa_nodes: u32,
pub llcs: u32,
pub cores: u32,
pub threads: u32,
pub memory_mb: u32,
}
impl From<&TopoOverride> for crate::vmm::topology::Topology {
fn from(t: &TopoOverride) -> Self {
crate::vmm::topology::Topology::new(t.numa_nodes, t.llcs, t.cores, t.threads)
}
}
impl TopoOverride {
pub(crate) fn validate(&self) -> anyhow::Result<()> {
if self.numa_nodes == 0 {
anyhow::bail!(
"TopoOverride.numa_nodes must be > 0 (a topology with zero \
NUMA nodes has nothing to attach LLCs or memory to; every \
downstream accessor would observe an empty node set)"
);
}
if self.llcs == 0 {
anyhow::bail!(
"TopoOverride.llcs must be > 0 (a topology with zero LLCs \
has zero CPUs — `total_cpus = llcs * cores * threads` — \
so the VM would boot with no addressable processors)"
);
}
if self.cores == 0 {
anyhow::bail!(
"TopoOverride.cores must be > 0 (a topology with zero cores \
per LLC has zero CPUs — `total_cpus = llcs * cores * \
threads` — so the VM would boot with no addressable \
processors)"
);
}
if self.threads == 0 {
anyhow::bail!(
"TopoOverride.threads must be > 0 (a topology with zero \
threads per core has zero CPUs — `total_cpus = llcs * \
cores * threads` — so the VM would boot with no \
addressable processors)"
);
}
if self.memory_mb == 0 {
anyhow::bail!(
"TopoOverride.memory_mb must be > 0 (a VM with zero memory \
cannot boot); no implicit floor is applied to override path"
);
}
let cpus_per_llc = self.cores.checked_mul(self.threads).ok_or_else(|| {
anyhow::anyhow!(
"TopoOverride: cores ({}) * threads ({}) overflows u32 — \
cpus_per_llc cannot be represented",
self.cores,
self.threads,
)
})?;
self.llcs.checked_mul(cpus_per_llc).ok_or_else(|| {
anyhow::anyhow!(
"TopoOverride: llcs ({}) * cores ({}) * threads ({}) \
overflows u32 — total_cpus cannot be represented",
self.llcs,
self.cores,
self.threads,
)
})?;
Ok(())
}
}
fn looks_like_legacy_topo(s: &str) -> bool {
if s.contains('n') {
return false;
}
let Some(s_pos) = s.find('s') else {
return false;
};
let Some(c_pos) = s.find('c') else {
return false;
};
let Some(t_pos) = s.find('t') else {
return false;
};
if s_pos >= c_pos || c_pos >= t_pos {
return false;
}
let prefix_is_digits =
|slice: &str| !slice.is_empty() && slice.chars().all(|c| c.is_ascii_digit());
let s_to_c = &s[s_pos + 1..c_pos];
let s_to_c_ok = if let Some(l_pos) = s_to_c.find('l') {
prefix_is_digits(&s_to_c[..l_pos]) && prefix_is_digits(&s_to_c[l_pos + 1..])
} else {
prefix_is_digits(s_to_c)
};
prefix_is_digits(&s[..s_pos]) && s_to_c_ok && prefix_is_digits(&s[c_pos + 1..t_pos])
}
pub(crate) fn parse_topo_string(s: &str) -> Option<(u32, u32, u32, u32)> {
if looks_like_legacy_topo(s) {
tracing::warn!(
topo = s,
"legacy NsNcNt / NsNlNcNt topology form detected; use NnNlNcNt (e.g. 1n2l4c2t)"
);
}
let n_pos = s.find('n')?;
let l_pos = s.find('l')?;
let c_pos = s.find('c')?;
let t_pos = s.find('t')?;
if n_pos >= l_pos || l_pos >= c_pos || c_pos >= t_pos {
return None;
}
if s[t_pos + 1..].chars().next().is_some() {
return None;
}
let numa_nodes: u32 = s[..n_pos].parse().ok()?;
let llcs: u32 = s[n_pos + 1..l_pos].parse().ok()?;
let cores: u32 = s[l_pos + 1..c_pos].parse().ok()?;
let threads: u32 = s[c_pos + 1..t_pos].parse().ok()?;
if numa_nodes == 0 || llcs == 0 || cores == 0 || threads == 0 {
return None;
}
Some((numa_nodes, llcs, cores, threads))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_topo_basic() {
assert_eq!(parse_topo_string("1n2l4c2t"), Some((1, 2, 4, 2)));
}
#[test]
fn parse_topo_multi_numa() {
assert_eq!(parse_topo_string("2n4l8c2t"), Some((2, 4, 8, 2)));
}
#[test]
fn parse_topo_single() {
assert_eq!(parse_topo_string("1n1l1c1t"), Some((1, 1, 1, 1)));
}
#[test]
fn parse_topo_large() {
assert_eq!(parse_topo_string("4n16l8c2t"), Some((4, 16, 8, 2)));
}
#[test]
fn parse_topo_double_digit_threads() {
assert_eq!(parse_topo_string("1n1l1c12t"), Some((1, 1, 1, 12)));
}
#[test]
fn parse_topo_zero_numa() {
assert!(parse_topo_string("0n2l4c2t").is_none());
}
#[test]
fn parse_topo_zero_llcs() {
assert!(parse_topo_string("1n0l4c2t").is_none());
}
#[test]
fn parse_topo_zero_cores() {
assert!(parse_topo_string("1n2l0c2t").is_none());
}
#[test]
fn parse_topo_zero_threads() {
assert!(parse_topo_string("1n2l4c0t").is_none());
}
#[test]
fn parse_topo_wrong_order() {
assert!(parse_topo_string("2l1n4c2t").is_none());
}
#[test]
fn parse_topo_missing_suffix() {
assert!(parse_topo_string("1n2l4c2").is_none());
}
#[test]
fn parse_topo_empty() {
assert!(parse_topo_string("").is_none());
}
#[test]
fn parse_topo_garbage() {
assert!(parse_topo_string("hello").is_none());
}
#[test]
fn parse_topo_legacy_form_rejected() {
assert!(parse_topo_string("2s4c2t").is_none());
assert!(parse_topo_string("1s2l4c2t").is_none());
}
#[test]
fn looks_like_legacy_topo_matches_both_legacy_forms() {
assert!(looks_like_legacy_topo("2s4c2t"));
assert!(looks_like_legacy_topo("1s2l4c2t"));
}
#[test]
fn looks_like_legacy_topo_rejects_false_positives() {
assert!(!looks_like_legacy_topo("sched"));
assert!(!looks_like_legacy_topo("abcs"));
assert!(!looks_like_legacy_topo(""));
assert!(!looks_like_legacy_topo("1s"));
assert!(!looks_like_legacy_topo("1s2c"));
assert!(!looks_like_legacy_topo("1n2l4c2t"));
assert!(!looks_like_legacy_topo("1sl4c2t"));
assert!(!looks_like_legacy_topo("1s2lc2t"));
}
#[test]
fn parse_topo_rejects_trailing_garbage() {
assert!(
parse_topo_string("1n2l4c2tEXTRA").is_none(),
"trailing garbage after 't' must cause None"
);
assert!(
parse_topo_string("1n2l4c2tb").is_none(),
"single trailing character must cause None"
);
}
#[test]
fn topo_override_fields() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 2,
cores: 4,
threads: 2,
memory_mb: 8192,
};
assert_eq!(t.numa_nodes, 1);
assert_eq!(t.llcs, 2);
assert_eq!(t.cores, 4);
assert_eq!(t.threads, 2);
assert_eq!(t.memory_mb, 8192);
}
#[test]
fn topo_override_validate_accepts_nonzero() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 2,
cores: 4,
threads: 2,
memory_mb: 8192,
};
t.validate().unwrap();
}
#[test]
fn topo_override_validate_rejects_zero_memory() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 2,
cores: 4,
threads: 2,
memory_mb: 0,
};
let err = t.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("memory_mb") && msg.contains("> 0"),
"error must name memory_mb: {msg}"
);
}
#[test]
fn topo_override_validate_rejects_zero_numa_nodes() {
let t = TopoOverride {
numa_nodes: 0,
llcs: 2,
cores: 4,
threads: 2,
memory_mb: 8192,
};
assert!(t.validate().is_err());
}
#[test]
fn topo_override_validate_rejects_zero_llcs() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 0,
cores: 4,
threads: 2,
memory_mb: 8192,
};
assert!(t.validate().is_err());
}
#[test]
fn topo_override_validate_rejects_zero_cores() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 2,
cores: 0,
threads: 2,
memory_mb: 8192,
};
assert!(t.validate().is_err());
}
#[test]
fn topo_override_validate_rejects_zero_threads() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 2,
cores: 4,
threads: 0,
memory_mb: 8192,
};
assert!(t.validate().is_err());
}
#[test]
fn topo_override_validate_rejects_cpus_per_llc_overflow() {
let t = TopoOverride {
numa_nodes: 1,
llcs: 1,
cores: 0x1_0001,
threads: 0x1_0000,
memory_mb: 8192,
};
let err = t.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("cpus_per_llc") && msg.contains("overflows u32"),
"expected overflow error for cores*threads, got: {msg}"
);
}
#[test]
fn topo_override_validate_rejects_total_cpus_overflow() {
let t = TopoOverride {
numa_nodes: 1,
llcs: u32::MAX,
cores: 2,
threads: 2,
memory_mb: 8192,
};
let err = t.validate().unwrap_err();
let msg = format!("{err}");
assert!(
msg.contains("total_cpus") && msg.contains("overflows u32"),
"expected overflow error for total_cpus, got: {msg}"
);
}
#[test]
fn topo_override_validate_accepts_max_non_overflowing() {
let t = TopoOverride {
numa_nodes: 1,
llcs: u32::MAX,
cores: 1,
threads: 1,
memory_mb: 8192,
};
t.validate().unwrap();
}
}