Skip to main content

lmflow/
context.rs

1//! 算子上下文:本次调用的输入、输出暂存、参数、side packet、自我信息。
2//!
3//! 关键设计:`emit`/`forward` **不直接写下游边**,而是写进本上下文的 `staging`;
4//! `process` 返回后由引擎统一分发。这样调用算子期间无需持任何边锁
5//! (docs/design.md §7.0 规则 R1)。
6
7use std::collections::BTreeMap;
8use std::ffi::c_char;
9use std::sync::Arc;
10
11use crate::kernel::PortTable;
12use crate::packet::Packet;
13use crate::runtime::{CStrArena, GraphShared};
14use crate::status::{Error, Result};
15use crate::timestamp::Timestamp;
16
17/// 节点参数(YAML 的 `options` 子树)。支持点号路径与数组。
18pub struct Options {
19    root: serde_yaml::Value,
20    strings: CStrArena,
21    json: String,
22}
23
24impl Options {
25    pub fn new(root: serde_yaml::Value) -> Self {
26        let json = serde_json::to_string(&root).unwrap_or_else(|_| "{}".to_string());
27        let json = if json == "null" {
28            "{}".to_string()
29        } else {
30            json
31        };
32        Self {
33            root,
34            strings: CStrArena::default(),
35            json,
36        }
37    }
38
39    /// 按点号路径取值:`"roi.x"` → `root["roi"]["x"]`。
40    pub fn get(&self, path: &str) -> Option<&serde_yaml::Value> {
41        let mut cur = &self.root;
42        for seg in path.split('.') {
43            cur = cur.get(seg)?;
44        }
45        Some(cur)
46    }
47
48    pub fn has(&self, path: &str) -> bool {
49        self.get(path).is_some()
50    }
51
52    pub fn i64(&self, path: &str) -> Option<i64> {
53        let v = self.get(path)?;
54        v.as_i64().or_else(|| v.as_u64().map(|u| u as i64))
55    }
56    pub fn f64(&self, path: &str) -> Option<f64> {
57        let v = self.get(path)?;
58        // 整数字面量也应能读成浮点(YAML 里 1 和 1.0 用户不会区分)
59        v.as_f64().or_else(|| v.as_i64().map(|i| i as f64))
60    }
61    pub fn bool(&self, path: &str) -> Option<bool> {
62        self.get(path)?.as_bool()
63    }
64    pub fn str(&self, path: &str) -> Option<&str> {
65        self.get(path)?.as_str()
66    }
67
68    /// 字符串参数的 C 指针(生命周期随 graph)。
69    pub fn str_cstr(&self, path: &str) -> Option<*const c_char> {
70        self.str(path).map(|s| self.strings.intern(s))
71    }
72
73    pub fn seq(&self, path: &str) -> Option<&Vec<serde_yaml::Value>> {
74        self.get(path)?.as_sequence()
75    }
76
77    pub fn count(&self, path: &str) -> usize {
78        self.seq(path).map_or(0, |s| s.len())
79    }
80
81    pub fn i64_array(&self, path: &str, out: &mut [i64]) -> usize {
82        let Some(seq) = self.seq(path) else { return 0 };
83        for (slot, v) in out.iter_mut().zip(seq.iter()) {
84            *slot = v
85                .as_i64()
86                .or_else(|| v.as_u64().map(|u| u as i64))
87                .unwrap_or(0);
88        }
89        seq.len()
90    }
91
92    pub fn f64_array(&self, path: &str, out: &mut [f64]) -> usize {
93        let Some(seq) = self.seq(path) else { return 0 };
94        for (slot, v) in out.iter_mut().zip(seq.iter()) {
95            *slot = v
96                .as_f64()
97                .or_else(|| v.as_i64().map(|i| i as f64))
98                .unwrap_or(0.0);
99        }
100        seq.len()
101    }
102
103    pub fn str_array(&self, path: &str, out: &mut [*const c_char]) -> usize {
104        let Some(seq) = self.seq(path) else { return 0 };
105        // 先收集,避免在持有 seq 借用时调用 intern(&self 方法,借用检查允许,但保持清晰)
106        let items: Vec<Option<&str>> = seq.iter().map(|v| v.as_str()).collect();
107        for (slot, item) in out.iter_mut().zip(items.iter()) {
108            *slot = match item {
109                Some(s) => self.strings.intern(s),
110                None => self.strings.intern(""),
111            };
112        }
113        seq.len()
114    }
115
116    pub fn json(&self) -> &str {
117        &self.json
118    }
119    pub fn json_cstr(&self) -> *const c_char {
120        self.strings.intern(&self.json)
121    }
122}
123
124/// 一次算子调用的上下文。
125pub struct Context {
126    /// 本次的输入。`None` 表示已被 `take_input` 取走(CoW 省拷贝的前提)。
127    pub inputs: Vec<Option<Packet>>,
128    /// 批处理(`batch` 策略)时本口的多个包;单包策略下恒空、走 `inputs`。见 `input_count`/`input_at`。
129    pub input_batches: Vec<Vec<Packet>>,
130    /// 每个输出口的暂存队列;`process` 返回后由引擎分发。
131    pub staging: Vec<Vec<Packet>>,
132    /// 算子显式推进的时间戳边界(不产出时用它告知下游)。
133    pub next_bounds: Vec<Option<Timestamp>>,
134    pub input_ts: Timestamp,
135
136    pub node_name: String,
137    pub kernel_name: String,
138    pub in_ports: Arc<PortTable>,
139    pub out_ports: Arc<PortTable>,
140    pub options: Arc<Options>,
141    pub side_packets: Arc<BTreeMap<String, Packet>>,
142    pub shared: Arc<GraphShared>,
143    /// 输入口是否已终结(上游关闭且排空)。
144    pub inputs_done: Vec<bool>,
145    /// 算子经 `set_error` 提供的失败原因。
146    pub error_msg: Option<String>,
147    /// A void C ABI output operation failed during this invocation.
148    pub(crate) output_failed: bool,
149    /// 供 `lmflow_ctx_close_reason` 返回;进入 close 前由引擎写入。
150    pub close_reason: i32,
151    /// 源算子经 `source_done()` 自报「已产完」;引擎在 process 返回后读取。
152    pub source_done: bool,
153    /// 源算子协作式让出 worker,并请求引擎在指定延迟后再次调度。
154    pub source_yield: Option<std::time::Duration>,
155    names: CStrArena,
156}
157
158impl Context {
159    pub fn new(
160        node_name: String,
161        kernel_name: String,
162        in_ports: Arc<PortTable>,
163        out_ports: Arc<PortTable>,
164        options: Arc<Options>,
165        side_packets: Arc<BTreeMap<String, Packet>>,
166        shared: Arc<GraphShared>,
167    ) -> Self {
168        let ni = in_ports.len();
169        let no = out_ports.len();
170        Self {
171            inputs: vec![None; ni],
172            input_batches: (0..ni).map(|_| Vec::new()).collect(),
173            staging: (0..no).map(|_| Vec::new()).collect(),
174            next_bounds: vec![None; no],
175            input_ts: Timestamp::unset(),
176            node_name,
177            kernel_name,
178            in_ports,
179            out_ports,
180            options,
181            side_packets,
182            shared,
183            inputs_done: vec![false; ni],
184            error_msg: None,
185            output_failed: false,
186            close_reason: crate::runtime::CLOSE_NORMAL,
187            source_done: false,
188            source_yield: None,
189            names: CStrArena::default(),
190        }
191    }
192
193    /// 每次调用前复位(输入由调用方填充)。
194    pub fn reset(&mut self) {
195        for slot in &mut self.inputs {
196            *slot = None;
197        }
198        for b in &mut self.input_batches {
199            b.clear();
200        }
201        for s in &mut self.staging {
202            s.clear();
203        }
204        self.next_bounds.fill(None);
205        self.input_ts = Timestamp::unset();
206        self.error_msg = None;
207        self.output_failed = false;
208        self.source_done = false;
209        self.source_yield = None;
210        // 下面两项在正常流程里使用前会被重写(claim 时写 inputs_done、进 close 前写
211        // close_reason),故单次调用不清也不出错;但 `reset` 要能用于「彻底静态复位」
212        // (图 reset 重跑),所以这里一并归位,不留任何上一轮残留。
213        self.inputs_done.fill(false);
214        self.close_reason = crate::runtime::CLOSE_NORMAL;
215    }
216
217    /// 处理结束后**立即释放**本次输入的引用。
218    ///
219    /// 这条至关重要:若把输入留到下次 `reset` 才清,上游节点就会一直持有已处理完的包,
220    /// 于是下游 `take_input` 看到的引用数 ≥ 2,CoW 必然复制 —— 「线性管线零拷贝」
221    /// 这条不变量(docs/design.md §3.4)会对任何多节点管线静默失效。
222    /// 顺带也避免大帧被无谓地多留一个周期。
223    pub fn clear_inputs(&mut self) {
224        for slot in &mut self.inputs {
225            *slot = None;
226        }
227        for b in &mut self.input_batches {
228            b.clear();
229        }
230    }
231
232    /// 失败时丢弃暂存,不把半成品输出传播下去(docs/design.md §7.7)。
233    pub fn discard_staging(&mut self) {
234        for s in &mut self.staging {
235            s.clear();
236        }
237        self.next_bounds.fill(None);
238    }
239
240    pub fn node_name_cstr(&self) -> *const c_char {
241        self.names.intern(&self.node_name)
242    }
243    pub fn kernel_name_cstr(&self) -> *const c_char {
244        self.names.intern(&self.kernel_name)
245    }
246    pub fn intern(&self, s: &str) -> *const c_char {
247        self.names.intern(s)
248    }
249
250    pub fn input(&self, idx: usize) -> Option<&Packet> {
251        self.input_at(idx, 0)
252    }
253
254    /// 本次调用某输入口的包数(单包策略恒 0/1;`batch` 策略为该批实际大小)。
255    pub fn input_count(&self, idx: usize) -> usize {
256        match self.input_batches.get(idx) {
257            Some(b) if !b.is_empty() => b.len(),
258            _ => self.inputs.get(idx).map_or(0, |s| s.is_some() as usize),
259        }
260    }
261
262    /// 借用某输入口的第 `k` 个包(单包策略仅 `k==0` 有效)。统一单包 / 批两种交付。
263    pub fn input_at(&self, idx: usize, k: usize) -> Option<&Packet> {
264        match self.input_batches.get(idx) {
265            Some(b) if !b.is_empty() => b.get(k),
266            _ if k == 0 => self.inputs.get(idx)?.as_ref(),
267            _ => None,
268        }
269    }
270
271    /// 取走输入包:所有权移交算子,槽位变空。这是 CoW 零拷贝的前提 ——
272    /// 不取走的话上下文仍持一份引用,`make_mutable` 必然复制。
273    pub fn take_input(&mut self, idx: usize) -> Packet {
274        match self.inputs.get_mut(idx) {
275            Some(slot) => slot.take().unwrap_or_default(),
276            None => Packet::empty(),
277        }
278    }
279
280    pub fn emit(&mut self, out_idx: usize, mut pkt: Packet) -> Result<()> {
281        if out_idx >= self.staging.len() {
282            return Err(Error::InvalidArg(format!(
283                "node `{}`: emit output port index {out_idx} out of range (of {} total)",
284                self.node_name,
285                self.staging.len()
286            )));
287        }
288        // 未设时间戳则继承当前输入时间戳(与 forward 行为一致)
289        if pkt.timestamp() == Timestamp::unset() {
290            pkt.set_timestamp(self.input_ts);
291        }
292        if !pkt.timestamp().is_allowed_in_stream() {
293            return Err(Error::InvalidArg(format!(
294                "node `{}`: emit output port {out_idx} has invalid packet timestamp {}; specify a legal stream timestamp",
295                self.node_name,
296                pkt.timestamp()
297            )));
298        }
299        self.staging[out_idx].push(pkt);
300        Ok(())
301    }
302
303    /// 零拷贝直通:只克隆引用计数。
304    pub fn forward(&mut self, in_idx: usize, out_idx: usize) -> Result<()> {
305        let pkt = self
306            .inputs
307            .get(in_idx)
308            .and_then(|s| s.clone())
309            .ok_or_else(|| {
310                Error::InvalidArg(format!(
311                    "node `{}`: forward input port {in_idx} is empty or out of range",
312                    self.node_name
313                ))
314            })?;
315        self.emit(out_idx, pkt)
316    }
317
318    pub fn set_next_bound(&mut self, out_idx: usize, bound: Timestamp) {
319        if let Some(slot) = self.next_bounds.get_mut(out_idx) {
320            *slot = Some(bound);
321        }
322    }
323
324    pub fn log(&self, level: i32, msg: &str) {
325        // 快路:没装 sink 时连这层 `format!`(加节点名前缀)都不该做 —— 它会堆分配,
326        // 而 `cc.Log` 在算子里可能是**每包**调用的。
327        if !crate::runtime::log_enabled() {
328            return;
329        }
330        crate::runtime::log(level, &format!("[{}] {}", self.node_name, msg));
331    }
332
333    pub fn set_error(&mut self, msg: &str) {
334        self.error_msg = Some(msg.to_string());
335    }
336
337    /// Void ABI output operations must fail the invocation even if the callback returns OK.
338    pub(crate) fn callback_status(&mut self, status: i32) -> i32 {
339        let failed = std::mem::take(&mut self.output_failed);
340        if status == 0 && failed {
341            crate::status::code::KERNEL
342        } else {
343            status
344        }
345    }
346
347    /// 组合出带节点名前缀的错误。
348    pub fn take_error(&mut self, code: i32) -> Error {
349        let detail = self
350            .error_msg
351            .take()
352            .unwrap_or_else(|| format!("returned status code {code}"));
353        Error::Kernel(format!("[{}] {}", self.node_name, detail))
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::config::GraphConfig;
361
362    fn ports(decls: &[&str]) -> Arc<PortTable> {
363        let v: Vec<String> = decls.iter().map(|s| s.to_string()).collect();
364        Arc::new(PortTable::build(&v, "test").unwrap())
365    }
366
367    fn ctx(opts: &str) -> Context {
368        let value: serde_yaml::Value = serde_yaml::from_str(opts).unwrap();
369        Context::new(
370            "n".into(),
371            "K".into(),
372            ports(&["a"]),
373            ports(&["b", "c"]),
374            Arc::new(Options::new(value)),
375            Arc::new(BTreeMap::new()),
376            Arc::new(GraphShared::new(
377                GraphConfig::from_yaml("nodes: []").unwrap(),
378            )),
379        )
380    }
381
382    #[test]
383    fn options_scalars_and_dotted_path() {
384        let c = ctx("threshold: 0.5\nn: 7\nflag: true\nname: hi\nroi:\n  x: 8\n  y: 9\n");
385        assert_eq!(c.options.f64("threshold"), Some(0.5));
386        assert_eq!(c.options.i64("n"), Some(7));
387        assert_eq!(c.options.bool("flag"), Some(true));
388        assert_eq!(c.options.str("name"), Some("hi"));
389        assert_eq!(c.options.i64("roi.x"), Some(8), "dotted path reads nested");
390        assert_eq!(c.options.i64("roi.y"), Some(9));
391        assert_eq!(c.options.i64("nope"), None);
392        assert_eq!(c.options.i64("roi.nope"), None);
393    }
394
395    #[test]
396    fn integer_reads_as_float() {
397        // YAML 里写 1 而不是 1.0 是常态,不该读不出来
398        let c = ctx("scale: 2");
399        assert_eq!(c.options.f64("scale"), Some(2.0));
400    }
401
402    #[test]
403    fn options_arrays() {
404        let c = ctx("mean: [0.1, 0.2, 0.3]\nsizes: [8, 16]\nnames: [a, b]");
405        assert_eq!(c.options.count("mean"), 3);
406        let mut f = [0.0f64; 4];
407        assert_eq!(c.options.f64_array("mean", &mut f), 3);
408        assert_eq!(&f[..3], &[0.1, 0.2, 0.3]);
409
410        let mut i = [0i64; 2];
411        assert_eq!(c.options.i64_array("sizes", &mut i), 2);
412        assert_eq!(i, [8, 16]);
413
414        // cap 小于实际长度:只填 cap 个,但返回真实长度(调用方据此判断是否被截断)
415        let mut small = [0.0f64; 2];
416        assert_eq!(c.options.f64_array("mean", &mut small), 3);
417        assert_eq!(&small, &[0.1, 0.2]);
418
419        assert_eq!(c.options.count("missing"), 0);
420        assert_eq!(c.options.f64_array("missing", &mut f), 0);
421    }
422
423    #[test]
424    fn options_json_fallback() {
425        let c = ctx("a: 1\nnested:\n  deep:\n    x: [1, 2]\n");
426        let j = c.options.json();
427        assert!(j.contains("\"a\":1"), "{j}");
428        assert!(j.contains("deep"), "{j}");
429    }
430
431    #[test]
432    fn empty_options_json_is_object() {
433        let c = ctx("~"); // YAML null
434        assert_eq!(
435            c.options.json(),
436            "{}",
437            "empty options should be {{}} not null"
438        );
439    }
440
441    #[test]
442    fn emit_inherits_input_timestamp() {
443        let mut c = ctx("{}");
444        c.input_ts = Timestamp(42);
445        c.emit(0, Packet::new(1i32)).unwrap();
446        assert_eq!(c.staging[0][0].timestamp(), Timestamp(42));
447        // 显式设过的不被覆盖
448        c.emit(1, Packet::new(2i32).at(Timestamp(7))).unwrap();
449        assert_eq!(c.staging[1][0].timestamp(), Timestamp(7));
450    }
451
452    #[test]
453    fn emit_out_of_range_is_error() {
454        let mut c = ctx("{}");
455        let err = c.emit(9, Packet::new(1i32)).unwrap_err();
456        assert!(err.to_string().contains("out of range"), "{err}");
457    }
458
459    #[test]
460    fn forward_clones_reference_only() {
461        let mut c = ctx("{}");
462        let p = Packet::new(5i32).at(Timestamp(0));
463        c.inputs[0] = Some(p);
464        c.forward(0, 0).unwrap();
465        // 输入槽与 staging 指向同一 payload
466        assert_eq!(c.inputs[0].as_ref().unwrap().ref_count(), 2);
467        assert_eq!(c.staging[0][0].get::<i32>(), Some(&5));
468    }
469
470    #[test]
471    fn forward_from_empty_slot_is_error() {
472        let mut c = ctx("{}");
473        assert!(
474            c.forward(0, 0).is_err(),
475            "forward from empty slot must error"
476        );
477    }
478
479    #[test]
480    fn take_input_empties_slot_enabling_cow() {
481        let mut c = ctx("{}");
482        c.inputs[0] = Some(Packet::new(5i32));
483        let p = c.take_input(0);
484        assert!(c.inputs[0].is_none(), "slot must be empty after take");
485        assert_eq!(
486            p.ref_count(),
487            1,
488            "must be exclusive after take so CoW is zero-copy"
489        );
490        // 重复取走返回空包而不是 panic
491        assert!(c.take_input(0).is_empty());
492    }
493
494    #[test]
495    fn discard_staging_on_failure() {
496        let mut c = ctx("{}");
497        c.input_ts = Timestamp(0);
498        c.emit(0, Packet::new(1i32)).unwrap();
499        c.set_next_bound(1, Timestamp(3));
500        c.discard_staging();
501        assert!(
502            c.staging[0].is_empty(),
503            "must not propagate half-finished output on failure"
504        );
505        assert!(c.next_bounds[1].is_none());
506    }
507
508    #[test]
509    fn error_carries_node_name() {
510        let mut c = ctx("{}");
511        c.set_error("model load failed");
512        let e = c.take_error(3);
513        assert!(
514            e.to_string().contains("[n]"),
515            "error should carry the node name: {e}"
516        );
517        assert!(e.to_string().contains("model load failed"), "{e}");
518    }
519
520    #[test]
521    fn error_without_message_still_useful() {
522        let mut c = ctx("{}");
523        let e = c.take_error(3);
524        assert!(e.to_string().contains("status code 3"), "{e}");
525    }
526}