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, "", "", true, 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    print_prefix: &str,
31    children_prefix: &str,
32    is_root: bool,
33    is_last: bool,
34) -> fmt::Result {
35    let branch = if is_root {
36        ""
37    } else if is_last {
38        "└── "
39    } else {
40        "├── "
41    };
42
43    write!(
44        f,
45        "{print_prefix}{branch}{:<28} {:>8} avg {:>8} p95 n={}",
46        node.name,
47        format_duration(node.wall.mean),
48        format_duration(node.wall.p95),
49        node.wall.samples,
50    )?;
51
52    if !node.children.is_empty() {
53        for (index, child) in node.children.iter().enumerate() {
54            writeln!(f)?;
55            let last = index + 1 == node.children.len();
56
57            let child_print_prefix = if is_root { "" } else { children_prefix };
58
59            let grand_children_prefix = format!(
60                "{}{}",
61                children_prefix,
62                if last { "    " } else { "│   " }
63            );
64
65            write_node(
66                f,
67                child,
68                child_print_prefix,
69                &grand_children_prefix,
70                false,
71                last,
72            )?;
73        }
74    }
75
76    Ok(())
77}
78
79fn format_duration(duration: Duration) -> String {
80    let nanos = duration.as_nanos();
81
82    if nanos >= 1_000_000_000 {
83        format!("{:.1}s", nanos as f64 / 1_000_000_000.0)
84    } else if nanos >= 1_000_000 {
85        format!("{:.1}ms", nanos as f64 / 1_000_000.0)
86    } else if nanos >= 1_000 {
87        format!("{:.1}us", nanos as f64 / 1_000.0)
88    } else {
89        format!("{nanos}ns")
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use crate::snapshot::{CallTreeSnapshot};
96    use crate::stats::TimingStats;
97    use std::time::Duration;
98
99    #[test]
100    fn renders_tree_display() {
101        use crate::snapshot::NodeSnapshot;
102        use super::format_duration;
103
104        let snapshot = CallTreeSnapshot {
105            roots: vec![NodeSnapshot {
106                name: "request".to_string(),
107                target: "app".to_string(),
108                module_path: None,
109                line: None,
110                total_calls: 4,
111                wall: TimingStats {
112                    samples: 4,
113                    min: Duration::from_millis(8),
114                    max: Duration::from_millis(12),
115                    mean: Duration::from_millis(10),
116                    p95: Duration::from_millis(12),
117                },
118                active: TimingStats {
119                    samples: 4,
120                    min: Duration::from_millis(4),
121                    max: Duration::from_millis(8),
122                    mean: Duration::from_millis(6),
123                    p95: Duration::from_millis(8),
124                },
125                suspended: TimingStats {
126                    samples: 4,
127                    min: Duration::from_millis(2),
128                    max: Duration::from_millis(4),
129                    mean: Duration::from_millis(3),
130                    p95: Duration::from_millis(4),
131                },
132                children: vec![
133                    NodeSnapshot {
134                        name: "authenticate".to_string(),
135                        target: "app".to_string(),
136                        module_path: None,
137                        line: None,
138                        total_calls: 1,
139                        wall: TimingStats {
140                            samples: 1,
141                            min: Duration::from_millis(5),
142                            max: Duration::from_millis(5),
143                            mean: Duration::from_millis(5),
144                            p95: Duration::from_millis(5),
145                        },
146                        active: TimingStats::from_nanos(std::iter::empty()),
147                        suspended: TimingStats::from_nanos(std::iter::empty()),
148                        children: vec![],
149                    },
150                    NodeSnapshot {
151                        name: "database".to_string(),
152                        target: "app".to_string(),
153                        module_path: None,
154                        line: None,
155                        total_calls: 1,
156                        wall: TimingStats {
157                            samples: 1,
158                            min: Duration::from_millis(20),
159                            max: Duration::from_millis(20),
160                            mean: Duration::from_millis(20),
161                            p95: Duration::from_millis(20),
162                        },
163                        active: TimingStats::from_nanos(std::iter::empty()),
164                        suspended: TimingStats::from_nanos(std::iter::empty()),
165                        children: vec![NodeSnapshot {
166                            name: "query".to_string(),
167                            target: "app".to_string(),
168                            module_path: None,
169                            line: None,
170                            total_calls: 1,
171                            wall: TimingStats {
172                                samples: 1,
173                                min: Duration::from_millis(1),
174                                max: Duration::from_millis(1),
175                                mean: Duration::from_millis(1),
176                                p95: Duration::from_millis(1),
177                            },
178                            active: TimingStats::from_nanos(std::iter::empty()),
179                            suspended: TimingStats::from_nanos(std::iter::empty()),
180                            children: vec![],
181                        }],
182                    },
183                ],
184            }],
185        };
186
187        let rendered = snapshot.to_string();
188
189        let line_root = format!("{:<28} {:>8} avg {:>8} p95 n={}",
190            "request",
191            format_duration(Duration::from_millis(10)),
192            format_duration(Duration::from_millis(12)),
193            4
194        );
195
196        let line_auth = format!("├── {:<28} {:>8} avg {:>8} p95 n={}",
197            "authenticate",
198            format_duration(Duration::from_millis(5)),
199            format_duration(Duration::from_millis(5)),
200            1
201        );
202
203        let line_db = format!("└── {:<28} {:>8} avg {:>8} p95 n={}",
204            "database",
205            format_duration(Duration::from_millis(20)),
206            format_duration(Duration::from_millis(20)),
207            1
208        );
209
210        let line_query = format!("    └── {:<28} {:>8} avg {:>8} p95 n={}",
211            "query",
212            format_duration(Duration::from_millis(1)),
213            format_duration(Duration::from_millis(1)),
214            1
215        );
216
217        let expected = format!("{}\n{}\n{}\n{}", line_root, line_auth, line_db, line_query);
218
219        assert_eq!(rendered, expected);
220    }
221}