Skip to main content

tracing_calltree/
display.rs

1use crate::snapshot::{CallTreeSnapshot, NodeSnapshot};
2use std::fmt;
3use std::time::Duration;
4
5pub struct SnapshotDisplay<'a>(pub(crate) &'a CallTreeSnapshot);
6
7impl fmt::Display for SnapshotDisplay<'_> {
8    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9        for (index, root) in self.0.roots.iter().enumerate() {
10            let is_last = index + 1 == self.0.roots.len();
11            write_node(f, root, "", is_last)?;
12            if !is_last {
13                writeln!(f)?;
14            }
15        }
16
17        Ok(())
18    }
19}
20
21impl fmt::Display for CallTreeSnapshot {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        self.display().fmt(f)
24    }
25}
26
27fn write_node(
28    f: &mut fmt::Formatter<'_>,
29    node: &NodeSnapshot,
30    prefix: &str,
31    is_last: bool,
32) -> fmt::Result {
33    let branch = if prefix.is_empty() {
34        ""
35    } else if is_last {
36        "└── "
37    } else {
38        "├── "
39    };
40
41    write!(
42        f,
43        "{prefix}{branch}{:<28} {:>8} avg {:>8} p95 n={}",
44        node.name,
45        format_duration(node.wall.mean),
46        format_duration(node.wall.p95),
47        node.wall.samples,
48    )?;
49
50    if !node.children.is_empty() {
51        let child_prefix = if prefix.is_empty() {
52            String::new()
53        } else if is_last {
54            format!("{prefix}    ")
55        } else {
56            format!("{prefix}│   ")
57        };
58
59        for (index, child) in node.children.iter().enumerate() {
60            writeln!(f)?;
61            write_node(f, child, &child_prefix, index + 1 == node.children.len())?;
62        }
63    }
64
65    Ok(())
66}
67
68fn format_duration(duration: Duration) -> String {
69    let nanos = duration.as_nanos();
70
71    if nanos >= 1_000_000_000 {
72        format!("{:.1}s", nanos as f64 / 1_000_000_000.0)
73    } else if nanos >= 1_000_000 {
74        format!("{:.1}ms", nanos as f64 / 1_000_000.0)
75    } else if nanos >= 1_000 {
76        format!("{:.1}us", nanos as f64 / 1_000.0)
77    } else {
78        format!("{nanos}ns")
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use crate::snapshot::{CallTreeSnapshot, NodeSnapshot};
85    use crate::stats::TimingStats;
86    use std::time::Duration;
87
88    #[test]
89    fn renders_tree_display() {
90        let snapshot = CallTreeSnapshot {
91            roots: vec![NodeSnapshot {
92                name: "request".to_string(),
93                target: "app".to_string(),
94                module_path: None,
95                line: None,
96                total_calls: 2,
97                wall: TimingStats {
98                    samples: 2,
99                    min: Duration::from_millis(8),
100                    max: Duration::from_millis(12),
101                    mean: Duration::from_millis(10),
102                    p95: Duration::from_millis(12),
103                },
104                active: TimingStats {
105                    samples: 2,
106                    min: Duration::from_millis(4),
107                    max: Duration::from_millis(8),
108                    mean: Duration::from_millis(6),
109                    p95: Duration::from_millis(8),
110                },
111                suspended: TimingStats {
112                    samples: 2,
113                    min: Duration::from_millis(2),
114                    max: Duration::from_millis(4),
115                    mean: Duration::from_millis(3),
116                    p95: Duration::from_millis(4),
117                },
118                children: vec![],
119            }],
120        };
121
122        let rendered = snapshot.to_string();
123        assert!(rendered.contains("request"));
124        assert!(rendered.contains("10.0ms avg"));
125        assert!(rendered.contains("12.0ms p95 n=2"));
126    }
127}