1use super::*;
2
3pub(super) const LATENCY_BUCKETS: usize = 32;
4
5pub(super) fn latency_bucket_upper_us(bucket: usize) -> u64 {
6 1u64 << bucket.min(LATENCY_BUCKETS - 1)
7}
8
9fn latency_bucket(us: u64) -> usize {
10 if us <= 1 {
11 0
12 } else {
13 (u64::BITS - (us - 1).leading_zeros()) as usize
14 }
15 .min(LATENCY_BUCKETS - 1)
16}
17
18#[derive(Debug)]
19pub(super) struct E2eStats {
20 pub(super) frames: AtomicU64,
21 pub(super) total_us: AtomicU64,
22 pub(super) max_us: AtomicU64,
23 pub(super) buckets: [AtomicU64; LATENCY_BUCKETS],
24}
25
26impl Default for E2eStats {
27 fn default() -> Self {
28 Self {
29 frames: AtomicU64::new(0),
30 total_us: AtomicU64::new(0),
31 max_us: AtomicU64::new(0),
32 buckets: std::array::from_fn(|_| AtomicU64::new(0)),
33 }
34 }
35}
36
37impl E2eStats {
38 pub(super) fn record(&self, us: u64) {
39 self.total_us.fetch_add(us, Ordering::Relaxed);
40 self.max_us.fetch_max(us, Ordering::Relaxed);
41 self.buckets[latency_bucket(us)].fetch_add(1, Ordering::Relaxed);
42 self.frames.fetch_add(1, Ordering::Release);
43 }
44
45 pub(super) fn reset(&self) {
46 self.frames.store(0, Ordering::Relaxed);
47 self.total_us.store(0, Ordering::Relaxed);
48 self.max_us.store(0, Ordering::Relaxed);
49 for bucket in &self.buckets {
50 bucket.store(0, Ordering::Relaxed);
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum InputPolicy {
58 Sync,
60 Immediate,
62 FixedSize { capacity: usize },
66 SyncSet { sets: Vec<Vec<usize>> },
70 Batch { size: usize },
74}
75
76impl InputPolicy {
77 pub(super) fn from_config(
79 c: &crate::config::InputPolicyConfig,
80 ins: &crate::kernel::PortTable,
81 ) -> Result<Self> {
82 Ok(match c.r#type.as_str() {
83 "immediate" => InputPolicy::Immediate,
84 "fixed_size" => InputPolicy::FixedSize {
85 capacity: c.capacity.max(1),
86 },
87 "batch" => InputPolicy::Batch {
88 size: c.capacity.max(1),
89 },
90 "sync_set" => {
91 if c.sets.is_empty() {
92 return Err(Error::InvalidArg(
93 "sync_set policy must provide sets (input port groups)".into(),
94 ));
95 }
96 let mut resolved: Vec<Vec<usize>> = Vec::new();
97 let mut seen = vec![false; ins.len()];
98 for set in &c.sets {
99 if set.is_empty() {
100 return Err(Error::InvalidArg("sync_set group must not be empty".into()));
101 }
102 let mut group = Vec::with_capacity(set.len());
103 for name in set {
104 let idx = ins.index_by_name(name).ok_or_else(|| {
105 Error::InvalidArg(format!(
106 "sync_set references nonexistent input port `{name}`"
107 ))
108 })?;
109 if seen[idx] {
110 return Err(Error::InvalidArg(format!(
111 "sync_set input port `{name}` appears in multiple groups (groups must be disjoint)"
112 )));
113 }
114 seen[idx] = true;
115 group.push(idx);
116 }
117 resolved.push(group);
118 }
119 if let Some(miss) = (0..ins.len()).find(|&i| !seen[i]) {
120 return Err(Error::InvalidArg(format!(
121 "sync_set must cover all input ports; at least `{}` is missing (put it in its own group to keep it independent)",
122 ins.name(miss).unwrap_or("?")
123 )));
124 }
125 InputPolicy::SyncSet { sets: resolved }
126 }
127 _ => InputPolicy::Sync,
128 })
129 }
130}
131
132pub(super) struct Ready {
136 pub(super) ts: Timestamp,
137 pub(super) ports: Option<Vec<usize>>,
138 pub(super) batch: Option<BatchPlan>,
142}
143
144pub(super) struct BatchPlan {
151 pub(super) take: Vec<(usize, usize)>,
153 pub(super) last_ts: Timestamp,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum State {
162 Created = 0,
163 Initialized = 1,
164 Running = 2,
165 Draining = 3,
166 Terminated = 4,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum DotView {
172 Topology,
174 Compact,
176 Diagnostics,
178}
179
180#[derive(Default)]
186pub(super) struct IngressState {
187 owner: Option<std::thread::ThreadId>,
188 closing: bool,
189}
190
191pub(super) struct IngressGuard<'a>(Option<&'a Edge>);
192impl IngressGuard<'_> {
193 pub(super) fn release_unless_closing(&mut self) -> bool {
197 let edge = self.0.expect("ingress guard already released");
198 let mut state = edge.ingress.lock().expect("ingress lock poisoned");
199 if state.closing {
200 return true;
201 }
202 state.owner = None;
203 self.0 = None;
204 drop(state);
205 edge.ingress_changed.notify_all();
206 false
207 }
208}
209impl Drop for IngressGuard<'_> {
210 fn drop(&mut self) {
211 if let Some(edge) = self.0 {
212 edge.ingress.lock().expect("ingress lock poisoned").owner = None;
213 edge.ingress_changed.notify_all();
214 }
215 }
216}
217
218pub struct Edge {
219 pub name: String,
220 pub producer: Option<NodeId>,
221 pub consumers: Vec<(NodeId, usize)>,
223 pub is_graph_input: bool,
224 pub is_graph_output: bool,
225 pub(super) closed: AtomicBool,
226 pub(super) dropped: AtomicU64,
227 pub(super) watermark_backpressure: BackpressureStats,
228 pub(super) last_sent: Mutex<Timestamp>,
231 ingress: Mutex<IngressState>,
232 ingress_changed: Condvar,
233 pub(super) last_published_bound: Mutex<Timestamp>,
236 pub(super) has_timestamp_bound_subscriber: AtomicBool,
238 pub(super) pollers: Mutex<Vec<Arc<PollerInner>>>,
239 pub(super) observers: Mutex<Vec<Observer>>,
240}
241
242impl Edge {
243 pub(super) fn new(name: String) -> Self {
244 Self {
245 name,
246 producer: None,
247 consumers: Vec::new(),
248 is_graph_input: false,
249 is_graph_output: false,
250 closed: AtomicBool::new(false),
251 dropped: AtomicU64::new(0),
252 watermark_backpressure: BackpressureStats::default(),
253 last_sent: Mutex::new(Timestamp::unset()),
254 ingress: Mutex::new(IngressState::default()),
255 ingress_changed: Condvar::new(),
256 last_published_bound: Mutex::new(Timestamp::unstarted()),
257 has_timestamp_bound_subscriber: AtomicBool::new(false),
258 pollers: Mutex::new(Vec::new()),
259 observers: Mutex::new(Vec::new()),
260 }
261 }
262 pub(super) fn is_ingress_owner(&self) -> bool {
263 self.ingress.lock().expect("ingress lock poisoned").owner
264 == Some(std::thread::current().id())
265 }
266
267 pub(super) fn enter_ingress(&self, blocking: bool) -> Result<IngressGuard<'_>> {
268 let current = std::thread::current().id();
269 let mut state = self.ingress.lock().expect("ingress lock poisoned");
270 loop {
271 if state.closing {
272 return Err(Error::Closed);
273 }
274 if state.owner == Some(current) {
275 return Err(Error::State(
276 "cannot send to the same input from its dispatch callback".into(),
277 ));
278 }
279 if state.owner.is_none() {
280 state.owner = Some(current);
281 return Ok(IngressGuard(Some(self)));
282 }
283 if !blocking {
284 return Err(Error::WouldBlock);
285 }
286 state = self
287 .ingress_changed
288 .wait(state)
289 .expect("ingress lock poisoned");
290 }
291 }
292
293 pub(super) fn enter_ingress_close(&self, defer_if_busy: bool) -> Option<IngressGuard<'_>> {
294 let current = std::thread::current().id();
295 let mut state = self.ingress.lock().expect("ingress lock poisoned");
296 state.closing = true;
297 self.ingress_changed.notify_all();
298 if state.owner == Some(current) || (defer_if_busy && state.owner.is_some()) {
299 return None;
300 }
301 while state.owner.is_some() {
302 state = self
303 .ingress_changed
304 .wait(state)
305 .expect("ingress lock poisoned");
306 }
307 state.owner = Some(current);
308 Some(IngressGuard(Some(self)))
309 }
310
311 pub(super) fn reset_ingress(&self) {
312 *self.ingress.lock().expect("ingress lock poisoned") = IngressState::default();
313 }
314
315 pub fn is_closed(&self) -> bool {
316 self.closed.load(Ordering::SeqCst)
317 }
318 pub fn dropped_count(&self) -> u64 {
321 self.dropped.load(Ordering::Relaxed)
322 }
323}
324
325#[derive(Clone)]
327pub(super) enum Observer {
328 C {
329 cb: unsafe extern "C" fn(*mut c_void, crate::ffi::LMFlowPacket),
330 user: *mut c_void,
331 observe_timestamp_bounds: bool,
332 },
333 Rust {
334 callback: Arc<dyn Fn(&Packet) + Send + Sync>,
335 observe_timestamp_bounds: bool,
336 },
337}
338unsafe impl Send for Observer {}
340#[derive(Debug)]
354pub(super) struct NodeSched {
355 pub(super) opened: bool,
356 pub(super) close_started: bool,
359 pub(super) closed: bool,
360 pub(super) in_flight: usize,
362 pub(super) free_slots: Vec<usize>,
364 pub(super) ready: VecDeque<(usize, u64)>,
366 pub(super) next_seq: u64,
368 pub(super) next_flush_seq: u64,
370 pub(super) pending_flush: BTreeMap<u64, (usize, bool)>,
372 pub(super) blocked_flush: Option<BlockedFlush>,
374 pub(super) flushing: bool,
376 pub(super) source_reschedule: Option<SourceReschedule>,
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub(super) enum SourceWaitReason {
382 Rate,
383 Yield,
384 RateAndYield,
385}
386
387#[derive(Debug, Clone, Copy)]
388pub(super) struct SourceReschedule {
389 pub(super) delay: std::time::Duration,
390 pub(super) reason: SourceWaitReason,
391}
392impl NodeSched {
393 pub(super) fn new(max_in_flight: usize) -> Self {
394 Self {
395 opened: false,
396 close_started: false,
397 closed: false,
398 in_flight: 0,
399 free_slots: (0..max_in_flight).collect(),
400 ready: VecDeque::new(),
401 next_seq: 0,
402 next_flush_seq: 0,
403 pending_flush: BTreeMap::new(),
404 blocked_flush: None,
405 flushing: false,
406 source_reschedule: None,
407 }
408 }
409}
410
411#[derive(Debug)]
417pub(super) struct NodeStats {
418 pub(super) processed: AtomicU64,
419 pub(super) errors: AtomicU64,
420 pub(super) total_us: AtomicI64,
421 pub(super) max_us: AtomicI64,
422 pub(super) latency_buckets: [AtomicU64; LATENCY_BUCKETS],
423 pub(super) cow_copies: AtomicU64,
424 pub(super) cow_bytes: AtomicU64,
425 pub(super) packets_in: AtomicU64,
427 pub(super) packets_out: AtomicU64,
429 pub(super) peak_queue_depth: AtomicUsize,
431 pub(super) in_flight: AtomicUsize,
433 pub(super) started_us: AtomicI64,
437}
438
439impl Default for NodeStats {
440 fn default() -> Self {
441 Self {
442 processed: AtomicU64::new(0),
443 errors: AtomicU64::new(0),
444 total_us: AtomicI64::new(0),
445 max_us: AtomicI64::new(0),
446 latency_buckets: std::array::from_fn(|_| AtomicU64::new(0)),
447 cow_copies: AtomicU64::new(0),
448 cow_bytes: AtomicU64::new(0),
449 packets_in: AtomicU64::new(0),
450 packets_out: AtomicU64::new(0),
451 peak_queue_depth: AtomicUsize::new(0),
452 in_flight: AtomicUsize::new(0),
453 started_us: AtomicI64::new(0),
454 }
455 }
456}
457
458impl NodeStats {
459 pub(super) fn record_latency(&self, us: u64) {
460 self.latency_buckets[latency_bucket(us)].fetch_add(1, Ordering::Relaxed);
461 }
462
463 pub(super) fn reset(&self) {
466 self.processed.store(0, Ordering::Relaxed);
467 self.errors.store(0, Ordering::Relaxed);
468 self.total_us.store(0, Ordering::Relaxed);
469 self.max_us.store(0, Ordering::Relaxed);
470 for bucket in &self.latency_buckets {
471 bucket.store(0, Ordering::Relaxed);
472 }
473 self.cow_copies.store(0, Ordering::Relaxed);
474 self.cow_bytes.store(0, Ordering::Relaxed);
475 self.packets_in.store(0, Ordering::Relaxed);
476 self.packets_out.store(0, Ordering::Relaxed);
477 self.peak_queue_depth.store(0, Ordering::Relaxed);
478 self.in_flight.store(0, Ordering::Relaxed);
479 self.started_us.store(0, Ordering::Relaxed);
480 }
481}
482
483#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum OnError {
486 Abort,
488 Skip,
493}
494
495impl OnError {
496 pub(super) fn from_config(s: &str) -> Self {
497 if s == "skip" {
499 OnError::Skip
500 } else {
501 OnError::Abort
502 }
503 }
504}
505
506pub struct Node {
507 pub name: String,
508 pub kernel_name: String,
509 pub inputs: Vec<EdgeId>,
510 pub outputs: Vec<EdgeId>,
511 pub in_ports: Arc<PortTable>,
512 pub out_ports: Arc<PortTable>,
513 pub(super) executor: usize,
516 pub(super) policy: InputPolicy,
517 pub(super) input_types: Vec<u64>,
518 pub(super) output_types: Vec<u64>,
519 pub(super) kernel: KernelInstance,
520 pub(super) ctxs: Vec<UnsafeCell<Context>>,
527 pub(super) max_in_flight: usize,
528 pub(super) sched: Mutex<NodeSched>,
529 pub(super) stats: NodeStats,
531 pub(super) input_queues: Vec<Mutex<VecDeque<Packet>>>,
533 pub(super) input_queue_capacity: Vec<Option<usize>>,
535 pub(super) input_queue_reserved: Vec<AtomicUsize>,
537 pub(super) input_queue_bytes: Vec<AtomicU64>,
539 pub(super) input_queue_stats: Vec<InputQueueStats>,
541 pub(super) input_closed: Vec<AtomicBool>,
542 pub(super) on_error: OnError,
544 pub(super) min_period: Option<std::time::Duration>,
546 pub(super) last_fire: Mutex<Option<Instant>>,
549 pub(super) input_is_back_edge: Vec<bool>,
552 pub(super) source_done: AtomicBool,
554 pub(super) source_waiting: AtomicBool,
556 pub(super) source_wait_reason: AtomicUsize,
558 pub(super) source_wake_deadline_us: AtomicI64,
560 pub(super) source_yield_count: AtomicU64,
562 pub(super) source_wake_generation: AtomicU64,
564 pub(super) input_bounds: Vec<Mutex<Timestamp>>,
568}
569
570unsafe impl Sync for Node {}
573
574impl Node {
575 #[allow(clippy::mut_from_ref)]
581 pub(super) unsafe fn ctx_slot(&self, slot: usize) -> &mut Context {
582 &mut *self.ctxs[slot].get()
583 }
584
585 pub(super) fn queue_len(&self, port: usize) -> usize {
586 self.input_queues[port]
587 .lock()
588 .expect("queue lock poisoned")
589 .len()
590 }
591 pub(super) fn bound(&self, port: usize) -> Timestamp {
592 *self.input_bounds[port].lock().expect("bound lock poisoned")
593 }
594
595 pub(super) fn advance_bound(&self, port: usize, to: Timestamp) {
597 let mut b = self.input_bounds[port].lock().expect("bound lock poisoned");
598 if to > *b {
599 *b = to;
600 }
601 }
602
603 pub(super) fn front_ts(&self, port: usize) -> Option<Timestamp> {
604 self.input_queues[port]
605 .lock()
606 .expect("queue lock poisoned")
607 .front()
608 .map(|p| p.timestamp())
609 }
610
611 pub(super) fn is_source(&self) -> bool {
613 self.input_queues.is_empty()
614 }
615
616 pub(super) fn forward_ports(&self) -> impl Iterator<Item = usize> + '_ {
619 (0..self.input_queues.len()).filter(move |&i| !self.input_is_back_edge[i])
620 }
621
622 pub(super) fn readiness(&self) -> Option<Ready> {
632 let n = self.input_queues.len();
633 if n == 0 {
634 return (!self.source_done.load(Ordering::SeqCst)
636 && !self.source_waiting.load(Ordering::SeqCst))
637 .then_some(Ready {
638 ts: Timestamp::unset(),
639 ports: None,
640 batch: None,
641 });
642 }
643 match &self.policy {
644 InputPolicy::Immediate => {
645 let mut min = Timestamp::done();
647 let mut any = false;
648 for i in self.forward_ports() {
649 if let Some(ts) = self.front_ts(i) {
650 any = true;
651 min = min.min(ts);
652 }
653 }
654 any.then_some(Ready {
655 ts: min,
656 ports: None,
657 batch: None,
658 })
659 }
660 InputPolicy::Sync | InputPolicy::FixedSize { .. } => {
661 self.sync_align(self.forward_ports()).map(|ts| Ready {
662 ts,
663 ports: None,
664 batch: None,
665 })
666 }
667 InputPolicy::SyncSet { sets } => {
668 let mut best: Option<Ready> = None;
670 for set in sets {
671 if let Some(ts) = self.sync_align(set.iter().copied()) {
672 if best.as_ref().is_none_or(|b| ts < b.ts) {
673 best = Some(Ready {
674 ts,
675 ports: Some(set.clone()),
676 batch: None,
677 });
678 }
679 }
680 }
681 best
682 }
683 InputPolicy::Batch { size } => self.batch_readiness(*size),
684 }
685 }
686
687 pub(super) fn batch_readiness(&self, size: usize) -> Option<Ready> {
702 let ports: Vec<usize> = self.forward_ports().collect();
703 if ports.is_empty() || size == 0 {
704 return None;
705 }
706
707 let ts_prefix: Vec<Vec<Timestamp>> = ports
708 .iter()
709 .map(|&p| {
710 let q = self.input_queues[p].lock().expect("queue lock poisoned");
711 q.iter().take(size).map(|pk| pk.timestamp()).collect()
712 })
713 .collect();
714
715 let mut take = vec![0usize; ports.len()];
716 let mut first_ts: Option<Timestamp> = None;
717 let mut last_ts = Timestamp::unset();
718 let mut rounds = 0usize;
719
720 while rounds < size {
721 let mut min_packet = Timestamp::done();
724 let mut min_bound = Timestamp::done();
725 for (pi, &p) in ports.iter().enumerate() {
726 match ts_prefix[pi].get(take[pi]) {
727 Some(&ts) => min_packet = min_packet.min(ts),
728 None => min_bound = min_bound.min(self.bound(p)),
729 }
730 }
731 if min_packet == Timestamp::done() || min_bound <= min_packet {
732 break;
733 }
734 for (pi, _) in ports.iter().enumerate() {
735 if ts_prefix[pi].get(take[pi]) == Some(&min_packet) {
736 take[pi] += 1;
737 }
738 }
739 first_ts.get_or_insert(min_packet);
740 last_ts = min_packet;
741 rounds += 1;
742 }
743
744 if rounds == 0 {
745 return None;
746 }
747 if rounds < size
748 && !ports
749 .iter()
750 .all(|&p| self.input_closed[p].load(Ordering::SeqCst))
751 {
752 return None; }
754
755 Some(Ready {
756 ts: first_ts.unwrap_or_else(Timestamp::unset),
757 ports: None,
758 batch: Some(BatchPlan {
759 take: ports.into_iter().zip(take).collect(),
760 last_ts,
761 }),
762 })
763 }
764
765 pub(super) fn sync_align(&self, ports: impl Iterator<Item = usize>) -> Option<Timestamp> {
767 let mut min_packet = Timestamp::done();
768 let mut min_bound = Timestamp::done();
769 for i in ports {
770 match self.front_ts(i) {
771 Some(ts) => min_packet = min_packet.min(ts),
772 None => min_bound = min_bound.min(self.bound(i)),
773 }
774 }
775 if min_packet == Timestamp::done() {
776 return None; }
778 if min_bound > min_packet {
779 Some(min_packet)
780 } else {
781 None }
783 }
784
785 pub(super) fn all_inputs_closed_and_drained(&self) -> bool {
786 if self.is_source() {
787 return self.source_done.load(Ordering::SeqCst);
789 }
790 self.forward_ports()
793 .all(|i| self.input_closed[i].load(Ordering::SeqCst) && self.queue_len(i) == 0)
794 }
795}