Skip to main content

running_process/
rust_debug.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::fmt::Write as _;
4use std::sync::{Arc, Mutex, OnceLock};
5use std::thread;
6
7#[derive(Clone)]
8struct RustDebugFrame {
9    label: &'static str,
10    file: &'static str,
11    line: u32,
12}
13
14type ThreadStack = Arc<Mutex<Vec<RustDebugFrame>>>;
15
16fn registry() -> &'static Mutex<HashMap<String, ThreadStack>> {
17    static REGISTRY: OnceLock<Mutex<HashMap<String, ThreadStack>>> = OnceLock::new();
18    REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
19}
20
21fn thread_key() -> String {
22    let current = thread::current();
23    match current.name() {
24        Some(name) => format!("{name} ({:?})", current.id()),
25        None => format!("{:?}", current.id()),
26    }
27}
28
29fn thread_stack() -> ThreadStack {
30    thread_local! {
31        static STACK: RefCell<Option<ThreadStack>> = const { RefCell::new(None) };
32    }
33
34    STACK.with(|cell| {
35        let mut slot = cell.borrow_mut();
36        if let Some(existing) = slot.as_ref() {
37            return Arc::clone(existing);
38        }
39        let handle = Arc::new(Mutex::new(Vec::new()));
40        registry()
41            .lock()
42            .expect("rust debug registry mutex poisoned")
43            .insert(thread_key(), Arc::clone(&handle));
44        *slot = Some(Arc::clone(&handle));
45        handle
46    })
47}
48
49/// RAII guard that records one debug frame for the current thread.
50///
51/// Create values with [`Self::enter`]. Dropping the guard pops the frame from
52/// the thread-local debug stack.
53pub struct RustDebugScopeGuard {
54    stack: ThreadStack,
55}
56
57impl RustDebugScopeGuard {
58    /// Push a debug frame onto the current thread's stack.
59    pub fn enter(label: &'static str, file: &'static str, line: u32) -> Self {
60        let stack = thread_stack();
61        stack
62            .lock()
63            .expect("rust debug stack mutex poisoned")
64            .push(RustDebugFrame { label, file, line });
65        Self { stack }
66    }
67}
68
69impl Drop for RustDebugScopeGuard {
70    fn drop(&mut self) {
71        let _ = self
72            .stack
73            .lock()
74            .expect("rust debug stack mutex poisoned")
75            .pop();
76    }
77}
78
79/// Render all non-empty thread debug stacks.
80pub fn render_rust_debug_traces() -> String {
81    let registry = registry()
82        .lock()
83        .expect("rust debug registry mutex poisoned");
84    let mut items: Vec<_> = registry.iter().collect();
85    items.sort_by(|left, right| left.0.cmp(right.0));
86
87    let mut rendered = String::new();
88    for (thread_name, stack) in items {
89        let stack = stack.lock().expect("rust debug stack mutex poisoned");
90        if stack.is_empty() {
91            continue;
92        }
93        let _ = writeln!(&mut rendered, "thread: {thread_name}");
94        for (index, frame) in stack.iter().enumerate() {
95            let _ = writeln!(
96                &mut rendered,
97                "  {index}: {} ({}:{})",
98                frame.label, frame.file, frame.line
99            );
100        }
101    }
102    rendered
103}