openvm_prof/
lib.rs

1use std::{collections::HashMap, fs::File, path::Path};
2
3use aggregate::{PROOF_TIME_LABEL, PROVE_EXCL_TRACE_TIME_LABEL, TRACE_GEN_TIME_LABEL};
4use eyre::Result;
5use memmap2::Mmap;
6
7use crate::{
8    aggregate::{EXECUTE_METERED_TIME_LABEL, EXECUTE_PREFLIGHT_TIME_LABEL},
9    types::{Labels, Metric, MetricDb, MetricsFile},
10};
11
12pub mod aggregate;
13pub mod instruction_count;
14pub mod summary;
15pub mod types;
16
17impl MetricDb {
18    pub fn new(metrics_file: impl AsRef<Path>) -> Result<Self> {
19        let file = File::open(metrics_file)?;
20        // SAFETY: File is read-only mapped. File will not be modified by other
21        // processes during the mapping's lifetime.
22        let mmap = unsafe { Mmap::map(&file)? };
23        let metrics: MetricsFile = serde_json::from_slice(&mmap)?;
24
25        let mut db = MetricDb::default();
26
27        // Process counters
28        for entry in metrics.counter {
29            if entry.value == 0.0 {
30                continue;
31            }
32            let labels = Labels::from(entry.labels);
33            db.add_to_flat_dict(labels, entry.metric, entry.value);
34        }
35
36        // Process gauges
37        for entry in metrics.gauge {
38            let labels = Labels::from(entry.labels);
39            db.add_to_flat_dict(labels, entry.metric, entry.value);
40        }
41
42        db.apply_aggregations();
43        db.separate_by_label_types();
44
45        Ok(db)
46    }
47
48    // Currently hardcoding aggregations
49    pub fn apply_aggregations(&mut self) {
50        for metrics in self.flat_dict.values_mut() {
51            let get = |key: &str| metrics.iter().find(|m| m.name == key).map(|m| m.value);
52            let total_proof_time = get(PROOF_TIME_LABEL);
53            if total_proof_time.is_some() {
54                // We have instrumented total_proof_time_ms
55                continue;
56            }
57            // otherwise, calculate it from sub-components
58            let execute_metered_time = get(EXECUTE_METERED_TIME_LABEL);
59            let execute_preflight_time = get(EXECUTE_PREFLIGHT_TIME_LABEL);
60            let trace_gen_time = get(TRACE_GEN_TIME_LABEL);
61            let prove_excl_trace_time = get(PROVE_EXCL_TRACE_TIME_LABEL);
62            if let (
63                Some(execute_preflight_time),
64                Some(trace_gen_time),
65                Some(prove_excl_trace_time),
66            ) = (
67                execute_preflight_time,
68                trace_gen_time,
69                prove_excl_trace_time,
70            ) {
71                let total_time = execute_metered_time.unwrap_or(0.0)
72                    + execute_preflight_time
73                    + trace_gen_time
74                    + prove_excl_trace_time;
75                metrics.push(Metric::new(PROOF_TIME_LABEL.to_string(), total_time));
76            }
77        }
78    }
79
80    pub fn add_to_flat_dict(&mut self, labels: Labels, metric: String, value: f64) {
81        self.flat_dict
82            .entry(labels)
83            .or_default()
84            .push(Metric::new(metric, value));
85    }
86
87    // Custom sorting function that ensures 'group' comes first.
88    // Other keys are sorted alphabetically.
89    pub fn custom_sort_label_keys(label_keys: &mut [String]) {
90        // Prioritize 'group' by giving it the lowest possible sort value
91        label_keys.sort_by_key(|key| {
92            if key == "group" {
93                (0, key.clone()) // Lowest priority for 'group'
94            } else {
95                (1, key.clone()) // Normal priority for other keys
96            }
97        });
98    }
99
100    pub fn separate_by_label_types(&mut self) {
101        self.dict_by_label_types.clear();
102
103        for (labels, metrics) in &self.flat_dict {
104            // Get sorted label keys
105            let mut label_keys: Vec<String> = labels.0.iter().map(|(key, _)| key.clone()).collect();
106            Self::custom_sort_label_keys(&mut label_keys);
107
108            // Create label_values based on sorted keys
109            let label_dict: HashMap<String, String> = labels.0.iter().cloned().collect();
110
111            let label_values: Vec<String> = label_keys
112                .iter()
113                .map(|key| {
114                    label_dict
115                        .get(key)
116                        .unwrap_or_else(|| panic!("Label key '{key}' should exist in label_dict"))
117                        .clone()
118                })
119                .collect();
120
121            // Remove cycle_tracker_span and dsl_ir if present as they are too long for markdown and
122            // visualized in flamegraphs
123            let mut keys = label_keys.clone();
124            let mut values = label_values.clone();
125
126            // Remove cycle_tracker_span if present
127            if let Some(index) = keys.iter().position(|k| k == "cycle_tracker_span") {
128                keys.remove(index);
129                values.remove(index);
130            }
131
132            // Remove dsl_ir if present
133            if let Some(index) = keys.iter().position(|k| k == "dsl_ir") {
134                keys.remove(index);
135                values.remove(index);
136            }
137
138            let (final_label_keys, final_label_values) = (keys, values);
139
140            // Add to dict_by_label_types, combining metrics with same name by summing values
141            let entry = self
142                .dict_by_label_types
143                .entry(final_label_keys)
144                .or_default()
145                .entry(final_label_values)
146                .or_default();
147
148            for metric in metrics.clone() {
149                if let Some(existing_metric) = entry.iter_mut().find(|m| m.name == metric.name) {
150                    // Sum the values for metrics with the same name
151                    existing_metric.value += metric.value;
152                } else {
153                    // Add new metric if no existing one with same name
154                    entry.push(metric);
155                }
156            }
157        }
158    }
159
160    /// Generate an SVG chart for GPU memory usage over modules.
161    /// Returns a tuple of (svg_string, markdown_table) where the SVG can be embedded
162    /// directly in HTML/markdown and the table provides per-module statistics.
163    pub fn generate_gpu_memory_chart(&self) -> Option<(String, String)> {
164        // (timestamp, current_gb, local_peak_gb, reserved_gb)
165        let mut data: Vec<(f64, f64, f64, f64)> = Vec::new();
166        // module -> [(local_peak_gb, context_label)]
167        let mut module_stats: HashMap<String, Vec<(f64, String)>> = HashMap::new();
168
169        for (label_keys, metrics_dict) in &self.dict_by_label_types {
170            let module_idx = match label_keys.iter().position(|k| k == "module") {
171                Some(idx) => idx,
172                None => continue,
173            };
174
175            for (label_values, metrics) in metrics_dict {
176                let get = |name: &str| metrics.iter().find(|m| m.name == name).map(|m| m.value);
177                let ts = get("gpu_mem.timestamp_ms");
178                let current = get("gpu_mem.current_bytes");
179                let local_peak = get("gpu_mem.local_peak_bytes");
180                let reserved = get("gpu_mem.reserved_bytes");
181
182                if let (Some(ts), Some(current), Some(local_peak), Some(reserved)) =
183                    (ts, current, local_peak, reserved)
184                {
185                    let current_gb = current / f64::from(1 << 30);
186                    let local_peak_gb = local_peak / f64::from(1 << 30);
187                    let reserved_gb = reserved / f64::from(1 << 30);
188                    data.push((ts, current_gb, local_peak_gb, reserved_gb));
189
190                    let module_name = label_values.get(module_idx).cloned().unwrap_or_default();
191                    let context_label: String = label_keys
192                        .iter()
193                        .zip(label_values.iter())
194                        .filter(|(k, _)| *k != "module" && *k != "block_number")
195                        .map(|(_, v)| v.as_str())
196                        .collect::<Vec<_>>()
197                        .join(".");
198
199                    module_stats
200                        .entry(module_name)
201                        .or_default()
202                        .push((local_peak_gb, context_label));
203                }
204            }
205        }
206
207        if data.is_empty() {
208            return None;
209        }
210
211        data.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
212
213        // Downsample if too many points (SVG handles ~1000 points well)
214        let max_points = 1000;
215        let data = if data.len() > max_points {
216            let step = data.len() / max_points;
217            data.into_iter()
218                .enumerate()
219                .filter(|(i, _)| i % step == 0)
220                .map(|(_, d)| d)
221                .collect::<Vec<_>>()
222        } else {
223            data
224        };
225
226        let svg = Self::render_gpu_memory_svg(&data);
227
228        // Per-module stats table
229        let mut table = String::new();
230        table.push_str("| Module | Max (GB) | Max At |\n");
231        table.push_str("| --- | ---: | --- |\n");
232
233        let mut module_rows: Vec<_> = module_stats
234            .iter()
235            .map(|(module, entries)| {
236                let (max_tracked, max_at) = entries
237                    .iter()
238                    .max_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
239                    .map(|(t, label)| (*t, label.as_str()))
240                    .unwrap_or((0.0, ""));
241                (module, max_tracked, max_at)
242            })
243            .collect();
244        module_rows.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
245
246        for (module, max_tracked, max_at) in module_rows {
247            table.push_str(&format!(
248                "| {} | {:.2} | {} |\n",
249                module, max_tracked, max_at
250            ));
251        }
252
253        Some((svg, table))
254    }
255
256    /// Render GPU memory data as an SVG chart
257    fn render_gpu_memory_svg(data: &[(f64, f64, f64, f64)]) -> String {
258        // Chart dimensions
259        let width = 800.0_f64;
260        let height = 400.0_f64;
261        let margin_left = 60.0_f64;
262        let margin_right = 20.0_f64;
263        let margin_top = 40.0_f64;
264        let margin_bottom = 50.0_f64;
265
266        let plot_width = width - margin_left - margin_right;
267        let plot_height = height - margin_top - margin_bottom;
268
269        // Calculate data ranges
270        let ts_min = data
271            .iter()
272            .map(|(ts, _, _, _)| *ts)
273            .fold(f64::MAX, f64::min);
274        let ts_max = data
275            .iter()
276            .map(|(ts, _, _, _)| *ts)
277            .fold(f64::MIN, f64::max);
278        let ts_range = if (ts_max - ts_min).abs() < f64::EPSILON {
279            1.0
280        } else {
281            ts_max - ts_min
282        };
283
284        let max_current = data.iter().map(|(_, c, _, _)| *c).fold(0.0_f64, f64::max);
285        let max_local_peak = data.iter().map(|(_, _, lp, _)| *lp).fold(0.0_f64, f64::max);
286        let max_reserved = data.iter().map(|(_, _, _, r)| *r).fold(0.0_f64, f64::max);
287        let y_max = max_current.max(max_local_peak).max(max_reserved) * 1.1;
288        let y_max = if y_max < f64::EPSILON { 1.0 } else { y_max };
289
290        // Helper to convert data coordinates to SVG coordinates
291        let to_svg_x = |ts: f64| -> f64 { margin_left + (ts - ts_min) / ts_range * plot_width };
292        let to_svg_y = |val: f64| -> f64 { margin_top + plot_height - (val / y_max) * plot_height };
293
294        // Build path strings for each series
295        let build_path = |series: &[(f64, f64)]| -> String {
296            series
297                .iter()
298                .enumerate()
299                .map(|(i, (x, y))| {
300                    let cmd = if i == 0 { "M" } else { "L" };
301                    format!("{}{:.1},{:.1}", cmd, x, y)
302                })
303                .collect::<Vec<_>>()
304                .join(" ")
305        };
306
307        let current_points: Vec<(f64, f64)> = data
308            .iter()
309            .map(|(ts, current, _, _)| (to_svg_x(*ts), to_svg_y(*current)))
310            .collect();
311        let local_peak_points: Vec<(f64, f64)> = data
312            .iter()
313            .map(|(ts, _, lp, _)| (to_svg_x(*ts), to_svg_y(*lp)))
314            .collect();
315        let reserved_points: Vec<(f64, f64)> = data
316            .iter()
317            .map(|(ts, _, _, r)| (to_svg_x(*ts), to_svg_y(*r)))
318            .collect();
319
320        let current_path = build_path(&current_points);
321        let local_peak_path = build_path(&local_peak_points);
322        let reserved_path = build_path(&reserved_points);
323
324        // Generate Y-axis ticks (5-6 ticks)
325        let y_tick_count = 5;
326        let y_tick_interval = y_max / y_tick_count as f64;
327        let y_ticks: Vec<f64> = (0..=y_tick_count)
328            .map(|i| i as f64 * y_tick_interval)
329            .collect();
330
331        // Generate gridlines and tick labels
332        let mut gridlines = String::new();
333        let mut tick_labels = String::new();
334        for y_val in &y_ticks {
335            let y_pos = to_svg_y(*y_val);
336            gridlines.push_str(&format!(
337                "<line x1=\"{:.1}\" y1=\"{:.1}\" x2=\"{:.1}\" y2=\"{:.1}\" stroke=\"#e5e7eb\" stroke-width=\"1\"/>",
338                margin_left,
339                y_pos,
340                width - margin_right,
341                y_pos
342            ));
343            gridlines.push('\n');
344            tick_labels.push_str(&format!(
345                "<text x=\"{:.1}\" y=\"{:.1}\" text-anchor=\"end\" font-size=\"12\" fill=\"#6b7280\">{:.1}</text>",
346                margin_left - 8.0,
347                y_pos + 4.0,
348                y_val
349            ));
350            tick_labels.push('\n');
351        }
352
353        // X-axis time labels (show duration)
354        let duration_sec = ts_range / 1000.0;
355        let duration_label = if duration_sec < 60.0 {
356            format!("{:.1}s", duration_sec)
357        } else if duration_sec < 3600.0 {
358            format!("{:.1}m", duration_sec / 60.0)
359        } else {
360            format!("{:.1}h", duration_sec / 3600.0)
361        };
362
363        // Colors
364        let color_current = "#2563eb"; // blue
365        let color_local_peak = "#16a34a"; // green
366        let color_reserved = "#dc2626"; // red
367
368        format!(
369            r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" width="{width}" height="{height}">
370  <style>
371    .title {{ font: bold 16px sans-serif; }}
372    .axis-label {{ font: 12px sans-serif; fill: #6b7280; }}
373    .legend-text {{ font: 12px sans-serif; }}
374  </style>
375
376  <!-- Background -->
377  <rect width="{width}" height="{height}" fill="white"/>
378
379  <!-- Title -->
380  <text x="{title_x}" y="24" text-anchor="middle" class="title">GPU Memory Usage</text>
381
382  <!-- Gridlines -->
383  {gridlines}
384
385  <!-- Y-axis tick labels -->
386  {tick_labels}
387
388  <!-- Y-axis label -->
389  <text x="16" y="{y_label_y}" transform="rotate(-90, 16, {y_label_y})" text-anchor="middle" class="axis-label">Memory (GB)</text>
390
391  <!-- X-axis -->
392  <line x1="{margin_left}" y1="{x_axis_y}" x2="{x_axis_end}" y2="{x_axis_y}" stroke="#9ca3af" stroke-width="1"/>
393  <text x="{margin_left}" y="{x_label_y}" text-anchor="start" class="axis-label">0</text>
394  <text x="{x_axis_end}" y="{x_label_y}" text-anchor="end" class="axis-label">{duration_label}</text>
395
396  <!-- Data lines -->
397  <path d="{reserved_path}" fill="none" stroke="{color_reserved}" stroke-width="1.5" stroke-opacity="0.8"/>
398  <path d="{local_peak_path}" fill="none" stroke="{color_local_peak}" stroke-width="1.5" stroke-opacity="0.8"/>
399  <path d="{current_path}" fill="none" stroke="{color_current}" stroke-width="2"/>
400
401  <!-- Legend -->
402  <g transform="translate({legend_x}, {legend_y})">
403    <rect x="0" y="0" width="180" height="70" fill="white" fill-opacity="0.9" stroke="#e5e7eb" rx="4"/>
404    <line x1="10" y1="18" x2="30" y2="18" stroke="{color_current}" stroke-width="2"/>
405    <text x="38" y="22" class="legend-text">Current</text>
406    <line x1="10" y1="38" x2="30" y2="38" stroke="{color_local_peak}" stroke-width="1.5"/>
407    <text x="38" y="42" class="legend-text">Local Peak</text>
408    <line x1="10" y1="58" x2="30" y2="58" stroke="{color_reserved}" stroke-width="1.5"/>
409    <text x="38" y="62" class="legend-text">Reserved (Pool)</text>
410  </g>
411</svg>"##,
412            width = width,
413            height = height,
414            title_x = width / 2.0,
415            gridlines = gridlines,
416            tick_labels = tick_labels,
417            y_label_y = margin_top + plot_height / 2.0,
418            margin_left = margin_left,
419            x_axis_y = margin_top + plot_height,
420            x_axis_end = width - margin_right,
421            x_label_y = margin_top + plot_height + 20.0,
422            duration_label = duration_label,
423            reserved_path = reserved_path,
424            local_peak_path = local_peak_path,
425            current_path = current_path,
426            color_current = color_current,
427            color_local_peak = color_local_peak,
428            color_reserved = color_reserved,
429            legend_x = width - margin_right - 190.0,
430            legend_y = margin_top + 10.0,
431        )
432    }
433
434    pub fn sum_metric_grouped_by(
435        &mut self,
436        metric_name: &str,
437        group_by_keys: &[&str],
438        new_metric_name: &str,
439    ) {
440        let mut sums: HashMap<Vec<(String, String)>, f64> = HashMap::new();
441
442        for (labels, metrics) in &self.flat_dict {
443            let group_values: Option<Vec<(String, String)>> = group_by_keys
444                .iter()
445                .map(|key| labels.get(key).map(|v| (key.to_string(), v.to_string())))
446                .collect();
447
448            let Some(group_values) = group_values else {
449                continue;
450            };
451
452            for metric in metrics {
453                if metric.name == metric_name {
454                    *sums.entry(group_values.clone()).or_default() += metric.value;
455                }
456            }
457        }
458
459        for (group_labels, sum) in sums {
460            let labels = Labels(group_labels);
461            self.add_to_flat_dict(labels, new_metric_name.to_string(), sum);
462        }
463    }
464
465    pub fn generate_markdown_tables(&self) -> String {
466        let mut markdown_output = String::new();
467        // Get sorted keys to iterate in consistent order
468        let mut sorted_keys: Vec<_> = self.dict_by_label_types.keys().cloned().collect();
469        sorted_keys.sort();
470
471        for label_keys in sorted_keys {
472            let metrics_dict = &self.dict_by_label_types[&label_keys];
473            let mut metric_names: Vec<String> = metrics_dict
474                .values()
475                .flat_map(|metrics| metrics.iter().map(|m| m.name.clone()))
476                .collect::<std::collections::HashSet<_>>()
477                .into_iter()
478                .collect();
479            metric_names.sort_by(|a, b| b.cmp(a));
480
481            // Filter out gpu_mem metrics - these are summarized in the GPU memory chart
482            metric_names.retain(|n| !n.starts_with("gpu_mem."));
483            // Filter out wrapper spans - these are just for propagating fields to child spans
484            metric_names.retain(|n| !n.starts_with("wrapper."));
485
486            // Skip tables that have no metrics left after filtering
487            if metric_names.is_empty() {
488                continue;
489            }
490
491            // Create table header
492            let header = if label_keys.is_empty() {
493                format!("| {} |", metric_names.join(" | "))
494            } else {
495                format!(
496                    "| {} | {} |",
497                    label_keys.join(" | "),
498                    metric_names.join(" | ")
499                )
500            };
501
502            let separator = "| ".to_string()
503                + &vec!["---"; label_keys.len() + metric_names.len()].join(" | ")
504                + " |";
505
506            markdown_output.push_str(&header);
507            markdown_output.push('\n');
508            markdown_output.push_str(&separator);
509            markdown_output.push('\n');
510
511            // Sort rows: first by segment (ascending) if present, then by frequency (descending) if
512            // present
513            let mut rows: Vec<_> = metrics_dict.iter().collect();
514            let segment_index = label_keys.iter().position(|k| k == "segment");
515            let has_frequency = metric_names.contains(&"frequency".to_string());
516
517            if segment_index.is_some() || has_frequency {
518                rows.sort_by(|(label_values_a, metrics_a), (label_values_b, metrics_b)| {
519                    // First, sort by segment (ascending) if present
520                    if let Some(seg_idx) = segment_index {
521                        let seg_a = label_values_a
522                            .get(seg_idx)
523                            .map(|s| s.as_str())
524                            .unwrap_or("");
525                        let seg_b = label_values_b
526                            .get(seg_idx)
527                            .map(|s| s.as_str())
528                            .unwrap_or("");
529                        let seg_cmp = seg_a.cmp(seg_b);
530                        if seg_cmp != std::cmp::Ordering::Equal {
531                            return seg_cmp;
532                        }
533                    }
534
535                    // Then, sort by frequency (descending) if present
536                    if has_frequency {
537                        let freq_a = metrics_a
538                            .iter()
539                            .find(|m| m.name == "frequency")
540                            .map(|m| m.value)
541                            .unwrap_or(0.0);
542                        let freq_b = metrics_b
543                            .iter()
544                            .find(|m| m.name == "frequency")
545                            .map(|m| m.value)
546                            .unwrap_or(0.0);
547                        return freq_b
548                            .partial_cmp(&freq_a)
549                            .unwrap_or(std::cmp::Ordering::Equal);
550                    }
551
552                    std::cmp::Ordering::Equal
553                });
554            }
555
556            // Fill table rows
557            for (label_values, metrics) in rows {
558                let mut row = String::new();
559                row.push_str("| ");
560                if !label_values.is_empty() {
561                    row.push_str(&label_values.join(" | "));
562                    row.push_str(" | ");
563                }
564
565                // Add metric values
566                for metric_name in &metric_names {
567                    let metric_value = metrics
568                        .iter()
569                        .find(|m| &m.name == metric_name)
570                        .map(|m| Self::format_number(m.value))
571                        .unwrap_or_default();
572
573                    row.push_str(&format!("{metric_value} | "));
574                }
575
576                markdown_output.push_str(&row);
577                markdown_output.push('\n');
578            }
579
580            markdown_output.push('\n');
581        }
582
583        markdown_output
584    }
585}