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}