1use std::{collections::HashMap, io::Write};
2
3use eyre::Result;
4use serde::{Deserialize, Serialize};
5
6use crate::types::{BencherValue, BenchmarkOutput, Labels, MdTableCell, MetricDb};
7
8type MetricName = String;
9type MetricsByName = HashMap<MetricName, Vec<(f64, Labels)>>;
10
11#[derive(Clone, Debug, Default)]
12pub struct GroupedMetrics {
13 pub by_group: HashMap<String, MetricsByName>,
15 pub ungrouped: MetricsByName,
16}
17
18#[derive(Clone, Debug, Default, Serialize, Deserialize)]
19pub struct AggregateMetrics {
20 #[serde(flatten)]
22 pub by_group: HashMap<String, HashMap<MetricName, Stats>>,
23 pub total_proof_time: MdTableCell,
25 pub total_par_proof_time: MdTableCell,
27 #[serde(skip)]
29 pub bounded_par_by_group: HashMap<String, MdTableCell>,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
33pub struct BencherAggregateMetrics {
34 #[serde(flatten)]
35 pub by_group: HashMap<String, HashMap<String, BencherValue>>,
36 pub total_proof_time: BencherValue,
38 pub total_par_proof_time: BencherValue,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
43pub struct Stats {
44 pub sum: MdTableCell,
45 pub max: MdTableCell,
46 pub min: MdTableCell,
47 pub avg: MdTableCell,
48 #[serde(skip)]
49 pub count: usize,
50 #[serde(skip)]
51 pub phase: Option<String>,
52}
53
54impl Default for Stats {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl Stats {
61 pub fn new() -> Self {
62 Self {
63 sum: MdTableCell::default(),
64 max: MdTableCell::default(),
65 min: MdTableCell::new(f64::MAX, None),
66 avg: MdTableCell::default(),
67 count: 0,
68 phase: None,
69 }
70 }
71 pub fn push(&mut self, value: f64) {
72 self.sum.val += value;
73 self.count += 1;
74 if value > self.max.val {
75 self.max.val = value;
76 }
77 if value < self.min.val {
78 self.min.val = value;
79 }
80 }
81
82 pub fn finalize(&mut self) {
83 assert!(self.count != 0);
84 self.avg.val = self.sum.val / self.count as f64;
85 }
86
87 pub fn set_diff(&mut self, prev: &Self) {
88 self.sum.diff = Some(self.sum.val - prev.sum.val);
89 self.max.diff = Some(self.max.val - prev.max.val);
90 self.min.diff = Some(self.min.val - prev.min.val);
91 self.avg.diff = Some(self.avg.val - prev.avg.val);
92 }
93}
94
95impl GroupedMetrics {
96 pub fn new(db: &MetricDb, group_label_name: &str) -> Result<Self> {
97 let mut by_group = HashMap::<String, MetricsByName>::new();
98 let mut ungrouped = MetricsByName::new();
99 for (labels, metrics) in db.flat_dict.iter() {
100 let group_name = labels.get(group_label_name);
101 if let Some(group_name) = group_name {
102 let group_entry = by_group.entry(group_name.to_string()).or_default();
103 let mut labels = labels.clone();
104 labels.remove(group_label_name);
105 for metric in metrics {
106 group_entry
107 .entry(metric.name.clone())
108 .or_default()
109 .push((metric.value, labels.clone()));
110 }
111 } else {
112 for metric in metrics {
113 ungrouped
114 .entry(metric.name.clone())
115 .or_default()
116 .push((metric.value, labels.clone()));
117 }
118 }
119 }
120 Ok(Self {
121 by_group,
122 ungrouped,
123 })
124 }
125
126 fn validate_instruction_counts(group_summaries: &HashMap<MetricName, Stats>) {
128 let e1_insns = group_summaries.get(EXECUTE_E1_INSNS_LABEL);
129 let metered_insns = group_summaries.get(EXECUTE_METERED_INSNS_LABEL);
130 let preflight_insns = group_summaries.get(EXECUTE_PREFLIGHT_INSNS_LABEL);
131
132 if let (Some(e1_insns), Some(preflight_insns)) = (e1_insns, preflight_insns) {
133 assert_eq!(e1_insns.sum.val as u64, preflight_insns.sum.val as u64);
134 }
135 if let (Some(e1_insns), Some(metered_insns)) = (e1_insns, metered_insns) {
136 assert_eq!(e1_insns.sum.val as u64, metered_insns.sum.val as u64);
137 }
138 if let (Some(metered_insns), Some(preflight_insns)) = (metered_insns, preflight_insns) {
139 assert_eq!(metered_insns.sum.val as u64, preflight_insns.sum.val as u64);
140 }
141 }
142
143 pub fn aggregate(&self, num_parallel: usize) -> AggregateMetrics {
144 let by_group: HashMap<String, _> = self
145 .by_group
146 .iter()
147 .map(|(group_name, metrics)| {
148 let group_summaries: HashMap<MetricName, Stats> = metrics
149 .iter()
150 .map(|(metric_name, metrics)| {
151 let mut summary = Stats::new();
152 for (value, labels) in metrics {
153 summary.push(*value);
154 if summary.phase.is_none() {
156 if let Some(phase) = labels.get("phase") {
157 summary.phase = Some(phase.to_string());
158 }
159 }
160 }
161 summary.finalize();
162 (metric_name.clone(), summary)
163 })
164 .collect();
165
166 if !group_name.contains("keygen") {
167 Self::validate_instruction_counts(&group_summaries);
168 }
169
170 (group_name.clone(), group_summaries)
171 })
172 .collect();
173 let mut metrics = AggregateMetrics {
174 by_group,
175 ..Default::default()
176 };
177 metrics.compute_total();
178 metrics.bounded_par_by_group = self
179 .compute_bounded_par_times(num_parallel, &metrics.by_group)
180 .into_iter()
181 .map(|(k, v)| (k, MdTableCell::new(v, Some(0.0))))
182 .collect();
183
184 metrics
185 }
186
187 fn compute_bounded_par_times(
189 &self,
190 num_parallel: usize,
191 stats_by_group: &HashMap<String, HashMap<MetricName, Stats>>,
192 ) -> HashMap<String, f64> {
193 let mut per_group = HashMap::new();
194
195 for (group_name, metrics) in &self.by_group {
196 if group_name.contains("keygen") {
197 continue;
198 }
199
200 let mut group_time = 0.0;
201
202 if is_app_proof_group(group_name) {
204 if let Some(stats) = stats_by_group.get(group_name) {
205 if let Some(metered) = stats.get(EXECUTE_METERED_TIME_LABEL) {
206 group_time += metered.avg.val / 1000.0;
207 }
208 if let Some(e1) = stats.get(EXECUTE_E1_TIME_LABEL) {
209 group_time += e1.avg.val / 1000.0;
210 }
211 }
212 }
213
214 if let Some(proof_times) = metrics.get(PROOF_TIME_LABEL) {
216 let times_s: Vec<f64> = proof_times.iter().map(|(ms, _)| ms / 1000.0).collect();
217 group_time += schedule_parallel(×_s, num_parallel);
218 }
219
220 per_group.insert(group_name.clone(), group_time);
221 }
222
223 per_group
224 }
225}
226
227fn schedule_parallel(proof_times: &[f64], num_parallel: usize) -> f64 {
229 if proof_times.is_empty() || num_parallel == 0 {
230 return 0.0;
231 }
232
233 let mut slot_times = vec![0.0_f64; num_parallel];
234 for (i, duration) in proof_times.iter().enumerate() {
235 slot_times[i % num_parallel] += duration;
236 }
237 slot_times.iter().cloned().fold(0.0_f64, f64::max)
238}
239
240fn is_app_proof_group(name: &str) -> bool {
241 name != "leaf"
242 && name != "root"
243 && name != "halo2_outer"
244 && name != "halo2_wrapper"
245 && !name.starts_with("internal")
246}
247
248pub(crate) fn group_weight(name: &str) -> usize {
250 let label_prefix = ["leaf", "internal", "root", "halo2_outer", "halo2_wrapper"];
251 if name.contains("keygen") {
252 return label_prefix.len() + 1;
253 }
254 for (i, prefix) in label_prefix.iter().enumerate().rev() {
255 if name.starts_with(prefix) {
256 return i + 1;
257 }
258 }
259 0
260}
261
262impl AggregateMetrics {
263 pub fn compute_total(&mut self) {
264 let mut total_proof_time = MdTableCell::new(0.0, Some(0.0));
265 let mut total_par_proof_time = MdTableCell::new(0.0, Some(0.0));
266 for (group_name, metrics) in &self.by_group {
267 let stats = metrics.get(PROOF_TIME_LABEL);
268 let execute_metered_stats = metrics.get(EXECUTE_METERED_TIME_LABEL);
269 let execute_e1_stats = metrics.get(EXECUTE_E1_TIME_LABEL);
270 if stats.is_none() {
271 continue;
272 }
273 let stats = stats.unwrap_or_else(|| {
274 panic!("Missing proof time statistics for group '{group_name}'")
275 });
276 let mut sum = stats.sum;
277 let mut max = stats.max;
278 sum.val /= 1000.0;
280 max.val /= 1000.0;
281 if let Some(diff) = &mut sum.diff {
282 *diff /= 1000.0;
283 }
284 if let Some(diff) = &mut max.diff {
285 *diff /= 1000.0;
286 }
287 if !group_name.contains("keygen") {
288 total_proof_time.val += sum.val;
290 *total_proof_time
291 .diff
292 .as_mut()
293 .expect("total_proof_time.diff should be initialized") +=
294 sum.diff.unwrap_or(0.0);
295 total_par_proof_time.val += max.val;
296 *total_par_proof_time
297 .diff
298 .as_mut()
299 .expect("total_par_proof_time.diff should be initialized") +=
300 max.diff.unwrap_or(0.0);
301
302 if is_app_proof_group(group_name) {
304 if let Some(execute_metered_stats) = execute_metered_stats {
305 total_proof_time.val += execute_metered_stats.avg.val / 1000.0;
309 total_par_proof_time.val += execute_metered_stats.avg.val / 1000.0;
310 if let Some(diff) = execute_metered_stats.avg.diff {
311 *total_proof_time
312 .diff
313 .as_mut()
314 .expect("total_proof_time.diff should be initialized") +=
315 diff / 1000.0;
316 *total_par_proof_time
317 .diff
318 .as_mut()
319 .expect("total_par_proof_time.diff should be initialized") +=
320 diff / 1000.0;
321 }
322 }
323
324 if let Some(execute_e1_stats) = execute_e1_stats {
325 total_proof_time.val += execute_e1_stats.avg.val / 1000.0;
326 total_par_proof_time.val += execute_e1_stats.avg.val / 1000.0;
327 if let Some(diff) = execute_e1_stats.avg.diff {
328 *total_proof_time
329 .diff
330 .as_mut()
331 .expect("total_proof_time.diff should be initialized") +=
332 diff / 1000.0;
333 *total_par_proof_time
334 .diff
335 .as_mut()
336 .expect("total_par_proof_time.diff should be initialized") +=
337 diff / 1000.0;
338 }
339 }
340 }
341 }
342 }
343 self.total_proof_time = total_proof_time;
344 self.total_par_proof_time = total_par_proof_time;
345 }
346
347 pub fn set_diff(&mut self, prev: &Self) {
348 for (group_name, metrics) in self.by_group.iter_mut() {
349 if let Some(prev_metrics) = prev.by_group.get(group_name) {
350 for (metric_name, stats) in metrics.iter_mut() {
351 if let Some(prev_stats) = prev_metrics.get(metric_name) {
352 stats.set_diff(prev_stats);
353 }
354 }
355 }
356 }
357 for (group_name, bounded) in self.bounded_par_by_group.iter_mut() {
358 if let Some(prev_bounded) = prev.bounded_par_by_group.get(group_name) {
359 bounded.diff = Some(bounded.val - prev_bounded.val);
360 }
361 }
362 self.compute_total();
363 }
364
365 pub fn to_vec(&self) -> Vec<(String, HashMap<MetricName, Stats>)> {
366 let mut group_names: Vec<_> = self.by_group.keys().collect();
367 group_names.sort_by(|a, b| {
368 let a_wt = group_weight(a);
369 let b_wt = group_weight(b);
370 if a_wt == b_wt {
371 a.cmp(b)
372 } else {
373 a_wt.cmp(&b_wt)
374 }
375 });
376 group_names
377 .into_iter()
378 .map(|group_name| {
379 let key = group_name.clone();
380 let value = self
381 .by_group
382 .get(group_name)
383 .unwrap_or_else(|| panic!("Group '{group_name}' should exist in by_group map"))
384 .clone();
385 (key, value)
386 })
387 .collect()
388 }
389
390 pub fn to_bencher_metrics(&self) -> BencherAggregateMetrics {
391 let by_group = self
392 .by_group
393 .iter()
394 .map(|(group_name, metrics)| {
395 let metrics = metrics
396 .iter()
397 .filter(|(_, stats)| stats.avg.val.is_finite() && stats.sum.val.is_finite())
398 .flat_map(|(metric_name, stats)| {
399 [
400 (format!("{metric_name}::sum"), stats.sum.into()),
401 (
402 metric_name.clone(),
403 BencherValue {
404 value: stats.avg.val,
405 lower_value: Some(stats.min.val),
406 upper_value: Some(stats.max.val),
407 },
408 ),
409 ]
410 })
411 .collect();
412 (group_name.clone(), metrics)
413 })
414 .collect();
415 let total_proof_time = self.total_proof_time.into();
416 let total_par_proof_time = self.total_par_proof_time.into();
417 BencherAggregateMetrics {
418 by_group,
419 total_proof_time,
420 total_par_proof_time,
421 }
422 }
423
424 pub fn write_markdown(
425 &self,
426 writer: &mut impl Write,
427 metric_names: &[&str],
428 num_parallel: usize,
429 ) -> Result<()> {
430 self.write_summary_markdown(writer, num_parallel)?;
431 writeln!(writer)?;
432
433 let metric_names = metric_names.to_vec();
434 for (group_name, summaries) in self.to_vec() {
435 if group_name.contains("keygen") {
436 continue;
437 }
438
439 let names: Vec<&str> = if metric_names.is_empty() {
440 summaries.keys().map(|s| s.as_str()).collect()
441 } else {
442 metric_names.clone()
443 };
444 let names: Vec<&str> = names
445 .into_iter()
446 .filter(|name| summaries.contains_key(*name))
447 .collect();
448 if names.is_empty() {
449 continue;
450 }
451
452 writeln!(writer, "| {group_name} |||||")?;
453 writeln!(writer, "|:---|---:|---:|---:|---:|")?;
454 writeln!(writer, "|metric|avg|sum|max|min|")?;
455
456 let get_phase = |name: &str| -> Option<&str> {
458 summaries.get(name).and_then(|stats| stats.phase.as_deref())
459 };
460
461 let mut phases: Vec<Option<&str>> = vec![None];
463 for name in &names {
464 if let Some(phase) = get_phase(name) {
465 if !phases.contains(&Some(phase)) {
466 phases.push(Some(phase));
467 }
468 }
469 }
470
471 for phase in &phases {
473 let phase_names: Vec<&str> = names
474 .iter()
475 .filter(|name| get_phase(name) == *phase)
476 .copied()
477 .collect();
478
479 if phase_names.is_empty() {
480 continue;
481 }
482
483 if let Some(p) = phase {
485 let label = p[0..1].to_uppercase() + &p[1..]; writeln!(writer, "| __{label}__ |||||")?;
487 }
488
489 for metric_name in &phase_names {
490 self.write_metric_row(writer, &group_name, &summaries, metric_name)?;
491 }
492 }
493
494 writeln!(writer)?;
495 }
496 writeln!(writer)?;
497
498 Ok(())
499 }
500
501 fn write_metric_row(
502 &self,
503 writer: &mut impl Write,
504 group_name: &str,
505 summaries: &HashMap<MetricName, Stats>,
506 metric_name: &str,
507 ) -> Result<()> {
508 let summary = summaries.get(metric_name);
509 if let Some(summary) = summary {
510 if (metric_name == EXECUTE_METERED_TIME_LABEL
513 || metric_name == EXECUTE_METERED_INSNS_LABEL)
514 && is_app_proof_group(group_name)
515 {
516 writeln!(
517 writer,
518 "| `{:<20}` | {:<10} | {:<10} | {:<10} | {:<10} |",
519 metric_name, summary.avg, "-", "-", "-",
520 )?;
521 } else if metric_name == EXECUTE_E1_INSN_MI_S_LABEL
522 || metric_name == EXECUTE_PREFLIGHT_INSN_MI_S_LABEL
523 || metric_name == EXECUTE_METERED_INSN_MI_S_LABEL
524 {
525 writeln!(
527 writer,
528 "| `{:<20}` | {:<10} | {:<10} | {:<10} | {:<10} |",
529 metric_name, summary.avg, "-", summary.max, summary.min,
530 )?;
531 } else {
532 writeln!(
533 writer,
534 "| `{:<20}` | {:<10} | {:<10} | {:<10} | {:<10} |",
535 metric_name, summary.avg, summary.sum, summary.max, summary.min,
536 )?;
537 }
538 }
539 Ok(())
540 }
541
542 fn write_summary_markdown(&self, writer: &mut impl Write, num_parallel: usize) -> Result<()> {
543 writeln!(
544 writer,
545 "| Summary | Proof Time (s) | Parallel Proof Time (s) | Parallel Proof Time ({} provers) (s) |",
546 num_parallel
547 )?;
548 writeln!(writer, "|:---|---:|---:|---:|")?;
549 let mut rows = Vec::new();
550 for (group_name, summaries) in self.to_vec() {
551 if group_name.contains("keygen") {
552 continue;
553 }
554 let stats = summaries.get(PROOF_TIME_LABEL);
555 if stats.is_none() {
556 continue;
557 }
558 let stats = stats.unwrap_or_else(|| {
559 panic!("Missing proof time statistics for group '{group_name}'")
560 });
561 let mut sum = stats.sum;
562 let mut max = stats.max;
563 sum.val /= 1000.0;
565 max.val /= 1000.0;
566 if let Some(diff) = &mut sum.diff {
567 *diff /= 1000.0;
568 }
569 if let Some(diff) = &mut max.diff {
570 *diff /= 1000.0;
571 }
572 if is_app_proof_group(&group_name) {
574 if let Some(metered) = summaries.get(EXECUTE_METERED_TIME_LABEL) {
575 sum.val += metered.avg.val / 1000.0;
576 max.val += metered.avg.val / 1000.0;
577 }
578 if let Some(e1) = summaries.get(EXECUTE_E1_TIME_LABEL) {
579 sum.val += e1.avg.val / 1000.0;
580 max.val += e1.avg.val / 1000.0;
581 }
582 }
583 rows.push((group_name, sum, max));
584 }
585 let mut total_bounded = MdTableCell::new(0.0, None);
586 for cell in self.bounded_par_by_group.values() {
587 total_bounded.val += cell.val;
588 if let Some(diff) = cell.diff {
589 *total_bounded.diff.get_or_insert(0.0) += diff;
590 }
591 }
592 writeln!(
593 writer,
594 "| Total | {} | {} | {} |",
595 self.total_proof_time, self.total_par_proof_time, total_bounded
596 )?;
597 for (group_name, proof_time, par_proof_time) in rows {
598 let bounded = self
599 .bounded_par_by_group
600 .get(&group_name)
601 .map(|v| v.to_string())
602 .unwrap_or_else(|| "-".to_string());
603 writeln!(
604 writer,
605 "| {group_name} | {proof_time} | {par_proof_time} | {bounded} |"
606 )?;
607 }
608 writeln!(writer)?;
609 Ok(())
610 }
611
612 pub fn name(&self) -> Option<String> {
613 let name = self
615 .by_group
616 .keys()
617 .find(|k| group_weight(k) == 0)
618 .or_else(|| self.by_group.keys().next())
619 .cloned();
620 if name.is_none() {
621 eprintln!("Warning: no group found to determine app name; by_group is empty");
622 }
623 name
624 }
625}
626
627impl BenchmarkOutput {
628 pub fn insert(&mut self, name: &str, metrics: BencherAggregateMetrics) {
629 for (group_name, metrics) in metrics.by_group {
630 self.by_name
631 .entry(format!("{name}::{group_name}"))
632 .or_default()
633 .extend(metrics);
634 }
635 if let Some(e) = self.by_name.insert(
636 name.to_owned(),
637 HashMap::from_iter([
638 ("total_proof_time".to_owned(), metrics.total_proof_time),
639 (
640 "total_par_proof_time".to_owned(),
641 metrics.total_par_proof_time,
642 ),
643 ]),
644 ) {
645 panic!("Duplicate metric: {e:?}");
646 }
647 }
648}
649
650pub const PROOF_TIME_LABEL: &str = "total_proof_time_ms";
651pub const MAIN_CELLS_USED_LABEL: &str = "main_cells_used";
652pub const TOTAL_CELLS_USED_LABEL: &str = "total_cells_used";
653pub const EXECUTE_E1_INSNS_LABEL: &str = "execute_e1_insns";
654pub const EXECUTE_METERED_INSNS_LABEL: &str = "execute_metered_insns";
655pub const EXECUTE_PREFLIGHT_INSNS_LABEL: &str = "execute_preflight_insns";
656pub const EXECUTE_E1_TIME_LABEL: &str = "execute_e1_time_ms";
657pub const EXECUTE_E1_INSN_MI_S_LABEL: &str = "execute_e1_insn_mi/s";
658pub const EXECUTE_METERED_TIME_LABEL: &str = "execute_metered_time_ms";
659pub const EXECUTE_METERED_INSN_MI_S_LABEL: &str = "execute_metered_insn_mi/s";
660pub const EXECUTE_PREFLIGHT_TIME_LABEL: &str = "execute_preflight_time_ms";
661pub const EXECUTE_PREFLIGHT_INSN_MI_S_LABEL: &str = "execute_preflight_insn_mi/s";
662pub const TRACE_GEN_TIME_LABEL: &str = "trace_gen_time_ms";
663pub const GENERATE_BLOB_TIME_LABEL: &str = "generate_blob_total_time_ms";
664pub const MEM_FIN_TIME_LABEL: &str = "memory_finalize_time_ms";
665pub const BOUNDARY_FIN_TIME_LABEL: &str = "boundary_finalize_time_ms";
666pub const MERKLE_FIN_TIME_LABEL: &str = "merkle_finalize_time_ms";
667pub const PROVE_EXCL_TRACE_TIME_LABEL: &str = "stark_prove_excluding_trace_time_ms";
668
669pub const HALO2_VERIFIER_K_LABEL: &str = "halo2_verifier_k";
670pub const HALO2_WRAPPER_K_LABEL: &str = "halo2_wrapper_k";
671
672pub const AGGREGATED_METRIC_NAMES: &[&str] = &[
673 PROOF_TIME_LABEL,
674 MAIN_CELLS_USED_LABEL,
675 TOTAL_CELLS_USED_LABEL,
676 EXECUTE_E1_TIME_LABEL,
677 EXECUTE_E1_INSN_MI_S_LABEL,
678 EXECUTE_METERED_TIME_LABEL,
679 EXECUTE_METERED_INSNS_LABEL,
680 EXECUTE_METERED_INSN_MI_S_LABEL,
681 EXECUTE_PREFLIGHT_INSNS_LABEL,
682 EXECUTE_PREFLIGHT_TIME_LABEL,
683 EXECUTE_PREFLIGHT_INSN_MI_S_LABEL,
684 TRACE_GEN_TIME_LABEL,
685 GENERATE_BLOB_TIME_LABEL,
686 MEM_FIN_TIME_LABEL,
687 BOUNDARY_FIN_TIME_LABEL,
688 MERKLE_FIN_TIME_LABEL,
689 PROVE_EXCL_TRACE_TIME_LABEL,
690 "prover.main_trace_commit_time_ms",
691 "prover.rap_constraints_time_ms",
692 "prover.openings_time_ms",
693 "prover.rap_constraints.logup_gkr_time_ms",
694 "prover.rap_constraints.round0_time_ms",
695 "prover.rap_constraints.mle_rounds_time_ms",
696 "prover.openings.stacked_reduction_time_ms",
697 "prover.openings.stacked_reduction.round0_time_ms",
698 "prover.openings.stacked_reduction.mle_rounds_time_ms",
699 "prover.openings.whir_time_ms",
700 HALO2_VERIFIER_K_LABEL,
701 HALO2_WRAPPER_K_LABEL,
702];