Skip to main content

dbgflow_core/
internal_runtime.rs

1//! Runtime state and event recording.
2//!
3//! This module manages the global runtime state for the debugger, including
4//! the call stack, event recording, and node/edge management. It provides
5//! the low-level API used by the trace macros.
6
7use std::any::type_name_of_val;
8use std::cell::RefCell;
9use std::collections::BTreeMap;
10use std::fmt::Debug;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Mutex, MutexGuard, OnceLock};
13
14use crate::session::{
15    Edge, EdgeKind, Event, EventKind, FunctionMeta, Node, NodeKind, Session, TypeMeta, ValueSlot,
16};
17
18// ---------------------------------------------------------------------------
19// Internal state
20// ---------------------------------------------------------------------------
21
22/// Stack frame representing an active traced function invocation.
23#[derive(Clone, Debug)]
24struct CallFrame {
25    call_id: u64,
26    node_id: String,
27}
28
29/// Global mutable runtime state.
30#[derive(Default)]
31struct RuntimeState {
32    title: String,
33    nodes: BTreeMap<String, Node>,
34    edges: BTreeMap<(String, String), Edge>,
35    events: Vec<Event>,
36    last_event_node_id: Option<String>,
37    next_seq: AtomicU64,
38    next_call_id: AtomicU64,
39}
40
41thread_local! {
42    static CALL_STACK: RefCell<Vec<CallFrame>> = const { RefCell::new(Vec::new()) };
43}
44
45static STATE: OnceLock<Mutex<RuntimeState>> = OnceLock::new();
46
47fn state() -> &'static Mutex<RuntimeState> {
48    STATE.get_or_init(|| Mutex::new(RuntimeState::default()))
49}
50
51fn lock_state() -> MutexGuard<'static, RuntimeState> {
52    state().lock().expect("dbgflow-core runtime mutex poisoned")
53}
54
55fn next_seq(state: &RuntimeState) -> u64 {
56    state.next_seq.fetch_add(1, Ordering::Relaxed) + 1
57}
58
59fn next_call_id(state: &RuntimeState) -> u64 {
60    state.next_call_id.fetch_add(1, Ordering::Relaxed) + 1
61}
62
63fn push_event(state: &mut RuntimeState, mut event: Event) {
64    event.seq = next_seq(state);
65    state.last_event_node_id = Some(event.node_id.clone());
66    state.events.push(event);
67}
68
69fn ensure_node(state: &mut RuntimeState, node: Node) {
70    state.nodes.entry(node.id.clone()).or_insert(node);
71}
72
73fn ensure_edge(state: &mut RuntimeState, from: &str, to: &str) {
74    ensure_edge_kind(state, from, to, EdgeKind::ControlFlow, None);
75}
76
77fn ensure_edge_kind(
78    state: &mut RuntimeState,
79    from: &str,
80    to: &str,
81    kind: EdgeKind,
82    label: Option<String>,
83) {
84    if from == to {
85        return;
86    }
87
88    state.edges.insert(
89        (from.to_owned(), to.to_owned()),
90        Edge {
91            from: from.to_owned(),
92            to: to.to_owned(),
93            kind,
94            label,
95        },
96    );
97}
98
99// ---------------------------------------------------------------------------
100// Public session management
101// ---------------------------------------------------------------------------
102
103/// Clears the runtime and starts a new in-memory session.
104pub fn reset_session(title: impl Into<String>) {
105    let mut state = lock_state();
106    state.title = title.into();
107    state.nodes.clear();
108    state.edges.clear();
109    state.events.clear();
110    state.last_event_node_id = None;
111    state.next_seq.store(0, Ordering::Relaxed);
112    state.next_call_id.store(0, Ordering::Relaxed);
113    CALL_STACK.with(|stack| stack.borrow_mut().clear());
114}
115
116/// Returns a snapshot of the current in-memory session.
117pub fn current_session() -> Session {
118    let state = lock_state();
119    let nodes = state.nodes.values().cloned().collect();
120    let edges = state.edges.values().cloned().collect();
121
122    Session {
123        title: state.title.clone(),
124        nodes,
125        edges,
126        events: state.events.clone(),
127    }
128}
129
130/// Produces a cheap type-oriented preview for values captured by trace arguments.
131pub fn type_preview<T>(value: &T) -> String {
132    format!("type {}", type_name_of_val(value))
133}
134
135// ---------------------------------------------------------------------------
136// UiDebugValue trait
137// ---------------------------------------------------------------------------
138
139/// Trait implemented for values that should appear as UI data nodes.
140pub trait UiDebugValue: Debug + Sized {
141    /// Returns the static metadata for the type.
142    fn ui_debug_type_meta() -> TypeMeta;
143
144    /// Returns the text snapshot shown in the UI.
145    fn ui_debug_snapshot(&self) -> String {
146        format!("{self:#?}")
147    }
148
149    /// Emits a value snapshot event for the current session.
150    fn emit_snapshot(&self, label: impl Into<String>) {
151        runtime::record_type_snapshot(self, label.into());
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Runtime helpers (public module)
157// ---------------------------------------------------------------------------
158
159/// Runtime helpers used by generated macros and advanced callers.
160pub mod runtime {
161    use super::{
162        CALL_STACK, CallFrame, EdgeKind, Event, EventKind, FunctionMeta, Node, NodeKind,
163        UiDebugValue, ValueSlot, ensure_edge, ensure_edge_kind, ensure_node, lock_state,
164        next_call_id, push_event,
165    };
166
167    /// Guard object representing an active traced function invocation.
168    #[derive(Debug)]
169    pub struct TraceFrame {
170        call_id: u64,
171        parent_call_id: Option<u64>,
172        node_id: &'static str,
173        finished: bool,
174    }
175
176    impl TraceFrame {
177        /// Enters a traced function and records the corresponding event.
178        pub fn enter(meta: FunctionMeta, values: Vec<ValueSlot>) -> Self {
179            let (call_id, parent_call_id) = {
180                let mut state = lock_state();
181
182                ensure_node(
183                    &mut state,
184                    Node {
185                        id: meta.id.to_owned(),
186                        label: meta.label.to_owned(),
187                        kind: NodeKind::Function,
188                        module_path: meta.module_path.to_owned(),
189                        file: meta.file.to_owned(),
190                        line: meta.line,
191                        source: Some(meta.source.to_owned()),
192                    },
193                );
194
195                let call_id = next_call_id(&state);
196                let parent_call_id = CALL_STACK.with(|stack| {
197                    let stack = stack.borrow();
198                    stack.last().map(|frame| frame.call_id)
199                });
200
201                let parent_node = CALL_STACK.with(|stack| {
202                    let stack = stack.borrow();
203                    stack.last().map(|frame| frame.node_id.clone())
204                });
205
206                if let Some(parent_node) = parent_node {
207                    ensure_edge(&mut state, &parent_node, meta.id);
208                }
209
210                push_event(
211                    &mut state,
212                    Event {
213                        seq: 0,
214                        call_id: Some(call_id),
215                        parent_call_id,
216                        node_id: meta.id.to_owned(),
217                        kind: EventKind::FunctionEnter,
218                        title: format!("enter {}", meta.label),
219                        values,
220                    },
221                );
222
223                (call_id, parent_call_id)
224            };
225
226            CALL_STACK.with(|stack| {
227                stack.borrow_mut().push(CallFrame {
228                    call_id,
229                    node_id: meta.id.to_owned(),
230                });
231            });
232
233            Self {
234                call_id,
235                parent_call_id,
236                node_id: meta.id,
237                finished: false,
238            }
239        }
240
241        /// Records a successful function return.
242        pub fn finish_return<T>(&mut self, result: &T) {
243            if self.finished {
244                return;
245            }
246
247            {
248                let mut state = lock_state();
249                push_event(
250                    &mut state,
251                    Event {
252                        seq: 0,
253                        call_id: Some(self.call_id),
254                        parent_call_id: self.parent_call_id,
255                        node_id: self.node_id.to_owned(),
256                        kind: EventKind::FunctionExit,
257                        title: format!(
258                            "return {}",
259                            self.node_id.rsplit("::").next().unwrap_or(self.node_id)
260                        ),
261                        values: vec![ValueSlot {
262                            name: "result".to_owned(),
263                            preview: super::type_preview(result),
264                        }],
265                    },
266                );
267            }
268
269            self.finished = true;
270            pop_stack(self.call_id);
271        }
272
273        /// Records an error or unwind outcome for the current frame.
274        pub fn finish_error(&mut self, message: impl Into<String>) {
275            if self.finished {
276                return;
277            }
278
279            {
280                let mut state = lock_state();
281                push_event(
282                    &mut state,
283                    Event {
284                        seq: 0,
285                        call_id: Some(self.call_id),
286                        parent_call_id: self.parent_call_id,
287                        node_id: self.node_id.to_owned(),
288                        kind: EventKind::FunctionExit,
289                        title: format!(
290                            "panic {}",
291                            self.node_id.rsplit("::").next().unwrap_or(self.node_id)
292                        ),
293                        values: vec![ValueSlot {
294                            name: "status".to_owned(),
295                            preview: message.into(),
296                        }],
297                    },
298                );
299            }
300
301            self.finished = true;
302            pop_stack(self.call_id);
303        }
304    }
305
306    impl Drop for TraceFrame {
307        fn drop(&mut self) {
308            if self.finished {
309                return;
310            }
311
312            self.finish_error("unwound before explicit return");
313        }
314    }
315
316    fn pop_stack(call_id: u64) {
317        CALL_STACK.with(|stack| {
318            let mut stack = stack.borrow_mut();
319            if stack.last().map(|frame| frame.call_id) == Some(call_id) {
320                stack.pop();
321            }
322        });
323    }
324
325    /// Builds a value preview for a traced function argument.
326    pub fn preview_argument<T>(name: impl Into<String>, value: &T) -> ValueSlot {
327        ValueSlot {
328            name: name.into(),
329            preview: super::type_preview(value),
330        }
331    }
332
333    /// Records a snapshot for a `#[ui_debug]` value.
334    pub fn record_type_snapshot<T: UiDebugValue>(value: &T, label: impl Into<String>) {
335        let meta = T::ui_debug_type_meta();
336        let label = label.into();
337        let mut state = lock_state();
338
339        ensure_node(
340            &mut state,
341            Node {
342                id: meta.id.to_owned(),
343                label: meta.label.to_owned(),
344                kind: NodeKind::Type,
345                module_path: meta.module_path.to_owned(),
346                file: meta.file.to_owned(),
347                line: meta.line,
348                source: Some(meta.source.to_owned()),
349            },
350        );
351
352        if let Some(parent_node) = CALL_STACK.with(|stack| {
353            let stack = stack.borrow();
354            stack.last().map(|frame| frame.node_id.clone())
355        }) {
356            ensure_edge_kind(
357                &mut state,
358                &parent_node,
359                meta.id,
360                EdgeKind::DataFlow,
361                Some(label.clone()),
362            );
363        }
364
365        push_event(
366            &mut state,
367            Event {
368                seq: 0,
369                call_id: CALL_STACK.with(|stack| {
370                    let stack = stack.borrow();
371                    stack.last().map(|frame| frame.call_id)
372                }),
373                parent_call_id: None,
374                node_id: meta.id.to_owned(),
375                kind: EventKind::ValueSnapshot,
376                title: label,
377                values: vec![ValueSlot {
378                    name: meta.label.to_owned(),
379                    preview: value.ui_debug_snapshot(),
380                }],
381            },
382        );
383    }
384
385    /// Records that a test started and explicitly links it to a node.
386    pub fn record_test_started(test_name: impl Into<String>, node_id: impl Into<String>) {
387        let test_name = test_name.into();
388        record_test_event(
389            EventKind::TestStarted,
390            test_name.clone(),
391            short_label(&test_name),
392            node_id.into(),
393            Vec::new(),
394        );
395    }
396
397    /// Records that a test passed and explicitly links it to a node.
398    pub fn record_test_passed(test_name: impl Into<String>, node_id: impl Into<String>) {
399        let test_name = test_name.into();
400        record_test_event(
401            EventKind::TestPassed,
402            test_name.clone(),
403            short_label(&test_name),
404            node_id.into(),
405            Vec::new(),
406        );
407    }
408
409    /// Records that a test failed and explicitly links it to a node.
410    pub fn record_test_failed(
411        test_name: impl Into<String>,
412        node_id: impl Into<String>,
413        failure: impl Into<String>,
414    ) {
415        let test_name = test_name.into();
416        record_test_event(
417            EventKind::TestFailed,
418            test_name.clone(),
419            short_label(&test_name),
420            node_id.into(),
421            vec![ValueSlot {
422                name: "failure".to_owned(),
423                preview: failure.into(),
424            }],
425        );
426    }
427
428    /// Returns the node id attached to the most recently recorded event.
429    pub fn latest_node_id() -> Option<String> {
430        let state = lock_state();
431        state.last_event_node_id.clone()
432    }
433
434    /// Records that a test started and links it to the latest active node.
435    pub fn record_test_started_latest(test_name: impl Into<String>) {
436        let test_name = test_name.into();
437        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
438        let label = short_label(&test_name);
439        record_test_event(
440            EventKind::TestStarted,
441            test_name,
442            label,
443            node_id,
444            Vec::new(),
445        );
446    }
447
448    /// Records that a test passed and links it to the latest active node.
449    pub fn record_test_passed_latest(test_name: impl Into<String>) {
450        let test_name = test_name.into();
451        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
452        let label = short_label(&test_name);
453        record_test_event(EventKind::TestPassed, test_name, label, node_id, Vec::new());
454    }
455
456    /// Records that a test failed and links it to the latest active node.
457    pub fn record_test_failed_latest(test_name: impl Into<String>, failure: impl Into<String>) {
458        let test_name = test_name.into();
459        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
460        let label = short_label(&test_name);
461        record_test_event(
462            EventKind::TestFailed,
463            test_name,
464            label,
465            node_id,
466            vec![ValueSlot {
467                name: "failure".to_owned(),
468                preview: failure.into(),
469            }],
470        );
471    }
472
473    /// Records that a test started and links it to the latest active node using a custom label.
474    pub fn record_test_started_latest_with_label(
475        test_name: impl Into<String>,
476        label: impl Into<String>,
477    ) {
478        let test_name = test_name.into();
479        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
480        record_test_event(
481            EventKind::TestStarted,
482            test_name,
483            label.into(),
484            node_id,
485            Vec::new(),
486        );
487    }
488
489    /// Records that a test passed and links it to the latest active node using a custom label.
490    pub fn record_test_passed_latest_with_label(
491        test_name: impl Into<String>,
492        label: impl Into<String>,
493    ) {
494        let test_name = test_name.into();
495        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
496        record_test_event(
497            EventKind::TestPassed,
498            test_name,
499            label.into(),
500            node_id,
501            Vec::new(),
502        );
503    }
504
505    /// Records that a test failed and links it to the latest active node using a custom label.
506    pub fn record_test_failed_latest_with_label(
507        test_name: impl Into<String>,
508        label: impl Into<String>,
509        failure: impl Into<String>,
510    ) {
511        let test_name = test_name.into();
512        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
513        record_test_event(
514            EventKind::TestFailed,
515            test_name,
516            label.into(),
517            node_id,
518            vec![ValueSlot {
519                name: "failure".to_owned(),
520                preview: failure.into(),
521            }],
522        );
523    }
524
525    fn record_test_event(
526        kind: EventKind,
527        test_name: String,
528        label: String,
529        node_id: String,
530        values: Vec<ValueSlot>,
531    ) {
532        let test_id = format!("test::{test_name}");
533        let mut state = lock_state();
534
535        ensure_node(
536            &mut state,
537            Node {
538                id: test_id.clone(),
539                label,
540                kind: NodeKind::Test,
541                module_path: "cargo::test".to_owned(),
542                file: "<runner>".to_owned(),
543                line: 0,
544                source: None,
545            },
546        );
547        ensure_edge_kind(&mut state, &test_id, &node_id, EdgeKind::TestLink, None);
548
549        push_event(
550            &mut state,
551            Event {
552                seq: 0,
553                call_id: None,
554                parent_call_id: None,
555                node_id: test_id,
556                kind,
557                title: short_label(&test_name),
558                values,
559            },
560        );
561    }
562
563    /// Extracts the last segment of a module path for use as a short label.
564    fn short_label(path: &str) -> String {
565        path.rsplit("::").next().unwrap_or(path).to_owned()
566    }
567}