1use std::{
14 collections::{HashMap, HashSet},
15 fmt::{self, Write as _},
16 sync::{Arc, Mutex},
17};
18
19use thiserror::Error as ThisError;
20
21use crate::{
22 element::ElementType,
23 log::{Level, enabled},
24 pp_log::{PpLog, pp_info},
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub struct ElementId(u64);
31
32impl fmt::Display for ElementId {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 self.0.fmt(f)
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct EdgeId(u64);
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct BranchId(u64);
46
47impl fmt::Display for BranchId {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 self.0.fmt(f)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct NodeInfo {
57 pub id: ElementId,
59 pub element_type: ElementType,
61 pub name: Arc<str>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PortRef {
68 pub element: ElementId,
70 pub port: Arc<str>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct EdgeInfo {
78 pub id: EdgeId,
80 pub branch_id: BranchId,
82 pub from: PortRef,
84 pub to: PortRef,
86}
87
88#[derive(Debug, Clone)]
89pub struct GraphSnapshot {
97 pub revision: u64,
100 pub nodes: Vec<NodeInfo>,
102 pub edges: Vec<EdgeInfo>,
104}
105
106impl GraphSnapshot {
107 pub fn node(&self, id: ElementId) -> Option<&NodeInfo> {
109 self.nodes.iter().find(|node| node.id == id)
110 }
111
112 pub fn topology(&self) -> String {
116 self.paths()
117 .into_iter()
118 .map(|path| {
119 path.into_iter()
120 .filter_map(|id| self.node(id))
121 .map(|node| format!("{:?}({})", node.element_type, node.name))
122 .collect::<Vec<_>>()
123 .join(" - ")
124 })
125 .collect::<Vec<_>>()
126 .join("\n")
127 }
128
129 pub(crate) fn topology_diagram(&self) -> String {
133 let roots: Vec<_> = self
134 .nodes
135 .iter()
136 .filter(|node| !self.edges.iter().any(|edge| edge.to.element == node.id))
137 .collect();
138 let mut output = String::new();
139
140 for (index, root) in roots.iter().enumerate() {
141 let is_last = index + 1 == roots.len();
142 let child_indent = if roots.len() == 1 {
143 let _ = write!(output, "{:?}({})#{}", root.element_type, root.name, root.id);
144 String::new()
145 } else {
146 let connector = if is_last { "└── " } else { "├── " };
147 let _ = write!(
148 output,
149 "{connector}{:?}({})#{}",
150 root.element_type, root.name, root.id
151 );
152 if is_last {
153 " ".to_owned()
154 } else {
155 "│ ".to_owned()
156 }
157 };
158 self.render_diagram_children(
159 root.id,
160 &child_indent,
161 &mut HashSet::from([root.id]),
162 &mut output,
163 );
164 if !is_last {
165 output.push('\n');
166 }
167 }
168
169 output
170 }
171
172 fn render_diagram_children(
173 &self,
174 parent: ElementId,
175 indent: &str,
176 visiting: &mut HashSet<ElementId>,
177 output: &mut String,
178 ) {
179 let children: Vec<_> = self
180 .edges
181 .iter()
182 .filter(|edge| edge.from.element == parent)
183 .collect();
184
185 for (index, edge) in children.iter().enumerate() {
186 let Some(child) = self.node(edge.to.element) else {
187 continue;
188 };
189 let is_last = index + 1 == children.len();
190 let connector = if is_last { "└── " } else { "├── " };
191 let link = format!("[{}] → ", edge.from.port);
192 let _ = write!(
193 output,
194 "\n{indent}{connector}{link}{:?}({})#{}",
195 child.element_type, child.name, child.id
196 );
197
198 if visiting.insert(child.id) {
199 let continuation = if is_last { " " } else { "│ " };
200 let child_indent =
201 format!("{indent}{continuation}{}", " ".repeat(link.chars().count()));
202 self.render_diagram_children(child.id, &child_indent, visiting, output);
203 visiting.remove(&child.id);
204 }
205 }
206 }
207
208 fn paths(&self) -> Vec<Vec<ElementId>> {
209 let leaves: Vec<_> = self
210 .nodes
211 .iter()
212 .filter(|node| !self.edges.iter().any(|edge| edge.from.element == node.id))
213 .collect();
214 let mut rendered = Vec::new();
215 for leaf in leaves {
216 self.paths_to(leaf.id, &mut HashSet::new(), &mut Vec::new(), &mut rendered);
217 }
218 rendered
219 }
220
221 fn paths_to(
222 &self,
223 current: ElementId,
224 visiting: &mut HashSet<ElementId>,
225 suffix: &mut Vec<ElementId>,
226 paths: &mut Vec<Vec<ElementId>>,
227 ) {
228 if !visiting.insert(current) {
229 return;
230 }
231 suffix.push(current);
232 let upstream: Vec<_> = self
233 .edges
234 .iter()
235 .filter(|edge| edge.to.element == current)
236 .map(|edge| edge.from.element)
237 .collect();
238 if upstream.is_empty() {
239 let mut path = suffix.clone();
240 path.reverse();
241 paths.push(path);
242 } else {
243 for parent in upstream {
244 self.paths_to(parent, visiting, suffix, paths);
245 }
246 }
247 suffix.pop();
248 visiting.remove(¤t);
249 }
250}
251
252pub(crate) fn log_topology(pp_log: &PpLog, event: &str, snapshot: &GraphSnapshot) {
261 if !enabled(Level::Info) {
262 return;
263 }
264 pp_info!(pp_log: pp_log, "{event}\n{}", snapshot.topology_diagram());
265}
266
267#[derive(Debug, ThisError, PartialEq, Eq)]
268pub enum GraphError {
274 #[error("source pad index {index} is out of range (source has {pad_count} pads)")]
276 PadOutOfRange {
277 index: usize,
279 pad_count: usize,
281 },
282
283 #[error("source pad '{0}' is already linked")]
285 PadAlreadyLinked(String),
286
287 #[error("element {0} is not attached to this pipeline")]
289 ParentNotAttached(ElementId),
290
291 #[error("element {0} is already attached to this pipeline")]
293 NodeAlreadyAttached(ElementId),
294
295 #[error("branch {0} is not attached")]
297 BranchNotAttached(BranchId),
298
299 #[error("a branch must contain at least one element")]
301 EmptyBranch,
302
303 #[error("ChainBuilder::pipe requires exactly one output pad, but {name} has {count}")]
306 NotSingleOutput {
307 name: Arc<str>,
309 count: usize,
311 },
312}
313
314#[derive(Debug, Clone)]
315pub(crate) struct PlannedEdge {
316 pub from: PortRef,
317 pub to: PortRef,
318}
319
320#[derive(Debug)]
321pub(crate) struct BranchPlan {
322 pub nodes: Vec<NodeInfo>,
323 pub edges: Vec<PlannedEdge>,
324 pub root: ElementId,
325}
326
327#[derive(Debug)]
328struct BranchRecord {
329 parent: ElementId,
330 owned_nodes: HashSet<ElementId>,
331}
332
333#[derive(Default)]
334struct GraphState {
335 next_element_id: u64,
336 next_edge_id: u64,
337 next_branch_id: u64,
338 revision: u64,
339 nodes: Vec<NodeInfo>,
340 edges: Vec<EdgeInfo>,
341 branches: HashMap<BranchId, BranchRecord>,
342}
343
344#[derive(Clone, Default)]
347pub struct PipelineGraph(Arc<Mutex<GraphState>>);
348
349impl PipelineGraph {
350 pub fn new() -> Self {
352 Self::default()
353 }
354
355 pub fn snapshot(&self) -> GraphSnapshot {
358 let state = self.0.lock().unwrap();
359 GraphSnapshot {
360 revision: state.revision,
361 nodes: state.nodes.clone(),
362 edges: state.edges.clone(),
363 }
364 }
365
366 pub fn branch_containing(&self, element: ElementId) -> Option<BranchId> {
371 let state = self.0.lock().unwrap();
372 state
373 .branches
374 .iter()
375 .find_map(|(id, branch)| branch.owned_nodes.contains(&element).then_some(*id))
376 }
377
378 pub(crate) fn reserve_element_id(&self) -> ElementId {
379 let mut state = self.0.lock().unwrap();
380 state.next_element_id += 1;
381 ElementId(state.next_element_id)
382 }
383
384 pub(crate) fn add_source(&self, element_type: ElementType, name: Arc<str>) -> ElementId {
385 let id = self.reserve_element_id();
386 let mut state = self.0.lock().unwrap();
387 state.nodes.push(NodeInfo {
388 id,
389 element_type,
390 name,
391 });
392 state.revision += 1;
393 id
394 }
395
396 pub(crate) fn attach_with(
399 &self,
400 parent: ElementId,
401 from_port: Arc<str>,
402 plan: BranchPlan,
403 attach_runtime: impl FnOnce(BranchId) -> Result<(), GraphError>,
404 ) -> Result<BranchId, GraphError> {
405 let mut state = self.0.lock().unwrap();
406 if !state.nodes.iter().any(|node| node.id == parent) {
407 return Err(GraphError::ParentNotAttached(parent));
408 }
409 if plan.nodes.is_empty() {
410 return Err(GraphError::EmptyBranch);
411 }
412 for node in &plan.nodes {
413 if state.nodes.iter().any(|current| current.id == node.id) {
414 return Err(GraphError::NodeAlreadyAttached(node.id));
415 }
416 }
417
418 state.next_branch_id += 1;
419 let branch_id = BranchId(state.next_branch_id);
420 attach_runtime(branch_id)?;
421
422 let mut edges = Vec::with_capacity(plan.edges.len() + 1);
423 state.next_edge_id += 1;
424 edges.push(EdgeInfo {
425 id: EdgeId(state.next_edge_id),
426 branch_id,
427 from: PortRef {
428 element: parent,
429 port: from_port,
430 },
431 to: PortRef {
432 element: plan.root,
433 port: "sink".into(),
434 },
435 });
436 for edge in plan.edges {
437 state.next_edge_id += 1;
438 edges.push(EdgeInfo {
439 id: EdgeId(state.next_edge_id),
440 branch_id,
441 from: edge.from,
442 to: edge.to,
443 });
444 }
445
446 let owned_nodes = plan.nodes.iter().map(|node| node.id).collect();
447 state.nodes.extend(plan.nodes);
448 state.edges.extend(edges);
449 state.branches.insert(
450 branch_id,
451 BranchRecord {
452 parent,
453 owned_nodes,
454 },
455 );
456 state.revision += 1;
457 Ok(branch_id)
458 }
459
460 pub(crate) fn detach_with(
463 &self,
464 branch_id: BranchId,
465 detach_runtime: impl FnOnce() -> Result<(), GraphError>,
466 ) -> Result<(), GraphError> {
467 let mut state = self.0.lock().unwrap();
468 if !state.branches.contains_key(&branch_id) {
469 return Err(GraphError::BranchNotAttached(branch_id));
470 }
471 detach_runtime()?;
472
473 let mut removed_branches = HashSet::from([branch_id]);
474 let mut removed_nodes = HashSet::new();
475 loop {
476 for id in removed_branches.clone() {
477 if let Some(branch) = state.branches.get(&id) {
478 removed_nodes.extend(branch.owned_nodes.iter().copied());
479 }
480 }
481 let before = removed_branches.len();
482 for (id, branch) in &state.branches {
483 if removed_nodes.contains(&branch.parent) {
484 removed_branches.insert(*id);
485 }
486 }
487 if removed_branches.len() == before {
488 break;
489 }
490 }
491
492 state
493 .branches
494 .retain(|id, _| !removed_branches.contains(id));
495 state.nodes.retain(|node| !removed_nodes.contains(&node.id));
496 state
497 .edges
498 .retain(|edge| !removed_branches.contains(&edge.branch_id));
499 state.revision += 1;
500 Ok(())
501 }
502}