1use std::{
2 collections::{BTreeMap, VecDeque},
3 fs::File,
4 io::{BufWriter, Write},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use miden_assembly::{DefaultSourceManager, SourceManager};
10use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report, WrapErr};
11use miden_debug_engine::LinkLibrary;
12use miden_processor::ExecutionError;
13
14use crate::{
15 config::{ColorChoice, DebuggerConfig},
16 debug::CallFrame,
17 exec::{DebugExecutor, ExecutionConfig, Executor, ReplaySnapshot},
18 input::InputFile,
19};
20
21pub type Samples = BTreeMap<String, usize>;
23
24#[derive(Clone, Debug, Default, Eq, PartialEq)]
26pub struct FlamegraphProfile {
27 samples: Samples,
28 total_cycles: usize,
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum FlamegraphOutput {
34 Svg,
35 FoldedStacks,
36}
37
38impl FlamegraphProfile {
39 pub fn collect(executor: &mut DebugExecutor) -> Result<Self, ExecutionError> {
41 let mut profile = Self::default();
42
43 loop {
44 if executor.stopped {
45 break;
46 }
47
48 let previous_cycle = executor.cycle;
49 match executor.step() {
50 Ok(_) if executor.cycle > previous_cycle => {
51 let cycle_delta = executor.cycle - previous_cycle;
52 profile.record_call_stack(executor.callstack.frames(), cycle_delta);
53 }
54 Ok(_) => {
55 if executor.stopped {
56 break;
57 }
58 }
59 Err(err) => return Err(err),
60 }
61 }
62
63 Ok(profile)
64 }
65
66 pub fn collect_replay(snapshot: ReplaySnapshot) -> Result<Self, ExecutionError> {
68 let ReplaySnapshot {
69 package,
70 stack_inputs,
71 advice_inputs,
72 options,
73 mast_forests,
74 event_log,
75 } = snapshot;
76 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
77 let executor = Executor::from_config(ExecutionConfig {
78 inputs: stack_inputs,
79 advice_inputs,
80 options,
81 });
82 let mut debug_executor = executor.into_debug_with_replay(
83 package,
84 source_manager,
85 mast_forests,
86 VecDeque::from(event_log),
87 );
88 Self::collect(&mut debug_executor)
89 }
90
91 pub fn record_call_stack(&mut self, frames: &[CallFrame], cycles: usize) {
93 let path = build_stack_path(frames);
94 self.record_stack_path(path, cycles);
95 }
96
97 pub fn record_stack<I, S>(&mut self, frames: I, cycles: usize)
101 where
102 I: IntoIterator<Item = S>,
103 S: AsRef<str>,
104 {
105 let path = build_stack_path_from_names(frames);
106 self.record_stack_path(path, cycles);
107 }
108
109 pub fn record_stack_path(&mut self, stack_path: impl Into<String>, cycles: usize) {
111 if cycles == 0 {
112 return;
113 }
114
115 self.total_cycles += cycles;
116 *self.samples.entry(stack_path.into()).or_default() += cycles;
117 }
118
119 pub fn samples(&self) -> &Samples {
120 &self.samples
121 }
122
123 pub fn total_cycles(&self) -> usize {
124 self.total_cycles
125 }
126
127 pub fn unique_stack_paths(&self) -> usize {
128 self.samples.len()
129 }
130
131 pub fn write_to_path(&self, path: impl AsRef<Path>) -> Result<FlamegraphOutput, Report> {
133 let path = path.as_ref();
134 if is_svg_path(path) {
135 self.write_svg(path)?;
136 Ok(FlamegraphOutput::Svg)
137 } else {
138 self.write_folded_stacks(path)?;
139 Ok(FlamegraphOutput::FoldedStacks)
140 }
141 }
142
143 pub fn write_folded_stacks(&self, path: impl AsRef<Path>) -> Result<(), Report> {
145 write_folded_stacks(&self.samples, path.as_ref())
146 }
147
148 pub fn write_svg(&self, path: impl AsRef<Path>) -> Result<(), Report> {
150 generate_svg(&self.samples, path.as_ref())
151 }
152}
153
154#[derive(clap::Args, Debug)]
155#[command(group(
156 clap::ArgGroup::new("execution")
157 .required(true)
158 .args(["input", "replay"])
159))]
160pub struct FlamegraphArgs {
161 #[arg(value_name = "FILE")]
163 pub input: Option<InputFile>,
164 #[arg(long, value_name = "FILE")]
166 pub replay: Option<PathBuf>,
167 #[arg(short, long, default_value = "flamegraph.svg")]
169 pub output: PathBuf,
170 #[arg(long, value_name = "FILE")]
172 pub inputs: Option<ExecutionConfig>,
173 #[arg(last(true), value_name = "ARGV")]
175 pub args: Vec<String>,
176 #[arg(long, value_name = "DIR", help_heading = "Execution")]
178 pub working_dir: Option<PathBuf>,
179 #[arg(
181 long,
182 value_name = "DIR",
183 env = "MIDEN_SYSROOT",
184 help_heading = "Linker"
185 )]
186 pub sysroot: Option<PathBuf>,
187 #[arg(long, help_heading = "Execution")]
189 pub entrypoint: Option<String>,
190 #[arg(
192 long = "search-path",
193 short = 'L',
194 value_name = "PATH",
195 help_heading = "Linker"
196 )]
197 pub search_path: Vec<PathBuf>,
198 #[arg(
200 long = "link-library",
201 short = 'l',
202 value_name = "[KIND=]NAME",
203 value_delimiter = ',',
204 next_line_help(true),
205 help_heading = "Linker"
206 )]
207 pub link_libraries: Vec<LinkLibrary>,
208}
209
210impl FlamegraphArgs {
211 fn into_debugger_config(self) -> DebuggerConfig {
212 DebuggerConfig {
213 input: self.input,
214 inputs: self.inputs,
215 args: self.args,
216 working_dir: self.working_dir,
217 sysroot: self.sysroot,
218 color: ColorChoice::Auto,
219 entrypoint: self.entrypoint,
220 #[cfg(all(feature = "dap", feature = "tui"))]
221 dap_connect: None,
222 #[cfg(feature = "dap")]
223 start_debug_adapter: None,
224 source_path_prefixes: Vec::new(),
225 search_path: self.search_path,
226 link_libraries: self.link_libraries,
227 repl: false,
228 commands: None,
229 #[cfg(feature = "tui")]
230 replay: None,
231 #[cfg(feature = "python")]
232 no_user_python_init: true,
233 profiler_cli_args: Default::default(), }
235 }
236}
237
238pub fn run(args: FlamegraphArgs) -> Result<(), Report> {
239 let output = args.output.clone();
240 let replay = args.replay.clone();
241 if let Some(replay) = replay {
242 let snapshot = ReplaySnapshot::read_from_file(&replay)
243 .map_err(|err| Report::msg(format!("failed to read {}: {err}", replay.display())))?;
244 let profile = FlamegraphProfile::collect_replay(snapshot)
245 .map_err(|err| Report::msg(format!("replay execution failed: {err}")))?;
246 report_and_write_profile(&profile, &output)?;
247 return Ok(());
248 }
249
250 let mut config = args.into_debugger_config();
251 ensure_working_dir(&mut config)?;
252
253 let source_manager: Arc<dyn SourceManager> = Arc::new(DefaultSourceManager::default());
254 let mut executor =
255 crate::program_loader::load_debug_executor(&config, source_manager, "flamegraph")?.executor;
256
257 let profile = match FlamegraphProfile::collect(&mut executor) {
258 Ok(profile) => profile,
259 Err(err) => {
260 return Err(Report::msg(format!(
261 "program execution failed at cycle {}: {err}",
262 executor.cycle
263 )));
264 }
265 };
266
267 report_and_write_profile(&profile, &output)?;
268
269 Ok(())
270}
271
272fn report_and_write_profile(profile: &FlamegraphProfile, output: &Path) -> Result<(), Report> {
273 eprintln!(
274 "Executed {} cycles across {} unique stack paths",
275 profile.total_cycles(),
276 profile.unique_stack_paths()
277 );
278 profile.write_to_path(output)?;
279 Ok(())
280}
281
282fn ensure_working_dir(config: &mut DebuggerConfig) -> Result<(), Report> {
283 if config.working_dir.is_none() {
284 let cwd = std::env::current_dir()
285 .into_diagnostic()
286 .wrap_err("could not read current working directory")?;
287 config.working_dir = Some(cwd);
288 }
289
290 Ok(())
291}
292
293fn build_stack_path(frames: &[CallFrame]) -> String {
294 build_stack_path_from_names(frames.iter().filter_map(|frame| frame.procedure("")))
295}
296
297fn build_stack_path_from_names<I, S>(frames: I) -> String
298where
299 I: IntoIterator<Item = S>,
300 S: AsRef<str>,
301{
302 let mut path = String::new();
303 for frame in frames {
304 append_frame(&mut path, frame.as_ref());
305 }
306
307 if path.is_empty() {
308 "[unknown]".to_string()
309 } else {
310 path
311 }
312}
313
314fn append_frame(path: &mut String, name: &str) {
315 if !path.is_empty() {
316 path.push(';');
317 }
318 append_sanitized_frame(path, name);
319}
320
321fn append_sanitized_frame(path: &mut String, name: &str) {
322 for ch in name.chars() {
323 match ch {
324 ';' => path.push(':'),
325 '\n' | '\r' => path.push(' '),
326 _ => path.push(ch),
327 }
328 }
329}
330
331fn is_svg_path(path: &Path) -> bool {
332 path.extension()
333 .and_then(|ext| ext.to_str())
334 .is_some_and(|ext| ext.eq_ignore_ascii_case("svg"))
335}
336
337fn write_folded_stacks(samples: &Samples, path: &Path) -> Result<(), Report> {
338 let file = File::create(path).into_diagnostic()?;
339 let mut writer = BufWriter::new(file);
340 for (stack, count) in samples {
341 writeln!(writer, "{stack} {count}").into_diagnostic()?;
342 }
343 writer.flush().into_diagnostic()?;
344
345 eprintln!("Wrote folded stacks to {}", path.display());
346 Ok(())
347}
348
349fn generate_svg(samples: &Samples, path: &Path) -> Result<(), Report> {
350 let input = samples
351 .iter()
352 .map(|(stack, count)| format!("{stack} {count}"))
353 .collect::<Vec<_>>()
354 .join("\n");
355
356 let mut opts = inferno::flamegraph::Options::default();
357 opts.title = "Miden VM Flame Graph (cycles)".to_string();
358 opts.count_name = "cycles".to_string();
359
360 let file = File::create(path).into_diagnostic()?;
361 let mut writer = BufWriter::new(file);
362 inferno::flamegraph::from_reader(&mut opts, input.as_bytes(), &mut writer).into_diagnostic()?;
363 writer.flush().into_diagnostic()?;
364
365 eprintln!("Wrote flame graph to {}", path.display());
366 Ok(())
367}
368
369#[cfg(test)]
370mod tests {
371 use super::FlamegraphProfile;
372
373 #[test]
374 fn record_stack_sanitizes_folded_stack_frames() {
375 let mut profile = FlamegraphProfile::default();
376
377 profile.record_stack(["root;proc", "child\nproc"], 3);
378 profile.record_stack(["root;proc", "child\nproc"], 2);
379 profile.record_stack(["ignored"], 0);
380
381 assert_eq!(profile.total_cycles(), 5);
382 assert_eq!(profile.unique_stack_paths(), 1);
383 assert_eq!(profile.samples().get("root:proc;child proc"), Some(&5));
384 assert!(!profile.samples().contains_key("ignored"));
385 }
386}