openvm_prof/
summary.rs

1use std::{io::Write, path::PathBuf};
2
3use eyre::Result;
4use itertools::Itertools;
5
6use crate::{
7    aggregate::{
8        AggregateMetrics, EXECUTE_METERED_TIME_LABEL, EXECUTE_PREFLIGHT_INSNS_LABEL,
9        EXECUTE_PREFLIGHT_TIME_LABEL, MAIN_CELLS_USED_LABEL, PROOF_TIME_LABEL,
10        PROVE_EXCL_TRACE_TIME_LABEL, TRACE_GEN_TIME_LABEL,
11    },
12    types::MdTableCell,
13};
14
15#[derive(Clone, Debug)]
16pub struct GithubSummary {
17    pub rows: Vec<SummaryRow>,
18    pub benchmark_results_link: String,
19}
20
21#[derive(Clone, Debug)]
22pub struct SummaryRow {
23    pub name: String,
24    pub md_filename: String,
25    pub metrics: BenchSummaryMetrics,
26}
27
28#[derive(Clone, Debug)]
29pub struct BenchSummaryMetrics {
30    pub app: SingleSummaryMetrics,
31    pub leaf: Option<SingleSummaryMetrics>,
32    pub internals: Vec<SingleSummaryMetrics>,
33    pub root: Option<SingleSummaryMetrics>,
34    pub halo2_outer: Option<SingleSummaryMetrics>,
35    pub halo2_wrapper: Option<SingleSummaryMetrics>,
36}
37
38#[derive(Clone, Debug)]
39pub struct SingleSummaryMetrics {
40    pub proof_time_ms: MdTableCell,
41    /// Parallel proof time is approximated as the max of proof times within a group
42    pub par_proof_time_ms: MdTableCell,
43    pub cells_used: MdTableCell,
44    pub insns: MdTableCell,
45}
46
47impl GithubSummary {
48    pub fn new(
49        names: &[String],
50        aggregated_metrics: &[(AggregateMetrics, Option<AggregateMetrics>)],
51        md_paths: &[PathBuf],
52        benchmark_results_link: &str,
53    ) -> Self {
54        let rows = aggregated_metrics
55            .iter()
56            .zip_eq(md_paths.iter())
57            .zip_eq(names)
58            .map(|(((aggregated, prev_aggregated), md_path), name)| {
59                let md_filename = md_path
60                    .file_name()
61                    .expect("Path should have a filename")
62                    .to_str()
63                    .expect("Filename should be valid UTF-8");
64                let mut row = aggregated.get_summary_row(md_filename).unwrap_or_else(|| {
65                    panic!("Failed to get summary row for file '{md_filename}'")
66                });
67                if let Some(prev_aggregated) = prev_aggregated {
68                    // md_filename doesn't matter
69                    if let Some(prev_row) = prev_aggregated.get_summary_row(md_filename) {
70                        if row.name == prev_row.name {
71                            row.metrics.set_diff(&prev_row.metrics);
72                        }
73                    }
74                }
75                row.name = name.clone();
76                row
77            })
78            .collect();
79
80        Self {
81            rows,
82            benchmark_results_link: benchmark_results_link.to_string(),
83        }
84    }
85
86    pub fn write_markdown(&self, writer: &mut impl Write) -> Result<()> {
87        writeln!(writer, "| group | app.proof_time_ms | app.cycles | app.cells_used | leaf.proof_time_ms | leaf.cycles | leaf.cells_used |")?;
88        write!(writer, "| -- |")?;
89        for _ in 0..6 {
90            write!(writer, " -- |")?;
91        }
92        writeln!(writer)?;
93
94        for row in self.rows.iter() {
95            write!(
96                writer,
97                "| [{}]({}/{}) |",
98                row.name, self.benchmark_results_link, row.md_filename
99            )?;
100            row.metrics.write_partial_md_row(writer)?;
101            writeln!(writer)?;
102        }
103        writeln!(writer)?;
104
105        Ok(())
106    }
107}
108
109impl BenchSummaryMetrics {
110    pub fn write_partial_md_row(&self, writer: &mut impl Write) -> Result<()> {
111        self.app.write_partial_md_row(writer)?;
112        if let Some(leaf) = &self.leaf {
113            leaf.write_partial_md_row(writer)?;
114        } else {
115            // Always write placeholder for leaf
116            write!(writer, "- | - | - |")?;
117        }
118        // Don't print other metrics in summary for now:
119
120        // for internal in &self.internals {
121        //     internal.write_partial_md_row(writer)?;
122        // }
123        // if let Some(root) = &self.root {
124        //     root.write_partial_md_row(writer)?;
125        // }
126
127        Ok(())
128    }
129
130    pub fn set_diff(&mut self, prev: &Self) {
131        self.app.set_diff(&prev.app);
132        if let (Some(leaf), Some(prev_leaf)) = (&mut self.leaf, &prev.leaf) {
133            leaf.set_diff(prev_leaf);
134        }
135        for (internal, prev_internal) in self.internals.iter_mut().zip(prev.internals.iter()) {
136            internal.set_diff(prev_internal);
137        }
138        if let (Some(root), Some(prev_root)) = (&mut self.root, &prev.root) {
139            root.set_diff(prev_root);
140        }
141    }
142}
143
144impl SingleSummaryMetrics {
145    pub fn write_partial_md_row(&self, writer: &mut impl Write) -> Result<()> {
146        write!(
147            writer,
148            "{} | {} | {} |",
149            self.proof_time_ms, self.insns, self.cells_used,
150        )?;
151        Ok(())
152    }
153
154    pub fn set_diff(&mut self, prev: &Self) {
155        self.cells_used.diff = Some(self.cells_used.val - prev.cells_used.val);
156        self.insns.diff = Some(self.insns.val - prev.insns.val);
157        self.proof_time_ms.diff = Some(self.proof_time_ms.val - prev.proof_time_ms.val);
158    }
159}
160
161impl AggregateMetrics {
162    pub fn get_single_summary(&self, name: &str) -> Option<SingleSummaryMetrics> {
163        let stats = self.by_group.get(name)?;
164        // Any group must have proof_time, but may not have cells_used or cycles (e.g., halo2)
165        let proof_time_ms = if let Some(proof_stats) = stats.get(PROOF_TIME_LABEL) {
166            proof_stats.sum
167        } else {
168            // Note: execute_metered is outside any segment scope, so it should have sum = max = avg
169            let execute_metered = stats
170                .get(EXECUTE_METERED_TIME_LABEL)
171                .map(|s| s.sum.val)
172                .unwrap_or(0.0);
173            let execute_preflight = stats
174                .get(EXECUTE_PREFLIGHT_TIME_LABEL)
175                .map(|s| s.sum.val)
176                .unwrap_or(0.0);
177            // If total_proof_time_ms is not available, compute it from components
178            let trace_gen = stats
179                .get(TRACE_GEN_TIME_LABEL)
180                .map(|s| s.sum.val)
181                .unwrap_or(0.0);
182            let stark_prove = stats
183                .get(PROVE_EXCL_TRACE_TIME_LABEL)
184                .map(|s| s.sum.val)
185                .unwrap_or(0.0);
186            println!("{execute_metered} {execute_preflight} {trace_gen} {stark_prove}");
187            MdTableCell::new(
188                execute_metered + execute_preflight + trace_gen + stark_prove,
189                None,
190            )
191        };
192        println!("{}", self.total_proof_time.val);
193        let par_proof_time_ms = if let Some(proof_stats) = stats.get(PROOF_TIME_LABEL) {
194            proof_stats.max
195        } else {
196            // Use the same computation for max
197            let execute_metered = stats
198                .get(EXECUTE_METERED_TIME_LABEL)
199                .map(|s| s.max.val)
200                .unwrap_or(0.0);
201            let execute_preflight = stats
202                .get(EXECUTE_PREFLIGHT_TIME_LABEL)
203                .map(|s| s.max.val)
204                .unwrap_or(0.0);
205            let trace_gen = stats
206                .get(TRACE_GEN_TIME_LABEL)
207                .map(|s| s.max.val)
208                .unwrap_or(0.0);
209            let stark_prove = stats
210                .get(PROVE_EXCL_TRACE_TIME_LABEL)
211                .map(|s| s.max.val)
212                .unwrap_or(0.0);
213            MdTableCell::new(
214                execute_metered + execute_preflight + trace_gen + stark_prove,
215                None,
216            )
217        };
218        let cells_used = stats
219            .get(MAIN_CELLS_USED_LABEL)
220            .map(|s| s.sum)
221            .unwrap_or_default();
222        let insns = stats
223            .get(EXECUTE_PREFLIGHT_INSNS_LABEL)
224            .map(|s| s.sum)
225            .unwrap_or_default();
226        Some(SingleSummaryMetrics {
227            cells_used,
228            insns,
229            proof_time_ms,
230            par_proof_time_ms,
231        })
232    }
233
234    /// Returns `None` if no group for app is found.
235    pub fn get_summary_row(&self, md_filename: &str) -> Option<SummaryRow> {
236        let app_name = self.name();
237        let app = self.get_single_summary(&app_name)?;
238        let leaf = self.get_single_summary("leaf");
239        let mut internals = Vec::new();
240        let mut hgt = 0;
241        while let Some(internal) = self.get_single_summary(&format!("internal.{hgt}")) {
242            internals.push(internal);
243            hgt += 1;
244        }
245        let root = self.get_single_summary("root");
246        let halo2_outer = self.get_single_summary("halo2_outer");
247        let halo2_wrapper = self.get_single_summary("halo2_wrapper");
248        Some(SummaryRow {
249            name: app_name.to_string(),
250            md_filename: md_filename.to_string(),
251            metrics: BenchSummaryMetrics {
252                app,
253                leaf,
254                internals,
255                root,
256                halo2_outer,
257                halo2_wrapper,
258            },
259        })
260    }
261}