use super::*;
pub(super) const LATENCY_BUCKETS: usize = 32;
pub(super) fn latency_bucket_upper_us(bucket: usize) -> u64 {
1u64 << bucket.min(LATENCY_BUCKETS - 1)
}
fn latency_bucket(us: u64) -> usize {
if us <= 1 {
0
} else {
(u64::BITS - (us - 1).leading_zeros()) as usize
}
.min(LATENCY_BUCKETS - 1)
}
#[derive(Debug)]
pub(super) struct E2eStats {
pub(super) frames: AtomicU64,
pub(super) total_us: AtomicU64,
pub(super) max_us: AtomicU64,
pub(super) buckets: [AtomicU64; LATENCY_BUCKETS],
}
impl Default for E2eStats {
fn default() -> Self {
Self {
frames: AtomicU64::new(0),
total_us: AtomicU64::new(0),
max_us: AtomicU64::new(0),
buckets: std::array::from_fn(|_| AtomicU64::new(0)),
}
}
}
impl E2eStats {
pub(super) fn record(&self, us: u64) {
self.total_us.fetch_add(us, Ordering::Relaxed);
self.max_us.fetch_max(us, Ordering::Relaxed);
self.buckets[latency_bucket(us)].fetch_add(1, Ordering::Relaxed);
self.frames.fetch_add(1, Ordering::Release);
}
pub(super) fn reset(&self) {
self.frames.store(0, Ordering::Relaxed);
self.total_us.store(0, Ordering::Relaxed);
self.max_us.store(0, Ordering::Relaxed);
for bucket in &self.buckets {
bucket.store(0, Ordering::Relaxed);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputPolicy {
Sync,
Immediate,
FixedSize { capacity: usize },
SyncSet { sets: Vec<Vec<usize>> },
Batch { size: usize },
}
impl InputPolicy {
pub(super) fn from_config(
c: &crate::config::InputPolicyConfig,
ins: &crate::kernel::PortTable,
) -> Result<Self> {
Ok(match c.r#type.as_str() {
"immediate" => InputPolicy::Immediate,
"fixed_size" => InputPolicy::FixedSize {
capacity: c.capacity.max(1),
},
"batch" => InputPolicy::Batch {
size: c.capacity.max(1),
},
"sync_set" => {
if c.sets.is_empty() {
return Err(Error::InvalidArg(
"sync_set policy must provide sets (input port groups)".into(),
));
}
let mut resolved: Vec<Vec<usize>> = Vec::new();
let mut seen = vec![false; ins.len()];
for set in &c.sets {
if set.is_empty() {
return Err(Error::InvalidArg("sync_set group must not be empty".into()));
}
let mut group = Vec::with_capacity(set.len());
for name in set {
let idx = ins.index_by_name(name).ok_or_else(|| {
Error::InvalidArg(format!(
"sync_set references nonexistent input port `{name}`"
))
})?;
if seen[idx] {
return Err(Error::InvalidArg(format!(
"sync_set input port `{name}` appears in multiple groups (groups must be disjoint)"
)));
}
seen[idx] = true;
group.push(idx);
}
resolved.push(group);
}
if let Some(miss) = (0..ins.len()).find(|&i| !seen[i]) {
return Err(Error::InvalidArg(format!(
"sync_set must cover all input ports; at least `{}` is missing (put it in its own group to keep it independent)",
ins.name(miss).unwrap_or("?")
)));
}
InputPolicy::SyncSet { sets: resolved }
}
_ => InputPolicy::Sync,
})
}
}
pub(super) struct Ready {
pub(super) ts: Timestamp,
pub(super) ports: Option<Vec<usize>>,
pub(super) batch: Option<BatchPlan>,
}
pub(super) struct BatchPlan {
pub(super) take: Vec<(usize, usize)>,
pub(super) last_ts: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Created = 0,
Initialized = 1,
Running = 2,
Draining = 3,
Terminated = 4,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DotView {
Topology,
Compact,
Diagnostics,
}
pub struct Edge {
pub name: String,
pub producer: Option<NodeId>,
pub consumers: Vec<(NodeId, usize)>,
pub is_graph_input: bool,
pub is_graph_output: bool,
pub(super) closed: AtomicBool,
pub(super) dropped: AtomicU64,
pub(super) watermark_backpressure: BackpressureStats,
pub(super) last_sent: Mutex<Timestamp>,
pub(super) last_published_bound: Mutex<Timestamp>,
pub(super) has_timestamp_bound_subscriber: AtomicBool,
pub(super) pollers: Mutex<Vec<Arc<PollerInner>>>,
pub(super) observers: Mutex<Vec<Observer>>,
}
impl Edge {
pub(super) fn new(name: String) -> Self {
Self {
name,
producer: None,
consumers: Vec::new(),
is_graph_input: false,
is_graph_output: false,
closed: AtomicBool::new(false),
dropped: AtomicU64::new(0),
watermark_backpressure: BackpressureStats::default(),
last_sent: Mutex::new(Timestamp::unset()),
last_published_bound: Mutex::new(Timestamp::unstarted()),
has_timestamp_bound_subscriber: AtomicBool::new(false),
pollers: Mutex::new(Vec::new()),
observers: Mutex::new(Vec::new()),
}
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::SeqCst)
}
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
#[derive(Clone)]
pub(super) enum Observer {
C {
cb: unsafe extern "C" fn(*mut c_void, crate::ffi::LMFlowPacket),
user: *mut c_void,
observe_timestamp_bounds: bool,
},
Rust {
callback: Arc<dyn Fn(&Packet) + Send + Sync>,
observe_timestamp_bounds: bool,
},
}
unsafe impl Send for Observer {}
#[derive(Debug)]
pub(super) struct NodeSched {
pub(super) opened: bool,
pub(super) close_started: bool,
pub(super) closed: bool,
pub(super) in_flight: usize,
pub(super) free_slots: Vec<usize>,
pub(super) ready: VecDeque<(usize, u64)>,
pub(super) next_seq: u64,
pub(super) next_flush_seq: u64,
pub(super) pending_flush: BTreeMap<u64, (usize, bool)>,
pub(super) blocked_flush: Option<BlockedFlush>,
pub(super) flushing: bool,
pub(super) source_reschedule: Option<SourceReschedule>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SourceWaitReason {
Rate,
Yield,
RateAndYield,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct SourceReschedule {
pub(super) delay: std::time::Duration,
pub(super) reason: SourceWaitReason,
}
impl NodeSched {
pub(super) fn new(max_in_flight: usize) -> Self {
Self {
opened: false,
close_started: false,
closed: false,
in_flight: 0,
free_slots: (0..max_in_flight).collect(),
ready: VecDeque::new(),
next_seq: 0,
next_flush_seq: 0,
pending_flush: BTreeMap::new(),
blocked_flush: None,
flushing: false,
source_reschedule: None,
}
}
}
#[derive(Debug)]
pub(super) struct NodeStats {
pub(super) processed: AtomicU64,
pub(super) errors: AtomicU64,
pub(super) total_us: AtomicI64,
pub(super) max_us: AtomicI64,
pub(super) latency_buckets: [AtomicU64; LATENCY_BUCKETS],
pub(super) cow_copies: AtomicU64,
pub(super) cow_bytes: AtomicU64,
pub(super) packets_in: AtomicU64,
pub(super) packets_out: AtomicU64,
pub(super) peak_queue_depth: AtomicUsize,
pub(super) in_flight: AtomicUsize,
pub(super) started_us: AtomicI64,
}
impl Default for NodeStats {
fn default() -> Self {
Self {
processed: AtomicU64::new(0),
errors: AtomicU64::new(0),
total_us: AtomicI64::new(0),
max_us: AtomicI64::new(0),
latency_buckets: std::array::from_fn(|_| AtomicU64::new(0)),
cow_copies: AtomicU64::new(0),
cow_bytes: AtomicU64::new(0),
packets_in: AtomicU64::new(0),
packets_out: AtomicU64::new(0),
peak_queue_depth: AtomicUsize::new(0),
in_flight: AtomicUsize::new(0),
started_us: AtomicI64::new(0),
}
}
}
impl NodeStats {
pub(super) fn record_latency(&self, us: u64) {
self.latency_buckets[latency_bucket(us)].fetch_add(1, Ordering::Relaxed);
}
pub(super) fn reset(&self) {
self.processed.store(0, Ordering::Relaxed);
self.errors.store(0, Ordering::Relaxed);
self.total_us.store(0, Ordering::Relaxed);
self.max_us.store(0, Ordering::Relaxed);
for bucket in &self.latency_buckets {
bucket.store(0, Ordering::Relaxed);
}
self.cow_copies.store(0, Ordering::Relaxed);
self.cow_bytes.store(0, Ordering::Relaxed);
self.packets_in.store(0, Ordering::Relaxed);
self.packets_out.store(0, Ordering::Relaxed);
self.peak_queue_depth.store(0, Ordering::Relaxed);
self.in_flight.store(0, Ordering::Relaxed);
self.started_us.store(0, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnError {
Abort,
Skip,
}
impl OnError {
pub(super) fn from_config(s: &str) -> Self {
if s == "skip" {
OnError::Skip
} else {
OnError::Abort
}
}
}
pub struct Node {
pub name: String,
pub kernel_name: String,
pub inputs: Vec<EdgeId>,
pub outputs: Vec<EdgeId>,
pub in_ports: Arc<PortTable>,
pub out_ports: Arc<PortTable>,
pub(super) executor: usize,
pub(super) policy: InputPolicy,
pub(super) input_types: Vec<u64>,
pub(super) output_types: Vec<u64>,
pub(super) kernel: KernelInstance,
pub(super) ctxs: Vec<UnsafeCell<Context>>,
pub(super) max_in_flight: usize,
pub(super) sched: Mutex<NodeSched>,
pub(super) stats: NodeStats,
pub(super) input_queues: Vec<Mutex<VecDeque<Packet>>>,
pub(super) input_queue_capacity: Vec<Option<usize>>,
pub(super) input_queue_reserved: Vec<AtomicUsize>,
pub(super) input_queue_bytes: Vec<AtomicU64>,
pub(super) input_queue_stats: Vec<InputQueueStats>,
pub(super) input_closed: Vec<AtomicBool>,
pub(super) on_error: OnError,
pub(super) min_period: Option<std::time::Duration>,
pub(super) last_fire: Mutex<Option<Instant>>,
pub(super) input_is_back_edge: Vec<bool>,
pub(super) source_done: AtomicBool,
pub(super) source_waiting: AtomicBool,
pub(super) source_wait_reason: AtomicUsize,
pub(super) source_wake_deadline_us: AtomicI64,
pub(super) source_yield_count: AtomicU64,
pub(super) source_wake_generation: AtomicU64,
pub(super) input_bounds: Vec<Mutex<Timestamp>>,
}
unsafe impl Sync for Node {}
impl Node {
#[allow(clippy::mut_from_ref)]
pub(super) unsafe fn ctx_slot(&self, slot: usize) -> &mut Context {
&mut *self.ctxs[slot].get()
}
pub(super) fn queue_len(&self, port: usize) -> usize {
self.input_queues[port]
.lock()
.expect("queue lock poisoned")
.len()
}
pub(super) fn bound(&self, port: usize) -> Timestamp {
*self.input_bounds[port].lock().expect("bound lock poisoned")
}
pub(super) fn advance_bound(&self, port: usize, to: Timestamp) {
let mut b = self.input_bounds[port].lock().expect("bound lock poisoned");
if to > *b {
*b = to;
}
}
pub(super) fn front_ts(&self, port: usize) -> Option<Timestamp> {
self.input_queues[port]
.lock()
.expect("queue lock poisoned")
.front()
.map(|p| p.timestamp())
}
pub(super) fn is_source(&self) -> bool {
self.input_queues.is_empty()
}
pub(super) fn forward_ports(&self) -> impl Iterator<Item = usize> + '_ {
(0..self.input_queues.len()).filter(move |&i| !self.input_is_back_edge[i])
}
pub(super) fn readiness(&self) -> Option<Ready> {
let n = self.input_queues.len();
if n == 0 {
return (!self.source_done.load(Ordering::SeqCst)
&& !self.source_waiting.load(Ordering::SeqCst))
.then_some(Ready {
ts: Timestamp::unset(),
ports: None,
batch: None,
});
}
match &self.policy {
InputPolicy::Immediate => {
let mut min = Timestamp::done();
let mut any = false;
for i in self.forward_ports() {
if let Some(ts) = self.front_ts(i) {
any = true;
min = min.min(ts);
}
}
any.then_some(Ready {
ts: min,
ports: None,
batch: None,
})
}
InputPolicy::Sync | InputPolicy::FixedSize { .. } => {
self.sync_align(self.forward_ports()).map(|ts| Ready {
ts,
ports: None,
batch: None,
})
}
InputPolicy::SyncSet { sets } => {
let mut best: Option<Ready> = None;
for set in sets {
if let Some(ts) = self.sync_align(set.iter().copied()) {
if best.as_ref().is_none_or(|b| ts < b.ts) {
best = Some(Ready {
ts,
ports: Some(set.clone()),
batch: None,
});
}
}
}
best
}
InputPolicy::Batch { size } => self.batch_readiness(*size),
}
}
pub(super) fn batch_readiness(&self, size: usize) -> Option<Ready> {
let ports: Vec<usize> = self.forward_ports().collect();
if ports.is_empty() || size == 0 {
return None;
}
let ts_prefix: Vec<Vec<Timestamp>> = ports
.iter()
.map(|&p| {
let q = self.input_queues[p].lock().expect("queue lock poisoned");
q.iter().take(size).map(|pk| pk.timestamp()).collect()
})
.collect();
let mut take = vec![0usize; ports.len()];
let mut first_ts: Option<Timestamp> = None;
let mut last_ts = Timestamp::unset();
let mut rounds = 0usize;
while rounds < size {
let mut min_packet = Timestamp::done();
let mut min_bound = Timestamp::done();
for (pi, &p) in ports.iter().enumerate() {
match ts_prefix[pi].get(take[pi]) {
Some(&ts) => min_packet = min_packet.min(ts),
None => min_bound = min_bound.min(self.bound(p)),
}
}
if min_packet == Timestamp::done() || min_bound <= min_packet {
break;
}
for (pi, _) in ports.iter().enumerate() {
if ts_prefix[pi].get(take[pi]) == Some(&min_packet) {
take[pi] += 1;
}
}
first_ts.get_or_insert(min_packet);
last_ts = min_packet;
rounds += 1;
}
if rounds == 0 {
return None;
}
if rounds < size
&& !ports
.iter()
.all(|&p| self.input_closed[p].load(Ordering::SeqCst))
{
return None; }
Some(Ready {
ts: first_ts.unwrap_or_else(Timestamp::unset),
ports: None,
batch: Some(BatchPlan {
take: ports.into_iter().zip(take).collect(),
last_ts,
}),
})
}
pub(super) fn sync_align(&self, ports: impl Iterator<Item = usize>) -> Option<Timestamp> {
let mut min_packet = Timestamp::done();
let mut min_bound = Timestamp::done();
for i in ports {
match self.front_ts(i) {
Some(ts) => min_packet = min_packet.min(ts),
None => min_bound = min_bound.min(self.bound(i)),
}
}
if min_packet == Timestamp::done() {
return None; }
if min_bound > min_packet {
Some(min_packet)
} else {
None }
}
pub(super) fn all_inputs_closed_and_drained(&self) -> bool {
if self.is_source() {
return self.source_done.load(Ordering::SeqCst);
}
self.forward_ports()
.all(|i| self.input_closed[i].load(Ordering::SeqCst) && self.queue_len(i) == 0)
}
}