use std::collections::BTreeMap;
use anyhow::{Result, bail};
use metrale_bench::gate::{self, GateBaseline, GateStatus};
use metrale_bench::hardware::energy::JOULES_PER_TOKEN_KEY;
use metrale_bench::hardware::limits::TimingLimits;
use metrale_bench::hardware::policy::Sensitivity;
use metrale_bench::registry;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Estimate {
Declared(u64),
Measured { secs: u64, recorded_at: u64 },
}
#[derive(Clone, Debug)]
pub struct Unit {
pub id: &'static str,
pub group: Option<&'static str>,
pub shard: Option<(usize, usize)>,
pub class: Sensitivity,
pub estimate: Estimate,
pub needs_confirmation: bool,
pub serve_allowance_s: u64,
}
impl Unit {
pub fn label(&self) -> String {
match self.shard {
Some((i, n)) => format!("{}[{i}/{n}]", self.id),
None => self.id.to_string(),
}
}
pub fn file_stem(&self) -> String {
format!("{}{}", self.id, gate::shard_suffix(self.shard))
}
pub fn shard_param(&self) -> Option<String> {
self.shard.map(|(i, n)| format!("shard={i}/{n}"))
}
}
pub fn shard_count(boxes: usize) -> usize {
if boxes <= 1 { 1 } else { 2 * boxes }
}
impl Unit {
pub fn deadline(&self, timeout_factor: f64) -> std::time::Duration {
std::time::Duration::from_secs(self.serve_allowance_s)
+ std::time::Duration::from_secs((self.secs() as f64 * timeout_factor) as u64)
}
pub fn secs(&self) -> u64 {
match self.estimate {
Estimate::Declared(s) | Estimate::Measured { secs: s, .. } => s,
}
}
}
pub fn remaining(
statuses: &BTreeMap<String, GateStatus>,
only: &[String],
) -> Result<Vec<&'static str>> {
let open: Vec<&'static str> = gate::REQUIRED_GATES
.iter()
.copied()
.filter(|id| !matches!(statuses.get(*id), Some(GateStatus::Pass)))
.collect();
if only.is_empty() {
return Ok(open);
}
let mut chosen = Vec::new();
for want in only {
match open.iter().find(|id| **id == want.as_str()) {
Some(id) => chosen.push(*id),
None => bail!(
"{want} already passes at this commit; pass no --gates to run everything \
that is still open ({})",
if open.is_empty() {
"nothing".to_string()
} else {
open.join(", ")
}
),
}
}
Ok(chosen)
}
pub type Owed<'a> = &'a dyn Fn(&'static gate::group::BenchmarkGroup) -> Vec<(usize, usize)>;
pub fn fresh_partition(
n: usize,
) -> impl Fn(&'static gate::group::BenchmarkGroup) -> Vec<(usize, usize)> {
move |_| (0..n).map(|i| (i, n)).collect()
}
pub fn expand(
gate_id: &'static str,
owed: Owed<'_>,
) -> Vec<(&'static str, Option<(usize, usize)>)> {
match gate::group::find(gate_id) {
Some(group) => owed(group)
.into_iter()
.map(|s| (gate_id, Some(s)))
.collect(),
None => vec![(gate_id, None)],
}
}
pub fn units(
gates: &[&'static str],
measured: &dyn Fn(&str) -> Option<(u64, u64)>,
owed: Owed<'_>,
timing: &TimingLimits,
) -> Result<Vec<Unit>> {
let mut out = Vec::new();
for gate_id in gates {
for (id, shard) in expand(gate_id, owed) {
let Some(d) = registry::find(id) else {
bail!("{id} is a required gate but not a registered benchmark");
};
let mut estimate = match measured(id) {
Some((secs, recorded_at)) if secs > 0 => Estimate::Measured { secs, recorded_at },
_ => Estimate::Declared(d.expected_secs),
};
if let Some((_, n)) = shard {
estimate = match estimate {
Estimate::Declared(s) => Estimate::Declared(shard_secs(s, n, timing)),
Estimate::Measured { secs, recorded_at } => Estimate::Measured {
secs: shard_secs(secs, n, timing),
recorded_at,
},
};
}
out.push(Unit {
id,
group: gate::group::find(id).map(|g| g.id),
shard,
class: d.sensitivity,
estimate,
needs_confirmation: d.needs_confirmation,
serve_allowance_s: timing.serve_allowance_s,
});
}
}
Ok(out)
}
pub fn shard_secs(whole: u64, n: usize, timing: &TimingLimits) -> u64 {
(whole / n as u64 + timing.shard_overhead_s).max(timing.shard_floor_s)
}
pub fn energy_bounded(baseline: &GateBaseline, hardware: &str) -> Result<bool> {
if !baseline.hardware.contains_key(hardware) {
return Ok(false);
}
let (_, entry) = baseline.resolve(hardware, None)?;
Ok(entry.metrics.iter().any(|(name, bound)| {
name.ends_with(JOULES_PER_TOKEN_KEY) && (bound.min.is_some() || bound.max.is_some())
}))
}
pub fn order_local(mut units: Vec<Unit>) -> Vec<Unit> {
fn rank(u: &Unit) -> (u8, u64, &'static str, Option<(usize, usize)>) {
let tier = match (u.group, u.class) {
(Some(_), _) => 0,
(None, Sensitivity::Speed) => 1,
(None, Sensitivity::Correctness) => 2,
};
let secs = if tier == 0 {
u64::MAX - u.secs()
} else {
u.secs()
};
(tier, secs, u.id, u.shard)
}
units.sort_by_key(rank);
units
}
pub fn serial_estimate_secs(units: &[Unit]) -> u64 {
units.iter().map(Unit::secs).sum()
}
#[cfg(test)]
#[path = "plan_tests.rs"]
mod plan_tests;