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 let mmap = unsafe { Mmap::map(&file)? };
23 let metrics: MetricsFile = serde_json::from_slice(&mmap)?;
24
25 let mut db = MetricDb::default();
26
27 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 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 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 continue;
56 }
57 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 pub fn custom_sort_label_keys(label_keys: &mut [String]) {
90 label_keys.sort_by_key(|key| {
92 if key == "group" {
93 (0, key.clone()) } else {
95 (1, key.clone()) }
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 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 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 let mut keys = label_keys.clone();
124 let mut values = label_values.clone();
125
126 if let Some(index) = keys.iter().position(|k| k == "cycle_tracker_span") {
128 keys.remove(index);
129 values.remove(index);
130 }
131
132 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 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 existing_metric.value += metric.value;
152 } else {
153 entry.push(metric);
155 }
156 }
157 }
158 }
159
160 pub fn generate_gpu_memory_chart(&self) -> Option<(String, String)> {
164 let mut data: Vec<(f64, f64, f64, f64)> = Vec::new();
166 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 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 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 fn render_gpu_memory_svg(data: &[(f64, f64, f64, f64)]) -> String {
258 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 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 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 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(¤t_points);
321 let local_peak_path = build_path(&local_peak_points);
322 let reserved_path = build_path(&reserved_points);
323
324 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 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 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 let color_current = "#2563eb"; let color_local_peak = "#16a34a"; let color_reserved = "#dc2626"; 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 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 metric_names.retain(|n| !n.starts_with("gpu_mem."));
483 metric_names.retain(|n| !n.starts_with("wrapper."));
485
486 if metric_names.is_empty() {
488 continue;
489 }
490
491 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 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 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 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 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 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}