use std::collections::BTreeSet;
use metrale_bench::hardware::equivalence::{EquivalencePolicy, equivalent};
use metrale_bench::hardware::policy::Sensitivity;
use super::super::plan::Unit;
use super::node::{Node, Rejection};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpeedMode {
Spread,
Bundle { node: usize, why: Vec<String> },
}
impl SpeedMode {
pub fn allows(&self, node: usize) -> bool {
match self {
Self::Spread => true,
Self::Bundle { node: home, .. } => *home == node,
}
}
}
pub fn speed_mode(nodes: &[Node], policy: Option<EquivalencePolicy>) -> SpeedMode {
if nodes.len() <= 1 {
return SpeedMode::Spread;
}
let mut why = vec![
"the Speed class runs on one box by default: equivalence at rest did not hold under \
load on 2026-09-15, and the class never sets the makespan"
.to_string(),
];
let Some(policy) = policy else {
why.push(
"this class declares no thermal envelope, so no two of its boxes are one box \
(--dangerous-ignore-thermals)"
.to_string(),
);
return SpeedMode::Bundle {
node: bundle_home(nodes),
why,
};
};
for (i, a) in nodes.iter().enumerate() {
for b in &nodes[i + 1..] {
if let Err(m) = equivalent(&a.hardware, &b.hardware, &policy) {
why.push(format!(
"{} vs {}: {}",
a.addr,
b.addr,
m.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
));
}
}
}
SpeedMode::Bundle {
node: bundle_home(nodes),
why,
}
}
fn bundle_home(nodes: &[Node]) -> usize {
(0..nodes.len())
.max_by(|&x, &y| {
let fx = nodes[x].free_fraction.unwrap_or(0.0);
let fy = nodes[y].free_fraction.unwrap_or(0.0);
fx.partial_cmp(&fy)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
let cx = nodes[x].hardware.hottest_chassis_c.unwrap_or(f64::MAX);
let cy = nodes[y].hardware.hottest_chassis_c.unwrap_or(f64::MAX);
cy.partial_cmp(&cx).unwrap_or(std::cmp::Ordering::Equal)
})
})
.unwrap_or(0)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnergyPin {
pub node: usize,
pub gates: BTreeSet<&'static str>,
}
pub fn energy_pin(
nodes: &[Node],
rejected: &[Rejection],
reference: &str,
gates: BTreeSet<&'static str>,
) -> Result<EnergyPin, String> {
if let Some(node) = nodes.iter().position(|n| n.addr == reference) {
return Ok(EnergyPin { node, gates });
}
match rejected.iter().find(|r| r.addr == reference) {
Some(r) => Err(format!(
"--energy-reference-node {reference} was not admitted ({}); its Speed-class and \
energy-bounded gates would run on another box",
r.why
)),
None => Err(format!(
"--energy-reference-node {reference} is not in the fleet ({})",
nodes
.iter()
.map(|n| n.addr.as_str())
.collect::<Vec<_>>()
.join(", ")
)),
}
}
pub fn bundle_on_reference(mode: SpeedMode, pin: &EnergyPin) -> SpeedMode {
match mode {
SpeedMode::Spread => SpeedMode::Spread,
SpeedMode::Bundle { mut why, .. } => {
why.push(
"--energy-reference-node names the box for every Speed-class and energy-bounded \
gate: the boxes do not read as one, so one box takes them all"
.to_string(),
);
SpeedMode::Bundle {
node: pin.node,
why,
}
}
}
}
fn may_run(unit: &Unit, node: usize, mode: &SpeedMode, energy: Option<&EnergyPin>) -> bool {
match energy {
Some(pin) if unit.class == Sensitivity::Speed || pin.gates.contains(unit.id) => {
pin.node == node
}
_ => unit.class != Sensitivity::Speed || mode.allows(node),
}
}
pub fn next_for(
node: usize,
units: &[Unit],
pending: &[bool],
placed: &[Option<usize>],
mode: &SpeedMode,
energy: Option<&EnergyPin>,
) -> Option<usize> {
let hosts_shard_of = |group: &str| {
units
.iter()
.enumerate()
.any(|(j, u)| u.group == Some(group) && placed[j] == Some(node))
};
(0..units.len())
.filter(|&i| pending[i])
.filter(|&i| may_run(&units[i], node, mode, energy))
.min_by_key(|&i| {
let crowded = units[i].group.is_some_and(hosts_shard_of);
(crowded, std::cmp::Reverse(units[i].secs()))
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Plan {
pub queues: Vec<Vec<usize>>,
pub finish_at: Vec<u64>,
pub makespan_secs: u64,
}
pub fn simulate(
units: &[Unit],
nodes: &[Node],
mode: &SpeedMode,
energy: Option<&EnergyPin>,
build_allowance: u64,
) -> Plan {
let n = nodes.len();
let mut pending = vec![true; units.len()];
let mut placed = vec![None; units.len()];
let mut queues = vec![Vec::new(); n];
let mut free_at: Vec<u64> = nodes
.iter()
.map(|nd| if nd.built { 0 } else { build_allowance })
.collect();
let mut idle = vec![false; n];
while pending.iter().any(|p| *p) {
let Some(node) = (0..n).filter(|&k| !idle[k]).min_by_key(|&k| free_at[k]) else {
break;
};
match next_for(node, units, &pending, &placed, mode, energy) {
Some(i) => {
pending[i] = false;
placed[i] = Some(node);
queues[node].push(i);
free_at[node] += units[i].secs();
}
None => idle[node] = true,
}
}
let makespan_secs = free_at.iter().copied().max().unwrap_or(0);
Plan {
queues,
finish_at: free_at,
makespan_secs,
}
}
#[cfg(test)]
#[path = "schedule_tests.rs"]
mod schedule_tests;