Skip to main content

media_pp/core/
graph.rs

1//! The pipeline's topology, recorded separately from the elements themselves.
2//!
3//! Elements own their pads and their downstream peers; nothing in that chain
4//! can answer "what does this pipeline look like right now". [`PipelineGraph`]
5//! keeps that record: stable [`ElementId`]/[`EdgeId`]/[`BranchId`] values that
6//! survive same-name churn, and [`GraphSnapshot`] as a consistent copy to read
7//! outside the lock.
8//!
9//! Names are labels only. IDs are what attachment, detachment, and lookup use,
10//! and what lets a log record name one specific element when several share a
11//! name.
12
13use 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/// Stable identity of one element inside a pipeline graph. Names are only
28/// labels; IDs are what graph mutation and lookup use.
29#[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/// Stable identity of one connection between two element ports.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct EdgeId(u64);
41
42/// Identity of one attached branch. A branch owns every node and edge that
43/// arrived in the same attachment transaction.
44#[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)]
54/// One element as it appears in a [`GraphSnapshot`] — its stable identity, its
55/// type, and the name its caller chose.
56pub struct NodeInfo {
57    /// Stable identity assigned by the owning pipeline graph.
58    pub id: ElementId,
59    /// Built-in kind reported by the element.
60    pub element_type: ElementType,
61    /// Caller-selected instance name; names need not be unique within a graph.
62    pub name: Arc<str>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66/// One end of an edge: the element, and the name of the pad on it.
67pub struct PortRef {
68    /// Stable identity of the element that owns this port.
69    pub element: ElementId,
70    /// Element-defined port name used in topology output.
71    pub port: Arc<str>,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75/// One link from a source pad to a downstream element, together with the
76/// branch whose attachment created it.
77pub struct EdgeInfo {
78    /// Stable identity of this connection.
79    pub id: EdgeId,
80    /// Attachment transaction that created this edge.
81    pub branch_id: BranchId,
82    /// Upstream source pad.
83    pub from: PortRef,
84    /// Downstream sink port.
85    pub to: PortRef,
86}
87
88#[derive(Debug, Clone)]
89/// A consistent copy of the whole topology, taken under the graph's lock and
90/// then read without it.
91///
92/// `revision` increments on every mutation, so two snapshots can be told apart
93/// even when they describe the same shape. This is also what
94/// [`crate::pipeline::Pipeline::topology`] renders and what a `run`/`attach`/
95/// `detach` log record embeds.
96pub struct GraphSnapshot {
97    /// Monotonically increasing graph version. Each successful mutation
98    /// increments it exactly once.
99    pub revision: u64,
100    /// Elements attached when this snapshot was taken.
101    pub nodes: Vec<NodeInfo>,
102    /// Connections attached when this snapshot was taken.
103    pub edges: Vec<EdgeInfo>,
104}
105
106impl GraphSnapshot {
107    /// Finds one node by stable identity within this snapshot.
108    pub fn node(&self, id: ElementId) -> Option<&NodeInfo> {
109        self.nodes.iter().find(|node| node.id == id)
110    }
111
112    /// Renders every root-to-leaf path in insertion order. Keeping edges
113    /// separate from nodes means this also remains meaningful for fan-in
114    /// graphs, where a node can have more than one upstream.
115    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    /// Logging-only flow diagram. Each child connector starts under its
130    /// upstream element, so a fan-out is visible at the element where it
131    /// actually occurs instead of repeating the common path for every leaf.
132    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(&current);
249    }
250}
251
252/// Emits `event` and the topology it produced as **one** record: the event
253/// word on the header line, the diagram in the body.
254///
255/// They cannot be two records. The private logger queues each `write_all`
256/// separately, so only the lines inside a single record are guaranteed to stay
257/// together — any live thread (a `Queue` worker this very call just started,
258/// say) can write between two of them. Emitting the diagram separately would
259/// mean it is merely *usually* adjacent to the event that caused it.
260pub(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)]
268/// A rejected topology change.
269///
270/// Every variant here is a refusal, not a partial result: attachment validates
271/// before it mutates, so a graph that returns one of these is left exactly as
272/// it was.
273pub enum GraphError {
274    /// The requested source pad index does not exist.
275    #[error("source pad index {index} is out of range (source has {pad_count} pads)")]
276    PadOutOfRange {
277        /// Requested zero-based source pad index.
278        index: usize,
279        /// Number of source pads available on the element.
280        pad_count: usize,
281    },
282
283    /// Attachment targeted a source pad that already owns a downstream sink.
284    #[error("source pad '{0}' is already linked")]
285    PadAlreadyLinked(String),
286
287    /// A dynamic attachment named an element that is no longer in this graph.
288    #[error("element {0} is not attached to this pipeline")]
289    ParentNotAttached(ElementId),
290
291    /// An attachment plan attempted to publish an already-attached node.
292    #[error("element {0} is already attached to this pipeline")]
293    NodeAlreadyAttached(ElementId),
294
295    /// A detach operation named a branch that is no longer attached.
296    #[error("branch {0} is not attached")]
297    BranchNotAttached(BranchId),
298
299    /// An attachment plan contained no terminal or processing element.
300    #[error("a branch must contain at least one element")]
301    EmptyBranch,
302
303    /// [`crate::pipeline::ChainBuilder::pipe`] received a filter whose output
304    /// shape cannot be represented as one linear chain stage.
305    #[error("ChainBuilder::pipe requires exactly one output pad, but {name} has {count}")]
306    NotSingleOutput {
307        /// Caller-selected name of the rejected filter.
308        name: Arc<str>,
309        /// Number of source pads exposed by the rejected filter.
310        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/// Live, transactionally-updated graph behind [`crate::pipeline::Pipeline`].
345/// A snapshot never observes half of an attach/detach operation.
346#[derive(Clone, Default)]
347pub struct PipelineGraph(Arc<Mutex<GraphState>>);
348
349impl PipelineGraph {
350    /// Creates an empty graph with revision zero and fresh ID counters.
351    pub fn new() -> Self {
352        Self::default()
353    }
354
355    /// Copies nodes, edges, and revision under the graph lock, then releases
356    /// the lock before returning.
357    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    /// Returns the attached branch that owns `element`, if the element was
367    /// introduced by a branch attachment transaction.
368    ///
369    /// Source nodes are created directly and therefore return `None`.
370    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    /// Validates a complete branch, performs its runtime mutation while the
397    /// graph is locked, then commits every node and edge as one revision.
398    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    /// Performs runtime detach first, then removes this branch and any
461    /// branches attached below nodes it owned as one graph revision.
462    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}