Skip to main content

nichlink_run_method/runtime/trace/frames/
frames.rs

1//! Persistent call-frame operations.
2//! 持久化调用帧操作。
3
4use std::fmt::Write as _;
5
6use super::*;
7
8/// Borrowed root-to-leaf path over the frame arena.
9/// 从帧 arena 借用的根到叶调用路径。
10///
11/// The iterator trades repeated parent-link walks for zero heap allocation and
12/// zero `CallSite` cloning. It is intended for high-frequency debug views.
13/// 迭代器用重复的父链接访问换取零堆分配和零 `CallSite` 复制,供高频调试视图使用。
14pub struct FramePath<'a> {
15    trace: &'a CallTrace,
16    target: u64,
17    next_depth: usize,
18    depth: usize,
19}
20
21impl<'a> Iterator for FramePath<'a> {
22    type Item = &'a CallSite;
23
24    fn next(&mut self) -> Option<Self::Item> {
25        if self.next_depth > self.depth {
26            return None;
27        }
28        let mut frame_id = self.target;
29        for _ in 0..(self.depth - self.next_depth) {
30            frame_id = self.trace.frame(frame_id)?.parent?;
31        }
32        self.next_depth += 1;
33        self.trace.frame(frame_id).map(|frame| &frame.call)
34    }
35}
36
37impl CallTrace {
38    /// Run `operation` inside a new frame for `node`/`function`.
39    /// 在 `node`/`function` 的新调用帧内执行 `operation`。
40    ///
41    /// The frame is pushed before the callback and popped afterwards, including
42    /// when it panics, so a stale frame never captures later values. `Off` mode
43    /// runs the callback without recording a frame.
44    /// 回调前压入该帧、之后弹出,panic 时同样弹出,陈旧帧不会捕获后续值。`Off` 模式只
45    /// 执行回调而不记录帧。
46    #[track_caller]
47    pub fn with<R>(
48        &mut self,
49        node: NodeId,
50        function: &'static str,
51        operation: impl FnOnce(&mut Self) -> R,
52    ) -> R {
53        let caller = std::panic::Location::caller();
54        self.with_at(
55            node,
56            function,
57            SourceLocation {
58                file: caller.file(),
59                line: caller.line(),
60                column: caller.column(),
61                function,
62            },
63            operation,
64        )
65    }
66
67    /// Record a call with the source line that entered it.
68    /// 记录调用及其进入位置的源码行。
69    pub fn with_at<R>(
70        &mut self,
71        node: NodeId,
72        function: &'static str,
73        source: SourceLocation,
74        operation: impl FnOnce(&mut Self) -> R,
75    ) -> R {
76        self.with_source(node, function, Some(source), operation)
77    }
78
79    fn with_source<R>(
80        &mut self,
81        node: NodeId,
82        function: &'static str,
83        source: Option<SourceLocation>,
84        operation: impl FnOnce(&mut Self) -> R,
85    ) -> R {
86        if matches!(self.mode, TraceMode::Off) {
87            return operation(self);
88        }
89        let frame_id = self.next_frame_id;
90        self.next_frame_id = self.next_frame_id.wrapping_add(1);
91        let call = CallSite {
92            node,
93            function,
94            frame_id,
95            source,
96        };
97        let parent = self.current.last().copied();
98        self.frames.push(FrameRecord { call, parent });
99        self.frame_index.insert(frame_id, self.frames.len() - 1);
100        self.current.push(frame_id);
101        // Pop the active frame even when the traced operation panics. A stale
102        // frame would attach later values to a call that no longer exists.
103        // 即使被追踪操作 panic 也要弹出活动帧,否则后续值会错误挂到已结束调用。
104        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| operation(self)));
105        self.current.pop();
106        match result {
107            Ok(value) => value,
108            Err(payload) => std::panic::resume_unwind(payload),
109        }
110    }
111
112    pub(super) fn frame(&self, frame_id: u64) -> Option<&FrameRecord> {
113        self.frame_index
114            .get(&frame_id)
115            .and_then(|index| self.frames.get(*index))
116    }
117
118    /// Return the number of caller frames above one invocation.
119    /// 返回某个调用实例上方的调用帧数量。
120    pub fn frame_depth(&self, frame_id: u64) -> usize {
121        let mut depth = 0;
122        let mut next = self.frame(frame_id).and_then(|frame| frame.parent);
123        while let Some(id) = next {
124            depth += 1;
125            next = self.frame(id).and_then(|frame| frame.parent);
126        }
127        depth
128    }
129
130    pub(super) fn frame_chain_matches(&self, frame_id: u64, query: &str) -> bool {
131        let mut next = Some(frame_id);
132        while let Some(id) = next {
133            let Some(frame) = self.frame(id) else { break };
134            if frame.call.function.to_ascii_lowercase().contains(query)
135                || frame
136                    .call
137                    .source
138                    .is_some_and(|source| source_file_matches(source.file, query))
139            {
140                return true;
141            }
142            next = frame.parent;
143        }
144        false
145    }
146
147    pub(super) fn path_for(&self, frame_id: u64) -> Vec<CallSite> {
148        let mut path = Vec::new();
149        let mut next = Some(frame_id);
150        while let Some(id) = next {
151            let Some(frame) = self.frame(id) else { break };
152            path.push(frame.call.clone());
153            next = frame.parent;
154        }
155        path.reverse();
156        path
157    }
158
159    /// Borrow one frame and its parent link without allocating.
160    /// 无分配地借用一个调用帧及其父链接。
161    pub fn frame_view(&self, frame_id: u64) -> Option<FrameView<'_>> {
162        self.frame(frame_id).map(|frame| FrameView {
163            call: &frame.call,
164            parent: frame.parent,
165        })
166    }
167
168    /// Iterate frame IDs in recording order for indexed debug views.
169    /// 按记录顺序遍历帧 ID,供索引型调试视图使用。
170    pub fn frame_ids(&self) -> impl Iterator<Item = u64> + '_ {
171        self.frames.iter().map(|frame| frame.call.frame_id)
172    }
173
174    /// Materialize only one requested invocation path.
175    /// 只生成指定调用实例的路径。
176    pub fn path_for_frame(&self, frame_id: u64) -> Option<Vec<CallSite>> {
177        self.frame(frame_id).map(|_| self.path_for(frame_id))
178    }
179
180    /// Borrow one invocation path without allocating a snapshot.
181    /// 借用一条调用路径,不分配快照。
182    pub fn path_iter(&self, frame_id: u64) -> Option<FramePath<'_>> {
183        self.frame(frame_id).map(|_| FramePath {
184            trace: self,
185            target: frame_id,
186            next_depth: 0,
187            depth: self.frame_depth(frame_id),
188        })
189    }
190
191    /// Borrow the currently active path without allocating.
192    /// 借用当前活动调用路径,不分配临时数组。
193    pub fn current_path_iter(&self) -> Option<FramePath<'_>> {
194        self.current
195            .last()
196            .and_then(|frame_id| self.path_iter(*frame_id))
197    }
198
199    /// Visit one invocation path from its root to the selected frame without
200    /// cloning `CallSite` values or allocating a temporary path vector.
201    /// 从根到目标帧访问一条调用路径,不复制 `CallSite`,也不分配临时路径数组。
202    ///
203    /// The callback runs once per frame. Returning `false` stops the walk and
204    /// makes the method return `false`; this is useful for bounded TUI renders.
205    /// 回调每帧执行一次;返回 `false` 会停止遍历并让方法返回 `false`,适合限制 TUI 输出。
206    pub fn visit_path<F>(&self, frame_id: u64, mut visit: F) -> bool
207    where
208        F: FnMut(&CallSite) -> bool,
209    {
210        if self.frame(frame_id).is_none() {
211            return false;
212        }
213        self.visit_path_inner(frame_id, &mut visit)
214    }
215
216    fn visit_path_inner<F>(&self, frame_id: u64, visit: &mut F) -> bool
217    where
218        F: FnMut(&CallSite) -> bool,
219    {
220        let Some(frame) = self.frame(frame_id) else {
221            return true;
222        };
223        if let Some(parent) = frame.parent
224            && !self.visit_path_inner(parent, visit)
225        {
226            return false;
227        }
228        visit(&frame.call)
229    }
230
231    /// Clone the active root-to-leaf call path, outermost frame first.
232    /// 克隆当前活动的根到叶调用路径,最外层帧在前。
233    pub fn current_path(&self) -> Vec<CallSite> {
234        self.current
235            .iter()
236            .filter_map(|id| self.frame(*id).map(|frame| frame.call.clone()))
237            .collect()
238    }
239
240    /// Rebuild paths on demand; the frame arena is the persistent form.
241    /// 按需重建路径;持久存储形式是帧表。
242    pub fn paths(&self) -> Vec<Vec<CallSite>> {
243        self.frames
244            .iter()
245            .map(|frame| self.path_for(frame.call.frame_id))
246            .collect()
247    }
248
249    /// Lazily yield invocation paths; no path is built before it is requested.
250    /// 惰性产生调用路径;只有请求某一项时才构造该路径。
251    pub fn paths_iter(&self) -> impl Iterator<Item = Vec<CallSite>> + '_ {
252        self.frames
253            .iter()
254            .map(|frame| self.path_for(frame.call.frame_id))
255    }
256
257    /// Lazily borrow every recorded path without cloning call sites.
258    /// 惰性借用所有已记录路径,不复制调用点。
259    pub fn borrowed_paths_iter(&self) -> impl Iterator<Item = FramePath<'_>> + '_ {
260        self.frames
261            .iter()
262            .filter_map(|frame| self.path_iter(frame.call.frame_id))
263    }
264
265    /// Number of persistent call frames, useful for memory/debug audits.
266    /// 持久化调用帧数量,便于内存和调试审计。
267    pub fn frame_count(&self) -> usize {
268        self.frames.len()
269    }
270
271    /// Match a function or step name without losing its complete call chain.
272    /// 按函数或步骤名匹配,同时保留完整调用链。
273    pub fn matching_paths(&self, query: &str) -> Vec<Vec<CallSite>> {
274        self.matching_paths_iter(query).collect()
275    }
276
277    /// Lazily match call paths without materializing unrelated paths.
278    /// 惰性匹配调用路径,不为无关调用生成路径。
279    pub fn matching_paths_iter(&self, query: &str) -> impl Iterator<Item = Vec<CallSite>> + '_ {
280        let query = query.trim().to_ascii_lowercase();
281        self.frames
282            .iter()
283            .filter(move |frame| {
284                query.is_empty()
285                    || frame.call.function.to_ascii_lowercase().contains(&query)
286                    || frame
287                        .call
288                        .source
289                        .is_some_and(|source| source_file_matches(source.file, &query))
290            })
291            .map(|frame| self.path_for(frame.call.frame_id))
292    }
293
294    /// Render every frame as one indented line, in recording order.
295    /// 按记录顺序把每个调用帧渲染成一行缩进文本。
296    pub fn render_tree(&self) -> String {
297        let mut output = String::new();
298        for frame in &self.frames {
299            let call = &frame.call;
300            writeln!(
301                output,
302                "{}- {} {}#{}{}",
303                "  ".repeat(self.frame_depth(call.frame_id)),
304                call.node,
305                call.function,
306                call.frame_id,
307                call.source
308                    .map_or_else(String::new, |source| format!(" @ {source}")),
309            )
310            .unwrap();
311        }
312        output
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn visit_path_reads_the_frame_arena_without_materializing_a_path() {
322        let mut trace = CallTrace::full();
323        let root = NodeId::from_raw([1; 16]);
324        let child = NodeId::from_raw([2; 16]);
325        trace.with(root, "root", |trace| {
326            trace.with(child, "child", |trace| trace.with(child, "leaf", |_| {}))
327        });
328        let target = trace.frame_ids().last().expect("leaf frame");
329        let mut names = Vec::new();
330        assert!(trace.visit_path(target, |call| {
331            names.push(call.function);
332            true
333        }));
334        assert_eq!(names, ["root", "child", "leaf"]);
335        let borrowed = trace
336            .path_iter(target)
337            .expect("leaf path")
338            .map(|call| call.function)
339            .collect::<Vec<_>>();
340        assert_eq!(borrowed, ["root", "child", "leaf"]);
341    }
342
343    #[test]
344    fn disabled_mode_does_not_retain_frames_locals_or_edges() {
345        let mut trace = CallTrace::disabled();
346        let node = NodeId::from_raw([7; 16]);
347        let result = trace.with(node, "work", |trace| {
348            let input = trace.local("input", "u32", 7, LocalKind::Input);
349            trace.transform(input, "output", "u32", 8)
350        });
351        assert_eq!(result, LocalId(0));
352        assert_eq!(trace.frame_count(), 0);
353        assert!(trace.locals().is_empty());
354        assert!(trace.data_edges().is_empty());
355    }
356
357    #[test]
358    fn errors_only_discards_success_and_keeps_failure_evidence() {
359        let node = NodeId::from_raw([8; 16]);
360        let mut trace = CallTrace::errors_only();
361        let success: Result<(), &str> = trace.with_result(|trace| {
362            trace.with(node, "successful", |trace| {
363                trace.local("value", "u32", 1, LocalKind::Binding);
364            });
365            Ok(())
366        });
367        assert!(success.is_ok());
368        assert_eq!(trace.frame_count(), 0);
369        assert!(trace.locals().is_empty());
370
371        let failure: Result<(), &str> = trace.with_result(|trace| {
372            trace.with(node, "failed", |trace| {
373                trace.local("value", "u32", 2, LocalKind::Binding);
374            });
375            Err("broken")
376        });
377        assert_eq!(failure, Err("broken"));
378        assert_eq!(trace.frame_count(), 1);
379        assert_eq!(trace.locals().len(), 1);
380        assert_eq!(trace.locals()[0].value, "2");
381    }
382
383    /// An id that escaped a discarded scope must not describe later evidence.
384    /// 逃出被丢弃作用域的 id 不得描述后来的证据。
385    #[test]
386    fn an_id_from_a_discarded_scope_is_never_reused() {
387        let mut trace = CallTrace::errors_only();
388        let escaped: Result<LocalId, &str> =
389            trace.with_result(|trace| Ok(trace.local("discarded", "u32", 1, LocalKind::Binding)));
390        let escaped = escaped.expect("a successful scope returns its local id");
391
392        let mut kept = None;
393        let failed: Result<(), &str> = trace.with_result(|trace| {
394            kept = Some(trace.local("kept", "u32", 2, LocalKind::Binding));
395            Err("broken")
396        });
397        assert_eq!(failed, Err("broken"));
398        let kept = kept.expect("the failing scope keeps its local");
399        assert_ne!(escaped, kept, "the discarded scope's id came back");
400        assert!(
401            trace.find_local(escaped).is_none(),
402            "an id from discarded evidence must resolve to nothing"
403        );
404        assert_eq!(
405            trace.find_local(kept).map(|local| local.value.as_str()),
406            Some("2")
407        );
408    }
409
410    #[test]
411    fn resetting_the_mode_inside_a_scope_does_not_break_the_pairing() {
412        let node = NodeId::from_raw([9; 16]);
413        let mut trace = CallTrace::errors_only();
414        let cleared: Result<(), &str> = trace.with_result(|trace| {
415            trace.clear();
416            Ok(())
417        });
418        assert_eq!(cleared, Ok(()));
419        let changed: Result<(), &str> = trace.with_result(|trace| {
420            trace.set_mode(TraceMode::Full);
421            Ok(())
422        });
423        assert_eq!(changed, Ok(()));
424        // The trace still works, and a later failure is still kept.
425        let failure: Result<(), &str> = trace.with_result(|trace| {
426            trace.with(node, "after-reset", |trace| {
427                trace.local("value", "u32", 3, LocalKind::Binding);
428            });
429            Err("broken")
430        });
431        assert_eq!(failure, Err("broken"));
432        assert_eq!(trace.locals().len(), 1);
433    }
434
435    #[test]
436    fn runtime_default_matches_the_build_profile() {
437        let trace = CallTrace::runtime();
438        if cfg!(debug_assertions) {
439            assert_eq!(trace.mode(), TraceMode::ErrorsOnly);
440        } else {
441            assert_eq!(trace.mode(), TraceMode::Off);
442        }
443    }
444
445    #[test]
446    fn trace_mode_accepts_human_facing_names() {
447        assert_eq!(TraceMode::parse("off"), Some(TraceMode::Off));
448        assert_eq!(TraceMode::parse("errors_only"), Some(TraceMode::ErrorsOnly));
449        assert_eq!(TraceMode::parse("FULL"), Some(TraceMode::Full));
450        assert_eq!(TraceMode::parse("verbose"), None);
451    }
452}