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),
}
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};
let layout = Layout::from_size_align(size, 64).ok()?;
let ptr = unsafe { alloc(layout) };
if ptr.is_null() {
self.stats.record_failure();
None
} else {
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};
let layout = Layout::from_size_align(size, 64).ok()?;
let ptr = unsafe { alloc(layout) };
if ptr.is_null() {
self.stats.record_failure();
None
} else {
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, 64) {
unsafe { dealloc(ptr, layout) };
self.stats.record_deallocation(size);
}
}
#[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;
Ok(()) }
}
#[cfg(target_os = "linux")]
fn bind_to_node_linux(node: NumaNode) -> Result<(), NumaError> {
if node.id() >= num_numa_nodes() {
return Err(NumaError::InvalidNode {
node: node.id(),
max: num_numa_nodes() - 1,
});
}
NUMA_STATS.record_bind(true);
Ok(())
}
#[inline]
pub fn bind_to_local_node() -> Result<(), NumaError> {
bind_to_node(NumaNode::current())
}
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() {
bind_to_node(node)?;
}
NUMA_STATS.record_init();
Ok(node)
}
#[derive(Debug, Clone)]
pub enum NumaError {
InvalidNode { node: usize, max: usize },
NotAvailable,
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::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,
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);
}
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 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() {
let _ = 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_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);
}
}
#[test]
fn test_bind_to_local_node() {
let result = bind_to_local_node();
assert!(result.is_ok());
}
#[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"));
}
#[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);
}
}