Skip to main content

miden_debug_engine/profiling/
profiler.rs

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