Skip to main content

miden_debug_engine/profiling/instrument/
op_histogram_proc.rs

1use alloc::{borrow::ToOwned, string::String, vec::Vec};
2
3#[cfg(feature = "std")]
4type Map<K, V> = std::collections::HashMap<K, V>;
5#[cfg(not(feature = "std"))]
6type Map<K, V> = alloc::collections::BTreeMap<K, V>;
7
8use miden_core::operations::Operation;
9
10use super::{Instrument, InstrumentRegistration};
11use crate::profiling::{OutputResult, OutputWriter, helpers::op_histogram::OpHistogram};
12
13/// Map key under which operations that cannot be attributed to a procedure are collected. The
14/// angle brackets cannot occur in a MASM identifier, so this never collides with a procedure name
15const UNKNOWN_PROCEDURE: &str = "<unknown>";
16
17/// An [`Instrument`] to create per-procedure operation histograms.
18///
19/// At each cycle, it records the current operation into the histogram of the most recent live
20/// procedure. Operations that cannot be attributed to a procedure are collected into a separate
21/// histogram, reported under [`UNKNOWN_PROCEDURE`].
22///
23/// The report contains one section per procedure, sorted by the number of cycles spent
24/// in that procedure (highest first).
25#[derive(Default)]
26pub struct OpHistogramProc {
27    histograms: Map<String, OpHistogram>,
28}
29
30impl InstrumentRegistration for OpHistogramProc {
31    const NAME: &'static str = "op-histogram-proc";
32
33    fn build(_config: &crate::profiling::ProfilerConfig) -> Result<Self, super::InstrumentError> {
34        Ok(Self::default())
35    }
36}
37
38#[cfg(feature = "std")]
39crate::register_instrument!(OpHistogramProc);
40
41impl Instrument for OpHistogramProc {
42    fn name(&self) -> &'static str {
43        Self::NAME
44    }
45
46    fn on_operation_execution_cycle(&mut self, op: Operation, proc: Option<&str>) {
47        let key = proc.unwrap_or(UNKNOWN_PROCEDURE);
48        // Look up by borrow first so the key is only cloned when a new procedure is seen.
49        match self.histograms.get_mut(key) {
50            Some(hist) => hist.record(op),
51            None => {
52                let mut hist = OpHistogram::default();
53                hist.record(op);
54                self.histograms.insert(key.to_owned(), hist);
55            }
56        }
57    }
58
59    fn write_report_to(&self, writer: &mut dyn OutputWriter) -> OutputResult<()> {
60        let mut entries: Vec<(&str, &OpHistogram)> =
61            self.histograms.iter().map(|(name, hist)| (name.as_str(), hist)).collect();
62        // Print the histogram with the highest total cycle count first, break ties by procedure
63        // name for a stable order.
64        entries
65            .sort_by(|a, b| b.1.total_cycles().cmp(&a.1.total_cycles()).then_with(|| a.0.cmp(b.0)));
66
67        for (name, hist) in entries {
68            writeln!(writer, "procedure: {name}")?;
69            writer.write_all(hist.report().as_bytes())?;
70        }
71        Ok(())
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use alloc::{string::String, vec::Vec};
78
79    use miden_core::operations::Operation;
80
81    use super::OpHistogramProc;
82    use crate::profiling::instrument::Instrument;
83
84    /// Returns the part of `report` that belongs to the section started by `procedure: <name>`.
85    fn section<'a>(report: &'a str, name: &str) -> &'a str {
86        let marker = format!("procedure: {name}\n");
87        let start = report.find(&marker).expect("section exists") + marker.len();
88        match report[start..].find("procedure: ") {
89            Some(rel) => &report[start..start + rel],
90            None => &report[start..],
91        }
92    }
93
94    /// Returns the count shown on the report line for `label` within `section`, or `None` if
95    /// the section has no line for `label`.
96    ///
97    /// Matching op and count on a single line ensures the count is actually attributed to
98    /// `label` and not to another op in the same section.
99    fn count_for(section: &str, label: &str) -> Option<u64> {
100        section
101            .lines()
102            .find(|line| line.split_whitespace().next() == Some(label))
103            .map(parse_count_from_line)
104    }
105
106    /// Sums `total_cycles` over all procedure sections in `report`, including `<unknown>`.
107    fn sum_total_cycles(report: &str) -> u64 {
108        report
109            .lines()
110            .filter(|line| line.split_whitespace().next() == Some("total_cycles"))
111            .map(parse_count_from_line)
112            .sum()
113    }
114
115    /// Parses the count from the last column of a report line.
116    fn parse_count_from_line(line: &str) -> u64 {
117        line.split_whitespace()
118            .last()
119            .and_then(|count| count.parse().ok())
120            .expect("report line ends with the count")
121    }
122
123    #[test]
124    fn op_histogram_proc_reports_per_procedure_histograms() {
125        let mut hist = OpHistogramProc::default();
126
127        hist.on_operation_execution_cycle(Operation::Add, Some("main"));
128        hist.on_operation_execution_cycle(Operation::Add, Some("main"));
129        hist.on_operation_execution_cycle(Operation::Noop, Some("sum"));
130        hist.on_operation_execution_cycle(Operation::Mul, Some("sum"));
131
132        let mut buf = Vec::new();
133        hist.write_report_to(&mut buf).unwrap();
134        let report = String::from_utf8(buf).unwrap();
135
136        // `main` recorded 2 cycles, both `add`.
137        let main = section(&report, "main");
138        assert_eq!(count_for(main, "total_cycles"), Some(2));
139        assert_eq!(count_for(main, "add"), Some(2));
140        assert_eq!(count_for(main, "noop"), None);
141
142        // `sum` recorded 2 cycles, one `noop` and one `mul`.
143        let sum = section(&report, "sum");
144        assert_eq!(count_for(sum, "total_cycles"), Some(2));
145        assert_eq!(count_for(sum, "noop"), Some(1));
146        assert_eq!(count_for(sum, "mul"), Some(1));
147
148        // All 4 recorded cycles are accounted for across the sections.
149        assert_eq!(sum_total_cycles(&report), 4);
150    }
151
152    #[test]
153    fn op_histogram_proc_sorts_by_total_cycles() {
154        let mut hist = OpHistogramProc::default();
155
156        // `z` has more cycles than `a`, so it's printed first despite the alphabetical order.
157        hist.on_operation_execution_cycle(Operation::Add, Some("z"));
158        hist.on_operation_execution_cycle(Operation::Add, Some("z"));
159        hist.on_operation_execution_cycle(Operation::Add, Some("z"));
160        hist.on_operation_execution_cycle(Operation::Noop, Some("a"));
161
162        let mut buf = Vec::new();
163        hist.write_report_to(&mut buf).unwrap();
164        let report = String::from_utf8(buf).unwrap();
165
166        let z_pos = report.find("procedure: z").unwrap();
167        let a_pos = report.find("procedure: a").unwrap();
168        assert!(z_pos < a_pos, "histogram with more cycles must be printed first:\n{report}");
169
170        // All 4 recorded cycles are accounted for across the sections.
171        assert_eq!(sum_total_cycles(&report), 4);
172    }
173
174    #[test]
175    fn op_histogram_proc_collects_unattributed_ops_separately() {
176        let mut hist = OpHistogramProc::default();
177
178        hist.on_operation_execution_cycle(Operation::Add, None);
179        hist.on_operation_execution_cycle(Operation::Add, None);
180        hist.on_operation_execution_cycle(Operation::Noop, Some("main"));
181
182        let mut buf = Vec::new();
183        hist.write_report_to(&mut buf).unwrap();
184        let report = String::from_utf8(buf).unwrap();
185
186        // The `<unknown>` section holds both unattributed `add`s and is not merged into `main`.
187        let unknown = section(&report, "<unknown>");
188        assert_eq!(count_for(unknown, "total_cycles"), Some(2));
189        assert_eq!(count_for(unknown, "add"), Some(2));
190        assert_eq!(count_for(unknown, "noop"), None);
191
192        let main = section(&report, "main");
193        assert_eq!(count_for(main, "noop"), Some(1));
194        assert_eq!(count_for(main, "add"), None);
195
196        // All 3 recorded cycles (2 unattributed, 1 in `main`) are accounted for.
197        assert_eq!(sum_total_cycles(&report), 3);
198    }
199}