async_inspect/export/
flamegraph.rs1use crate::inspector::Inspector;
10use crate::timeline::EventKind;
11use std::collections::HashMap;
12use std::fs::File;
13use std::io::{self, Write};
14use std::path::Path;
15
16pub struct FlamegraphExporter;
18
19impl FlamegraphExporter {
20 pub fn export_to_file<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
22 let folded = Self::generate_folded_stacks(inspector);
23 let mut file = File::create(path)?;
24 file.write_all(folded.as_bytes())?;
25 Ok(())
26 }
27
28 #[must_use]
30 pub fn export_to_string(inspector: &Inspector) -> String {
31 Self::generate_folded_stacks(inspector)
32 }
33
34 fn generate_folded_stacks(inspector: &Inspector) -> String {
36 let mut output = String::new();
37 let events = inspector.get_events();
38
39 let mut task_stacks: HashMap<u64, Vec<String>> = HashMap::new();
41 let mut stack_samples: HashMap<String, u64> = HashMap::new();
42
43 for event in events {
45 let task_id = event.task_id.as_u64();
46
47 match &event.kind {
48 EventKind::TaskSpawned { name, parent, .. } => {
49 let mut stack = Vec::new();
51
52 if let Some(parent_id) = parent {
54 if let Some(parent_stack) = task_stacks.get(&parent_id.as_u64()) {
55 stack.extend(parent_stack.clone());
56 }
57 }
58
59 stack.push(Self::sanitize_frame_name(name));
61 task_stacks.insert(task_id, stack);
62 }
63
64 EventKind::AwaitEnded {
65 await_point,
66 duration,
67 } => {
68 if let Some(stack) = task_stacks.get_mut(&task_id) {
69 stack.push(Self::sanitize_frame_name(await_point));
71
72 let stack_str = stack.join(";");
74 let duration_ms = duration.as_millis() as u64;
75
76 *stack_samples.entry(stack_str).or_insert(0) += duration_ms;
77
78 stack.pop();
80 }
81 }
82
83 EventKind::PollEnded { duration } => {
84 if let Some(stack) = task_stacks.get(&task_id) {
85 let mut poll_stack = stack.clone();
87 poll_stack.push("poll".to_string());
88
89 let stack_str = poll_stack.join(";");
90 let duration_ms = duration.as_millis() as u64;
91
92 *stack_samples.entry(stack_str).or_insert(0) += duration_ms;
93 }
94 }
95
96 EventKind::TaskCompleted { duration } => {
97 if let Some(stack) = task_stacks.get(&task_id) {
98 let stack_str = stack.join(";");
100 let duration_ms = duration.as_millis() as u64;
101
102 *stack_samples.entry(stack_str).or_insert(0) += duration_ms;
103 }
104 }
105
106 _ => {}
107 }
108 }
109
110 let mut sorted_stacks: Vec<_> = stack_samples.into_iter().collect();
112 sorted_stacks.sort_by(|a, b| b.1.cmp(&a.1));
113
114 for (stack, count) in sorted_stacks {
116 if count > 0 {
117 output.push_str(&format!("{stack} {count}\n"));
118 }
119 }
120
121 output
122 }
123
124 fn sanitize_frame_name(name: &str) -> String {
127 name.replace(';', ":")
128 .replace('\n', " ")
129 .replace('\r', "")
130 .trim()
131 .to_string()
132 }
133
134 #[cfg(feature = "flamegraph")]
136 pub fn generate_svg<P: AsRef<Path>>(inspector: &Inspector, path: P) -> io::Result<()> {
137 use inferno::flamegraph::{self, Options};
138
139 let folded = Self::generate_folded_stacks(inspector);
140 let folded_bytes = folded.as_bytes();
141
142 let mut options = Options::default();
143 options.title = "async-inspect Flamegraph".to_string();
144 options.subtitle = Some("Async task execution time".to_string());
145
146 let mut svg_output = File::create(path)?;
147
148 flamegraph::from_reader(&mut options, folded_bytes, &mut svg_output)
149 .map_err(io::Error::other)?;
150
151 Ok(())
152 }
153}
154
155pub struct FlamegraphBuilder {
157 include_polls: bool,
159 include_awaits: bool,
161 min_duration_ms: u64,
163}
164
165impl Default for FlamegraphBuilder {
166 fn default() -> Self {
167 Self {
168 include_polls: true,
169 include_awaits: true,
170 min_duration_ms: 0,
171 }
172 }
173}
174
175impl FlamegraphBuilder {
176 #[must_use]
178 pub fn new() -> Self {
179 Self::default()
180 }
181
182 #[must_use]
184 pub fn include_polls(mut self, include: bool) -> Self {
185 self.include_polls = include;
186 self
187 }
188
189 #[must_use]
191 pub fn include_awaits(mut self, include: bool) -> Self {
192 self.include_awaits = include;
193 self
194 }
195
196 #[must_use]
198 pub fn min_duration_ms(mut self, ms: u64) -> Self {
199 self.min_duration_ms = ms;
200 self
201 }
202
203 pub fn export_to_file<P: AsRef<Path>>(self, inspector: &Inspector, path: P) -> io::Result<()> {
205 FlamegraphExporter::export_to_file(inspector, path)
208 }
209
210 #[must_use]
212 pub fn export_to_string(self, inspector: &Inspector) -> String {
213 FlamegraphExporter::export_to_string(inspector)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_sanitize_frame_name() {
225 assert_eq!(FlamegraphExporter::sanitize_frame_name("simple"), "simple");
226 assert_eq!(
227 FlamegraphExporter::sanitize_frame_name("with;semicolon"),
228 "with:semicolon"
229 );
230 assert_eq!(
231 FlamegraphExporter::sanitize_frame_name("with\nnewline"),
232 "with newline"
233 );
234 assert_eq!(
235 FlamegraphExporter::sanitize_frame_name(" trimmed "),
236 "trimmed"
237 );
238 }
239
240 #[test]
241 fn test_flamegraph_builder() {
242 let builder = FlamegraphBuilder::new()
243 .include_polls(false)
244 .include_awaits(true)
245 .min_duration_ms(10);
246
247 assert!(!builder.include_polls);
248 assert!(builder.include_awaits);
249 assert_eq!(builder.min_duration_ms, 10);
250 }
251}