Skip to main content

uiharu_core/
exec.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Running a built graph: feeding sources, polling the nodes until nothing can move, and
4//! reading the exposed outputs.
5//!
6//! Everything runs on the caller's thread, in a fixed order, so the same input through the
7//! same graph always yields the same events, however it is cut into blocks and however often
8//! the caller stops to read the outputs.
9
10use std::any::Any;
11use std::cell::RefCell;
12use std::fmt;
13use std::rc::Rc;
14use std::sync::Arc;
15use std::task::{Context, Waker};
16
17use crate::graph::{
18    DynExposedRef, ExposedRef, Exposure, GraphConfig, Owner, PortKey, SourceId, SourceSpec,
19};
20use crate::node::{Io, Node, NodeCx, NodeFuture, NodeSpec, SharedQueue, Wait};
21use crate::schema::Props;
22use crate::signal::{LogicBlock, SignalItem};
23use crate::stream::{Broadcast, Event, Item, QueueControl, QueueHandles, ReaderId};
24use crate::time::{Tick, TimeBase};
25
26/// Why a source did not take an item.
27#[derive(Debug, thiserror::Error)]
28pub enum PushError {
29    /// The source holds as many blocks as it may; run the pipeline and read its outputs, then
30    /// push the item again. If the samples cannot wait, push a gap for them instead.
31    #[error("the source is full")]
32    Full(SignalItem),
33    /// The item does not follow the previous one; lost samples must be announced as a gap.
34    #[error("the item starts at tick {found} but the source stands at {expected}")]
35    Discontinuity {
36        /// The tick the source expects next.
37        expected: Tick,
38        /// The tick the item starts at.
39        found: Tick,
40    },
41    /// The block has another channel count than the source.
42    #[error("the block has {found} channels but the source has {expected}")]
43    Channels {
44        /// The source's channels.
45        expected: u8,
46        /// The block's channels.
47        found: u8,
48    },
49    /// The source has ended.
50    #[error("the source has ended")]
51    Ended,
52    /// The source is full, the last run moved nothing and no exposed output has anything to
53    /// read: the pipeline can make no progress, however it is driven. A node that waits on
54    /// one input while another holds the source's queue does this; see design.md §4.
55    #[error(
56        "the pipeline is stalled: the source is full, nothing can be read and every node waits ({waiting:?})"
57    )]
58    Stalled {
59        /// What each unfinished node waited for when the last run ended.
60        waiting: Vec<(String, Wait)>,
61    },
62    /// No such source.
63    #[error("no source {0} in the pipeline")]
64    UnknownSource(usize),
65}
66
67struct SourceState {
68    name: String,
69    channels: u8,
70    time_base: TimeBase,
71    queue: SharedQueue<Arc<LogicBlock>>,
72    handles: QueueHandles,
73    /// The tick the next item starts at; the first item decides where the stream starts.
74    next: Option<Tick>,
75    ended: bool,
76}
77
78struct Running {
79    name: String,
80    cx: Rc<NodeCx>,
81    /// `None` once the node has finished.
82    future: Option<NodeFuture>,
83}
84
85struct QueueEntry {
86    name: String,
87    control: Rc<dyn QueueControl>,
88}
89
90struct ExposedEntry {
91    name: String,
92    queue: Rc<dyn Any>,
93    control: Rc<dyn QueueControl>,
94    /// The reader's id, kept to count what it has not read.
95    reader_id: ReaderId,
96    /// The reader, until the caller takes it.
97    reader: Option<ReaderId>,
98}
99
100/// What the last run left behind, to tell a stall from a source that is merely full.
101#[derive(Clone, Debug)]
102struct LastRun {
103    ops: u64,
104    exposed_pending: usize,
105    waiting: Vec<(String, Wait)>,
106}
107
108/// What one run of the pipeline did.
109#[derive(Clone, Debug, Default, PartialEq, Eq)]
110pub struct RunReport {
111    /// Operations the nodes completed.
112    pub ops: u64,
113    /// Times a node was polled.
114    pub polls: u64,
115    /// Passes over the nodes.
116    pub rounds: u32,
117    /// What each unfinished node waits for now, by name.
118    pub waiting: Vec<(String, Wait)>,
119}
120
121/// The state of one queue.
122#[derive(Clone, Debug, PartialEq, Eq)]
123pub struct QueueStat {
124    /// The queue, as `owner.port`.
125    pub name: String,
126    /// Items held.
127    pub len: usize,
128    /// Items it holds at most.
129    pub capacity: usize,
130    /// The most it held at any time.
131    pub high_water: usize,
132    /// Whether its stream has ended.
133    pub ended: bool,
134}
135
136/// The state of every queue of a pipeline.
137#[derive(Clone, Debug, Default, PartialEq, Eq)]
138pub struct Stats {
139    /// The source queues, then the node outputs in node order.
140    pub queues: Vec<QueueStat>,
141}
142
143impl Stats {
144    /// The most items any queue held at one time.
145    pub fn high_water(&self) -> usize {
146        self.queues
147            .iter()
148            .map(|queue| queue.high_water)
149            .max()
150            .unwrap_or(0)
151    }
152
153    /// Whether every queue stayed within its capacity (which the queues enforce; this checks
154    /// the bookkeeping).
155    pub fn within_capacity(&self) -> bool {
156        self.queues
157            .iter()
158            .all(|queue| queue.high_water <= queue.capacity)
159    }
160}
161
162/// A built graph, ready to take samples and run.
163pub struct Pipeline {
164    budget: u32,
165    sources: Vec<SourceState>,
166    /// In an order where every node comes after the ones it reads.
167    nodes: Vec<Running>,
168    queues: Vec<QueueEntry>,
169    exposed: Vec<ExposedEntry>,
170    /// What the last `run` did, until the next push changes things.
171    last_run: Option<LastRun>,
172}
173
174impl fmt::Debug for Pipeline {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("Pipeline")
177            .field("sources", &self.sources.len())
178            .field("nodes", &self.nodes.len())
179            .field("exposed", &self.exposed.len())
180            .finish()
181    }
182}
183
184impl Pipeline {
185    #[allow(clippy::too_many_arguments)]
186    pub(crate) fn start(
187        config: GraphConfig,
188        sources: Vec<SourceSpec>,
189        nodes: Vec<Box<dyn Node>>,
190        specs: Vec<NodeSpec>,
191        inputs: Vec<Vec<Option<PortKey>>>,
192        order: Vec<usize>,
193        domains: Vec<Option<usize>>,
194        input_props: Vec<Vec<Props>>,
195        exposed: Vec<Exposure>,
196    ) -> Result<Self, crate::graph::GraphError> {
197        let mut queues = Vec::new();
198        let sources: Vec<SourceState> = sources
199            .into_iter()
200            .enumerate()
201            .map(|(index, spec)| {
202                let handles = spec.output().make_queue(spec.capacity);
203                queues.push(QueueEntry {
204                    name: format!("{}#{index}.samples", spec.name),
205                    control: Rc::clone(&handles.control),
206                });
207                let queue = Rc::clone(&handles.any)
208                    .downcast::<RefCell<Broadcast<Arc<LogicBlock>>>>()
209                    .unwrap_or_else(|_| unreachable!("source queues carry blocks"));
210                SourceState {
211                    name: spec.name,
212                    channels: spec.channels,
213                    time_base: spec.time_base,
214                    queue,
215                    handles,
216                    next: None,
217                    ended: false,
218                }
219            })
220            .collect();
221
222        // Every output queue first, so that inputs can be bound to them in any order.
223        let outputs: Vec<Vec<QueueHandles>> = specs
224            .iter()
225            .enumerate()
226            .map(|(index, spec)| {
227                spec.outputs
228                    .iter()
229                    .map(|output| {
230                        let handles = output.make_queue(config.queue_capacity);
231                        queues.push(QueueEntry {
232                            name: format!("{}#{index}.{}", spec.name, output.name),
233                            control: Rc::clone(&handles.control),
234                        });
235                        handles
236                    })
237                    .collect()
238            })
239            .collect();
240        let queue_of = |key: PortKey| -> QueueHandles {
241            match key.owner {
242                Owner::Source(index) => sources[index].handles.clone(),
243                Owner::Node(index) => outputs[index][key.index].clone(),
244            }
245        };
246
247        // Exposed readers attach before any node runs, so they see every item.
248        let exposed = exposed
249            .into_iter()
250            .map(|exposure| {
251                let handles = queue_of(exposure.from);
252                let reader = handles.control.add_reader();
253                ExposedEntry {
254                    name: exposure.name,
255                    queue: handles.any,
256                    control: handles.control,
257                    reader_id: reader,
258                    reader: Some(reader),
259                }
260            })
261            .collect();
262
263        let mut running: Vec<Option<Running>> = (0..nodes.len()).map(|_| None).collect();
264        let mut specs = specs.into_iter().map(Some).collect::<Vec<_>>();
265        let mut nodes = nodes.into_iter().map(Some).collect::<Vec<_>>();
266        for &index in &order {
267            let (Some(node), Some(spec)) = (nodes[index].take(), specs[index].take()) else {
268                continue;
269            };
270            let cx = NodeCx::new();
271            let bound_inputs = inputs[index]
272                .iter()
273                .flatten()
274                .map(|key| queue_of(*key).any)
275                .collect();
276            let bound_outputs = outputs[index]
277                .iter()
278                .map(|handles| Rc::clone(&handles.any))
279                .collect();
280            let time_base = domains[index].map(|domain| sources[domain].time_base);
281            let io = Io::new(
282                Rc::clone(&cx),
283                bound_inputs,
284                bound_outputs,
285                time_base,
286                input_props[index].clone(),
287            );
288            let future = node.start(io);
289            running[index] = Some(Running {
290                name: format!("{}#{index}", spec.name),
291                cx,
292                future: Some(future),
293            });
294        }
295        let nodes = order
296            .into_iter()
297            .filter_map(|index| running[index].take())
298            .collect();
299        Ok(Self {
300            budget: config.budget,
301            sources,
302            nodes,
303            queues,
304            exposed,
305            last_run: None,
306        })
307    }
308
309    /// The names and time bases of the sources, in id order.
310    pub fn sources(&self) -> impl Iterator<Item = (SourceId, &str, TimeBase)> {
311        self.sources
312            .iter()
313            .enumerate()
314            .map(|(index, source)| (SourceId(index), source.name.as_str(), source.time_base))
315    }
316
317    /// Feeds `item` to `source`. The first item decides where the stream starts, and a
318    /// progress item at that tick goes ahead of it so that readers know nothing came before;
319    /// after it, blocks must follow each other or a gap without a hole. The item is handed
320    /// back when the source is full.
321    pub fn push(&mut self, source: SourceId, item: SignalItem) -> Result<(), PushError> {
322        let state = self
323            .sources
324            .get_mut(source.0)
325            .ok_or(PushError::UnknownSource(source.0))?;
326        if state.ended {
327            return Err(PushError::Ended);
328        }
329        let expected = state.next.unwrap_or(match &item {
330            SignalItem::Data(block) => block.start(),
331            SignalItem::Gap(range) => range.start,
332            SignalItem::Progress(tick) | SignalItem::End(tick) => *tick,
333        });
334        let found = match &item {
335            SignalItem::Data(block) => {
336                if block.channels() != state.channels {
337                    return Err(PushError::Channels {
338                        expected: state.channels,
339                        found: block.channels(),
340                    });
341                }
342                block.start()
343            }
344            SignalItem::Gap(range) => range.start,
345            SignalItem::End(tick) => *tick,
346            SignalItem::Progress(tick) => {
347                if *tick < expected {
348                    return Err(PushError::Discontinuity {
349                        expected,
350                        found: *tick,
351                    });
352                }
353                expected
354            }
355        };
356        if found != expected {
357            return Err(PushError::Discontinuity { expected, found });
358        }
359        // The first item decides where the stream starts; a progress item ahead of it tells
360        // every reader so, which decoders built on time need to know (nothing came before).
361        if state.next.is_none() {
362            let mut queue = state.queue.borrow_mut();
363            if queue.is_full() {
364                drop(queue);
365                return Err(full_error(self.last_run.as_ref(), &self.exposed, item));
366            }
367            let _ = queue.try_push(Item::Progress(expected));
368            drop(queue);
369            state.next = Some(expected);
370        }
371        let (queued, next, ended) = match item {
372            SignalItem::Data(block) => {
373                let end = block.end();
374                let last = Tick(end.0 - 1);
375                let event = Event {
376                    seq: 0,
377                    span: block.range(),
378                    settled: last,
379                    payload: block,
380                };
381                (Item::Event(event), end, false)
382            }
383            SignalItem::Gap(range) => (Item::Gap(range), range.end.max(expected), false),
384            SignalItem::Progress(tick) => (Item::Progress(tick), expected, false),
385            SignalItem::End(tick) => (Item::End(tick), tick, true),
386        };
387        if let Err(item) = state.queue.borrow_mut().try_push(queued) {
388            let item = match item {
389                Item::Event(event) => SignalItem::Data(event.payload),
390                Item::Gap(range) => SignalItem::Gap(range),
391                Item::Progress(tick) => SignalItem::Progress(tick),
392                Item::End(tick) => SignalItem::End(tick),
393            };
394            return Err(full_error(self.last_run.as_ref(), &self.exposed, item));
395        }
396        state.next = Some(next);
397        state.ended = ended;
398        self.last_run = None;
399        Ok(())
400    }
401
402    /// The tick `source` expects next; `None` before its first item, which may start
403    /// anywhere.
404    pub fn next_tick(&self, source: SourceId) -> Option<Tick> {
405        self.sources.get(source.0).and_then(|state| state.next)
406    }
407
408    /// Whether `source` has room for another item.
409    pub fn has_room(&self, source: SourceId) -> bool {
410        self.sources
411            .get(source.0)
412            .is_some_and(|state| !state.queue.borrow().is_full())
413    }
414
415    /// Polls the nodes, in order, until none of them can go on: every unfinished node waits
416    /// for input that has not arrived or for an output the caller has not read.
417    pub fn run(&mut self) -> RunReport {
418        let mut report = RunReport::default();
419        let mut context = Context::from_waker(Waker::noop());
420        loop {
421            report.rounds += 1;
422            let mut moved = false;
423            for node in &mut self.nodes {
424                let Some(future) = node.future.as_mut() else {
425                    continue;
426                };
427                node.cx.refill(self.budget);
428                let before = node.cx.ops();
429                report.polls += 1;
430                let poll = future.as_mut().poll(&mut context);
431                let done = node.cx.ops() - before;
432                report.ops += done;
433                moved |= done > 0;
434                // A node that only ran out of budget goes on in the next round, after the
435                // others have had their turn.
436                if poll.is_ready() {
437                    node.future = None;
438                }
439            }
440            if !moved {
441                break;
442            }
443        }
444        report.waiting = self
445            .nodes
446            .iter()
447            .filter(|node| node.future.is_some())
448            .map(|node| (node.name.clone(), node.cx.waiting()))
449            .collect();
450        self.last_run = Some(LastRun {
451            ops: report.ops,
452            exposed_pending: exposed_pending(&self.exposed),
453            waiting: report.waiting.clone(),
454        });
455        report
456    }
457
458    /// Whether every node has finished: their inputs ended and they wrote all they had.
459    pub fn is_finished(&self) -> bool {
460        self.nodes.iter().all(|node| node.future.is_none())
461    }
462
463    /// The state of every queue.
464    pub fn stats(&self) -> Stats {
465        Stats {
466            queues: self
467                .queues
468                .iter()
469                .map(|entry| QueueStat {
470                    name: entry.name.clone(),
471                    len: entry.control.len(),
472                    capacity: entry.control.capacity(),
473                    high_water: entry.control.high_water(),
474                    ended: entry.control.is_ended(),
475                })
476                .collect(),
477        }
478    }
479
480    /// Takes the reader of an exposed output; `None` once taken.
481    ///
482    /// # Panics
483    ///
484    /// When `T` is not the output's event type, which a typed [`ExposedRef`] rules out.
485    pub fn exposed<T: 'static>(&mut self, exposed: ExposedRef<T>) -> Option<Exposed<T>> {
486        let name = self.exposed.get(exposed.index)?.name.clone();
487        let taken = self.exposed_dyn::<T>(DynExposedRef {
488            index: exposed.index,
489        });
490        if taken.is_none() && self.exposed[exposed.index].reader.is_some() {
491            panic!("exposed output {name} carries another event type");
492        }
493        taken
494    }
495
496    /// Takes the reader of an output exposed by name, as `T` events; `None` once taken or when
497    /// the output carries another event type, which a caller working from a description finds
498    /// out this way.
499    pub fn exposed_dyn<T: 'static>(&mut self, exposed: DynExposedRef) -> Option<Exposed<T>> {
500        let entry = self.exposed.get_mut(exposed.index)?;
501        let queue = Rc::clone(&entry.queue)
502            .downcast::<RefCell<Broadcast<T>>>()
503            .ok()?;
504        let reader = entry.reader.take()?;
505        Some(Exposed {
506            name: entry.name.clone(),
507            queue,
508            reader,
509        })
510    }
511}
512
513/// The error for a full source: a stall when the last run moved nothing and nothing has been
514/// readable since — the caller can neither push, read nor run anything to a change.
515fn full_error(last_run: Option<&LastRun>, exposed: &[ExposedEntry], item: SignalItem) -> PushError {
516    match last_run {
517        Some(last)
518            if last.ops == 0 && last.exposed_pending == 0 && exposed_pending(exposed) == 0 =>
519        {
520            PushError::Stalled {
521                waiting: last.waiting.clone(),
522            }
523        }
524        _ => PushError::Full(item),
525    }
526}
527
528/// The items the caller's exposed readers have not read; an output exposed but never taken
529/// counts as read, since nobody will ever drain it.
530fn exposed_pending(exposed: &[ExposedEntry]) -> usize {
531    exposed
532        .iter()
533        .filter(|entry| entry.reader.is_none())
534        .map(|entry| entry.control.pending(entry.reader_id))
535        .sum()
536}
537
538/// The caller's reader on an exposed output.
539pub struct Exposed<T> {
540    name: String,
541    queue: SharedQueue<T>,
542    reader: ReaderId,
543}
544
545impl<T> fmt::Debug for Exposed<T> {
546    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
547        f.debug_struct("Exposed")
548            .field("name", &self.name)
549            .finish_non_exhaustive()
550    }
551}
552
553impl<T> Exposed<T> {
554    /// The name the output was exposed under.
555    pub fn name(&self) -> &str {
556        &self.name
557    }
558
559    /// Looks at the next item without taking it.
560    pub fn with_next<R>(&self, f: impl FnOnce(&Item<T>) -> R) -> Option<R> {
561        self.queue.borrow().peek(self.reader).map(f)
562    }
563
564    /// Moves past the next item; the end of the stream stays.
565    pub fn advance(&self) {
566        self.queue.borrow_mut().advance(self.reader);
567    }
568
569    /// Takes the next item, if one is waiting. The end of the stream is returned again and
570    /// again; use [`Exposed::is_ended`] to stop.
571    pub fn next(&self) -> Option<Item<T>>
572    where
573        T: Clone,
574    {
575        let item = self.with_next(Item::clone)?;
576        self.advance();
577        Some(item)
578    }
579
580    /// Whether the next item is the end of the stream.
581    pub fn is_ended(&self) -> bool {
582        self.with_next(Item::is_end).unwrap_or(false)
583    }
584
585    /// The items waiting to be read.
586    pub fn pending(&self) -> usize {
587        self.queue.borrow().pending(self.reader)
588    }
589}
590
591impl<T> Drop for Exposed<T> {
592    fn drop(&mut self) {
593        self.queue.borrow_mut().remove_reader(self.reader);
594    }
595}