Skip to main content

miden_debug_engine/profiling/instrument/
op_histogram_proc.rs

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