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        let rows = build_rows(self.0);
10        let widths = ColumnWidths::from_rows(&rows);
11
12        for (index, row) in rows.iter().enumerate() {
13            write_row(f, row, &widths)?;
14            if index + 1 != rows.len() {
15                writeln!(f)?;
16            }
17        }
18
19        Ok(())
20    }
21}
22
23impl fmt::Display for CallTreeSnapshot {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        self.display().fmt(f)
26    }
27}
28
29fn write_row(f: &mut fmt::Formatter<'_>, row: &DisplayRow, widths: &ColumnWidths) -> fmt::Result {
30    write!(
31        f,
32        "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
33        row.label,
34        row.avg,
35        row.p95,
36        row.samples,
37        name_width = widths.name,
38        avg_width = widths.avg,
39        p95_width = widths.p95,
40        samples_width = widths.samples,
41    )
42}
43
44fn build_rows(snapshot: &CallTreeSnapshot) -> Vec<DisplayRow> {
45    let mut roots = snapshot.roots.iter().collect::<Vec<_>>();
46    roots.sort_by(|left, right| {
47        right
48            .wall
49            .mean
50            .cmp(&left.wall.mean)
51            .then_with(|| left.name.cmp(&right.name))
52    });
53
54    let mut rows = Vec::new();
55    for (index, root) in roots.into_iter().enumerate() {
56        collect_rows(root, "", true, index + 1 == snapshot.roots.len(), &mut rows);
57    }
58    rows
59}
60
61fn collect_rows(
62    node: &NodeSnapshot,
63    prefix: &str,
64    is_root: bool,
65    is_last: bool,
66    rows: &mut Vec<DisplayRow>,
67) {
68    let branch = if is_root {
69        ""
70    } else if is_last {
71        "└── "
72    } else {
73        "├── "
74    };
75
76    rows.push(DisplayRow {
77        label: format!("{prefix}{branch}{}", node.name),
78        avg: format!("{} avg", format_duration(node.wall.mean)),
79        p95: format!("{} p95", format_duration(node.wall.p95)),
80        samples: format!("n={}", node.wall.samples),
81    });
82
83    let child_prefix = if is_root {
84        String::new()
85    } else {
86        format!("{prefix}{}", if is_last { "    " } else { "│   " })
87    };
88
89    for (index, child) in node.children.iter().enumerate() {
90        collect_rows(
91            child,
92            &child_prefix,
93            false,
94            index + 1 == node.children.len(),
95            rows,
96        );
97    }
98}
99
100fn format_duration(duration: Duration) -> String {
101    let nanos = duration.as_nanos();
102
103    if nanos >= 1_000_000_000 {
104        format!("{:.1}s", nanos as f64 / 1_000_000_000.0)
105    } else if nanos >= 1_000_000 {
106        format!("{:.1}ms", nanos as f64 / 1_000_000.0)
107    } else if nanos >= 1_000 {
108        format!("{:.1}us", nanos as f64 / 1_000.0)
109    } else {
110        format!("{nanos}ns")
111    }
112}
113
114#[derive(Debug)]
115struct DisplayRow {
116    label: String,
117    avg: String,
118    p95: String,
119    samples: String,
120}
121
122#[derive(Debug)]
123struct ColumnWidths {
124    name: usize,
125    avg: usize,
126    p95: usize,
127    samples: usize,
128}
129
130impl ColumnWidths {
131    fn from_rows(rows: &[DisplayRow]) -> Self {
132        Self {
133            name: rows.iter().map(|row| row.label.len()).max().unwrap_or(0),
134            avg: rows.iter().map(|row| row.avg.len()).max().unwrap_or(0),
135            p95: rows.iter().map(|row| row.p95.len()).max().unwrap_or(0),
136            samples: rows.iter().map(|row| row.samples.len()).max().unwrap_or(0),
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use crate::snapshot::CallTreeSnapshot;
144    use crate::stats::TimingStats;
145    use std::time::Duration;
146
147    #[test]
148    fn renders_tree_display_with_dynamic_columns_and_sorted_roots() {
149        use super::format_duration;
150        use crate::snapshot::NodeSnapshot;
151
152        let snapshot = CallTreeSnapshot {
153            roots: vec![
154                NodeSnapshot {
155                    name: "request".to_string(),
156                    target: "app".to_string(),
157                    module_path: None,
158                    line: None,
159                    total_calls: 4,
160                    wall: TimingStats {
161                        samples: 4,
162                        min: Duration::from_millis(8),
163                        max: Duration::from_millis(12),
164                        mean: Duration::from_millis(10),
165                        p95: Duration::from_millis(12),
166                    },
167                    active: TimingStats {
168                        samples: 4,
169                        min: Duration::from_millis(4),
170                        max: Duration::from_millis(8),
171                        mean: Duration::from_millis(6),
172                        p95: Duration::from_millis(8),
173                    },
174                    suspended: TimingStats {
175                        samples: 4,
176                        min: Duration::from_millis(2),
177                        max: Duration::from_millis(4),
178                        mean: Duration::from_millis(3),
179                        p95: Duration::from_millis(4),
180                    },
181                    children: vec![
182                        NodeSnapshot {
183                            name: "authenticate".to_string(),
184                            target: "app".to_string(),
185                            module_path: None,
186                            line: None,
187                            total_calls: 1,
188                            wall: TimingStats {
189                                samples: 1,
190                                min: Duration::from_millis(5),
191                                max: Duration::from_millis(5),
192                                mean: Duration::from_millis(5),
193                                p95: Duration::from_millis(5),
194                            },
195                            active: TimingStats::from_nanos(std::iter::empty()),
196                            suspended: TimingStats::from_nanos(std::iter::empty()),
197                            children: vec![],
198                        },
199                        NodeSnapshot {
200                            name: "database".to_string(),
201                            target: "app".to_string(),
202                            module_path: None,
203                            line: None,
204                            total_calls: 1,
205                            wall: TimingStats {
206                                samples: 1,
207                                min: Duration::from_millis(20),
208                                max: Duration::from_millis(20),
209                                mean: Duration::from_millis(20),
210                                p95: Duration::from_millis(20),
211                            },
212                            active: TimingStats::from_nanos(std::iter::empty()),
213                            suspended: TimingStats::from_nanos(std::iter::empty()),
214                            children: vec![NodeSnapshot {
215                                name: "query".to_string(),
216                                target: "app".to_string(),
217                                module_path: None,
218                                line: None,
219                                total_calls: 1,
220                                wall: TimingStats {
221                                    samples: 1,
222                                    min: Duration::from_millis(1),
223                                    max: Duration::from_millis(1),
224                                    mean: Duration::from_millis(1),
225                                    p95: Duration::from_millis(1),
226                                },
227                                active: TimingStats::from_nanos(std::iter::empty()),
228                                suspended: TimingStats::from_nanos(std::iter::empty()),
229                                children: vec![],
230                            }],
231                        },
232                    ],
233                },
234                NodeSnapshot {
235                    name: "maintenance_job_with_long_name".to_string(),
236                    target: "app".to_string(),
237                    module_path: None,
238                    line: None,
239                    total_calls: 12,
240                    wall: TimingStats {
241                        samples: 12,
242                        min: Duration::from_secs(2),
243                        max: Duration::from_secs(2),
244                        mean: Duration::from_secs(2),
245                        p95: Duration::from_secs(2),
246                    },
247                    active: TimingStats::from_nanos(std::iter::empty()),
248                    suspended: TimingStats::from_nanos(std::iter::empty()),
249                    children: vec![],
250                },
251            ],
252        };
253
254        let rendered = snapshot.to_string();
255
256        let name_width = "maintenance_job_with_long_name".len();
257        let avg_width = "20.0ms avg".len();
258        let p95_width = "20.0ms p95".len();
259        let samples_width = "n=12".len();
260
261        let line_root = format!(
262            "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
263            "maintenance_job_with_long_name", "2.0s avg", "2.0s p95", "n=12",
264        );
265
266        let line_request = format!(
267            "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
268            "request",
269            format!("{} avg", format_duration(Duration::from_millis(10))),
270            format!("{} p95", format_duration(Duration::from_millis(12))),
271            "n=4",
272        );
273
274        let line_auth = format!(
275            "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
276            "├── authenticate",
277            format!("{} avg", format_duration(Duration::from_millis(5))),
278            format!("{} p95", format_duration(Duration::from_millis(5))),
279            "n=1",
280        );
281
282        let line_db = format!(
283            "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
284            "└── database",
285            format!("{} avg", format_duration(Duration::from_millis(20))),
286            format!("{} p95", format_duration(Duration::from_millis(20))),
287            "n=1",
288        );
289
290        let line_query = format!(
291            "{:<name_width$} {:>avg_width$} {:>p95_width$} {:>samples_width$}",
292            "    └── query",
293            format!("{} avg", format_duration(Duration::from_millis(1))),
294            format!("{} p95", format_duration(Duration::from_millis(1))),
295            "n=1",
296        );
297
298        let expected = format!(
299            "{}\n{}\n{}\n{}\n{}",
300            line_root, line_request, line_auth, line_db, line_query
301        );
302
303        assert_eq!(rendered, expected);
304    }
305}