use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
#[inline]
pub fn numa_available() -> bool {
#[cfg(target_os = "linux")]
{
is_numa_available_linux()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
#[cfg(target_os = "linux")]
fn is_numa_available_linux() -> bool {
std::path::Path::new("/sys/devices/system/node/node1").exists()
}
#[inline]
pub fn num_numa_nodes() -> usize {
#[cfg(target_os = "linux")]
{
num_numa_nodes_linux()
}
#[cfg(not(target_os = "linux"))]
{
1
}
}
#[cfg(target_os = "linux")]
fn num_numa_nodes_linux() -> usize {
let node_path = std::path::Path::new("/sys/devices/system/node");
if let Ok(entries) = std::fs::read_dir(node_path) {
entries
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_str()
.map(|n| n.starts_with("node"))
.unwrap_or(false)
})
.count()
} else {
1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NumaNode {
id: usize,
}
impl NumaNode {
#[inline]
pub const fn new(id: usize) -> Self {
Self { id }
}
#[inline]
pub const fn id(&self) -> usize {
self.id
}
#[inline]
pub fn current() -> Self {
Self::new(current_numa_node())
}
pub fn all() -> Vec<Self> {
(0..num_numa_nodes()).map(Self::new).collect()
}
pub fn cpus(&self) -> Vec<usize> {
#[cfg(target_os = "linux")]
{
self.cpus_linux()
}
#[cfg(not(target_os = "linux"))]
{
(0..std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(1))
.collect()
}
}
#[cfg(target_os = "linux")]
fn cpus_linux(&self) -> Vec<usize> {
let path = format!("/sys/devices/system/node/node{}/cpulist", self.id);
if let Ok(content) = std::fs::read_to_string(&path) {
parse_cpu_list(&content)
} else {
Vec::new()
}
}
pub fn total_memory(&self) -> u64 {
#[cfg(target_os = "linux")]
{
self.total_memory_linux()
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
#[cfg(target_os = "linux")]
fn total_memory_linux(&self) -> u64 {
let path = format!("/sys/devices/system/node/node{}/meminfo", self.id);
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
if line.contains("MemTotal:")
&& let Some(kb_str) = line.split_whitespace().nth(3)
&& let Ok(kb) = kb_str.parse::<u64>()
{
return kb * 1024;
}
}
}
0
}
pub fn free_memory(&self) -> u64 {
#[cfg(target_os = "linux")]
{
self.free_memory_linux()
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
#[cfg(target_os = "linux")]
fn free_memory_linux(&self) -> u64 {
let path = format!("/sys/devices/system/node/node{}/meminfo", self.id);
if let Ok(content) = std::fs::read_to_string(&path) {
for line in content.lines() {
if line.contains("MemFree:")
&& let Some(kb_str) = line.split_whitespace().nth(3)
&& let Ok(kb) = kb_str.parse::<u64>()
{
return kb * 1024;
}
}
}
0
}
pub fn distance_to(&self, other: &NumaNode) -> u32 {
#[cfg(target_os = "linux")]
{
self.distance_to_linux(other)
}
#[cfg(not(target_os = "linux"))]
{
let _ = other;
10 }
}
#[cfg(target_os = "linux")]
fn distance_to_linux(&self, other: &NumaNode) -> u32 {
let path = format!("/sys/devices/system/node/node{}/distance", self.id);
if let Ok(content) = std::fs::read_to_string(&path)
&& let Some(dist_str) = content.split_whitespace().nth(other.id)
&& let Ok(dist) = dist_str.parse::<u32>()
{
return dist;
}
if self.id == other.id { 10 } else { 20 }
}
}
#[inline]
pub fn current_numa_node() -> usize {
#[cfg(target_os = "linux")]
{
current_numa_node_linux()
}
#[cfg(not(target_os = "linux"))]
{
0
}
}
#[cfg(target_os = "linux")]
fn current_numa_node_linux() -> usize {
if let Ok(content) = std::fs::read_to_string("/proc/self/stat") {
let fields: Vec<&str> = content.split_whitespace().collect();
if fields.len() > 38
&& let Ok(cpu) = fields[38].parse::<usize>()
{
return cpu_to_numa_node(cpu);
}
}
0
}
#[cfg(target_os = "linux")]
fn cpu_to_numa_node(cpu: usize) -> usize {
for node_id in 0..num_numa_nodes() {
let path = format!("/sys/devices/system/node/node{}/cpulist", node_id);
if let Ok(content) = std::fs::read_to_string(&path) {
let cpus = parse_cpu_list(&content);
if cpus.contains(&cpu) {
return node_id;
}
}
}
0
}
#[cfg(target_os = "linux")]
fn parse_cpu_list(s: &str) -> Vec<usize> {
let mut cpus = Vec::new();
for part in s.trim().split(',') {
if let Some((start, end)) = part.split_once('-') {
if let (Ok(s), Ok(e)) = (start.parse::<usize>(), end.parse::<usize>()) {
cpus.extend(s..=e);
}
} else if let Ok(cpu) = part.parse::<usize>() {
cpus.push(cpu);
}
}
cpus
}
#[derive(Debug, Clone)]
pub struct NumaConfig {
num_nodes: usize,
nodes: Vec<NumaNode>,
total_memory: u64,
policy: NumaPolicy,
}
impl NumaConfig {
pub fn detect() -> Self {
let num_nodes = num_numa_nodes();
let nodes: Vec<NumaNode> = (0..num_nodes).map(NumaNode::new).collect();
let total_memory: u64 = nodes.iter().map(|n| n.total_memory()).sum();
Self {
num_nodes,
nodes,
total_memory,
policy: NumaPolicy::Local,
}
}
#[inline]
pub fn num_nodes(&self) -> usize {
self.num_nodes
}
#[inline]
pub fn nodes(&self) -> &[NumaNode] {
&self.nodes
}
#[inline]
pub fn total_memory(&self) -> u64 {
self.total_memory
}
#[inline]
pub fn policy(&self) -> NumaPolicy {
self.policy
}
#[inline]
pub fn set_policy(&mut self, policy: NumaPolicy) {
self.policy = policy;
}
#[inline]
pub fn is_numa(&self) -> bool {
self.num_nodes > 1
}
#[inline]
pub fn node_for_worker(&self, worker_id: usize) -> NumaNode {
if self.num_nodes > 0 {
self.nodes[worker_id % self.num_nodes]
} else {
NumaNode::new(0)
}
}
pub fn node_with_most_free_memory(&self) -> NumaNode {
self.nodes
.iter()
.max_by_key(|n| n.free_memory())
.copied()
.unwrap_or(NumaNode::new(0))
}
}
impl Default for NumaConfig {
fn default() -> Self {
Self::detect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NumaPolicy {
#[default]
Local,
Interleave,
Preferred(usize),
Bind(usize),
}
#[cfg(target_os = "linux")]
mod mempolicy {
#![allow(dead_code)]
pub const MPOL_DEFAULT: libc::c_int = 0;
pub const MPOL_PREFERRED: libc::c_int = 1;
pub const MPOL_BIND: libc::c_int = 2;
pub const MPOL_INTERLEAVE: libc::c_int = 3;
pub const MPOL_LOCAL: libc::c_int = 4;
pub const MPOL_MF_MOVE: libc::c_uint = 1 << 1;
pub const MAX_NODES: usize = 64;
}
#[cfg(target_os = "linux")]
fn set_mempolicy(
mode: libc::c_int,
nodemask: Option<&u64>,
maxnode: usize,
) -> Result<(), std::io::Error> {
let mask_ptr = nodemask.map_or(std::ptr::null(), |m| m as *const u64);
let rc = unsafe {
libc::syscall(
libc::SYS_set_mempolicy,
mode,
mask_ptr,
maxnode as libc::c_ulong,
)
};
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(target_os = "linux")]
fn numa_alloc_align() -> usize {
static ALIGN: OnceLock<usize> = OnceLock::new();
*ALIGN.get_or_init(|| {
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if page > 0 { page as usize } else { 4096 }
})
}
#[cfg(not(target_os = "linux"))]
fn numa_alloc_align() -> usize {
64
}
pub struct NumaAllocator {
node: NumaNode,
stats: NumaAllocStats,
}
impl NumaAllocator {
pub fn new(node: NumaNode) -> Self {
Self {
node,
stats: NumaAllocStats::new(),
}
}
pub fn local() -> Self {
Self::new(NumaNode::current())
}
#[inline]
pub fn allocate(&self, size: usize) -> Option<*mut u8> {
if size == 0 {
return None;
}
#[cfg(target_os = "linux")]
{
self.allocate_linux(size)
}
#[cfg(not(target_os = "linux"))]
{
self.allocate_fallback(size)
}
}
#[cfg(target_os = "linux")]
fn allocate_linux(&self, size: usize) -> Option<*mut u8> {
use std::alloc::{Layout, alloc_zeroed};
let layout = Layout::from_size_align(size, numa_alloc_align()).ok()?;
let ptr = unsafe { alloc_zeroed(layout) };
if ptr.is_null() {
self.stats.record_failure();
return None;
}
if self.node.id() < mempolicy::MAX_NODES {
let nodemask: u64 = 1u64 << self.node.id();
let rc = unsafe {
libc::syscall(
libc::SYS_mbind,
ptr as *mut libc::c_void,
size as libc::c_ulong,
mempolicy::MPOL_BIND,
&nodemask as *const u64,
mempolicy::MAX_NODES as libc::c_ulong,
mempolicy::MPOL_MF_MOVE,
)
};
if rc != 0 {
let err = std::io::Error::last_os_error();
tracing::warn!(
node = self.node.id(),
size,
error = %err,
"mbind failed; keeping allocation without NUMA placement"
);
self.stats.record_mbind_miss();
}
} else {
tracing::warn!(
node = self.node.id(),
"NUMA node id exceeds nodemask width; keeping allocation without NUMA placement"
);
self.stats.record_mbind_miss();
}
self.stats.record_allocation(size, self.node.id());
Some(ptr)
}
#[cfg(not(target_os = "linux"))]
fn allocate_fallback(&self, size: usize) -> Option<*mut u8> {
use std::alloc::{Layout, alloc_zeroed};
let layout = Layout::from_size_align(size, numa_alloc_align()).ok()?;
let ptr = unsafe { alloc_zeroed(layout) };
if ptr.is_null() {
self.stats.record_failure();
None
} else {
self.stats.record_mbind_miss();
self.stats.record_allocation(size, 0);
Some(ptr)
}
}
#[inline]
pub unsafe fn deallocate(&self, ptr: *mut u8, size: usize) {
use std::alloc::{Layout, dealloc};
if let Ok(layout) = Layout::from_size_align(size, numa_alloc_align()) {
unsafe { dealloc(ptr, layout) };
self.stats.record_deallocation(size);
} else {
debug_assert!(
false,
"NumaAllocator::deallocate: invalid Layout for size={size}, align={}; \
caller violated documented safety preconditions, leaking ptr={ptr:p}",
numa_alloc_align()
);
tracing::warn!(
size,
align = numa_alloc_align(),
?ptr,
"NumaAllocator::deallocate: invalid Layout, leaking memory \
(caller violated documented unsafe preconditions)"
);
}
}
#[inline]
pub fn node(&self) -> NumaNode {
self.node
}
#[inline]
pub fn stats(&self) -> &NumaAllocStats {
&self.stats
}
}
#[derive(Debug)]
pub struct NumaBuffer {
ptr: *mut u8,
size: usize,
node: NumaNode,
}
impl NumaBuffer {
pub fn new(size: usize) -> Option<Self> {
Self::on_node(NumaNode::current(), size)
}
pub fn on_node(node: NumaNode, size: usize) -> Option<Self> {
let allocator = NumaAllocator::new(node);
let ptr = allocator.allocate(size)?;
Some(Self { ptr, size, node })
}
#[inline]
pub fn size(&self) -> usize {
self.size
}
#[inline]
pub fn node(&self) -> NumaNode {
self.node
}
#[inline]
pub fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
}
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size) }
}
#[inline]
pub fn as_ptr(&self) -> *const u8 {
self.ptr
}
#[inline]
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.ptr
}
}
impl Drop for NumaBuffer {
fn drop(&mut self) {
if !self.ptr.is_null() {
let allocator = NumaAllocator::new(self.node);
unsafe {
allocator.deallocate(self.ptr, self.size);
}
}
}
}
unsafe impl Send for NumaBuffer {}
unsafe impl Sync for NumaBuffer {}
#[inline]
pub fn bind_to_node(node: NumaNode) -> Result<(), NumaError> {
#[cfg(target_os = "linux")]
{
bind_to_node_linux(node)
}
#[cfg(not(target_os = "linux"))]
{
let _ = node;
NUMA_STATS.record_bind(false);
Err(NumaError::NotSupported)
}
}
#[cfg(target_os = "linux")]
fn bind_to_node_linux(node: NumaNode) -> Result<(), NumaError> {
let num_nodes = num_numa_nodes();
if node.id() >= num_nodes || node.id() >= mempolicy::MAX_NODES {
NUMA_STATS.record_bind(false);
return Err(NumaError::InvalidNode {
node: node.id(),
max: num_nodes.saturating_sub(1),
});
}
let nodemask: u64 = 1u64 << node.id();
match set_mempolicy(mempolicy::MPOL_BIND, Some(&nodemask), mempolicy::MAX_NODES) {
Ok(()) => {
NUMA_STATS.record_bind(true);
Ok(())
}
Err(err) => {
NUMA_STATS.record_bind(false);
if err.raw_os_error() == Some(libc::ENOSYS) {
Err(NumaError::NotSupported)
} else {
Err(NumaError::BindFailed {
reason: format!(
"set_mempolicy(MPOL_BIND, node {}) failed: {}",
node.id(),
err
),
})
}
}
}
}
#[inline]
pub fn bind_to_local_node() -> Result<(), NumaError> {
#[cfg(target_os = "linux")]
{
match set_mempolicy(mempolicy::MPOL_LOCAL, None, 0) {
Ok(()) => {
NUMA_STATS.record_bind(true);
Ok(())
}
Err(err) if err.raw_os_error() == Some(libc::EINVAL) => {
bind_to_node(NumaNode::current())
}
Err(err) => {
NUMA_STATS.record_bind(false);
if err.raw_os_error() == Some(libc::ENOSYS) {
Err(NumaError::NotSupported)
} else {
Err(NumaError::BindFailed {
reason: format!("set_mempolicy(MPOL_LOCAL) failed: {}", err),
})
}
}
}
}
#[cfg(not(target_os = "linux"))]
{
NUMA_STATS.record_bind(false);
Err(NumaError::NotSupported)
}
}
pub fn init_worker_numa(worker_id: usize, config: &NumaConfig) -> Result<NumaNode, NumaError> {
let node = config.node_for_worker(worker_id);
if config.is_numa() {
match bind_to_node(node) {
Ok(()) => {}
Err(err @ (NumaError::NotSupported | NumaError::BindFailed { .. })) => {
tracing::warn!(
worker_id,
node = node.id(),
error = %err,
"NUMA bind unavailable for worker; continuing without NUMA placement"
);
}
Err(err) => return Err(err),
}
}
NUMA_STATS.record_init();
Ok(node)
}
#[derive(Debug, Clone)]
pub enum NumaError {
InvalidNode { node: usize, max: usize },
NotAvailable,
NotSupported,
BindFailed { reason: String },
AllocationFailed { size: usize },
}
impl std::fmt::Display for NumaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidNode { node, max } => {
write!(f, "Invalid NUMA node {}, max is {}", node, max)
}
Self::NotAvailable => write!(f, "NUMA not available on this system"),
Self::NotSupported => {
write!(f, "NUMA binding not supported on this platform or kernel")
}
Self::BindFailed { reason } => write!(f, "NUMA binding failed: {}", reason),
Self::AllocationFailed { size } => {
write!(f, "NUMA allocation failed for {} bytes", size)
}
}
}
}
impl std::error::Error for NumaError {}
#[derive(Debug, Default)]
pub struct NumaAllocStats {
allocations: AtomicU64,
bytes_allocated: AtomicU64,
deallocations: AtomicU64,
bytes_deallocated: AtomicU64,
failures: AtomicU64,
mbind_misses: AtomicU64,
per_node: [AtomicU64; 8], }
impl NumaAllocStats {
pub fn new() -> Self {
Self::default()
}
#[inline]
fn record_allocation(&self, size: usize, node: usize) {
self.allocations.fetch_add(1, Ordering::Relaxed);
self.bytes_allocated
.fetch_add(size as u64, Ordering::Relaxed);
if node < 8 {
self.per_node[node].fetch_add(1, Ordering::Relaxed);
}
}
#[inline]
fn record_deallocation(&self, size: usize) {
self.deallocations.fetch_add(1, Ordering::Relaxed);
self.bytes_deallocated
.fetch_add(size as u64, Ordering::Relaxed);
}
#[inline]
fn record_failure(&self) {
self.failures.fetch_add(1, Ordering::Relaxed);
}
#[inline]
fn record_mbind_miss(&self) {
self.mbind_misses.fetch_add(1, Ordering::Relaxed);
}
pub fn allocations(&self) -> u64 {
self.allocations.load(Ordering::Relaxed)
}
pub fn bytes_allocated(&self) -> u64 {
self.bytes_allocated.load(Ordering::Relaxed)
}
pub fn deallocations(&self) -> u64 {
self.deallocations.load(Ordering::Relaxed)
}
pub fn failures(&self) -> u64 {
self.failures.load(Ordering::Relaxed)
}
pub fn mbind_misses(&self) -> u64 {
self.mbind_misses.load(Ordering::Relaxed)
}
pub fn allocations_on_node(&self, node: usize) -> u64 {
if node < 8 {
self.per_node[node].load(Ordering::Relaxed)
} else {
0
}
}
}
#[derive(Debug, Default)]
pub struct GlobalNumaStats {
inits: AtomicU64,
binds_success: AtomicU64,
binds_failed: AtomicU64,
}
impl GlobalNumaStats {
pub fn new() -> Self {
Self::default()
}
#[inline]
fn record_init(&self) {
self.inits.fetch_add(1, Ordering::Relaxed);
}
#[inline]
fn record_bind(&self, success: bool) {
if success {
self.binds_success.fetch_add(1, Ordering::Relaxed);
} else {
self.binds_failed.fetch_add(1, Ordering::Relaxed);
}
}
pub fn inits(&self) -> u64 {
self.inits.load(Ordering::Relaxed)
}
pub fn binds_success(&self) -> u64 {
self.binds_success.load(Ordering::Relaxed)
}
pub fn binds_failed(&self) -> u64 {
self.binds_failed.load(Ordering::Relaxed)
}
}
static NUMA_STATS: GlobalNumaStats = GlobalNumaStats {
inits: AtomicU64::new(0),
binds_success: AtomicU64::new(0),
binds_failed: AtomicU64::new(0),
};
pub fn numa_stats() -> &'static GlobalNumaStats {
&NUMA_STATS
}
static NUMA_CONFIG: OnceLock<NumaConfig> = OnceLock::new();
pub fn cached_numa_config() -> &'static NumaConfig {
NUMA_CONFIG.get_or_init(NumaConfig::detect)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_numa_available() {
#[cfg(target_os = "linux")]
{
let has_node1 = std::path::Path::new("/sys/devices/system/node/node1").exists();
assert_eq!(numa_available(), has_node1);
}
#[cfg(not(target_os = "linux"))]
{
assert!(!numa_available());
}
}
#[test]
fn test_num_numa_nodes() {
let nodes = num_numa_nodes();
assert!(nodes >= 1);
}
#[test]
fn test_numa_node_basic() {
let node = NumaNode::new(0);
assert_eq!(node.id(), 0);
}
#[test]
fn test_numa_node_current() {
let node = NumaNode::current();
assert!(node.id() < num_numa_nodes() || num_numa_nodes() == 1);
}
#[test]
fn test_numa_node_all() {
let nodes = NumaNode::all();
assert_eq!(nodes.len(), num_numa_nodes());
}
#[test]
fn test_numa_config_detect() {
let config = NumaConfig::detect();
assert!(config.num_nodes() >= 1);
assert_eq!(config.nodes().len(), config.num_nodes());
}
#[test]
fn test_numa_config_node_for_worker() {
let config = NumaConfig::detect();
let node0 = config.node_for_worker(0);
let node1 = config.node_for_worker(config.num_nodes());
assert_eq!(node0.id(), node1.id());
}
#[test]
fn test_numa_policy_default() {
let policy = NumaPolicy::default();
assert_eq!(policy, NumaPolicy::Local);
}
#[test]
fn test_numa_buffer_allocation() {
if let Some(buffer) = NumaBuffer::new(1024) {
assert_eq!(buffer.size(), 1024);
assert!(buffer.node().id() < num_numa_nodes() || num_numa_nodes() == 1);
}
}
#[test]
fn test_numa_buffer_zero_initialized() {
if let Some(buffer) = NumaBuffer::new(4096) {
assert!(buffer.as_slice().iter().all(|&b| b == 0));
}
}
#[test]
fn test_numa_buffer_read_write() {
if let Some(mut buffer) = NumaBuffer::new(64) {
let slice = buffer.as_mut_slice();
slice[0] = 42;
slice[63] = 99;
let read_slice = buffer.as_slice();
assert_eq!(read_slice[0], 42);
assert_eq!(read_slice[63], 99);
}
}
#[cfg(target_os = "linux")]
fn reset_mempolicy() {
let _ = set_mempolicy(mempolicy::MPOL_DEFAULT, None, 0);
}
#[test]
fn test_bind_to_local_node() {
match bind_to_local_node() {
Ok(()) => {
#[cfg(target_os = "linux")]
reset_mempolicy();
}
Err(NumaError::NotSupported) => {
eprintln!("skipping: NUMA binding not supported here");
}
Err(e) => panic!("bind_to_local_node failed unexpectedly: {e}"),
}
}
#[cfg(target_os = "linux")]
#[test]
fn test_bind_to_node_zero() {
match bind_to_node(NumaNode::new(0)) {
Ok(()) => reset_mempolicy(),
Err(NumaError::NotSupported) => {
eprintln!("skipping: set_mempolicy returned ENOSYS");
}
Err(e) => panic!("bind_to_node(0) failed unexpectedly: {e}"),
}
}
#[test]
fn test_bind_to_invalid_node() {
let result = bind_to_node(NumaNode::new(usize::MAX));
assert!(result.is_err());
#[cfg(target_os = "linux")]
assert!(matches!(result, Err(NumaError::InvalidNode { .. })));
#[cfg(not(target_os = "linux"))]
assert!(matches!(result, Err(NumaError::NotSupported)));
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_bind_not_supported_off_linux() {
assert!(matches!(
bind_to_node(NumaNode::new(0)),
Err(NumaError::NotSupported)
));
assert!(matches!(bind_to_local_node(), Err(NumaError::NotSupported)));
}
#[test]
fn test_numa_allocator_placement() {
let allocator = NumaAllocator::new(NumaNode::new(0));
let size = 8192;
if let Some(ptr) = allocator.allocate(size) {
assert_eq!(ptr as usize % numa_alloc_align(), 0);
assert_eq!(allocator.stats().allocations(), 1);
assert_eq!(allocator.stats().bytes_allocated(), size as u64);
assert!(allocator.stats().mbind_misses() <= 1);
unsafe { allocator.deallocate(ptr, size) };
assert_eq!(allocator.stats().deallocations(), 1);
}
}
#[test]
fn test_init_worker_numa_degrades_when_bind_unsupported() {
let config = NumaConfig {
num_nodes: 2,
nodes: vec![NumaNode::new(0), NumaNode::new(0)],
total_memory: 0,
policy: NumaPolicy::Local,
};
assert!(config.is_numa());
let result = init_worker_numa(0, &config);
assert!(
result.is_ok(),
"init_worker_numa must not error when NUMA binding is unsupported: {result:?}"
);
assert_eq!(result.unwrap().id(), 0);
#[cfg(target_os = "linux")]
reset_mempolicy();
}
#[test]
fn test_numa_error_display() {
let err1 = NumaError::InvalidNode { node: 10, max: 3 };
assert!(err1.to_string().contains("10"));
let err2 = NumaError::NotAvailable;
assert!(err2.to_string().contains("not available"));
let err3 = NumaError::NotSupported;
assert!(err3.to_string().contains("not supported"));
}
#[test]
fn test_numa_stats() {
let stats = numa_stats();
let _ = stats.inits();
let _ = stats.binds_success();
let _ = stats.binds_failed();
}
#[test]
fn test_cached_numa_config() {
let config1 = cached_numa_config();
let config2 = cached_numa_config();
assert_eq!(config1.num_nodes(), config2.num_nodes());
}
#[test]
fn test_numa_alloc_stats() {
let stats = NumaAllocStats::new();
stats.record_allocation(1024, 0);
assert_eq!(stats.allocations(), 1);
assert_eq!(stats.bytes_allocated(), 1024);
assert_eq!(stats.allocations_on_node(0), 1);
}
}