use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet, VecDeque};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScheduleUnit {
pub id: String,
#[serde(default)]
pub duration: u64,
#[serde(default)]
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScheduleGraph {
#[serde(default)]
pub units: Vec<ScheduleUnit>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScheduleAnalysis {
pub has_cycle: bool,
pub critical_path: Vec<String>,
pub makespan: u64,
pub max_parallelism: u64,
pub levels: Vec<Vec<String>>,
pub serial: bool,
}
fn valid_deps<'a>(u: &'a ScheduleUnit, ids: &HashSet<&str>) -> Vec<&'a str> {
u.depends_on
.iter()
.map(|d| d.as_str())
.filter(|d| ids.contains(*d))
.collect()
}
pub fn analyze_schedule(graph: &ScheduleGraph) -> ScheduleAnalysis {
let ids: HashSet<&str> = graph.units.iter().map(|u| u.id.as_str()).collect();
let by_id: HashMap<&str, &ScheduleUnit> =
graph.units.iter().map(|u| (u.id.as_str(), u)).collect();
let mut indegree: HashMap<&str, usize> = graph.units.iter().map(|u| (u.id.as_str(), 0)).collect();
let mut dependents: HashMap<&str, Vec<&str>> = HashMap::new();
for u in &graph.units {
for d in valid_deps(u, &ids) {
*indegree.get_mut(u.id.as_str()).unwrap() += 1;
dependents.entry(d).or_default().push(u.id.as_str());
}
}
let mut level_of: HashMap<&str, usize> = HashMap::new();
let mut q: VecDeque<&str> = indegree
.iter()
.filter(|(_, &d)| d == 0)
.map(|(&id, _)| id)
.collect();
let mut ready: Vec<&str> = q.drain(..).collect();
ready.sort_unstable();
let mut q: VecDeque<&str> = ready.into_iter().collect();
for &id in &q {
level_of.insert(id, 0);
}
let mut processed = 0usize;
let mut indeg = indegree.clone();
while let Some(id) = q.pop_front() {
processed += 1;
let lvl = level_of[id];
let mut next_ready: Vec<&str> = Vec::new();
if let Some(deps) = dependents.get(id) {
for &dep in deps {
let lv = level_of.get(dep).copied().unwrap_or(0).max(lvl + 1);
level_of.insert(dep, lv);
let e = indeg.get_mut(dep).unwrap();
*e -= 1;
if *e == 0 {
next_ready.push(dep);
}
}
}
next_ready.sort_unstable();
for dep in next_ready {
q.push_back(dep);
}
}
let has_cycle = processed < graph.units.len();
let max_level = level_of.values().copied().max().unwrap_or(0);
let mut levels: Vec<Vec<String>> = vec![Vec::new(); max_level + 1];
for (&id, &lvl) in &level_of {
levels[lvl].push(id.to_string());
}
for wave in &mut levels {
wave.sort();
}
if level_of.is_empty() {
levels.clear();
}
let max_parallelism = levels.iter().map(|w| w.len() as u64).max().unwrap_or(0);
let mut ef: HashMap<&str, u64> = HashMap::new();
let mut pred: HashMap<&str, Option<&str>> = HashMap::new();
for lvl in 0..=max_level {
for id in level_of
.iter()
.filter(|(_, &l)| l == lvl)
.map(|(&id, _)| id)
{
let unit = by_id[id];
let mut best = 0u64;
let mut best_pred: Option<&str> = None;
for d in valid_deps(unit, &ids) {
if let Some(&fin) = ef.get(d) {
if fin > best {
best = fin;
best_pred = Some(d);
}
}
}
ef.insert(id, best + unit.duration);
pred.insert(id, best_pred);
}
}
let makespan = ef.values().copied().max().unwrap_or(0);
let mut tail: Option<&str> = None;
let mut best_ef = 0u64;
let mut tail_ids: Vec<&str> = ef.keys().copied().collect();
tail_ids.sort_unstable();
for id in tail_ids {
let f = ef[id];
if f > best_ef {
best_ef = f;
tail = Some(id);
}
}
let mut critical_path = Vec::new();
let mut cur = tail;
while let Some(id) = cur {
critical_path.push(id.to_string());
cur = pred.get(id).copied().flatten();
}
critical_path.reverse();
ScheduleAnalysis {
has_cycle,
critical_path,
makespan,
max_parallelism,
serial: max_parallelism <= 1,
levels,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unit(id: &str, duration: u64, deps: &[&str]) -> ScheduleUnit {
ScheduleUnit {
id: id.into(),
duration,
depends_on: deps.iter().map(|s| s.to_string()).collect(),
}
}
fn graph(units: Vec<ScheduleUnit>) -> ScheduleGraph {
ScheduleGraph { units }
}
#[test]
fn linear_chain_is_serial() {
let g = graph(vec![
unit("a", 1, &[]),
unit("b", 2, &["a"]),
unit("c", 3, &["b"]),
]);
let r = analyze_schedule(&g);
assert!(!r.has_cycle);
assert!(r.serial);
assert_eq!(r.max_parallelism, 1);
assert_eq!(r.makespan, 6);
assert_eq!(
r.critical_path,
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
assert_eq!(r.levels.len(), 3);
}
#[test]
fn diamond_has_parallelism_two() {
let g = graph(vec![
unit("a", 1, &[]),
unit("b", 5, &["a"]),
unit("c", 2, &["a"]),
unit("d", 1, &["b", "c"]),
]);
let r = analyze_schedule(&g);
assert!(!r.has_cycle);
assert!(!r.serial);
assert_eq!(r.max_parallelism, 2); assert_eq!(r.makespan, 7); assert_eq!(
r.critical_path,
vec!["a".to_string(), "b".to_string(), "d".to_string()]
);
}
#[test]
fn fully_parallel_fan() {
let g = graph(vec![unit("a", 3, &[]), unit("b", 1, &[]), unit("c", 2, &[])]);
let r = analyze_schedule(&g);
assert_eq!(r.max_parallelism, 3);
assert_eq!(r.makespan, 3); assert!(!r.serial);
assert_eq!(r.levels.len(), 1);
}
#[test]
fn cycle_is_detected() {
let g = graph(vec![unit("a", 1, &["b"]), unit("b", 1, &["a"])]);
let r = analyze_schedule(&g);
assert!(r.has_cycle);
}
#[test]
fn empty_graph_is_trivial() {
let r = analyze_schedule(&graph(vec![]));
assert!(!r.has_cycle);
assert_eq!(r.makespan, 0);
assert_eq!(r.max_parallelism, 0);
assert!(r.serial); assert!(r.critical_path.is_empty());
}
#[test]
fn single_unit_is_serial() {
let r = analyze_schedule(&graph(vec![unit("only", 4, &[])]));
assert!(r.serial);
assert_eq!(r.makespan, 4);
assert_eq!(r.critical_path, vec!["only".to_string()]);
}
#[test]
fn dangling_dependency_is_ignored() {
let g = graph(vec![unit("a", 2, &["ghost"])]);
let r = analyze_schedule(&g);
assert!(!r.has_cycle);
assert_eq!(r.makespan, 2);
assert_eq!(r.levels.len(), 1);
}
}