Skip to main content

miden_debug_engine/profiling/
profiler.rs

1use alloc::{boxed::Box, vec::Vec};
2
3use miden_core::operations::Operation;
4
5use crate::profiling::{OutputResult, OutputWriter, ProfilerConfig, instrument::Instrument};
6
7/// Holds the loaded [`Instrument`]s and dispatches event handlers to them.
8///
9/// The default is a no-op `Profiler`.
10#[derive(Default)]
11pub struct Profiler {
12    instruments: Vec<Box<dyn Instrument>>,
13    #[cfg(feature = "std")]
14    reports_dir: Option<std::path::PathBuf>,
15}
16
17impl Profiler {
18    pub fn from_config(config: ProfilerConfig) -> Self {
19        Self {
20            instruments: config.instruments,
21            #[cfg(feature = "std")]
22            reports_dir: config.reports_dir,
23        }
24    }
25
26    /// Records the op with every active instrument, otherwise it's a no-op.
27    pub fn on_operation_execution_cycle(&mut self, op: Operation, proc: Option<&str>) {
28        for instrument in &mut self.instruments {
29            instrument.on_operation_execution_cycle(op, proc);
30        }
31    }
32
33    /// Writes every instrument's report to the configured reports output directory.
34    ///
35    /// Failure to write a report should not abort execution, therefore this function always
36    /// succeeds. If any errors occur they are logged.
37    #[cfg(feature = "std")]
38    pub fn write_reports(&self) {
39        if self.instruments.is_empty() {
40            return;
41        }
42
43        let Some(ref reports_dir) = self.reports_dir else {
44            log::warn!("cannot write profiler reports: no reports directory configured");
45            return;
46        };
47
48        if let Err(e) = std::fs::create_dir_all(reports_dir) {
49            log::error!(
50                "failed to create profiler reports directory {}: {e}",
51                reports_dir.display()
52            );
53            return;
54        }
55
56        for instrument in &self.instruments {
57            let name = instrument.name();
58            let path = reports_dir.join(name);
59            let mut file = match std::fs::File::create(&path) {
60                Ok(file) => file,
61                Err(e) => {
62                    log::error!(
63                        "failed to create profiler output file for `{name}` at {}: {e}",
64                        path.display()
65                    );
66                    continue;
67                }
68            };
69            if let Err(e) = instrument.write_report_to(&mut file) {
70                log::error!("failed to write `{name}` report to {}: {e}", path.display());
71            }
72        }
73    }
74
75    /// Writes every instrument's report to the given output writer.
76    pub fn write_reports_to(&self, writer: &mut dyn OutputWriter) -> OutputResult<()> {
77        if self.instruments.is_empty() {
78            return Ok(());
79        }
80
81        for instrument in &self.instruments {
82            instrument.write_report_to(writer)?;
83        }
84        Ok(())
85    }
86}
87
88#[cfg(all(test, feature = "std"))]
89mod tests {
90    use alloc::string::String;
91    use std::collections::HashMap;
92
93    use miden_core::operations::Operation;
94
95    use super::*;
96    use crate::profiling::{OutputResult, OutputWriter, ProfilerConfig};
97
98    /// A minimal `Instrument` to test event dispatch.
99    struct CountingInstrument {
100        name: &'static str,
101        ops: u32,
102    }
103
104    impl Instrument for CountingInstrument {
105        fn name(&self) -> &'static str {
106            self.name
107        }
108
109        fn on_operation_execution_cycle(&mut self, _op: Operation, _proc: Option<&str>) {
110            self.ops += 1;
111        }
112
113        fn write_report_to(&self, writer: &mut dyn OutputWriter) -> OutputResult<()> {
114            writer.write_fmt(format_args!("{}:{}", self.name, self.ops))
115        }
116    }
117
118    #[test]
119    fn profiler_writes_reports_to_directory() {
120        let tmp_dir = tempfile::tempdir().unwrap();
121
122        let config = ProfilerConfig {
123            instruments: vec![
124                Box::new(CountingInstrument {
125                    name: "alpha",
126                    ops: 0,
127                }),
128                Box::new(CountingInstrument {
129                    name: "beta",
130                    ops: 0,
131                }),
132            ],
133            reports_dir: Some(tmp_dir.path().to_path_buf()),
134        };
135        let mut profiler = Profiler::from_config(config);
136
137        // Record some operations so the instruments have data to report.
138        profiler.on_operation_execution_cycle(Operation::Add, None);
139        profiler.on_operation_execution_cycle(Operation::Noop, None);
140        profiler.on_operation_execution_cycle(Operation::Add, None);
141
142        profiler.write_reports();
143
144        // Verify files were created with expected content.
145        let alpha_content = std::fs::read_to_string(tmp_dir.path().join("alpha")).unwrap();
146        assert_eq!(alpha_content, "alpha:3");
147
148        let beta_content = std::fs::read_to_string(tmp_dir.path().join("beta")).unwrap();
149        assert_eq!(beta_content, "beta:3");
150    }
151
152    #[test]
153    fn profiler_dispatches_events_to_all_instruments() {
154        let config = ProfilerConfig {
155            instruments: vec![
156                Box::new(CountingInstrument { name: "a", ops: 0 }),
157                Box::new(CountingInstrument { name: "b", ops: 0 }),
158            ],
159            ..Default::default()
160        };
161        let mut profiler = Profiler::from_config(config);
162
163        // Each instrument must observe every recorded op.
164        profiler.on_operation_execution_cycle(Operation::Add, None);
165        profiler.on_operation_execution_cycle(Operation::Noop, None);
166
167        // Collect each report into an in-memory buffer and construct string from there.
168        fn report_of(instrument: &dyn Instrument) -> String {
169            let mut buf = Vec::new();
170            instrument.write_report_to(&mut buf).unwrap();
171            String::from_utf8(buf).unwrap()
172        }
173
174        let reports: HashMap<&'static str, String> = profiler
175            .instruments
176            .iter()
177            .map(|instrument| (instrument.name(), report_of(instrument.as_ref())))
178            .collect();
179
180        assert_eq!(reports.get("a").unwrap(), "a:2");
181        assert_eq!(reports.get("b").unwrap(), "b:2");
182    }
183}