Skip to main content

media_pp\core/
graph.rs

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/// Stable identity of one element inside a pipeline graph. Names are only
16/// labels; IDs are what graph mutation and lookup use.
17#[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/// Stable identity of one connection between two element ports.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
28pub struct EdgeId(u64);
29
30/// Identity of one attached branch. A branch owns every node and edge that
31/// arrived in the same attachment transaction.
32#[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    /// Renders every root-to-leaf path in insertion order. Keeping edges
75    /// separate from nodes means this also remains meaningful for fan-in
76    /// graphs, where a node can have more than one upstream.
77    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    /// Logging-only flow diagram. Each child connector starts under its
92    /// upstream element, so a fan-out is visible at the element where it
93    /// actually occurs instead of repeating the common path for every leaf.
94    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(&current);
211    }
212}
213
214/// Emits `event` and the topology it produced as **one** record: the event
215/// word on the header line, the diagram in the body.
216///
217/// They cannot be two records. The private logger queues each `write_all`
218/// separately, so only the lines inside a single record are guaranteed to stay
219/// together — any live thread (a `Queue` worker this very call just started,
220/// say) can write between two of them. Emitting the diagram separately would
221/// mean it is merely *usually* adjacent to the event that caused it.
222pub(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/// Live, transactionally-updated graph behind [`crate::pipeline::Pipeline`].
284/// A snapshot never observes half of an attach/detach operation.
285#[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    /// Validates a complete branch, performs its runtime mutation while the
329    /// graph is locked, then commits every node and edge as one revision.
330    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    /// Performs runtime detach first, then removes this branch and any
393    /// branches attached below nodes it owned as one graph revision.
394    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}