Skip to main content

lmflow/graph/
scheduler.rs

1//! Node claiming, dispatch, execution, and ordered output flushing.
2
3use super::*;
4
5impl GraphInner {
6    pub(super) fn state(&self) -> State {
7        *self.state.lock().expect("state lock poisoned")
8    }
9    pub(super) fn set_state(&self, s: State) {
10        *self.state.lock().expect("state lock poisoned") = s;
11        if s == State::Terminated {
12            self.request_wakeup();
13        }
14    }
15
16    pub(super) fn send(&self, edge: EdgeId, mut pkt: Packet, blocking: bool) -> Result<()> {
17        match self.state() {
18            State::Running => {}
19            State::Draining | State::Terminated => return Err(Error::Closed),
20            s => {
21                return Err(Error::State(format!(
22                    "send requires the graph to be Running (current {s:?}); call start first"
23                )))
24            }
25        }
26        if self.shared.is_cancelled() {
27            return Err(Error::Cancelled);
28        }
29        if let Some(e) = self.shared.first_error() {
30            return Err(e);
31        }
32        if self.edges[edge].is_closed() {
33            return Err(Error::Closed);
34        }
35        // Stream data cannot carry internal lifecycle/bound sentinels.
36        if !pkt.timestamp().is_allowed_in_stream() {
37            return Err(Error::InvalidArg(
38                format!("packets on a graph input port must carry an explicit timestamp in the legal stream range (UNSET and other internal sentinels are invalid): {}", pkt.timestamp()),
39            ));
40        }
41
42        // Reject callback reentry before pumping delegated work for backpressure.
43        if self.edges[edge].is_ingress_owner() {
44            return Err(Error::State(
45                "cannot send to the same input from its dispatch callback".into(),
46            ));
47        }
48        // 全局水位:超限时把压力转化成图输入口背压(§7.5)。
49        let mut watermark_blocked = false;
50        while self.shared.over_watermark() {
51            if !blocking {
52                return Err(Error::WouldBlock);
53            }
54            if !watermark_blocked {
55                watermark_blocked = true;
56                self.begin_watermark_block(edge);
57            }
58            // 长时间背压等待期间图可能被取消/出错,及时退出而不是傻等。
59            if self.shared.is_cancelled() {
60                self.finish_watermark_block(edge, false);
61                return Err(Error::Cancelled);
62            }
63            if let Some(e) = self.shared.first_error() {
64                self.finish_watermark_block(edge, false);
65                return Err(e);
66            }
67            // 先记活动代数,再尝试推进/等待 —— 避免判定与等待之间丢唤醒。
68            let before = self.activity_gen();
69            if self.pump_step() {
70                continue; // 在调用线程上推进了主线程执行器
71            }
72            // 本线程推不动。若线程池还有在飞任务,就等它们排水(这才是真正的背压);
73            // 若全图都空了水位却下不去(如下游无人消费),那是真卡死 —— 报错而非永久阻塞。
74            if self.workers_idle() {
75                self.finish_watermark_block(edge, false);
76                return Err(Error::WouldBlock);
77            }
78            self.wait_activity_since(before, std::time::Duration::from_millis(100));
79        }
80        if watermark_blocked {
81            self.finish_watermark_block(edge, true);
82        }
83
84        // Do not reserve this input while pumping/waiting for watermark space:
85        // delegated callbacks may need to close or send on it. Serialize only commit.
86        let mut ingress = self.edges[edge].enter_ingress(blocking)?;
87        let result = (|| {
88            if self.edges[edge].is_closed() {
89                return Err(Error::Closed);
90            }
91            if self.shared.is_cancelled() {
92                return Err(Error::Cancelled);
93            }
94            if let Some(error) = self.shared.first_error() {
95                return Err(error);
96            }
97            // 时间戳单调性:图输入口强制校验(ADR #23)
98            self.check_input_monotonic(edge, &pkt)?;
99            if self.full_stats() {
100                pkt.rebase_ingress_us(self.epoch_us());
101            }
102
103            // 分发给该边的所有消费者(各自一份引用)与 poller/observer
104            self.dispatch(edge, std::slice::from_ref(&pkt)); // 单包不必为它分配 Vec
105            self.schedule_consumers(edge);
106            Ok(())
107        })();
108        if ingress.release_unless_closing() {
109            self.close_edge_committed(edge);
110            self.set_state_draining_if_all_inputs_closed();
111        }
112        result
113    }
114
115    /// 图输入口的时间戳必须严格递增(ADR #23)。
116    ///
117    /// 参照值单独记录在边上,而不是看队列里剩什么 —— 否则队列一排空,
118    /// 回退甚至重复的时间戳就能悄悄混进来,下游行为随之变得难以解释。
119    pub(super) fn check_input_monotonic(&self, edge: EdgeId, pkt: &Packet) -> Result<()> {
120        let e = &self.edges[edge];
121        let mut last = e.last_sent.lock().expect("timestamp lock poisoned");
122        if *last != Timestamp::unset() && pkt.timestamp() <= *last {
123            return Err(Error::InvalidArg(format!(
124                "graph input port `{}` timestamps must be strictly increasing: previous {}, this one {}",
125                e.name,
126                *last,
127                pkt.timestamp()
128            )));
129        }
130        *last = pkt.timestamp();
131        Ok(())
132    }
133
134    /// 把一批包投递到边的消费者与订阅者。
135    /// 把一批包投递到边的每个消费者队列。**只读 `packets`**(逐个 `clone` 引用计数),
136    /// 故取切片而非 `Vec` —— 让调用方保留缓冲的所有权与容量。
137    pub(super) fn dispatch(&self, edge_id: EdgeId, packets: &[Packet]) {
138        let edge = &self.edges[edge_id];
139        if edge.is_graph_output && self.full_stats() {
140            for pkt in packets {
141                self.record_e2e(pkt);
142            }
143        }
144
145        // 订阅者(poller / observer)各自独立一份
146        {
147            let pollers = edge
148                .pollers
149                .lock()
150                .expect("poller list lock poisoned")
151                .clone();
152            let mut any = false;
153            for p in &pollers {
154                for pkt in packets {
155                    any |= p.push(self, pkt.clone());
156                }
157            }
158            if any {
159                self.notify_activity();
160            }
161        }
162        {
163            // 快照订阅者后**释放锁再回调** —— 回调是宿主代码(可能慢、可能回调进引擎),
164            // 持锁调用会造成争用甚至重入死锁(observer 若又触达同一条边的 observers 锁)。
165            // observer 只增不删,快照是安全的。
166            let observers: Vec<Observer> = {
167                let guard = edge.observers.lock().expect("observer list lock poisoned");
168                if guard.is_empty() {
169                    Vec::new()
170                } else {
171                    guard.clone()
172                }
173            };
174            for o in &observers {
175                for pkt in packets {
176                    match o {
177                        Observer::C { cb, user, .. } => {
178                            let ffi = crate::ffi::borrow_packet(pkt);
179                            unsafe { cb(*user, ffi) };
180                        }
181                        Observer::Rust { callback, .. } => callback(pkt),
182                    }
183                }
184            }
185        }
186
187        // 内部消费者:每个输入口一份(仅克隆引用计数)
188        for &(node, port) in &edge.consumers {
189            let cap = if self.nodes[node].input_is_back_edge[port] {
190                Some(1) // 反馈寄存器:cap-1 drop-old,只留最新一包
191            } else {
192                match &self.nodes[node].policy {
193                    InputPolicy::FixedSize { capacity } => Some(*capacity),
194                    _ => None,
195                }
196            };
197            let mut dropped = 0u64;
198            let mut q = self.nodes[node].input_queues[port]
199                .lock()
200                .expect("queue lock poisoned");
201            // An accepted send or producer flush may still be dispatching when
202            // cancellation/error closes a consumer. Check under the queue lock
203            // so it cannot refill an input after forced-close cleanup drains it.
204            if self.shared.is_cancelled() || self.shared.has_error() {
205                continue;
206            }
207            for pkt in packets {
208                // fixed_size:满则丢最旧的。这是**有意的有损**策略,且不阻塞上游,
209                // 故与「内部边不背压」不冲突,而是其配套的内存约束手段。
210                if let Some(cap) = cap {
211                    while q.len() >= cap {
212                        if let Some(old) = q.pop_front() {
213                            self.nodes[node].input_queue_bytes[port]
214                                .fetch_sub(old.byte_size(), Ordering::SeqCst);
215                            self.shared.on_dequeue(old.byte_size());
216                            dropped += 1;
217                        } else {
218                            break;
219                        }
220                    }
221                }
222                self.shared.on_enqueue(pkt.byte_size());
223                self.nodes[node].input_queue_bytes[port]
224                    .fetch_add(pkt.byte_size(), Ordering::SeqCst);
225                q.push_back(pkt.clone());
226            }
227            let depth = q.len();
228            let queued_bytes = self.nodes[node].input_queue_bytes[port].load(Ordering::SeqCst);
229            self.nodes[node].input_queue_stats[port]
230                .peak_packets
231                .fetch_max(depth, Ordering::Relaxed);
232            self.nodes[node].input_queue_stats[port]
233                .peak_bytes
234                .fetch_max(queued_bytes, Ordering::Relaxed);
235            // 高水位:depth 本就为软限告警算好了,这里顺手 fetch_max —— 定位积压节点。
236            if self.basic_stats() {
237                self.nodes[node]
238                    .stats
239                    .peak_queue_depth
240                    .fetch_max(depth, Ordering::Relaxed);
241            }
242            drop(q);
243            // 入队后,该口不会再来 <= 最后这个包时间戳的数据
244            if let Some(last) = packets.last() {
245                self.nodes[node].advance_bound(port, last.timestamp().next_allowed_in_stream());
246            }
247            if dropped > 0 {
248                self.note_dropped(edge_id, dropped);
249            }
250            self.warn_if_over_soft_limit(edge_id, depth);
251        }
252        if let Some(last) = packets.last() {
253            self.publish_bound(edge_id, last.timestamp().next_allowed_in_stream());
254        }
255    }
256
257    /// 记录丢包。**绝不静默**:首次丢弃打 WARN,之后按指数退避,避免日志洪水。
258    pub(super) fn note_dropped(&self, edge_id: EdgeId, n: u64) {
259        let e = &self.edges[edge_id];
260        let before = e.dropped.fetch_add(n, Ordering::Relaxed); // 纯计数器
261        let after = before + n;
262        if before == 0 || after.is_power_of_two() {
263            runtime::log_warn(&format!(
264                "edge `{}` has dropped {} packets total due to the fixed_size policy (consumer can't keep up; observe with dropped_count)",
265                e.name, after
266            ));
267        }
268    }
269
270    /// 内部边只有软水位:超了告警,但**不阻塞生产者**(§7.5)。
271    pub(super) fn warn_if_over_soft_limit(&self, edge_id: EdgeId, depth: usize) {
272        let limit = self.shared.config.max_queue_size;
273        if limit == 0 || depth <= limit {
274            return;
275        }
276        // 指数退避,避免日志洪水:depth 恰为 limit 的 2^k 倍时才打
277        let ratio = depth / limit;
278        if ratio.is_power_of_two() {
279            runtime::log_warn(&format!(
280                "edge `{}` has {} packets backlogged (soft limit {}); consumer may not be keeping up",
281                self.edges[edge_id].name, depth, limit
282            ));
283        }
284    }
285
286    pub(super) fn schedule_consumers(&self, edge: EdgeId) {
287        let consumers: Vec<NodeId> = self.edges[edge].consumers.iter().map(|&(n, _)| n).collect();
288        for n in consumers {
289            self.schedule_node(n);
290        }
291    }
292
293    /// 把一个已认领的调用派给节点所属的执行器。
294    /// 与 `try_claim` 1:1 配对(每次成功认领派一个任务)。
295    ///
296    /// 全局 `in_flight` 已在认领仍持节点调度锁时递增,这里不能再加。否则
297    /// `try_claim` 返回到任务真正入队之间会出现「节点已有已认领调用、全局仍显示空闲」
298    /// 的窗口,`wait_done` 可能把正常排空误报成卡死。
299    pub(super) fn dispatch_task(&self, n: NodeId) {
300        let executor = &self.executors[self.nodes[n].executor];
301        if !executor.submit(n) {
302            // 池已关停(仅发生在拆图时):撤销全局计数。该次认领残留在 ready 里,
303            // 但拆图路径不依赖精确排空(GraphInner::drop 兜底关流),不会死锁。
304            self.in_flight.fetch_sub(1, Ordering::SeqCst);
305        } else if executor.is_delegating() {
306            self.request_wakeup();
307        }
308        self.notify_activity();
309    }
310
311    /// 线程池工作线程的入口。
312    pub fn run_node_on_worker(&self, n: NodeId) {
313        self.run_node(n);
314        self.in_flight.fetch_sub(1, Ordering::SeqCst);
315        if self.shared.is_cancelled() || self.shared.has_error() {
316            self.resume_blocked_flushes();
317            self.finish_all_backpressure_blocks();
318        }
319        self.notify_activity();
320    }
321
322    /// 线程池延迟任务入口:只有当前代次的等待 Source 会被重新放行。
323    pub fn wake_source_on_worker(&self, n: NodeId, generation: u64) {
324        let node = &self.nodes[n];
325        if node.source_wake_generation.load(Ordering::SeqCst) != generation {
326            self.notify_activity();
327            return;
328        }
329        node.source_waiting.store(false, Ordering::SeqCst);
330        node.source_wait_reason.store(0, Ordering::Relaxed);
331        node.source_wake_deadline_us.store(0, Ordering::Relaxed);
332        if matches!(self.state(), State::Running | State::Draining)
333            && !self.shared.is_cancelled()
334            && !self.shared.has_error()
335            && !node.source_done.load(Ordering::SeqCst)
336        {
337            self.schedule_node(n);
338        } else {
339            self.maybe_close(n);
340        }
341        self.notify_activity();
342    }
343
344    pub fn executor_task_completed(&self) {
345        self.notify_activity();
346        self.request_wakeup();
347    }
348
349    /// 执行器空闲 = 没有已认领调用,且没有委托给宿主线程的待办。
350    ///
351    /// `in_flight` 在 `try_claim` 内、仍持节点调度锁时递增,覆盖「已认领但尚未入队」
352    /// 的阶段;否则并发关流可能撞上瞬时假空闲。
353    pub(super) fn workers_idle(&self) -> bool {
354        self.in_flight.load(Ordering::SeqCst) == 0
355            && !self.delegated_running.load(Ordering::Acquire)
356            && self
357                .executors
358                .iter()
359                .all(|executor| !executor.has_pending_work())
360    }
361
362    /// 逻辑空闲还要求没有因内部容量不足而保留的待刷新 staging。
363    pub(super) fn is_idle(&self) -> bool {
364        self.workers_idle()
365            && self
366                .blocked_flush_nodes
367                .lock()
368                .expect("blocked flush lock poisoned")
369                .is_empty()
370    }
371
372    /// 任何进展都要通知:取到输出、节点关闭、出错、任务入队/完成。
373    /// 否则阻塞中的宿主线程会白等到超时。
374    pub(super) fn notify_activity(&self) {
375        let (m, cv) = &self.activity;
376        let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
377        a.gen = a.gen.wrapping_add(1);
378        // 代数**必须**递增(防丢唤醒的本体);但没人在等时就别去做那次 futex 唤醒。
379        let wake = a.waiters > 0;
380        drop(a);
381        if wake {
382            cv.notify_all();
383        }
384    }
385
386    /// 读取当前活动代数。**必须在判断 is_idle/is_done 之前读取**,再据此 `wait_activity_since`,
387    /// 否则会丢唤醒:若在「判断非空闲」与「开始等待」之间任务恰好全部完成,
388    /// 等待会一直睡到超时(那 55ms 的假慢就是这么来的)。
389    pub(super) fn activity_gen(&self) -> u64 {
390        self.activity
391            .0
392            .lock()
393            .unwrap_or_else(|e| e.into_inner())
394            .gen
395    }
396
397    /// 等到活动代数不等于 `before`(即有新进展)或超时。
398    pub(super) fn wait_activity_since(&self, before: u64, timeout: std::time::Duration) {
399        let (m, cv) = &self.activity;
400        let mut a = m.lock().unwrap_or_else(|e| e.into_inner());
401        if a.gen != before {
402            return; // 已有进展,不必等(也就不必登记为等待者)
403        }
404        // 在**持锁**期间登记:notifier 要么看见它(会 wake),要么还没拿到锁
405        // (那它递增的 gen 会让下面的谓词立刻为假)。见 `Activity` 的说明。
406        a.waiters += 1;
407        let (mut guard, _res) = cv
408            .wait_timeout_while(a, timeout, |x| x.gen == before)
409            .unwrap_or_else(|e| e.into_inner());
410        guard.waiters -= 1;
411        drop(guard);
412    }
413
414    /// 认领一次调用:在锁下**原子地**取一个 context 槽、按对齐时间戳弹入输入、分配序号,
415    /// 放进 `ready` 待执行。成功返回 true —— 调用方应随即 `dispatch_task` 派一个任务。
416    ///
417    /// readiness 与弹包必须在同一把锁下完成,否则两个并发认领会取到同一时间戳的包。
418    pub(super) fn try_claim(&self, n: NodeId) -> bool {
419        if self.shared.has_error()
420            || self.shared.is_cancelled()
421            || self.paused.load(Ordering::SeqCst)
422        {
423            return false;
424        }
425        let node = &self.nodes[n];
426        let mut s = node.sched.lock().expect("scheduler lock poisoned");
427        if !s.opened || s.close_started {
428            return false;
429        }
430        if s.blocked_flush.is_some() {
431            return false; // 先把已完成调用的 staging 刷出去,再认领新输入
432        }
433        if s.in_flight >= node.max_in_flight {
434            return false; // 达容量上限;释放槽后由 finish→schedule_node 重扫
435        }
436        // 就绪判定(会短暂锁 input_queues / input_bounds,锁序 sched→queue/bound 一致)
437        let Some(ready) = node.readiness() else {
438            return false;
439        };
440        let ts = ready.ts;
441        let slot = s
442            .free_slots
443            .pop()
444            .expect("a free slot must exist when in_flight < max");
445        let seq = s.next_seq;
446        s.next_seq += 1;
447        s.in_flight += 1;
448        s.ready.push_back((slot, seq));
449        self.in_flight.fetch_add(1, Ordering::SeqCst);
450
451        // 仍持 sched 锁弹入输入 —— 保证 readiness+pop 原子。该槽此刻独占(刚从 free 取出)。
452        let ctx = unsafe { node.ctx_slot(slot) };
453        ctx.reset();
454        // 批处理:按就绪期算好的计划,给每个正向口整批弹包。各口取数可以不同
455        // (对齐语义,见 `batch_readiness`)。input_ts = 本批末尾的对齐时间戳,下游单调。
456        if let Some(plan) = &ready.batch {
457            for &(port, count) in &plan.take {
458                let remaining = {
459                    let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
460                    for _ in 0..count {
461                        let Some(p) = q.pop_front() else { break };
462                        node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
463                        self.shared.on_dequeue(p.byte_size());
464                        if self.basic_stats() {
465                            node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
466                        }
467                        ctx.input_batches[port].push(p);
468                    }
469                    q.len() // 顺手读,省一次同一把锁的再获取(ADR #36)
470                };
471                // 每个参与口都推进到末尾时间戳之后 —— 与 sync 一致:即便某口本批一个包
472                // 都没取到,也要告诉下游「本口不会再有 <= last_ts 的数据」。对齐保证了
473                // 各口 <= last_ts 的包都已被消费(每轮取的是全局最小)。
474                node.advance_bound(port, plan.last_ts.next_allowed_in_stream());
475                ctx.inputs_done[port] =
476                    node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
477            }
478            // 反馈口:多输入口的 batch 才使 batch + back_edges 成为可能(单口时它凑不出
479            // 「至少一个正向口 + 一个反馈口」)。语义与其它策略一致 —— 每次触发读一次最新值,
480            // 不参与对齐、不推进 bound。
481            for port in 0..node.input_queues.len() {
482                if !node.input_is_back_edge[port] {
483                    continue;
484                }
485                let (popped, remaining) = {
486                    let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
487                    let p = q.pop_front();
488                    (p, q.len())
489                };
490                if let Some(p) = popped {
491                    node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
492                    self.shared.on_dequeue(p.byte_size());
493                    if self.basic_stats() {
494                        node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
495                    }
496                    ctx.inputs[port] = Some(p);
497                }
498                ctx.inputs_done[port] =
499                    node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
500            }
501            ctx.input_ts = plan.last_ts;
502            return true;
503        }
504        for port in 0..node.input_queues.len() {
505            if node.input_is_back_edge[port] {
506                // 反馈寄存器:取最新一包(队列 cap-1),不参与 ts 对齐、不推进 bound。
507                // 首拍(尚无反馈)队列为空 → ctx.inputs[port] = None,内核看到空反馈,自处理。
508                let (popped, remaining) = {
509                    let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
510                    let p = q.pop_front();
511                    (p, q.len())
512                };
513                if let Some(p) = popped {
514                    node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
515                    self.shared.on_dequeue(p.byte_size());
516                    if self.basic_stats() {
517                        node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
518                    }
519                    ctx.inputs[port] = Some(p);
520                }
521                ctx.inputs_done[port] =
522                    node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
523                continue;
524            }
525            // 只处理「参与本次触发」的口(SyncSet:就绪组;其余策略:全部口)。
526            // 非参与口原样不动:不弹包、不推进 bound —— 它的包(可能属别的组)留给下次。
527            let participates = ready.ports.as_ref().is_none_or(|set| set.contains(&port));
528            if !participates {
529                continue;
530            }
531            // 只取时间戳恰好等于 ts 的包;某口在该时刻没有数据是合法的(算子看到空包),
532            // 这正是时间戳对齐的语义 —— 若无条件每口弹一个,就会把不同时刻的数据配到一起。
533            // 一次临界区办三件事:读队首 ts、按需弹包、读剩余长度。
534            // 原先 `front_ts` / `pop_front` / `queue_len` 各拿一次**同一把**队列锁(每口 3 次)。
535            // 安全性:全程持 `sched`,而只有 `try_claim` 会 pop(ADR #30 pop-at-claim),
536            // 别的线程只 push(追加尾部、不动队首)—— 故队首稳定。
537            // 只取时间戳恰好等于 ts 的包;某口在该时刻没有数据是合法的(算子看到空包),
538            // 这正是时间戳对齐的语义 —— 若无条件每口弹一个,就会把不同时刻的数据配到一起。
539            let (popped, remaining) = {
540                let mut q = node.input_queues[port].lock().expect("queue lock poisoned");
541                let hit = q.front().map(|p| p.timestamp()) == Some(ts);
542                let p = if hit { q.pop_front() } else { None };
543                (p, q.len())
544            };
545            if let Some(p) = popped {
546                node.input_queue_bytes[port].fetch_sub(p.byte_size(), Ordering::SeqCst);
547                self.shared.on_dequeue(p.byte_size());
548                if self.basic_stats() {
549                    node.stats.packets_in.fetch_add(1, Ordering::Relaxed);
550                }
551                ctx.inputs[port] = Some(p);
552            }
553            node.advance_bound(port, ts.next_allowed_in_stream());
554            // `remaining` 与 pop 同一临界区内读得。这不改语义:`inputs_done` 还要求
555            // `input_closed`,而关流后不再有 push,长度已稳定。
556            ctx.inputs_done[port] =
557                node.input_closed[port].load(Ordering::SeqCst) && remaining == 0;
558        }
559        // 源节点无输入包,用认领序号当单调时间戳(auto-emit 继承 → 下游单调,复用 seq 重排)。
560        ctx.input_ts = if node.is_source() {
561            Timestamp(seq as i64)
562        } else {
563            ts
564        };
565        true
566    }
567
568    /// 尽力填满容量:反复认领并派任务,直到无法再认领。
569    /// `max_in_flight == 1` 时每轮至多派一个,与串行行为一致。
570    pub(super) fn schedule_node(&self, n: NodeId) {
571        while self.try_claim(n) {
572            self.dispatch_task(n);
573            // 本次认领已从某些内部输入队列弹包,可能为上游腾出了容量。
574            self.resume_blocked_flushes();
575        }
576    }
577
578    /// 跑一个委托给宿主线程的任务。返回是否真的跑了。
579    ///
580    /// 多个宿主线程可能同时进入阻塞接口;原子闸门保证同一张图一次只由一个线程
581    /// 执行委托任务。游标则让多个委托执行器轮询取任务,避免固定从第一个开始导致饥饿。
582    pub(super) fn run_one_main_task(&self) -> bool {
583        if self
584            .delegated_running
585            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
586            .is_err()
587        {
588            return false;
589        }
590        struct RunningGuard<'a>(&'a AtomicBool);
591        impl Drop for RunningGuard<'_> {
592            fn drop(&mut self) {
593                self.0.store(false, Ordering::Release);
594            }
595        }
596        let _running = RunningGuard(&self.delegated_running);
597
598        let len = self.executors.len();
599        let start = self.delegated_cursor.fetch_add(1, Ordering::Relaxed) % len;
600        let next = (0..len).find_map(|offset| {
601            let index = (start + offset) % len;
602            self.executors[index]
603                .take_delegated()
604                .map(|node| (index, node))
605        });
606        match next {
607            Some((index, n)) => {
608                self.delegated_cursor
609                    .store(index.wrapping_add(1), Ordering::Relaxed);
610                let started = self.full_stats().then(Instant::now);
611                self.run_node(n);
612                self.in_flight.fetch_sub(1, Ordering::SeqCst);
613                self.executors[index].complete_delegated(started.map(|started| started.elapsed()));
614                self.notify_activity();
615                true
616            }
617            None => false,
618        }
619    }
620
621    /// 执行一步:跑一个主线程任务,或推进关流。返回是否真的做了事。
622    pub(super) fn pump_step(&self) -> bool {
623        self.run_one_main_task() || self.try_advance_closing()
624    }
625
626    pub(super) fn delegated_tasks_pending(&self) -> bool {
627        self.executors
628            .iter()
629            .any(|executor| executor.is_delegating() && executor.pending() > 0)
630    }
631
632    pub(super) fn request_wakeup(&self) {
633        self.wakeup_generation.fetch_add(1, Ordering::AcqRel);
634        if self
635            .wakeup_pending
636            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
637            .is_err()
638        {
639            return;
640        }
641        let callback = self
642            .wakeup_callback
643            .lock()
644            .unwrap_or_else(|error| error.into_inner());
645        let Some(callback) = callback.as_ref() else {
646            self.wakeup_pending.store(false, Ordering::Release);
647            return;
648        };
649        // 持锁调用是刻意的:clear_wakeup_callback 必须等已进入的回调返回,C/Python
650        // 宿主随后才能安全释放 user 指针。回调契约禁止重入 graph API,只允许投递事件。
651        if catch_unwind(AssertUnwindSafe(|| callback())).is_err() {
652            self.wakeup_pending.store(false, Ordering::Release);
653            runtime::log_warn("graph wakeup callback panicked; wakeup re-armed");
654        }
655    }
656
657    pub(super) fn set_wakeup_callback(&self, callback: Option<WakeupCallback>) {
658        *self
659            .wakeup_callback
660            .lock()
661            .unwrap_or_else(|error| error.into_inner()) = callback;
662        self.wakeup_pending.store(false, Ordering::Release);
663        if self.delegated_tasks_pending()
664            || self.shared.is_cancelled()
665            || self.shared.has_error()
666            || self.state() == State::Terminated
667        {
668            self.request_wakeup();
669        }
670    }
671
672    pub(super) fn run_node(&self, n: NodeId) {
673        let node = &self.nodes[n];
674        // 取出一个待执行的调用(认领时已把输入弹进对应槽)。
675        let inv = {
676            node.sched
677                .lock()
678                .expect("scheduler lock poisoned")
679                .ready
680                .pop_front()
681        };
682        let Some((slot, seq)) = inv else {
683            // 认领与派任务 1:1,理论上不会为空;稳妥起见走一遍收尾。
684            self.finish(n);
685            return;
686        };
687
688        // A claim may have waited in the executor queue while the graph stopped.
689        // Retire it through the normal ordered-completion path without entering
690        // user code, so slots, input references, and in-flight counts are released.
691        if self.shared.is_cancelled() || self.shared.has_error() {
692            self.complete_invocation(n, slot, seq, false);
693            return;
694        }
695
696        // 契约类型校验(在本槽上)。类型不符宁可报错,也不让算子按错误类型解读内存。
697        let ok = match self.check_input_types(n, slot) {
698            Err(e) => self.on_node_error(n, slot, e),
699            Ok(()) => {
700                let rc = self.call_kernel(n, slot, KernelPhase::Process);
701                let ctx = unsafe { node.ctx_slot(slot) };
702                if node.is_source() {
703                    if ctx.source_yield.is_some() {
704                        node.source_yield_count.fetch_add(1, Ordering::Relaxed);
705                    }
706                    if ctx.source_done {
707                        node.source_done.store(true, Ordering::SeqCst);
708                        node.source_waiting.store(false, Ordering::SeqCst);
709                        node.source_wait_reason.store(0, Ordering::Relaxed);
710                        node.source_wake_deadline_us.store(0, Ordering::Relaxed);
711                        node.sched
712                            .lock()
713                            .expect("scheduler lock poisoned")
714                            .source_reschedule = None;
715                    } else {
716                        let rate_delay = node.min_period.and_then(|period| {
717                            let last = node.last_fire.lock().expect("last_fire lock poisoned");
718                            last.and_then(|started| period.checked_sub(started.elapsed()))
719                        });
720                        let reschedule = match (ctx.source_yield, rate_delay) {
721                            (Some(yield_delay), Some(rate_delay)) => Some(SourceReschedule {
722                                delay: yield_delay.max(rate_delay),
723                                reason: SourceWaitReason::RateAndYield,
724                            }),
725                            (Some(delay), None) => Some(SourceReschedule {
726                                delay,
727                                reason: SourceWaitReason::Yield,
728                            }),
729                            (None, Some(delay)) => Some(SourceReschedule {
730                                delay,
731                                reason: SourceWaitReason::Rate,
732                            }),
733                            (None, None) => None,
734                        };
735                        if let Some(reschedule) = reschedule {
736                            node.source_waiting.store(true, Ordering::SeqCst);
737                            node.source_wait_reason.store(
738                                match reschedule.reason {
739                                    SourceWaitReason::Rate => 1,
740                                    SourceWaitReason::Yield => 2,
741                                    SourceWaitReason::RateAndYield => 3,
742                                },
743                                Ordering::Relaxed,
744                            );
745                            node.sched
746                                .lock()
747                                .expect("scheduler lock poisoned")
748                                .source_reschedule = Some(reschedule);
749                        }
750                    }
751                } else if ctx.source_yield.is_some() && rc == 0 {
752                    return self.complete_invocation(
753                        n,
754                        slot,
755                        seq,
756                        self.on_node_error(
757                            n,
758                            slot,
759                            Error::Kernel(format!(
760                                "[{}] source_yield is only valid for source nodes",
761                                node.name
762                            )),
763                        ),
764                    );
765                }
766                if rc != 0 {
767                    let e = unsafe { node.ctx_slot(slot) }.take_error(rc);
768                    self.on_node_error(n, slot, e)
769                } else if let Err(e) = self.check_output_types(n, slot) {
770                    self.on_node_error(n, slot, e)
771                } else {
772                    if self.basic_stats() {
773                        node.stats.processed.fetch_add(1, Ordering::Relaxed);
774                    }
775                    true
776                }
777            }
778        };
779        self.complete_invocation(n, slot, seq, ok);
780    }
781
782    /// 调用完成:按 `seq` 顺序刷新输出并释放槽。保证下游看到的时间戳单调 ——
783    /// 即使后面的时间戳先算完,也要等前面的先刷。
784    /// 算子失败时按本节点的 [`OnError`] 分流。返回值是给 `complete_invocation` 的 `ok`:
785    /// **`true` 表示「走刷新路径」**(而非「成功」)。
786    ///
787    /// `Skip` 之所以返回 `true`,是因为刷新路径才会**推进下游时间戳边界** ——
788    /// staging 已被 `discard_staging` 清空,于是 `flush_one` 落到「无产出」分支,
789    /// 自动 `propagate_bound(input_ts + 1)`。这正是 `Filter` 丢包时依赖的同一套机制。
790    /// 不推进边界的话下游会永远等这一刻,等于把一帧出错升级成整图卡死。
791    pub(super) fn on_node_error(&self, n: NodeId, slot: usize, e: Error) -> bool {
792        let node = &self.nodes[n];
793        let before = node.stats.errors.fetch_add(1, Ordering::Relaxed);
794        match node.on_error {
795            OnError::Abort => {
796                self.shared.record_error(e);
797                false // 丢弃产出,不刷新;has_error 置位后调度不再放行 → 全图终止
798            }
799            OnError::Skip => {
800                // 有损行为绝不静默:计数(node_stats().errors)+ 打 WARN。
801                // 指数退避,避免每帧都错时刷爆日志(与 note_dropped 同法)。
802                let after = before + 1;
803                if before == 0 || after.is_power_of_two() {
804                    runtime::log_warn(&format!(
805                        "node `{}`: skipping a failed packet (on_error=skip), {} so far: {}",
806                        node.name, after, e
807                    ));
808                }
809                // 清掉这一包可能已写了一半的产出,再走刷新路径(仅为推进边界)。
810                unsafe { node.ctx_slot(slot) }.discard_staging();
811                true
812            }
813        }
814    }
815
816    pub(super) fn complete_invocation(&self, n: NodeId, slot: usize, seq: u64, ok: bool) {
817        let node = &self.nodes[n];
818        if self.full_stats() {
819            self.propagate_output_ingress(n, slot);
820        }
821        // 算子回调已经返回,后续刷新只依赖 staging / next_bounds / input_ts。
822        // 立即释放本次输入引用;否则本调用若因顺序刷新或内部背压暂存,已经投递到
823        // 下游的同一 payload 仍会被上游 Context 持有,使下游 CoW 静默复制。
824        unsafe { node.ctx_slot(slot) }.clear_inputs();
825        // 登记结果;当前无人刷新则由本线程担任刷新者。
826        //
827        // **快路**:重排缓冲为空、本次恰好就是待刷新序号、且当前无人刷新 —— 直接接手,
828        // 不必经 `pending_flush`。`max_in_flight == 1`(默认)时这条恒成立:同一时刻只有
829        // 一次调用在飞,`seq` 必然等于 `next_flush_seq`。避免每次调用一次 BTreeMap
830        // 插入 + 删除(perf 实测这对增删连带堆分配约占 5~6%)。
831        let mut first: Option<(usize, bool)> = None;
832        let be_flusher = {
833            let mut s = node.sched.lock().expect("scheduler lock poisoned");
834            if !s.flushing
835                && s.blocked_flush.is_none()
836                && s.pending_flush.is_empty()
837                && seq == s.next_flush_seq
838            {
839                s.flushing = true;
840                first = Some((slot, ok));
841                true
842            } else {
843                s.pending_flush.insert(seq, (slot, ok));
844                if s.flushing {
845                    false
846                } else {
847                    s.flushing = true;
848                    true
849                }
850            }
851        };
852        if be_flusher {
853            self.drive_invocation_flushes(n, first);
854        }
855        self.finish(n);
856    }
857
858    pub(super) fn propagate_output_ingress(&self, n: NodeId, slot: usize) {
859        let node = &self.nodes[n];
860        let ctx = unsafe { node.ctx_slot(slot) };
861        let mut ingress = ctx
862            .inputs
863            .iter()
864            .filter_map(|pkt| pkt.as_ref().map(Packet::ingress_us))
865            .filter(|value| *value != 0)
866            .min()
867            .unwrap_or_else(|| self.epoch_us());
868        for batch in &ctx.input_batches {
869            for pkt in batch {
870                let value = pkt.ingress_us();
871                if value != 0 {
872                    ingress = ingress.min(value);
873                }
874            }
875        }
876        for packets in &ctx.staging {
877            for pkt in packets {
878                pkt.mark_ingress_us(ingress);
879            }
880        }
881    }
882
883    /// Source 的输出必须先按序刷新并释放槽,之后才能安排下一次延迟唤醒。
884    pub(super) fn schedule_source_resumption(&self, n: NodeId) {
885        let node = &self.nodes[n];
886        if !node.is_source() || !node.source_waiting.load(Ordering::SeqCst) {
887            return;
888        }
889        if self.shared.is_cancelled()
890            || self.shared.has_error()
891            || !matches!(self.state(), State::Running | State::Draining)
892        {
893            node.source_waiting.store(false, Ordering::SeqCst);
894            node.source_wait_reason.store(0, Ordering::Relaxed);
895            node.source_wake_deadline_us.store(0, Ordering::Relaxed);
896            node.sched
897                .lock()
898                .expect("scheduler lock poisoned")
899                .source_reschedule = None;
900            return;
901        }
902        let reschedule = {
903            let mut sched = node.sched.lock().expect("scheduler lock poisoned");
904            if sched.in_flight != 0 || sched.blocked_flush.is_some() || sched.flushing {
905                return;
906            }
907            sched.source_reschedule.take()
908        };
909        let Some(reschedule) = reschedule else {
910            return;
911        };
912        let generation = node.source_wake_generation.fetch_add(1, Ordering::SeqCst) + 1;
913        node.source_wake_deadline_us.store(
914            self.epoch_us()
915                .saturating_add(reschedule.delay.as_micros().min(i64::MAX as u128) as i64)
916                .saturating_add(1),
917            Ordering::Relaxed,
918        );
919        if !self.executors[node.executor].submit_source_wake(n, generation, reschedule.delay) {
920            node.source_waiting.store(false, Ordering::SeqCst);
921            node.source_wait_reason.store(0, Ordering::Relaxed);
922            node.source_wake_deadline_us.store(0, Ordering::Relaxed);
923        }
924        self.notify_activity();
925    }
926
927    /// 契约声明的输入类型校验。类型不符宁可报错,也不让算子按错误类型解读内存。
928    pub(super) fn check_input_types(&self, n: NodeId, slot: usize) -> Result<()> {
929        let node = &self.nodes[n];
930        let ctx = unsafe { node.ctx_slot(slot) };
931        for (port, &want) in node.input_types.iter().enumerate() {
932            let Some(pkt) = ctx.inputs.get(port).and_then(|s| s.as_ref()) else {
933                continue;
934            };
935            if pkt.is_empty() {
936                continue; // 空包(时间戳边界)不参与类型校验
937            }
938            let got = pkt.type_id();
939
940            crate::packet::validate_type_id(got).map_err(|error| {
941                Error::Kernel(format!(
942                    "[{}] input port `{}` carries an invalid type: {error}",
943                    node.name,
944                    node.in_ports.name(port).unwrap_or("?"),
945                ))
946            })?;
947
948            if want == 0 {
949                continue; // 未声明类型 = 接受任意
950            }
951            if got != want {
952                // `got == NONE` 是一个**有明确出路**的特例,但出路取决于包是谁造的 ——
953                // 按 payload 形态分别给建议,否则会把 Rust API 推给 C/C++ 宿主(或反之)。
954                // NONE 的来源不止一个:`Packet::new`(Native)、`from_foreign(.., 0, ..)`、
955                // 以及 C ABI 侧 type_id 填 0 的自建包(Foreign)。
956                let hint = if got == crate::packet::type_id::NONE {
957                    match pkt.payload() {
958                        Some(crate::packet::Payload::Foreign(_)) => {
959                            " (the packet carries no declared type: its type_id is \
960                             LMFLOW_TYPE_NONE, which means \"skip type checking\"; set a real \
961                             LMFLOW_TYPE_* on the packet you submit, or declare this port as \
962                             any-type)"
963                        }
964                        // Native = Rust 原生 payload,只可能来自 Rust 宿主
965                        _ => {
966                            " (the packet carries no declared type because its payload is \
967                             Rust-native, e.g. built with `Packet::new`; use \
968                             `Packet::from_i64` / `from_f64` / `from_builtin` for built-in \
969                             payloads, implement `InteropType` and use `Packet::from_interop` \
970                             for a custom type, or use unsafe `Packet::new_interop` only after \
971                             manually proving the ABI layout)"
972                        }
973                    }
974                } else {
975                    ""
976                };
977                return Err(Error::Kernel(format!(
978                    "[{}] input port `{}` type mismatch: contract declares {}, actual {}{}",
979                    node.name,
980                    node.in_ports.name(port).unwrap_or("?"),
981                    crate::packet::type_name(want),
982                    crate::packet::type_name(got),
983                    hint,
984                )));
985            }
986        }
987        Ok(())
988    }
989
990    /// 算子暂存输出的类型校验。必须在离开回调后、派发前统一做,因为 C/C++/Python 的
991    /// `emit` ABI 是 `void`:不能依赖算子检查返回值。放在这里也能覆盖源节点、图输出、
992    /// `close` 产出以及所有语言的算子。
993    pub(super) fn check_output_types(&self, n: NodeId, slot: usize) -> Result<()> {
994        let node = &self.nodes[n];
995        let ctx = unsafe { node.ctx_slot(slot) };
996        for (port, packets) in ctx.staging.iter().enumerate() {
997            let want = node.output_types[port];
998            for pkt in packets {
999                if pkt.is_empty() {
1000                    continue;
1001                }
1002                let got = pkt.type_id();
1003                crate::packet::validate_type_id(got).map_err(|error| {
1004                    Error::Kernel(format!(
1005                        "[{}] output port `{}` carries an invalid type: {error}",
1006                        node.name,
1007                        node.out_ports.name(port).unwrap_or("?"),
1008                    ))
1009                })?;
1010                if want != crate::packet::type_id::NONE && got != want {
1011                    return Err(Error::Kernel(format!(
1012                        "[{}] output port `{}` type mismatch: contract declares {}, actual {}",
1013                        node.name,
1014                        node.out_ports.name(port).unwrap_or("?"),
1015                        crate::packet::type_name(want),
1016                        crate::packet::type_name(got),
1017                    )));
1018                }
1019            }
1020        }
1021        Ok(())
1022    }
1023
1024    /// 把时间戳边界推给某条边的所有消费者,并重扫其就绪性。
1025    pub(super) fn propagate_bound(&self, edge: EdgeId, bound: Timestamp) {
1026        let consumers: Vec<(NodeId, usize)> = self.edges[edge].consumers.clone();
1027        for (node, port) in consumers {
1028            self.nodes[node].advance_bound(port, bound);
1029            self.schedule_node(node);
1030        }
1031        self.publish_bound(edge, bound);
1032    }
1033
1034    /// 把单调推进的边界发布给显式订阅者。事件编码为 `payload=None` 的空包,
1035    /// timestamp 即新边界;普通订阅者完全不受影响。
1036    pub(super) fn publish_bound(&self, edge_id: EdgeId, bound: Timestamp) {
1037        let edge = &self.edges[edge_id];
1038        if !edge.has_timestamp_bound_subscriber.load(Ordering::Relaxed) {
1039            return;
1040        }
1041        let should_publish = {
1042            let mut last = edge
1043                .last_published_bound
1044                .lock()
1045                .expect("published-bound lock poisoned");
1046            if bound <= *last {
1047                false
1048            } else {
1049                *last = bound;
1050                true
1051            }
1052        };
1053        if !should_publish {
1054            return;
1055        }
1056
1057        let event = Packet::empty().at(bound);
1058        let pollers = edge
1059            .pollers
1060            .lock()
1061            .expect("poller list lock poisoned")
1062            .clone();
1063        let mut any = false;
1064        for poller in &pollers {
1065            if poller.observe_timestamp_bounds {
1066                any |= poller.push(self, event.clone());
1067            }
1068        }
1069        if any {
1070            self.notify_activity();
1071        }
1072
1073        let observers = edge
1074            .observers
1075            .lock()
1076            .expect("observer list lock poisoned")
1077            .clone();
1078        for observer in &observers {
1079            match observer {
1080                Observer::C {
1081                    cb,
1082                    user,
1083                    observe_timestamp_bounds: true,
1084                } => {
1085                    let ffi = crate::ffi::borrow_packet(&event);
1086                    unsafe { cb(*user, ffi) };
1087                }
1088                Observer::Rust {
1089                    callback,
1090                    observe_timestamp_bounds: true,
1091                } => callback(&event),
1092                Observer::C { .. } | Observer::Rust { .. } => {}
1093            }
1094        }
1095    }
1096
1097    /// 调用算子回调(在指定 context 槽上)。**调用期间不持有任何引擎锁**(R1),
1098    /// 并记录耗时以便定位卡死。可被并发调用(不同槽),故 `process` 必须可重入。
1099    pub(super) fn call_kernel(&self, n: NodeId, slot: usize, phase: KernelPhase) -> i32 {
1100        let node = &self.nodes[n];
1101        if self.full_stats() {
1102            crate::packet::begin_cow_copy_scope();
1103        }
1104        // 直接交出 UnsafeCell 内部指针:不构造 Rust 引用,故与回调内
1105        // 从该指针造出的 `&mut Context` 不冲突(该槽此刻由本调用独占持有)。
1106        let ctx_cell = node.ctxs[slot].get();
1107        let ctx_ptr = ctx_cell as *mut c_void;
1108        // 记账全走原子:改造前这里每次调用要拿 2 次 running_timing 锁 + 1 次 stats 锁
1109        // (再加 run_node 里的 processed 一次)。R1 要求「调算子时不持任何引擎锁」——
1110        // 原子天然满足,也顺带把 4 对 mutex 从每包热路径上去掉了。
1111        // 仅 full 统计读取时钟:`Instant::now()` + 末尾的 `elapsed()` 是每次
1112        // process 两次时钟读。
1113        // 一次时钟读两用:既作本次耗时起点,也作「本节点开始在跑」的时刻。
1114        let started = if self.full_stats() {
1115            Some(Instant::now())
1116        } else {
1117            None
1118        };
1119        if node.stats.in_flight.fetch_add(1, Ordering::Relaxed) == 0 {
1120            if let Some(t) = started {
1121                let since_epoch = t.saturating_duration_since(self.epoch).as_micros() as i64;
1122                node.stats.started_us.store(since_epoch, Ordering::Relaxed);
1123            }
1124        }
1125
1126        // Source rate 只记录本次实际开始时刻;完成后由延迟唤醒节流,不在 worker 内 sleep。
1127        if matches!(phase, KernelPhase::Process) && node.min_period.is_some() {
1128            *node.last_fire.lock().expect("last_fire lock poisoned") = Some(Instant::now());
1129        }
1130
1131        // 安全性:ctx_ptr 来自本槽的 UnsafeCell,该槽此刻独占。
1132        let _buffer_pool = crate::buffer_pool::enter(self.shared.buffer_pool.clone());
1133        let rc = unsafe {
1134            match phase {
1135                KernelPhase::Open => node.kernel.open(ctx_ptr),
1136                KernelPhase::Process => node.kernel.process(ctx_ptr),
1137                KernelPhase::Close => node.kernel.close(ctx_ptr),
1138            }
1139        };
1140        if self.full_stats() {
1141            let cow = crate::packet::end_cow_copy_scope();
1142            if matches!(phase, KernelPhase::Process) {
1143                node.stats
1144                    .cow_copies
1145                    .fetch_add(cow.copies, Ordering::Relaxed);
1146                node.stats.cow_bytes.fetch_add(cow.bytes, Ordering::Relaxed);
1147            }
1148        }
1149
1150        // 归零时**不清** started_us:读侧按 in_flight > 0 判断是否在跑,
1151        // 故无需清零,也就不存在「清零」与「新一次开始」互相覆盖的竞争。
1152        node.stats.in_flight.fetch_sub(1, Ordering::Relaxed);
1153        let Some(t0) = started else { return rc }; // 计时关闭:统计与 watchdog 都不适用
1154        let us = t0.elapsed().as_micros() as i64;
1155        // 逐次调用 trace:记录本次回调的 span(所有阶段,不只 Process)。仅在开启时;
1156        // 关闭时 record_trace 内部立即返回,且上面若非 full 也不会读到这里。
1157        if self.shared.trace_enabled() {
1158            let start_us = t0.saturating_duration_since(self.epoch).as_micros() as i64;
1159            // input_ts = 本次激活的对齐时间戳。回调只读它、且本槽此刻独占,故安全读;
1160            // Open/Close 阶段可能是 UNSET 哨兵。
1161            let input_ts = unsafe { (*ctx_cell).input_ts.0 };
1162            let tphase = match phase {
1163                KernelPhase::Open => crate::trace::TracePhase::Open,
1164                KernelPhase::Process => crate::trace::TracePhase::Process,
1165                KernelPhase::Close => crate::trace::TracePhase::Close,
1166            };
1167            self.shared
1168                .record_trace(n as u32, tphase, start_us, us, input_ts);
1169        }
1170        if matches!(phase, KernelPhase::Process) {
1171            node.stats.total_us.fetch_add(us, Ordering::Relaxed);
1172            node.stats.max_us.fetch_max(us, Ordering::Relaxed);
1173            node.stats.record_latency(us.max(0) as u64);
1174        }
1175        let wd = self.shared.config.watchdog_ms;
1176        if wd > 0 && us as u64 > wd * 1000 {
1177            runtime::log_warn(&format!(
1178                "node `{}`: one {:?} took {} ms, exceeding watchdog {} ms",
1179                node.name,
1180                phase,
1181                us / 1000,
1182                wd
1183            ));
1184        }
1185        rc
1186    }
1187}
1188
1189#[derive(Debug, Clone, Copy)]
1190pub(super) enum KernelPhase {
1191    Open,
1192    Process,
1193    Close,
1194}