1#[derive(Debug, Clone)]
12pub struct TaskNode {
13 pub id: u64,
15 pub name: String,
17 pub estimated_ms: u64,
19 pub resource_requirements: Vec<String>,
21}
22
23impl TaskNode {
24 #[must_use]
26 pub fn new(id: u64, name: impl Into<String>, estimated_ms: u64) -> Self {
27 Self {
28 id,
29 name: name.into(),
30 estimated_ms,
31 resource_requirements: Vec::new(),
32 }
33 }
34
35 #[must_use]
37 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
38 self.resource_requirements.push(resource.into());
39 self
40 }
41
42 #[must_use]
44 pub fn total_resource_count(&self) -> usize {
45 self.resource_requirements.len()
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum EdgeType {
52 Sequential,
54 Parallel,
56 ConditionalSuccess,
58 ConditionalFailure,
60}
61
62impl EdgeType {
63 #[must_use]
65 pub const fn is_conditional(self) -> bool {
66 matches!(self, Self::ConditionalSuccess | Self::ConditionalFailure)
67 }
68}
69
70#[derive(Debug, Clone)]
72pub struct TaskEdge {
73 pub from_id: u64,
75 pub to_id: u64,
77 pub edge_type: EdgeType,
79}
80
81impl TaskEdge {
82 #[must_use]
84 pub fn new(from_id: u64, to_id: u64, edge_type: EdgeType) -> Self {
85 Self {
86 from_id,
87 to_id,
88 edge_type,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Default)]
95pub struct TaskGraph {
96 pub nodes: Vec<TaskNode>,
98 pub edges: Vec<TaskEdge>,
100}
101
102impl TaskGraph {
103 #[must_use]
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn add_node(&mut self, node: TaskNode) {
111 self.nodes.push(node);
112 }
113
114 pub fn add_edge(&mut self, edge: TaskEdge) {
116 self.edges.push(edge);
117 }
118
119 #[must_use]
121 pub fn successors(&self, id: u64) -> Vec<u64> {
122 self.edges
123 .iter()
124 .filter(|e| e.from_id == id)
125 .map(|e| e.to_id)
126 .collect()
127 }
128
129 #[must_use]
131 pub fn predecessors(&self, id: u64) -> Vec<u64> {
132 self.edges
133 .iter()
134 .filter(|e| e.to_id == id)
135 .map(|e| e.from_id)
136 .collect()
137 }
138
139 #[must_use]
141 pub fn root_nodes(&self) -> Vec<u64> {
142 self.nodes
143 .iter()
144 .filter(|n| self.predecessors(n.id).is_empty())
145 .map(|n| n.id)
146 .collect()
147 }
148
149 #[must_use]
156 pub fn critical_path_ms(&self) -> u64 {
157 if self.nodes.is_empty() {
158 return 0;
159 }
160
161 let mut best: std::collections::HashMap<u64, u64> =
163 self.nodes.iter().map(|n| (n.id, 0u64)).collect();
164
165 let mut in_degree: std::collections::HashMap<u64, usize> =
167 self.nodes.iter().map(|n| (n.id, 0)).collect();
168
169 for e in &self.edges {
170 *in_degree.entry(e.to_id).or_insert(0) += 1;
171 }
172
173 let mut queue: std::collections::VecDeque<u64> = in_degree
174 .iter()
175 .filter(|(_, °)| deg == 0)
176 .map(|(&id, _)| id)
177 .collect();
178
179 for &id in &queue {
181 if let Some(node) = self.nodes.iter().find(|n| n.id == id) {
182 best.insert(id, node.estimated_ms);
183 }
184 }
185
186 while let Some(node_id) = queue.pop_front() {
187 let current_best = best[&node_id];
188 let node_cost = self
189 .nodes
190 .iter()
191 .find(|n| n.id == node_id)
192 .map_or(0, |n| n.estimated_ms);
193
194 for succ_id in self.successors(node_id) {
195 let candidate = current_best
196 + node_cost.saturating_sub(
197 node_cost, );
202 let succ_cost = self
204 .nodes
205 .iter()
206 .find(|n| n.id == succ_id)
207 .map_or(0, |n| n.estimated_ms);
208
209 let new_val = current_best + succ_cost;
210 let entry = best.entry(succ_id).or_insert(0);
211 if new_val > *entry {
212 *entry = new_val;
213 }
214 let _ = candidate; if let Some(deg) = in_degree.get_mut(&succ_id) {
218 if *deg > 0 {
219 *deg -= 1;
220 }
221 if *deg == 0 {
222 queue.push_back(succ_id);
223 }
224 }
225 }
226 }
227
228 best.values().copied().max().unwrap_or(0)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
239 fn test_task_node_resource_count_empty() {
240 let n = TaskNode::new(1, "encode", 5000);
241 assert_eq!(n.total_resource_count(), 0);
242 }
243
244 #[test]
245 fn test_task_node_resource_count_with_resources() {
246 let n = TaskNode::new(1, "encode", 5000)
247 .with_resource("gpu")
248 .with_resource("network");
249 assert_eq!(n.total_resource_count(), 2);
250 }
251
252 #[test]
255 fn test_edge_type_is_conditional_false() {
256 assert!(!EdgeType::Sequential.is_conditional());
257 assert!(!EdgeType::Parallel.is_conditional());
258 }
259
260 #[test]
261 fn test_edge_type_is_conditional_true() {
262 assert!(EdgeType::ConditionalSuccess.is_conditional());
263 assert!(EdgeType::ConditionalFailure.is_conditional());
264 }
265
266 #[test]
269 fn test_add_node_and_edge() {
270 let mut g = TaskGraph::new();
271 g.add_node(TaskNode::new(1, "a", 100));
272 g.add_node(TaskNode::new(2, "b", 200));
273 g.add_edge(TaskEdge::new(1, 2, EdgeType::Sequential));
274 assert_eq!(g.nodes.len(), 2);
275 assert_eq!(g.edges.len(), 1);
276 }
277
278 #[test]
279 fn test_successors() {
280 let mut g = TaskGraph::new();
281 g.add_node(TaskNode::new(1, "a", 100));
282 g.add_node(TaskNode::new(2, "b", 100));
283 g.add_node(TaskNode::new(3, "c", 100));
284 g.add_edge(TaskEdge::new(1, 2, EdgeType::Sequential));
285 g.add_edge(TaskEdge::new(1, 3, EdgeType::Parallel));
286 let mut succs = g.successors(1);
287 succs.sort_unstable();
288 assert_eq!(succs, vec![2, 3]);
289 }
290
291 #[test]
292 fn test_predecessors() {
293 let mut g = TaskGraph::new();
294 g.add_node(TaskNode::new(1, "a", 100));
295 g.add_node(TaskNode::new(2, "b", 100));
296 g.add_node(TaskNode::new(3, "c", 100));
297 g.add_edge(TaskEdge::new(1, 3, EdgeType::Sequential));
298 g.add_edge(TaskEdge::new(2, 3, EdgeType::Sequential));
299 let mut preds = g.predecessors(3);
300 preds.sort_unstable();
301 assert_eq!(preds, vec![1, 2]);
302 }
303
304 #[test]
305 fn test_root_nodes_single() {
306 let mut g = TaskGraph::new();
307 g.add_node(TaskNode::new(1, "root", 0));
308 g.add_node(TaskNode::new(2, "leaf", 0));
309 g.add_edge(TaskEdge::new(1, 2, EdgeType::Sequential));
310 assert_eq!(g.root_nodes(), vec![1]);
311 }
312
313 #[test]
314 fn test_root_nodes_multiple() {
315 let mut g = TaskGraph::new();
316 g.add_node(TaskNode::new(1, "a", 0));
317 g.add_node(TaskNode::new(2, "b", 0));
318 g.add_node(TaskNode::new(3, "c", 0));
319 g.add_edge(TaskEdge::new(1, 3, EdgeType::Sequential));
320 g.add_edge(TaskEdge::new(2, 3, EdgeType::Sequential));
321 let mut roots = g.root_nodes();
322 roots.sort_unstable();
323 assert_eq!(roots, vec![1, 2]);
324 }
325
326 #[test]
327 fn test_successors_empty() {
328 let g = TaskGraph::new();
329 assert!(g.successors(99).is_empty());
330 }
331
332 #[test]
333 fn test_predecessors_empty() {
334 let g = TaskGraph::new();
335 assert!(g.predecessors(99).is_empty());
336 }
337
338 #[test]
341 fn test_critical_path_empty_graph() {
342 let g = TaskGraph::new();
343 assert_eq!(g.critical_path_ms(), 0);
344 }
345
346 #[test]
347 fn test_critical_path_single_node() {
348 let mut g = TaskGraph::new();
349 g.add_node(TaskNode::new(1, "only", 500));
350 assert_eq!(g.critical_path_ms(), 500);
351 }
352
353 #[test]
354 fn test_critical_path_linear_chain() {
355 let mut g = TaskGraph::new();
357 g.add_node(TaskNode::new(1, "a", 100));
358 g.add_node(TaskNode::new(2, "b", 200));
359 g.add_node(TaskNode::new(3, "c", 300));
360 g.add_edge(TaskEdge::new(1, 2, EdgeType::Sequential));
361 g.add_edge(TaskEdge::new(2, 3, EdgeType::Sequential));
362 assert_eq!(g.critical_path_ms(), 600);
363 }
364
365 #[test]
366 fn test_critical_path_parallel_paths() {
367 let mut g = TaskGraph::new();
370 g.add_node(TaskNode::new(1, "a", 100));
371 g.add_node(TaskNode::new(2, "b", 200));
372 g.add_node(TaskNode::new(3, "c", 500));
373 g.add_edge(TaskEdge::new(1, 2, EdgeType::Sequential));
374 g.add_edge(TaskEdge::new(3, 2, EdgeType::Sequential));
375 assert_eq!(g.critical_path_ms(), 700);
376 }
377}