1use std::{
2 collections::{HashMap, HashSet},
3 fmt::{self, Write as _},
4 sync::{Arc, Mutex},
5};
6
7use thiserror::Error as ThisError;
8
9use crate::{
10 element::ElementType,
11 log::{Level, enabled},
12 pp_log::{PpLog, pp_info},
13};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct ElementId(u64);
19
20impl fmt::Display for ElementId {
21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22 self.0.fmt(f)
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct EdgeId(u64);
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct BranchId(u64);
34
35impl fmt::Display for BranchId {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 self.0.fmt(f)
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct NodeInfo {
43 pub id: ElementId,
44 pub element_type: ElementType,
45 pub name: Arc<str>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct PortRef {
50 pub element: ElementId,
51 pub port: Arc<str>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct EdgeInfo {
56 pub id: EdgeId,
57 pub branch_id: BranchId,
58 pub from: PortRef,
59 pub to: PortRef,
60}
61
62#[derive(Debug, Clone)]
63pub struct GraphSnapshot {
64 pub revision: u64,
65 pub nodes: Vec<NodeInfo>,
66 pub edges: Vec<EdgeInfo>,
67}
68
69impl GraphSnapshot {
70 pub fn node(&self, id: ElementId) -> Option<&NodeInfo> {
71 self.nodes.iter().find(|node| node.id == id)
72 }
73
74 pub fn topology(&self) -> String {
78 self.paths()
79 .into_iter()
80 .map(|path| {
81 path.into_iter()
82 .filter_map(|id| self.node(id))
83 .map(|node| format!("{:?}({})", node.element_type, node.name))
84 .collect::<Vec<_>>()
85 .join(" - ")
86 })
87 .collect::<Vec<_>>()
88 .join("\n")
89 }
90
91 pub(crate) fn topology_diagram(&self) -> String {
95 let roots: Vec<_> = self
96 .nodes
97 .iter()
98 .filter(|node| !self.edges.iter().any(|edge| edge.to.element == node.id))
99 .collect();
100 let mut output = String::new();
101
102 for (index, root) in roots.iter().enumerate() {
103 let is_last = index + 1 == roots.len();
104 let child_indent = if roots.len() == 1 {
105 let _ = write!(output, "{:?}({})#{}", root.element_type, root.name, root.id);
106 String::new()
107 } else {
108 let connector = if is_last { "└── " } else { "├── " };
109 let _ = write!(
110 output,
111 "{connector}{:?}({})#{}",
112 root.element_type, root.name, root.id
113 );
114 if is_last {
115 " ".to_owned()
116 } else {
117 "│ ".to_owned()
118 }
119 };
120 self.render_diagram_children(
121 root.id,
122 &child_indent,
123 &mut HashSet::from([root.id]),
124 &mut output,
125 );
126 if !is_last {
127 output.push('\n');
128 }
129 }
130
131 output
132 }
133
134 fn render_diagram_children(
135 &self,
136 parent: ElementId,
137 indent: &str,
138 visiting: &mut HashSet<ElementId>,
139 output: &mut String,
140 ) {
141 let children: Vec<_> = self
142 .edges
143 .iter()
144 .filter(|edge| edge.from.element == parent)
145 .collect();
146
147 for (index, edge) in children.iter().enumerate() {
148 let Some(child) = self.node(edge.to.element) else {
149 continue;
150 };
151 let is_last = index + 1 == children.len();
152 let connector = if is_last { "└── " } else { "├── " };
153 let link = format!("[{}] → ", edge.from.port);
154 let _ = write!(
155 output,
156 "\n{indent}{connector}{link}{:?}({})#{}",
157 child.element_type, child.name, child.id
158 );
159
160 if visiting.insert(child.id) {
161 let continuation = if is_last { " " } else { "│ " };
162 let child_indent =
163 format!("{indent}{continuation}{}", " ".repeat(link.chars().count()));
164 self.render_diagram_children(child.id, &child_indent, visiting, output);
165 visiting.remove(&child.id);
166 }
167 }
168 }
169
170 fn paths(&self) -> Vec<Vec<ElementId>> {
171 let leaves: Vec<_> = self
172 .nodes
173 .iter()
174 .filter(|node| !self.edges.iter().any(|edge| edge.from.element == node.id))
175 .collect();
176 let mut rendered = Vec::new();
177 for leaf in leaves {
178 self.paths_to(leaf.id, &mut HashSet::new(), &mut Vec::new(), &mut rendered);
179 }
180 rendered
181 }
182
183 fn paths_to(
184 &self,
185 current: ElementId,
186 visiting: &mut HashSet<ElementId>,
187 suffix: &mut Vec<ElementId>,
188 paths: &mut Vec<Vec<ElementId>>,
189 ) {
190 if !visiting.insert(current) {
191 return;
192 }
193 suffix.push(current);
194 let upstream: Vec<_> = self
195 .edges
196 .iter()
197 .filter(|edge| edge.to.element == current)
198 .map(|edge| edge.from.element)
199 .collect();
200 if upstream.is_empty() {
201 let mut path = suffix.clone();
202 path.reverse();
203 paths.push(path);
204 } else {
205 for parent in upstream {
206 self.paths_to(parent, visiting, suffix, paths);
207 }
208 }
209 suffix.pop();
210 visiting.remove(¤t);
211 }
212}
213
214pub(crate) fn log_topology(pp_log: &PpLog, event: &str, snapshot: &GraphSnapshot) {
223 if !enabled(Level::Info) {
224 return;
225 }
226 pp_info!(pp_log: pp_log, "{event}\n{}", snapshot.topology_diagram());
227}
228
229#[derive(Debug, ThisError, PartialEq, Eq)]
230pub enum GraphError {
231 #[error("source pad index {index} is out of range (source has {pad_count} pads)")]
232 PadOutOfRange { index: usize, pad_count: usize },
233
234 #[error("source pad '{0}' is already linked")]
235 PadAlreadyLinked(String),
236
237 #[error("element {0} is not attached to this pipeline")]
238 ParentNotAttached(ElementId),
239
240 #[error("element {0} is already attached to this pipeline")]
241 NodeAlreadyAttached(ElementId),
242
243 #[error("branch {0} is not attached")]
244 BranchNotAttached(BranchId),
245
246 #[error("a branch must contain at least one element")]
247 EmptyBranch,
248
249 #[error("ChainBuilder::pipe requires exactly one output pad, but {name} has {count}")]
250 NotSingleOutput { name: Arc<str>, count: usize },
251}
252
253#[derive(Debug, Clone)]
254pub(crate) struct PlannedEdge {
255 pub from: PortRef,
256 pub to: PortRef,
257}
258
259#[derive(Debug)]
260pub(crate) struct BranchPlan {
261 pub nodes: Vec<NodeInfo>,
262 pub edges: Vec<PlannedEdge>,
263 pub root: ElementId,
264}
265
266#[derive(Debug)]
267struct BranchRecord {
268 parent: ElementId,
269 owned_nodes: HashSet<ElementId>,
270}
271
272#[derive(Default)]
273struct GraphState {
274 next_element_id: u64,
275 next_edge_id: u64,
276 next_branch_id: u64,
277 revision: u64,
278 nodes: Vec<NodeInfo>,
279 edges: Vec<EdgeInfo>,
280 branches: HashMap<BranchId, BranchRecord>,
281}
282
283#[derive(Clone, Default)]
286pub struct PipelineGraph(Arc<Mutex<GraphState>>);
287
288impl PipelineGraph {
289 pub fn new() -> Self {
290 Self::default()
291 }
292
293 pub fn snapshot(&self) -> GraphSnapshot {
294 let state = self.0.lock().unwrap();
295 GraphSnapshot {
296 revision: state.revision,
297 nodes: state.nodes.clone(),
298 edges: state.edges.clone(),
299 }
300 }
301
302 pub fn branch_containing(&self, element: ElementId) -> Option<BranchId> {
303 let state = self.0.lock().unwrap();
304 state
305 .branches
306 .iter()
307 .find_map(|(id, branch)| branch.owned_nodes.contains(&element).then_some(*id))
308 }
309
310 pub(crate) fn reserve_element_id(&self) -> ElementId {
311 let mut state = self.0.lock().unwrap();
312 state.next_element_id += 1;
313 ElementId(state.next_element_id)
314 }
315
316 pub(crate) fn add_source(&self, element_type: ElementType, name: Arc<str>) -> ElementId {
317 let id = self.reserve_element_id();
318 let mut state = self.0.lock().unwrap();
319 state.nodes.push(NodeInfo {
320 id,
321 element_type,
322 name,
323 });
324 state.revision += 1;
325 id
326 }
327
328 pub(crate) fn attach_with(
331 &self,
332 parent: ElementId,
333 from_port: Arc<str>,
334 plan: BranchPlan,
335 attach_runtime: impl FnOnce(BranchId) -> Result<(), GraphError>,
336 ) -> Result<BranchId, GraphError> {
337 let mut state = self.0.lock().unwrap();
338 if !state.nodes.iter().any(|node| node.id == parent) {
339 return Err(GraphError::ParentNotAttached(parent));
340 }
341 if plan.nodes.is_empty() {
342 return Err(GraphError::EmptyBranch);
343 }
344 for node in &plan.nodes {
345 if state.nodes.iter().any(|current| current.id == node.id) {
346 return Err(GraphError::NodeAlreadyAttached(node.id));
347 }
348 }
349
350 state.next_branch_id += 1;
351 let branch_id = BranchId(state.next_branch_id);
352 attach_runtime(branch_id)?;
353
354 let mut edges = Vec::with_capacity(plan.edges.len() + 1);
355 state.next_edge_id += 1;
356 edges.push(EdgeInfo {
357 id: EdgeId(state.next_edge_id),
358 branch_id,
359 from: PortRef {
360 element: parent,
361 port: from_port,
362 },
363 to: PortRef {
364 element: plan.root,
365 port: "sink".into(),
366 },
367 });
368 for edge in plan.edges {
369 state.next_edge_id += 1;
370 edges.push(EdgeInfo {
371 id: EdgeId(state.next_edge_id),
372 branch_id,
373 from: edge.from,
374 to: edge.to,
375 });
376 }
377
378 let owned_nodes = plan.nodes.iter().map(|node| node.id).collect();
379 state.nodes.extend(plan.nodes);
380 state.edges.extend(edges);
381 state.branches.insert(
382 branch_id,
383 BranchRecord {
384 parent,
385 owned_nodes,
386 },
387 );
388 state.revision += 1;
389 Ok(branch_id)
390 }
391
392 pub(crate) fn detach_with(
395 &self,
396 branch_id: BranchId,
397 detach_runtime: impl FnOnce() -> Result<(), GraphError>,
398 ) -> Result<(), GraphError> {
399 let mut state = self.0.lock().unwrap();
400 if !state.branches.contains_key(&branch_id) {
401 return Err(GraphError::BranchNotAttached(branch_id));
402 }
403 detach_runtime()?;
404
405 let mut removed_branches = HashSet::from([branch_id]);
406 let mut removed_nodes = HashSet::new();
407 loop {
408 for id in removed_branches.clone() {
409 if let Some(branch) = state.branches.get(&id) {
410 removed_nodes.extend(branch.owned_nodes.iter().copied());
411 }
412 }
413 let before = removed_branches.len();
414 for (id, branch) in &state.branches {
415 if removed_nodes.contains(&branch.parent) {
416 removed_branches.insert(*id);
417 }
418 }
419 if removed_branches.len() == before {
420 break;
421 }
422 }
423
424 state
425 .branches
426 .retain(|id, _| !removed_branches.contains(id));
427 state.nodes.retain(|node| !removed_nodes.contains(&node.id));
428 state
429 .edges
430 .retain(|edge| !removed_branches.contains(&edge.branch_id));
431 state.revision += 1;
432 Ok(())
433 }
434}