1use alloc::string::String;
19use alloc::sync::Arc;
20use alloc::vec::Vec;
21
22use spin::Mutex;
23
24use crate::caps::Caps;
25use crate::graph::NodeKind;
26use crate::runtime::channel::ProbeSlot;
27use crate::runtime::instrument::{EdgeCounters, EdgeCounts, Probe, StageVisit};
28use crate::runtime::ElementLatency;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum NodeRole {
35 Source,
36 Transform,
37 Sink,
38 Tee,
39 Muxer,
40}
41
42impl From<NodeKind> for NodeRole {
43 fn from(k: NodeKind) -> Self {
44 match k {
45 NodeKind::Source => NodeRole::Source,
46 NodeKind::Transform => NodeRole::Transform,
47 NodeKind::Sink => NodeRole::Sink,
48 NodeKind::Tee(_) => NodeRole::Tee,
49 NodeKind::Muxer(_) | NodeKind::FaninSink(_) => NodeRole::Muxer,
50 NodeKind::FanoutSrc(_) => NodeRole::Source,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
60pub struct Observer {
61 inner: Arc<Inner>,
62}
63
64#[derive(Debug)]
65struct Inner {
66 start_ns: u64,
67 state: Mutex<State>,
68}
69
70#[derive(Debug, Default)]
71struct State {
72 names: Vec<String>,
75 roles: Vec<NodeRole>,
76 probes: Vec<Probe>,
79 edges: Vec<EdgeInfo>,
80 edge_probes: Vec<ProbeSlot>,
84 edge_caps: Vec<Caps>,
85 edge_counters: Vec<Option<Arc<EdgeCounters>>>,
88 link_capacity: usize,
91}
92
93impl Observer {
94 pub fn new() -> Self {
95 Self {
96 inner: Arc::new(Inner {
97 start_ns: crate::metrics::monotonic_ns(),
98 state: Mutex::new(State::default()),
99 }),
100 }
101 }
102
103 pub(crate) fn register(
108 &self,
109 names: Vec<String>,
110 roles: Vec<NodeRole>,
111 probes: Vec<Probe>,
112 edges: Vec<EdgeInfo>,
113 ) {
114 let mut s = self.inner.state.lock();
115 s.names = names;
116 s.roles = roles;
117 s.probes = probes;
118 s.edges = edges;
119 }
120
121 pub(crate) fn register_edges(
127 &self,
128 edge_probes: Vec<ProbeSlot>,
129 edge_caps: Vec<Caps>,
130 edge_counters: Vec<Option<Arc<EdgeCounters>>>,
131 ) {
132 let mut s = self.inner.state.lock();
133 s.edge_probes = edge_probes;
134 s.edge_caps = edge_caps;
135 s.edge_counters = edge_counters;
136 }
137
138 pub(crate) fn add_node(&self, name: String, role: NodeRole, probe: Probe) -> usize {
144 let mut s = self.inner.state.lock();
145 let id = s.names.len();
146 s.names.push(name);
147 s.roles.push(role);
148 s.probes.push(probe);
149 id
150 }
151
152 pub(crate) fn add_edge(&self, from: usize, to: usize, caps: Caps, tap: EdgeTap) {
156 let mut s = self.inner.state.lock();
157 s.edges.push(EdgeInfo {
158 from,
159 to,
160 ..Default::default()
161 });
162 s.edge_probes.push(tap.probe);
163 s.edge_caps.push(caps);
164 s.edge_counters.push(tap.counters);
165 }
166
167 pub(crate) fn set_link_capacity(&self, capacity: usize) {
171 self.inner.state.lock().link_capacity = capacity;
172 }
173
174 pub fn edge_probe(&self, idx: usize) -> Option<ProbeSlot> {
178 self.inner.state.lock().edge_probes.get(idx).cloned()
179 }
180
181 pub fn edge_caps(&self, idx: usize) -> Option<Caps> {
184 self.inner.state.lock().edge_caps.get(idx).cloned()
185 }
186
187 pub fn edge_count(&self) -> usize {
189 self.inner.state.lock().edges.len()
190 }
191
192 pub fn snapshot(&self) -> TelemetrySnapshot {
196 let s = self.inner.state.lock();
197 let nodes = s
198 .names
199 .iter()
200 .zip(s.roles.iter())
201 .zip(s.probes.iter())
202 .enumerate()
203 .map(|(id, ((name, role), probe))| NodeTelemetry {
204 id,
205 name: name.clone(),
206 role: *role,
207 latency: probe.as_ref().map(|p| p.snapshot()),
208 })
209 .collect();
210 let edges = s
214 .edges
215 .iter()
216 .enumerate()
217 .map(|(i, e)| {
218 let counters = s.edge_counters.get(i).and_then(|c| c.as_ref());
219 EdgeInfo {
220 from: e.from,
221 to: e.to,
222 caps: s.edge_caps.get(i).map(|c| c.to_gst_string()),
223 observed_caps: counters
224 .and_then(|c| c.last_caps())
225 .map(|c| c.to_gst_string()),
226 counts: counters.map(|c| c.snapshot()).unwrap_or_default(),
227 }
228 })
229 .collect();
230 TelemetrySnapshot {
231 uptime_ns: crate::metrics::monotonic_ns().saturating_sub(self.inner.start_ns),
232 nodes,
233 edges,
234 journey: assemble_journey(&s),
235 }
236 }
237}
238
239fn linear_chain(state: &State) -> Option<(Vec<usize>, bool)> {
245 let n = state.roles.len();
246 let mut in_deg = alloc::vec![0usize; n];
247 let mut out_deg = alloc::vec![0usize; n];
248 for e in &state.edges {
249 if e.from < n && e.to < n {
250 out_deg[e.from] += 1;
251 in_deg[e.to] += 1;
252 }
253 }
254 let start = (0..n).find(|&i| in_deg[i] == 0)?;
255 let mut chain = alloc::vec![start];
256 let mut cur = start;
257 while out_deg[cur] == 1 {
258 let Some(next) = state.edges.iter().find(|e| e.from == cur).map(|e| e.to) else {
259 break;
260 };
261 if next >= n
262 || in_deg[next] != 1
263 || matches!(state.roles[next], NodeRole::Tee | NodeRole::Muxer)
264 {
265 break;
266 }
267 chain.push(next);
268 cur = next;
269 }
270 Some((chain, out_deg[cur] != 0))
271}
272
273fn assemble_journey(state: &State) -> Option<FrameJourney> {
280 let (chain, mut truncated) = linear_chain(state)?;
281 let mut stages: Vec<(usize, Vec<StageVisit>)> = Vec::new();
284 for &node in &chain {
285 let visits = state
286 .probes
287 .get(node)
288 .and_then(|p| p.as_ref())
289 .map(|p| p.visits())
290 .unwrap_or_default();
291 if visits.is_empty() {
292 if !stages.is_empty() {
293 truncated = true;
294 break;
295 }
296 continue;
297 }
298 stages.push((node, visits));
299 }
300 let (last_node, last_visits) = stages.last()?;
301 truncated |= Some(*last_node) != chain.last().copied();
302 let mut seqs: Vec<u64> = last_visits.iter().map(|v| v.sequence).collect();
303 seqs.sort_unstable();
304 seqs.dedup();
305
306 for &sequence in seqs.iter().rev() {
307 let path: Option<Vec<StageVisit>> = stages
308 .iter()
309 .map(|(_, v)| v.iter().rev().find(|x| x.sequence == sequence).copied())
310 .collect();
311 let Some(path) = path else { continue };
312 if !one_frame_downstream(&path) {
313 continue;
314 }
315 let first = path[0];
316 let last = path[path.len() - 1];
317 let frame_period_ns = mean_period_ns(&stages[0].1);
318 let stage_rows = stages
319 .iter()
320 .zip(path.iter())
321 .map(|((node, _), v)| JourneyStage {
322 node: *node,
323 name: state.names.get(*node).cloned().unwrap_or_default(),
324 wait_ns: v.wait_ns,
325 work_ns: v
326 .exit_ns
327 .saturating_sub(v.enter_ns)
328 .saturating_sub(v.push_wait_ns),
329 blocked_ns: v.push_wait_ns,
330 })
331 .collect();
332 return Some(FrameJourney {
333 sequence,
334 stages: stage_rows,
335 total_ns: last
338 .exit_ns
339 .saturating_sub(first.enter_ns.saturating_sub(first.wait_ns)),
340 frame_period_ns,
341 capacity: state.link_capacity,
342 floor_ns: 2 * state.link_capacity as u64 * frame_period_ns,
343 truncated,
344 });
345 }
346 None
347}
348
349fn one_frame_downstream(path: &[StageVisit]) -> bool {
354 path.windows(2).all(|w| {
355 w[1].exit_ns >= w[1].enter_ns && w[1].enter_ns.saturating_sub(w[1].wait_ns) >= w[0].enter_ns
356 }) && path[0].exit_ns >= path[0].enter_ns
357}
358
359fn mean_period_ns(visits: &[StageVisit]) -> u64 {
362 let (Some(first), Some(last)) = (visits.first(), visits.last()) else {
363 return 0;
364 };
365 let spans = visits.len().saturating_sub(1) as u64;
366 last.enter_ns
367 .saturating_sub(first.enter_ns)
368 .checked_div(spans)
369 .unwrap_or(0)
370}
371
372impl Default for Observer {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378#[derive(Debug, Default)]
381pub(crate) struct EdgeTap {
382 pub(crate) probe: ProbeSlot,
383 pub(crate) counters: Option<Arc<EdgeCounters>>,
384}
385
386pub(crate) fn link_tapped(
390 capacity: usize,
391 tap: bool,
392) -> (
393 crate::runtime::LinkSender,
394 crate::runtime::LinkReceiver,
395 EdgeTap,
396) {
397 let (mut tx, rx) = crate::runtime::link(capacity);
398 let counters = tap.then(|| {
399 let c = Arc::new(EdgeCounters::default());
400 tx.set_counters(c.clone());
401 c
402 });
403 let edge = EdgeTap {
404 probe: tx.probe.clone(),
405 counters,
406 };
407 (tx, rx, edge)
408}
409
410pub(crate) type TapNode = (String, NodeRole, Probe);
414
415pub(crate) type TapEdge = (usize, usize, Caps, EdgeTap);
418
419pub(crate) fn register_runner_tap(obs: &Observer, nodes: Vec<TapNode>, edges: Vec<TapEdge>) {
424 let mut names = Vec::with_capacity(nodes.len());
425 let mut roles = Vec::with_capacity(nodes.len());
426 let mut probes = Vec::with_capacity(nodes.len());
427 for (name, role, probe) in nodes {
428 names.push(name);
429 roles.push(role);
430 probes.push(probe);
431 }
432 let mut infos = Vec::with_capacity(edges.len());
433 let mut caps = Vec::with_capacity(edges.len());
434 let mut slots = Vec::with_capacity(edges.len());
435 let mut counters = Vec::with_capacity(edges.len());
436 for (from, to, edge_caps, tap) in edges {
437 infos.push(EdgeInfo {
438 from,
439 to,
440 ..Default::default()
441 });
442 caps.push(edge_caps);
443 slots.push(tap.probe);
444 counters.push(tap.counters);
445 }
446 obs.register(names, roles, probes, infos);
447 obs.register_edges(slots, caps, counters);
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct TelemetrySnapshot {
453 pub uptime_ns: u64,
455 pub nodes: Vec<NodeTelemetry>,
457 pub edges: Vec<EdgeInfo>,
459 pub journey: Option<FrameJourney>,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
474pub struct FrameJourney {
475 pub sequence: u64,
477 pub stages: Vec<JourneyStage>,
480 pub total_ns: u64,
483 pub frame_period_ns: u64,
485 pub capacity: usize,
487 pub floor_ns: u64,
491 pub truncated: bool,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
498pub struct JourneyStage {
499 pub node: usize,
501 pub name: String,
503 pub wait_ns: u64,
506 pub work_ns: u64,
509 pub blocked_ns: u64,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq)]
517pub struct NodeTelemetry {
518 pub id: usize,
520 pub name: String,
522 pub role: NodeRole,
523 pub latency: Option<ElementLatency>,
527}
528
529#[derive(Debug, Clone, Default, PartialEq, Eq)]
535pub struct EdgeInfo {
536 pub from: usize,
537 pub to: usize,
538 pub caps: Option<alloc::string::String>,
539 pub observed_caps: Option<alloc::string::String>,
544 pub counts: EdgeCounts,
545}
546
547#[cfg(test)]
548mod tests {
549 use super::*;
550 use crate::runtime::instrument::ElementProbe;
551
552 #[test]
553 fn snapshot_before_register_is_empty() {
554 let obs = Observer::new();
555 let snap = obs.snapshot();
556 assert!(snap.nodes.is_empty());
557 assert!(snap.edges.is_empty());
558 }
559
560 #[test]
561 fn snapshot_reflects_live_probe_writes() {
562 let obs = Observer::new();
563 let probe = ElementProbe::new(String::from("decode0"));
564 obs.register(
565 alloc::vec![String::from("src0"), String::from("decode0")],
566 alloc::vec![NodeRole::Source, NodeRole::Transform],
567 alloc::vec![None, Some(probe.clone())],
568 alloc::vec![EdgeInfo {
569 from: 0,
570 to: 1,
571 ..Default::default()
572 }],
573 );
574
575 let before = obs.snapshot();
577 assert_eq!(before.nodes.len(), 2);
578 assert_eq!(before.nodes[0].role, NodeRole::Source);
579 assert!(before.nodes[0].latency.is_none(), "source has no probe");
580 assert_eq!(before.nodes[1].latency.as_ref().unwrap().proc.count, 0);
581
582 probe.record_fill(80);
584 probe.record_fill(100);
585 let after = obs.snapshot();
586 let lat = after.nodes[1].latency.as_ref().unwrap();
587 assert_eq!(lat.fill_max_pct, 100);
588 assert!(lat.fill_mean_pct > 0);
589 assert_eq!(
590 after.edges,
591 alloc::vec![EdgeInfo {
592 from: 0,
593 to: 1,
594 ..Default::default()
595 }]
596 );
597 }
598
599 #[test]
603 fn incremental_node_and_edge_join_a_registered_topology() {
604 let obs = Observer::new();
605 obs.register(
606 alloc::vec![String::from("src0")],
607 alloc::vec![NodeRole::Source],
608 alloc::vec![None],
609 Vec::new(),
610 );
611 assert_eq!(obs.snapshot().nodes.len(), 1);
612
613 let probe = ElementProbe::new(String::from("fakesink0"));
614 let counters = Arc::new(EdgeCounters::default());
615 let id = obs.add_node(
616 String::from("fakesink0"),
617 NodeRole::Sink,
618 Some(probe.clone()),
619 );
620 assert_eq!(id, 1, "appended after the registered source");
621 obs.add_edge(
622 0,
623 id,
624 Caps::Klv,
625 EdgeTap {
626 probe: ProbeSlot::default(),
627 counters: Some(counters.clone()),
628 },
629 );
630
631 probe.record_fill(60);
632 counters.record_packet(128, 0);
633
634 let snap = obs.snapshot();
635 assert_eq!(snap.nodes.len(), 2);
636 assert_eq!(snap.nodes[1].name, "fakesink0");
637 assert_eq!(snap.nodes[1].role, NodeRole::Sink);
638 assert_eq!(snap.nodes[1].latency.as_ref().unwrap().fill_max_pct, 60);
639 assert_eq!(snap.edges.len(), 1);
640 assert_eq!((snap.edges[0].from, snap.edges[0].to), (0, 1));
641 assert!(snap.edges[0].caps.is_some(), "late edge carries its caps");
642 assert_eq!(snap.edges[0].counts.packets, 1);
643 assert_eq!(snap.edges[0].counts.bytes, 128);
644 assert!(obs.edge_probe(0).is_some(), "late edge has an inspect slot");
645 }
646
647 #[test]
651 fn journey_joins_one_frame_across_stages() {
652 let obs = Observer::new();
653 let xform = ElementProbe::with_journeys(String::from("scale0"));
654 let sink = ElementProbe::with_journeys(String::from("fakesink0"));
655 obs.register(
656 alloc::vec![
657 String::from("src0"),
658 String::from("scale0"),
659 String::from("fakesink0"),
660 ],
661 alloc::vec![NodeRole::Source, NodeRole::Transform, NodeRole::Sink],
662 alloc::vec![None, Some(xform.clone()), Some(sink.clone())],
663 alloc::vec![
664 EdgeInfo {
665 from: 0,
666 to: 1,
667 ..Default::default()
668 },
669 EdgeInfo {
670 from: 1,
671 to: 2,
672 ..Default::default()
673 },
674 ],
675 );
676 obs.set_link_capacity(4);
677
678 for (seq, base) in [(0u64, 10_000u64), (1, 11_000)] {
681 xform.push_visit(StageVisit {
682 sequence: seq,
683 wait_ns: 100,
684 enter_ns: base,
685 exit_ns: base + 200,
686 push_wait_ns: 60,
687 });
688 sink.push_visit(StageVisit {
689 sequence: seq,
690 wait_ns: 50,
691 enter_ns: base + 250,
692 exit_ns: base + 400,
693 push_wait_ns: 0,
694 });
695 }
696
697 let j = obs.snapshot().journey.expect("journey assembles");
698 assert_eq!(j.sequence, 1, "newest fully-crossed frame");
699 assert!(!j.truncated, "chain reached the sink");
700 assert_eq!(
703 j.stages
704 .iter()
705 .map(|s| (s.node, s.name.as_str(), s.wait_ns, s.work_ns, s.blocked_ns))
706 .collect::<Vec<_>>(),
707 alloc::vec![(1, "scale0", 100, 140, 60), (2, "fakesink0", 50, 150, 0)],
708 );
709 assert_eq!(j.total_ns, 500);
711 let stage_sum: u64 = j
712 .stages
713 .iter()
714 .map(|s| s.wait_ns + s.work_ns + s.blocked_ns)
715 .sum();
716 assert!(j.total_ns >= stage_sum, "{} >= {}", j.total_ns, stage_sum);
717 assert_eq!(j.frame_period_ns, 1_000, "measured inter-frame spacing");
718 assert_eq!(j.capacity, 4);
719 assert_eq!(j.floor_ns, 2 * 4 * 1_000);
720 }
721
722 #[test]
726 fn journey_rejects_an_inconsistent_join() {
727 let obs = Observer::new();
728 let xform = ElementProbe::with_journeys(String::from("dec0"));
729 let sink = ElementProbe::with_journeys(String::from("fakesink0"));
730 obs.register(
731 alloc::vec![
732 String::from("src0"),
733 String::from("dec0"),
734 String::from("fakesink0"),
735 ],
736 alloc::vec![NodeRole::Source, NodeRole::Transform, NodeRole::Sink],
737 alloc::vec![None, Some(xform.clone()), Some(sink.clone())],
738 alloc::vec![
739 EdgeInfo {
740 from: 0,
741 to: 1,
742 ..Default::default()
743 },
744 EdgeInfo {
745 from: 1,
746 to: 2,
747 ..Default::default()
748 },
749 ],
750 );
751 xform.push_visit(StageVisit {
752 sequence: 0,
753 wait_ns: 0,
754 enter_ns: 5_000,
755 exit_ns: 5_100,
756 push_wait_ns: 0,
757 });
758 sink.push_visit(StageVisit {
759 sequence: 0,
760 wait_ns: 0,
761 enter_ns: 1_000,
762 exit_ns: 1_100,
763 push_wait_ns: 0,
764 });
765 assert!(obs.snapshot().journey.is_none());
766 }
767
768 #[test]
771 fn journey_stops_at_a_fan_node() {
772 let obs = Observer::new();
773 let xform = ElementProbe::with_journeys(String::from("scale0"));
774 obs.register(
775 alloc::vec![
776 String::from("src0"),
777 String::from("scale0"),
778 String::new(),
779 String::from("fakesink0"),
780 ],
781 alloc::vec![
782 NodeRole::Source,
783 NodeRole::Transform,
784 NodeRole::Tee,
785 NodeRole::Sink,
786 ],
787 alloc::vec![None, Some(xform.clone()), None, None],
788 alloc::vec![
789 EdgeInfo {
790 from: 0,
791 to: 1,
792 ..Default::default()
793 },
794 EdgeInfo {
795 from: 1,
796 to: 2,
797 ..Default::default()
798 },
799 EdgeInfo {
800 from: 2,
801 to: 3,
802 ..Default::default()
803 },
804 ],
805 );
806 xform.push_visit(StageVisit {
807 sequence: 3,
808 wait_ns: 10,
809 enter_ns: 900,
810 exit_ns: 1_000,
811 push_wait_ns: 0,
812 });
813 let j = obs.snapshot().journey.expect("prefix assembles");
814 assert_eq!(j.stages.len(), 1, "only the pre-tee stage");
815 assert!(j.truncated, "the tee cut the walk short");
816 }
817
818 #[test]
819 fn journey_absent_without_journey_probes() {
820 let obs = Observer::new();
821 obs.register(
822 alloc::vec![String::from("src0"), String::from("fakesink0")],
823 alloc::vec![NodeRole::Source, NodeRole::Sink],
824 alloc::vec![None, Some(ElementProbe::new(String::from("fakesink0")))],
825 alloc::vec![EdgeInfo {
826 from: 0,
827 to: 1,
828 ..Default::default()
829 }],
830 );
831 assert!(obs.snapshot().journey.is_none());
832 }
833
834 #[test]
835 fn node_role_projects_kind() {
836 assert_eq!(NodeRole::from(NodeKind::Tee(3)), NodeRole::Tee);
837 assert_eq!(NodeRole::from(NodeKind::Muxer(2)), NodeRole::Muxer);
838 assert_eq!(NodeRole::from(NodeKind::Sink), NodeRole::Sink);
839 }
840}