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                        function_id: None,
187                        call_id: None,
188                        label: meta.label.to_owned(),
189                        kind: NodeKind::Function,
190                        module_path: meta.module_path.to_owned(),
191                        file: meta.file.to_owned(),
192                        line: meta.line,
193                        source: Some(meta.source.to_owned()),
194                    },
195                );
196
197                let call_id = next_call_id(&state);
198                let parent_call_id = CALL_STACK.with(|stack| {
199                    let stack = stack.borrow();
200                    stack.last().map(|frame| frame.call_id)
201                });
202
203                let parent_node = CALL_STACK.with(|stack| {
204                    let stack = stack.borrow();
205                    stack.last().map(|frame| frame.node_id.clone())
206                });
207
208                if let Some(parent_node) = parent_node {
209                    ensure_edge(&mut state, &parent_node, meta.id);
210                }
211
212                push_event(
213                    &mut state,
214                    Event {
215                        seq: 0,
216                        call_id: Some(call_id),
217                        parent_call_id,
218                        node_id: meta.id.to_owned(),
219                        kind: EventKind::FunctionEnter,
220                        title: format!("enter {}", meta.label),
221                        values,
222                    },
223                );
224
225                (call_id, parent_call_id)
226            };
227
228            CALL_STACK.with(|stack| {
229                stack.borrow_mut().push(CallFrame {
230                    call_id,
231                    node_id: meta.id.to_owned(),
232                });
233            });
234
235            Self {
236                call_id,
237                parent_call_id,
238                node_id: meta.id,
239                finished: false,
240            }
241        }
242
243        /// Records a successful function return.
244        pub fn finish_return<T: std::fmt::Debug>(&mut self, result: &T) {
245            if self.finished {
246                return;
247            }
248
249            {
250                let mut state = lock_state();
251                push_event(
252                    &mut state,
253                    Event {
254                        seq: 0,
255                        call_id: Some(self.call_id),
256                        parent_call_id: self.parent_call_id,
257                        node_id: self.node_id.to_owned(),
258                        kind: EventKind::FunctionExit,
259                        title: format!(
260                            "return {}",
261                            self.node_id.rsplit("::").next().unwrap_or(self.node_id)
262                        ),
263                        values: vec![ValueSlot {
264                            name: "result".to_owned(),
265                            preview: format!("{result:#?}"),
266                        }],
267                    },
268                );
269            }
270
271            self.finished = true;
272            pop_stack(self.call_id);
273        }
274
275        /// Records an error or unwind outcome for the current frame.
276        pub fn finish_error(&mut self, message: impl Into<String>) {
277            if self.finished {
278                return;
279            }
280
281            {
282                let mut state = lock_state();
283                push_event(
284                    &mut state,
285                    Event {
286                        seq: 0,
287                        call_id: Some(self.call_id),
288                        parent_call_id: self.parent_call_id,
289                        node_id: self.node_id.to_owned(),
290                        kind: EventKind::FunctionExit,
291                        title: format!(
292                            "panic {}",
293                            self.node_id.rsplit("::").next().unwrap_or(self.node_id)
294                        ),
295                        values: vec![ValueSlot {
296                            name: "status".to_owned(),
297                            preview: message.into(),
298                        }],
299                    },
300                );
301            }
302
303            self.finished = true;
304            pop_stack(self.call_id);
305        }
306    }
307
308    impl Drop for TraceFrame {
309        fn drop(&mut self) {
310            if self.finished {
311                return;
312            }
313
314            self.finish_error("unwound before explicit return");
315        }
316    }
317
318    fn pop_stack(call_id: u64) {
319        CALL_STACK.with(|stack| {
320            let mut stack = stack.borrow_mut();
321            if stack.last().map(|frame| frame.call_id) == Some(call_id) {
322                stack.pop();
323            }
324        });
325    }
326
327    /// Builds a value preview for a traced function argument.
328    pub fn preview_argument<T: std::fmt::Debug>(name: impl Into<String>, value: &T) -> ValueSlot {
329        ValueSlot {
330            name: name.into(),
331            preview: format!("{value:#?}"),
332        }
333    }
334
335    /// Records a snapshot for a `#[ui_debug]` value.
336    pub fn record_type_snapshot<T: UiDebugValue>(value: &T, label: impl Into<String>) {
337        let meta = T::ui_debug_type_meta();
338        let label = label.into();
339        let mut state = lock_state();
340
341        ensure_node(
342            &mut state,
343            Node {
344                id: meta.id.to_owned(),
345                function_id: None,
346                call_id: None,
347                label: meta.label.to_owned(),
348                kind: NodeKind::Type,
349                module_path: meta.module_path.to_owned(),
350                file: meta.file.to_owned(),
351                line: meta.line,
352                source: Some(meta.source.to_owned()),
353            },
354        );
355
356        if let Some(parent_node) = CALL_STACK.with(|stack| {
357            let stack = stack.borrow();
358            stack.last().map(|frame| frame.node_id.clone())
359        }) {
360            ensure_edge_kind(
361                &mut state,
362                &parent_node,
363                meta.id,
364                EdgeKind::DataFlow,
365                Some(label.clone()),
366            );
367        }
368
369        push_event(
370            &mut state,
371            Event {
372                seq: 0,
373                call_id: CALL_STACK.with(|stack| {
374                    let stack = stack.borrow();
375                    stack.last().map(|frame| frame.call_id)
376                }),
377                parent_call_id: None,
378                node_id: meta.id.to_owned(),
379                kind: EventKind::ValueSnapshot,
380                title: label,
381                values: vec![ValueSlot {
382                    name: meta.label.to_owned(),
383                    preview: value.ui_debug_snapshot(),
384                }],
385            },
386        );
387    }
388
389    /// Records that a test started and explicitly links it to a node.
390    pub fn record_test_started(test_name: impl Into<String>, node_id: impl Into<String>) {
391        let test_name = test_name.into();
392        record_test_event(
393            EventKind::TestStarted,
394            test_name.clone(),
395            short_label(&test_name),
396            node_id.into(),
397            Vec::new(),
398        );
399    }
400
401    /// Records that a test passed and explicitly links it to a node.
402    pub fn record_test_passed(test_name: impl Into<String>, node_id: impl Into<String>) {
403        let test_name = test_name.into();
404        record_test_event(
405            EventKind::TestPassed,
406            test_name.clone(),
407            short_label(&test_name),
408            node_id.into(),
409            Vec::new(),
410        );
411    }
412
413    /// Records that a test failed and explicitly links it to a node.
414    pub fn record_test_failed(
415        test_name: impl Into<String>,
416        node_id: impl Into<String>,
417        failure: impl Into<String>,
418    ) {
419        let test_name = test_name.into();
420        record_test_event(
421            EventKind::TestFailed,
422            test_name.clone(),
423            short_label(&test_name),
424            node_id.into(),
425            vec![ValueSlot {
426                name: "failure".to_owned(),
427                preview: failure.into(),
428            }],
429        );
430    }
431
432    /// Returns the node id attached to the most recently recorded event.
433    pub fn latest_node_id() -> Option<String> {
434        let state = lock_state();
435        state.last_event_node_id.clone()
436    }
437
438    /// Records that a test started and links it to the latest active node.
439    pub fn record_test_started_latest(test_name: impl Into<String>) {
440        let test_name = test_name.into();
441        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
442        let label = short_label(&test_name);
443        record_test_event(
444            EventKind::TestStarted,
445            test_name,
446            label,
447            node_id,
448            Vec::new(),
449        );
450    }
451
452    /// Records that a test passed and links it to the latest active node.
453    pub fn record_test_passed_latest(test_name: impl Into<String>) {
454        let test_name = test_name.into();
455        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
456        let label = short_label(&test_name);
457        record_test_event(EventKind::TestPassed, test_name, label, node_id, Vec::new());
458    }
459
460    /// Records that a test failed and links it to the latest active node.
461    pub fn record_test_failed_latest(test_name: impl Into<String>, failure: impl Into<String>) {
462        let test_name = test_name.into();
463        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
464        let label = short_label(&test_name);
465        record_test_event(
466            EventKind::TestFailed,
467            test_name,
468            label,
469            node_id,
470            vec![ValueSlot {
471                name: "failure".to_owned(),
472                preview: failure.into(),
473            }],
474        );
475    }
476
477    /// Records that a test started and links it to the latest active node using a custom label.
478    pub fn record_test_started_latest_with_label(
479        test_name: impl Into<String>,
480        label: impl Into<String>,
481    ) {
482        let test_name = test_name.into();
483        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
484        record_test_event(
485            EventKind::TestStarted,
486            test_name,
487            label.into(),
488            node_id,
489            Vec::new(),
490        );
491    }
492
493    /// Records that a test passed and links it to the latest active node using a custom label.
494    pub fn record_test_passed_latest_with_label(
495        test_name: impl Into<String>,
496        label: impl Into<String>,
497    ) {
498        let test_name = test_name.into();
499        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
500        record_test_event(
501            EventKind::TestPassed,
502            test_name,
503            label.into(),
504            node_id,
505            Vec::new(),
506        );
507    }
508
509    /// Records that a test failed and links it to the latest active node using a custom label.
510    pub fn record_test_failed_latest_with_label(
511        test_name: impl Into<String>,
512        label: impl Into<String>,
513        failure: impl Into<String>,
514    ) {
515        let test_name = test_name.into();
516        let node_id = latest_node_id().unwrap_or_else(|| format!("test::{test_name}"));
517        record_test_event(
518            EventKind::TestFailed,
519            test_name,
520            label.into(),
521            node_id,
522            vec![ValueSlot {
523                name: "failure".to_owned(),
524                preview: failure.into(),
525            }],
526        );
527    }
528
529    fn record_test_event(
530        kind: EventKind,
531        test_name: String,
532        label: String,
533        node_id: String,
534        values: Vec<ValueSlot>,
535    ) {
536        let test_id = format!("test::{test_name}");
537        let mut state = lock_state();
538
539        ensure_node(
540            &mut state,
541            Node {
542                id: test_id.clone(),
543                function_id: None,
544                call_id: None,
545                label,
546                kind: NodeKind::Test,
547                module_path: "cargo::test".to_owned(),
548                file: "<runner>".to_owned(),
549                line: 0,
550                source: None,
551            },
552        );
553        ensure_edge_kind(&mut state, &test_id, &node_id, EdgeKind::TestLink, None);
554
555        push_event(
556            &mut state,
557            Event {
558                seq: 0,
559                call_id: None,
560                parent_call_id: None,
561                node_id: test_id,
562                kind,
563                title: short_label(&test_name),
564                values,
565            },
566        );
567    }
568
569    /// Extracts the last segment of a module path for use as a short label.
570    fn short_label(path: &str) -> String {
571        path.rsplit("::").next().unwrap_or(path).to_owned()
572    }
573
574    // ---------------------------------------------------------------------------
575    // Advanced async support
576    // ---------------------------------------------------------------------------
577
578    use std::future::Future;
579    use std::pin::Pin;
580    use std::task::{Context, Poll};
581
582    pin_project_lite::pin_project! {
583        /// A wrapper that traces an asynchronous Future's execution.
584        pub struct InstrumentedFuture<F> {
585            #[pin]
586            inner: F,
587            meta: FunctionMeta,
588            values: Option<Vec<ValueSlot>>,
589            call_id: Option<u64>,
590            parent_call_id: Option<u64>,
591            node_id: &'static str,
592            finished: bool,
593        }
594    }
595
596    /// Wraps a future to record its execution spanning multiple await points.
597    pub fn trace_future<F: Future>(
598        meta: FunctionMeta,
599        values: Vec<ValueSlot>,
600        inner: F,
601    ) -> InstrumentedFuture<F>
602    where
603        F::Output: std::fmt::Debug,
604    {
605        let parent_call_id =
606            CALL_STACK.with(|stack| stack.borrow().last().map(|frame| frame.call_id));
607
608        InstrumentedFuture {
609            inner,
610            meta,
611            values: Some(values),
612            call_id: None,
613            parent_call_id,
614            node_id: meta.id,
615            finished: false,
616        }
617    }
618
619    impl<F: Future> Future for InstrumentedFuture<F>
620    where
621        F::Output: std::fmt::Debug,
622    {
623        type Output = F::Output;
624
625        fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
626            let this = self.project();
627
628            if this.call_id.is_none() {
629                let mut state = lock_state();
630
631                ensure_node(
632                    &mut state,
633                    Node {
634                        id: this.meta.id.to_owned(),
635                        function_id: None,
636                        call_id: None,
637                        label: this.meta.label.to_owned(),
638                        kind: NodeKind::Function,
639                        module_path: this.meta.module_path.to_owned(),
640                        file: this.meta.file.to_owned(),
641                        line: this.meta.line,
642                        source: Some(this.meta.source.to_owned()),
643                    },
644                );
645
646                let new_call_id = next_call_id(&state);
647                *this.call_id = Some(new_call_id);
648
649                let parent_node = CALL_STACK.with(|stack| {
650                    let stack = stack.borrow();
651                    stack.last().map(|frame| frame.node_id.clone())
652                });
653
654                if let Some(parent_node) = parent_node {
655                    ensure_edge(&mut state, &parent_node, this.meta.id);
656                }
657
658                push_event(
659                    &mut state,
660                    Event {
661                        seq: 0,
662                        call_id: Some(new_call_id),
663                        parent_call_id: *this.parent_call_id,
664                        node_id: this.meta.id.to_owned(),
665                        kind: EventKind::FunctionEnter,
666                        title: format!("enter {}", this.meta.label),
667                        values: this.values.take().unwrap_or_default(),
668                    },
669                );
670            }
671
672            let call_id = this.call_id.unwrap();
673
674            CALL_STACK.with(|stack| {
675                stack.borrow_mut().push(CallFrame {
676                    call_id,
677                    node_id: this.node_id.to_owned(),
678                });
679            });
680
681            let res = this.inner.poll(cx);
682
683            CALL_STACK.with(|stack| {
684                stack.borrow_mut().pop();
685            });
686
687            if res.is_ready() && !*this.finished {
688                *this.finished = true;
689                let mut state = lock_state();
690                push_event(
691                    &mut state,
692                    Event {
693                        seq: 0,
694                        call_id: Some(call_id),
695                        parent_call_id: *this.parent_call_id,
696                        node_id: this.node_id.to_owned(),
697                        kind: EventKind::FunctionExit,
698                        title: format!(
699                            "return {}",
700                            this.node_id.rsplit("::").next().unwrap_or(this.node_id)
701                        ),
702                        values: vec![ValueSlot {
703                            name: "result".to_owned(),
704                            preview: match &res {
705                                std::task::Poll::Ready(val) => format!("{val:#?}"),
706                                _ => unreachable!(),
707                            },
708                        }],
709                    },
710                );
711            }
712
713            res
714        }
715    }
716}