use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use parking_lot::RwLock;
use rayon::prelude::*;
use super::{NodeResources, Workload, ResourceRequirements};
use crate::types::NodeId;
#[derive(Debug)]
struct NodeScoreCache {
node_id: NodeId,
cpu_score: u32,
memory_score: u32,
gpu_score: u32,
combined_score: u32,
cpu_available: u64,
memory_available: u64,
gpu_available: u32,
schedulable: bool,
}
impl NodeScoreCache {
fn from_node(node: &NodeResources) -> Self {
let cpu_available = node.cpu_available();
let memory_available = node.memory_available();
let gpu_available = node.gpus_available() as u32;
let cpu_score = ((cpu_available as f64 / node.cpu_capacity.max(1) as f64) * 1000.0) as u32;
let memory_score = ((memory_available as f64 / node.memory_capacity.max(1) as f64) * 1000.0) as u32;
let gpu_score = if node.gpus.is_empty() {
500
} else {
((gpu_available as f64 / node.gpus.len() as f64) * 1000.0) as u32
};
let combined_score = (cpu_score + memory_score + gpu_score) / 3;
Self {
node_id: node.node_id,
cpu_score,
memory_score,
gpu_score,
combined_score,
cpu_available,
memory_available,
gpu_available,
schedulable: node.schedulable,
}
}
#[inline(always)]
fn can_fit(&self, req: &ResourceRequirements) -> bool {
self.schedulable
&& self.cpu_available >= req.cpu_millis
&& self.memory_available >= req.memory_mb
&& self.gpu_available >= req.gpu_count
}
#[inline(always)]
fn score_for_workload(&self, req: &ResourceRequirements) -> u32 {
if !self.can_fit(req) {
return 0;
}
let cpu_fit = 1000 - ((self.cpu_available - req.cpu_millis) * 1000 / self.cpu_available.max(1)) as u32;
let mem_fit = 1000 - ((self.memory_available - req.memory_mb) * 1000 / self.memory_available.max(1)) as u32;
(cpu_fit * 4 + mem_fit * 4 + self.gpu_score * 2) / 10
}
}
pub struct WorkloadBatch {
workloads: Vec<Workload>,
results: Vec<Option<NodeId>>,
}
impl WorkloadBatch {
pub fn new(workloads: Vec<Workload>) -> Self {
let len = workloads.len();
Self {
workloads,
results: vec![None; len],
}
}
pub fn results(&self) -> &[Option<NodeId>] {
&self.results
}
pub fn workloads(&self) -> &[Workload] {
&self.workloads
}
}
pub struct OptimizedScheduler {
node_cache: RwLock<Vec<NodeScoreCache>>,
nodes: RwLock<Vec<NodeResources>>,
scheduled_count: AtomicU64,
total_time_ns: AtomicU64,
cache_generation: AtomicUsize,
}
impl OptimizedScheduler {
pub fn new() -> Self {
Self {
node_cache: RwLock::new(Vec::new()),
nodes: RwLock::new(Vec::new()),
scheduled_count: AtomicU64::new(0),
total_time_ns: AtomicU64::new(0),
cache_generation: AtomicUsize::new(0),
}
}
pub fn register_node(&self, node: NodeResources) {
let cache = NodeScoreCache::from_node(&node);
self.nodes.write().push(node);
self.node_cache.write().push(cache);
self.cache_generation.fetch_add(1, Ordering::Relaxed);
}
pub fn refresh_cache(&self) {
let nodes = self.nodes.read();
let mut cache = self.node_cache.write();
cache.clear();
cache.extend(nodes.iter().map(NodeScoreCache::from_node));
self.cache_generation.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn schedule_fast(&self, workload: &Workload) -> Option<NodeId> {
let start = std::time::Instant::now();
let cache = self.node_cache.read();
if cache.is_empty() {
return None;
}
let req = &workload.resources;
let best = if cache.len() > 16 {
cache.par_iter()
.filter(|n| n.can_fit(req))
.max_by_key(|n| n.score_for_workload(req))
.map(|n| n.node_id)
} else {
cache.iter()
.filter(|n| n.can_fit(req))
.max_by_key(|n| n.score_for_workload(req))
.map(|n| n.node_id)
};
self.scheduled_count.fetch_add(1, Ordering::Relaxed);
self.total_time_ns.fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
best
}
pub fn schedule_batch(&self, batch: &mut WorkloadBatch) {
let start = std::time::Instant::now();
let cache = self.node_cache.read();
if cache.is_empty() {
return;
}
let mut indices: Vec<usize> = (0..batch.workloads.len()).collect();
indices.sort_by(|&a, &b| {
batch.workloads[b].priority.cmp(&batch.workloads[a].priority)
});
let mut node_allocated: Vec<(u64, u64, u32)> = cache.iter()
.map(|n| (n.cpu_available, n.memory_available, n.gpu_available))
.collect();
for idx in indices {
let workload = &batch.workloads[idx];
let req = &workload.resources;
let mut best_node: Option<usize> = None;
let mut best_score: u32 = 0;
for (i, (n, alloc)) in cache.iter().zip(node_allocated.iter()).enumerate() {
if !n.schedulable {
continue;
}
if alloc.0 < req.cpu_millis || alloc.1 < req.memory_mb || alloc.2 < req.gpu_count {
continue;
}
let remaining_cpu = alloc.0 - req.cpu_millis;
let remaining_mem = alloc.1 - req.memory_mb;
let score = 2000 - (remaining_cpu * 1000 / n.cpu_available.max(1)) as u32
- (remaining_mem * 1000 / n.memory_available.max(1)) as u32;
if score > best_score {
best_score = score;
best_node = Some(i);
}
}
if let Some(node_idx) = best_node {
batch.results[idx] = Some(cache[node_idx].node_id);
node_allocated[node_idx].0 -= req.cpu_millis;
node_allocated[node_idx].1 -= req.memory_mb;
node_allocated[node_idx].2 -= req.gpu_count;
}
}
let count = batch.workloads.len() as u64;
self.scheduled_count.fetch_add(count, Ordering::Relaxed);
self.total_time_ns.fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
}
pub fn stats(&self) -> SchedulerStats {
let count = self.scheduled_count.load(Ordering::Relaxed);
let time_ns = self.total_time_ns.load(Ordering::Relaxed);
SchedulerStats {
total_scheduled: count,
total_time_ns: time_ns,
avg_time_ns: if count > 0 { time_ns / count } else { 0 },
decisions_per_sec: if time_ns > 0 {
(count as f64 * 1_000_000_000.0 / time_ns as f64) as u64
} else {
0
},
node_count: self.node_cache.read().len(),
}
}
pub fn reset_stats(&self) {
self.scheduled_count.store(0, Ordering::Relaxed);
self.total_time_ns.store(0, Ordering::Relaxed);
}
pub fn node_count(&self) -> usize {
self.node_cache.read().len()
}
pub fn utilization(&self) -> ClusterUtilization {
let nodes = self.nodes.read();
let mut total_cpu: u64 = 0;
let mut used_cpu: u64 = 0;
let mut total_mem: u64 = 0;
let mut used_mem: u64 = 0;
let mut total_gpu: u32 = 0;
let mut used_gpu: u32 = 0;
for node in nodes.iter() {
total_cpu += node.cpu_capacity;
used_cpu += node.cpu_allocated;
total_mem += node.memory_capacity;
used_mem += node.memory_allocated;
total_gpu += node.gpus.len() as u32;
used_gpu += node.gpus_allocated.len() as u32;
}
ClusterUtilization {
cpu_percent: if total_cpu > 0 { (used_cpu as f64 / total_cpu as f64) * 100.0 } else { 0.0 },
memory_percent: if total_mem > 0 { (used_mem as f64 / total_mem as f64) * 100.0 } else { 0.0 },
gpu_percent: if total_gpu > 0 { (used_gpu as f64 / total_gpu as f64) * 100.0 } else { 0.0 },
total_cpu,
used_cpu,
total_memory: total_mem,
used_memory: used_mem,
total_gpus: total_gpu,
used_gpus: used_gpu,
}
}
}
impl Default for OptimizedScheduler {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SchedulerStats {
pub total_scheduled: u64,
pub total_time_ns: u64,
pub avg_time_ns: u64,
pub decisions_per_sec: u64,
pub node_count: usize,
}
#[derive(Debug, Clone)]
pub struct ClusterUtilization {
pub cpu_percent: f64,
pub memory_percent: f64,
pub gpu_percent: f64,
pub total_cpu: u64,
pub used_cpu: u64,
pub total_memory: u64,
pub used_memory: u64,
pub total_gpus: u32,
pub used_gpus: u32,
}
pub struct FFDBinPacker {
nodes: Vec<NodeResources>,
}
impl FFDBinPacker {
pub fn new(mut nodes: Vec<NodeResources>) -> Self {
nodes.sort_by(|a, b| {
let cap_a = a.cpu_capacity + a.memory_capacity;
let cap_b = b.cpu_capacity + b.memory_capacity;
cap_b.cmp(&cap_a)
});
Self { nodes }
}
pub fn pack(&mut self, mut workloads: Vec<Workload>) -> (Vec<(String, NodeId)>, f64) {
workloads.sort_by(|a, b| {
let req_a = a.resources.cpu_millis + a.resources.memory_mb;
let req_b = b.resources.cpu_millis + b.resources.memory_mb;
req_b.cmp(&req_a)
});
let mut assignments = Vec::new();
let mut node_usage: Vec<(u64, u64)> = self.nodes.iter()
.map(|n| (0u64, 0u64))
.collect();
for workload in &workloads {
let req = &workload.resources;
for (i, node) in self.nodes.iter().enumerate() {
let (used_cpu, used_mem) = node_usage[i];
let avail_cpu = node.cpu_capacity.saturating_sub(used_cpu);
let avail_mem = node.memory_capacity.saturating_sub(used_mem);
if avail_cpu >= req.cpu_millis && avail_mem >= req.memory_mb {
assignments.push((workload.id.clone(), node.node_id));
node_usage[i].0 += req.cpu_millis;
node_usage[i].1 += req.memory_mb;
break;
}
}
}
let total_cpu: u64 = self.nodes.iter().map(|n| n.cpu_capacity).sum();
let used_cpu: u64 = node_usage.iter().map(|(c, _)| c).sum();
let utilization = if total_cpu > 0 {
(used_cpu as f64 / total_cpu as f64) * 100.0
} else {
0.0
};
(assignments, utilization)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_nodes(count: usize) -> Vec<NodeResources> {
(0..count).map(|_| {
NodeResources::new(NodeId::new(), 8000, 32768)
}).collect()
}
fn create_workloads(count: usize) -> Vec<Workload> {
(0..count).map(|i| {
Workload::new(format!("w-{}", i), "test")
.with_resources(ResourceRequirements::new()
.cpu(100 + (i as u64 % 10) * 100)
.memory(256 + (i as u64 % 8) * 256))
}).collect()
}
#[test]
fn test_optimized_scheduler_fast() {
let scheduler = OptimizedScheduler::new();
for node in create_nodes(100) {
scheduler.register_node(node);
}
let workloads = create_workloads(1000);
let mut scheduled = 0;
for workload in &workloads {
if scheduler.schedule_fast(workload).is_some() {
scheduled += 1;
}
}
assert!(scheduled > 0);
let stats = scheduler.stats();
println!("Scheduled: {}, Rate: {} decisions/sec", scheduled, stats.decisions_per_sec);
assert!(stats.decisions_per_sec > 10_000, "Expected >10K/sec, got {}", stats.decisions_per_sec);
}
#[test]
fn test_batch_scheduling() {
let scheduler = OptimizedScheduler::new();
for node in create_nodes(50) {
scheduler.register_node(node);
}
let workloads = create_workloads(100);
let mut batch = WorkloadBatch::new(workloads);
scheduler.schedule_batch(&mut batch);
let scheduled: usize = batch.results().iter().filter(|r| r.is_some()).count();
assert!(scheduled > 0);
println!("Batch scheduled: {}/100", scheduled);
}
#[test]
fn test_ffd_bin_packing() {
let nodes = create_nodes(10);
let workloads = create_workloads(50);
let mut packer = FFDBinPacker::new(nodes);
let (assignments, utilization) = packer.pack(workloads);
println!("FFD packed {} workloads, utilization: {:.1}%", assignments.len(), utilization);
assert!(assignments.len() > 0);
}
}