Skip to main content

rusty_stack/widgets/
benchmarks_page.rs

1//! Benchmark performance page for Rusty-Stack TUI
2//! Renders real benchmark results with ratatui charts and summaries.
3
4use crate::benchmark_logs::{
5    benchmark_log_directories, collect_matching_logs, extract_benchmark_json_string,
6    find_latest_log_in_dirs, is_json_log,
7};
8use chrono::Local;
9use ratatui::{
10    layout::{Alignment, Constraint, Direction, Layout, Rect},
11    style::{Color, Modifier, Style},
12    symbols,
13    text::{Line, Span, Text},
14    widgets::{Axis, Block, Borders, Chart, Dataset, GraphType, Paragraph, Tabs},
15    Frame,
16};
17use serde_json;
18use std::fs;
19use std::path::{Path, PathBuf};
20use std::time::{Duration, SystemTime};
21
22const STALE_FULL_BENCHMARK_WINDOW: Duration = Duration::from_secs(15 * 60);
23const STALE_ERROR_WINDOW: Duration = Duration::from_secs(15 * 60);
24
25#[derive(Debug, Clone)]
26pub struct BenchmarkResults {
27    pub gpus: Vec<GpuInfo>,
28    pub memory_bandwidth: Option<MemoryBandwidthData>,
29    pub tensor_core: Option<TensorCoreData>,
30    pub pytorch: Option<PyTorchData>,
31    pub llama_cpp: Option<LlamaCppData>,
32    pub flash_attention: Option<FlashAttentionData>,
33    pub vllm: Option<VllmData>,
34    pub deepspeed: Option<DeepspeedData>,
35    pub megatron: Option<MegatronData>,
36    pub onnx: Option<OnnxData>,
37    pub errors: Vec<String>,
38    pub baseline: Option<Box<BenchmarkResults>>,
39}
40
41#[derive(Debug, Clone)]
42pub struct DeepspeedData {
43    pub throughput_samples_per_sec: f64,
44    pub avg_latency_ms: f64,
45    pub stage: u32,
46    pub accelerator: String,
47    pub samples: Vec<f64>,
48}
49
50#[derive(Debug, Clone)]
51pub struct MegatronData {
52    pub throughput_samples_per_sec: f64,
53    pub avg_latency_ms: f64,
54    pub backend: String,
55    pub samples: Vec<f64>,
56}
57
58#[derive(Debug, Clone)]
59pub struct OnnxData {
60    pub ort_version: String,
61    pub provider: String,
62    pub providers_available: Vec<String>,
63    pub graph_opt_level: String,
64    pub session_create_ms: f64,
65    pub inference_latency_p50_ms: f64,
66    pub inference_latency_p95_ms: f64,
67    pub inference_latency_p99_ms: f64,
68    pub throughput_inf_per_sec: f64,
69    pub peak_rss_mb: f64,
70    pub quantized_supported: bool,
71    pub inference_samples: Vec<f64>,
72}
73
74#[derive(Debug, Clone)]
75pub struct GpuInfo {
76    pub index: usize,
77    pub model: String,
78    pub vram_gb: f64,
79    pub compute_units: u32,
80    pub tensor_cores: bool,
81    pub temperature_c: Option<f64>,
82    pub power_watts: Option<f64>,
83    pub utilization_percent: Option<f64>,
84    pub memory_percent: Option<f64>,
85    pub sclk_mhz: Option<u32>,
86    pub mclk_mhz: Option<u32>,
87}
88
89#[derive(Debug, Clone)]
90pub struct MemoryBandwidthData {
91    pub hbm_peak_gb_s: f64,
92    pub system_peak_gb_s: f64,
93    pub hbm_ratio: f64,
94    pub hbm_samples: Vec<f64>,
95    pub system_samples: Vec<f64>,
96}
97
98#[derive(Debug, Clone)]
99pub struct TensorCoreData {
100    pub fp16_tflops: f64,
101    pub bf16_tflops: f64,
102    pub fp32_tflops: f64,
103    pub fp16_samples: Vec<f64>,
104}
105
106#[derive(Debug, Clone)]
107pub struct PyTorchData {
108    pub gemm_gflops: f64,
109    pub convolution_gflops: f64,
110    pub autograd_overhead_percent: f64,
111    pub gemm_samples: Vec<f64>,
112    pub conv_samples: Vec<f64>,
113}
114
115#[derive(Debug, Clone)]
116pub struct LlamaCppData {
117    pub gpus: Vec<LlamaCppGpu>,
118    pub prefill: Vec<LlamaCppPerfPoint>,
119    pub decode: Vec<LlamaCppPerfPoint>,
120    pub model: String,
121    pub rocm_version: String,
122}
123
124#[derive(Debug, Clone)]
125pub struct LlamaCppGpu {
126    pub name: String,
127    pub arch: String,
128    pub vram_gb: f64,
129}
130
131#[derive(Debug, Clone)]
132pub struct LlamaCppPerfPoint {
133    pub gpu: usize,
134    pub context: Option<u32>,
135    pub n_gen: Option<u32>,
136    pub avg_tps: f64,
137    pub stddev: f64,
138}
139
140#[derive(Debug, Clone)]
141pub struct FlashAttentionData {
142    pub standard_attention_speed: f64,
143    pub flash_attention_speed: f64,
144    pub speedup: f64,
145    pub memory_savings_gb: f64,
146    pub standard_samples: Vec<f64>,
147    pub flash_samples: Vec<f64>,
148}
149
150#[derive(Debug, Clone)]
151pub struct VllmData {
152    pub throughput_tokens_per_sec: f64,
153    pub latency_ms: f64,
154    pub model: String,
155    pub model_format: String,
156    pub throughput_samples: Vec<f64>,
157}
158
159/// Load benchmark results from all available log files
160pub fn load_benchmark_results() -> BenchmarkResults {
161    let log_dirs = benchmark_log_directories();
162
163    if log_dirs.is_empty() {
164        return BenchmarkResults::empty();
165    }
166
167    let mut results = BenchmarkResults::empty();
168    let mut selected_logs: Vec<(&str, PathBuf, SystemTime)> = Vec::new();
169
170    // primary JSON sources written by the wrapper scripts
171    for pattern in &[
172        "rocm_benchmarks",
173        "gpu_memory_bandwidth",
174        "pytorch_performance",
175        "llama_cpp_benchmarks",
176        "llama-cpp",
177        "mlperf_inference",
178        "vllm_benchmarks",
179        "deepspeed_benchmarks",
180        "megatron_benchmarks",
181        "onnx_benchmarks",
182        "full_benchmarks",
183    ] {
184        if let Some(log_path) = find_latest_log_in_dirs(&log_dirs, pattern) {
185            let modified = fs::metadata(&log_path)
186                .and_then(|m| m.modified())
187                .unwrap_or(SystemTime::UNIX_EPOCH);
188            selected_logs.push((pattern, log_path, modified));
189        }
190    }
191
192    let newest_component_log = selected_logs
193        .iter()
194        .filter(|(pattern, _, _)| *pattern != "full_benchmarks")
195        .map(|(_, _, modified)| *modified)
196        .max();
197
198    for (pattern, log_path, modified) in selected_logs {
199        if pattern == "full_benchmarks" {
200            if let Some(component_mtime) = newest_component_log {
201                let is_stale = component_mtime
202                    .duration_since(modified)
203                    .map(|delta| delta > STALE_FULL_BENCHMARK_WINDOW)
204                    .unwrap_or(false);
205                if is_stale {
206                    continue;
207                }
208            }
209        }
210
211        let include_errors = newest_component_log
212            .and_then(|latest| latest.duration_since(modified).ok())
213            .map(|delta| delta <= STALE_ERROR_WINDOW)
214            .unwrap_or(true);
215
216        if let Ok(contents) = std::fs::read_to_string(&log_path) {
217            let json = extract_json(&contents);
218            if !json.is_empty() {
219                parse_benchmark_json(&json, &mut results, include_errors);
220            }
221        }
222    }
223
224    if results.gpus.is_empty() {
225        results.gpus.push(GpuInfo {
226            index: 0,
227            model: "No GPU detected".to_string(),
228            vram_gb: 0.0,
229            compute_units: 0,
230            tensor_cores: false,
231            temperature_c: None,
232            power_watts: None,
233            utilization_percent: None,
234            memory_percent: None,
235            sclk_mhz: None,
236            mclk_mhz: None,
237        });
238    }
239
240    // Try to load baseline (first log file that isn't the current one)
241    results.baseline = load_baseline(&log_dirs);
242    dedupe_errors_in_place(&mut results.errors);
243
244    results
245}
246
247fn load_baseline(log_dirs: &[PathBuf]) -> Option<Box<BenchmarkResults>> {
248    let baseline_patterns = [
249        "rocm_benchmarks",
250        "vllm_benchmarks",
251        "deepspeed_benchmarks",
252        "megatron_benchmarks",
253        "onnx_benchmarks",
254        "full_benchmarks",
255    ];
256    let mut log_files = collect_matching_logs(log_dirs, &baseline_patterns);
257    let json_files: Vec<PathBuf> = log_files
258        .iter()
259        .filter(|path| is_json_log(path))
260        .cloned()
261        .collect();
262    if !json_files.is_empty() {
263        log_files = json_files;
264    }
265
266    if log_files.len() > 1 {
267        for file in log_files {
268            if let Ok(contents) = std::fs::read_to_string(file) {
269                let json = extract_json(&contents);
270                if json.is_empty() {
271                    continue;
272                }
273                let mut baseline_results = BenchmarkResults {
274                    gpus: Vec::new(),
275                    memory_bandwidth: None,
276                    tensor_core: None,
277                    pytorch: None,
278                    llama_cpp: None,
279                    flash_attention: None,
280                    vllm: None,
281                    deepspeed: None,
282                    megatron: None,
283                    onnx: None,
284                    errors: Vec::new(),
285                    baseline: None,
286                };
287                parse_benchmark_json(&json, &mut baseline_results, false);
288                if !baseline_results.gpus.is_empty()
289                    || baseline_results.memory_bandwidth.is_some()
290                    || baseline_results.tensor_core.is_some()
291                    || baseline_results.pytorch.is_some()
292                    || baseline_results.flash_attention.is_some()
293                    || baseline_results.vllm.is_some()
294                    || baseline_results.deepspeed.is_some()
295                    || baseline_results.megatron.is_some()
296                    || baseline_results.onnx.is_some()
297                {
298                    return Some(Box::new(baseline_results));
299                }
300            }
301        }
302    }
303    None
304}
305
306fn push_error_unique(results: &mut BenchmarkResults, message: impl Into<String>) {
307    let message = message.into();
308    let trimmed = message.trim();
309    if trimmed.is_empty() {
310        return;
311    }
312    if results
313        .errors
314        .iter()
315        .any(|existing| existing.trim() == trimmed)
316    {
317        return;
318    }
319    results.errors.push(trimmed.to_string());
320}
321
322fn dedupe_errors_in_place(errors: &mut Vec<String>) {
323    let mut deduped: Vec<String> = Vec::new();
324    for err in errors.drain(..) {
325        let trimmed = err.trim();
326        if trimmed.is_empty() {
327            continue;
328        }
329        if deduped.iter().any(|existing| existing == trimmed) {
330            continue;
331        }
332        deduped.push(trimmed.to_string());
333    }
334    *errors = deduped;
335}
336
337fn parse_benchmark_json(json: &str, results: &mut BenchmarkResults, include_errors: bool) {
338    if let Ok(value) = serde_json::from_str::<serde_json::Value>(json) {
339        if let Some(obj) = value.as_object() {
340            if let Some(res) = obj.get("results") {
341                if res.is_object() {
342                    apply_metrics(res, results, include_errors);
343                }
344                if include_errors {
345                    if let Some(errors) = obj.get("errors").and_then(|v| v.as_array()).map(|arr| {
346                        arr.iter()
347                            .filter_map(|e| e.as_str().map(|s| s.to_string()))
348                            .collect::<Vec<_>>()
349                    }) {
350                        for err in errors {
351                            push_error_unique(results, err);
352                        }
353                    }
354                }
355            } else {
356                for (_, val) in obj {
357                    apply_metrics(val, results, include_errors);
358                }
359            }
360        }
361    }
362}
363
364fn to_f64_vec(v: &serde_json::Value) -> Vec<f64> {
365    v.as_array()
366        .map(|arr| arr.iter().filter_map(|x| x.as_f64()).collect())
367        .unwrap_or_default()
368}
369
370fn is_integrated_gpu_model(model: &str) -> bool {
371    let lowered = model.to_ascii_lowercase();
372    if lowered.contains("ryzen") {
373        return true;
374    }
375    if lowered.contains("integrated") || lowered.contains("igpu") || lowered.contains("apu") {
376        return true;
377    }
378    if lowered.contains("radeon graphics")
379        && !lowered.contains("rx")
380        && !lowered.contains("pro")
381        && !lowered.contains("instinct")
382    {
383        return true;
384    }
385    [
386        "raphael",
387        "phoenix",
388        "rembrandt",
389        "cezanne",
390        "barcelo",
391        "yellow carp",
392        "green sardine",
393        "pink sardine",
394        "hawk point",
395    ]
396    .iter()
397    .any(|token| lowered.contains(token))
398}
399
400fn apply_metrics(val: &serde_json::Value, results: &mut BenchmarkResults, include_errors: bool) {
401    if let Some(obj) = val.as_object() {
402        // If this object contains recognized benchmark fields, process it
403        if obj.contains_key("metrics")
404            || obj.contains_key("results")
405            || obj.contains_key("gpus")
406            || obj.contains_key("gpu_model")
407            || obj.contains_key("hbm_peak_gb_s")
408            || obj.contains_key("fp16_tflops")
409            || obj.contains_key("gemm_gflops")
410            || obj.contains_key("flash_attention_speed")
411            || obj.contains_key("throughput_tokens_per_sec")
412            || obj.contains_key("throughput_samples_per_sec")
413            || obj.contains_key("megatron_throughput_samples_per_sec")
414            || obj.contains_key("ort_version")
415            || obj.keys().any(|k| k.contains("_tps_gpu"))
416        {
417            apply_metrics_internal(val, results, include_errors);
418        } else {
419            // Otherwise, iterate through the object as it might be a container of results
420            for (_, nested_val) in obj {
421                apply_metrics(nested_val, results, include_errors);
422            }
423        }
424    }
425}
426
427fn apply_metrics_internal(
428    val: &serde_json::Value,
429    results: &mut BenchmarkResults,
430    include_errors: bool,
431) {
432    if include_errors {
433        if let Some(root_obj) = val.as_object() {
434            if let Some(errors) = root_obj
435                .get("errors")
436                .and_then(|v| v.as_array())
437                .map(|arr| {
438                    arr.iter()
439                        .filter_map(|e| e.as_str().map(|s| s.to_string()))
440                        .collect::<Vec<_>>()
441                })
442            {
443                for err in errors {
444                    push_error_unique(results, err);
445                }
446            }
447        }
448    }
449
450    let metrics = val
451        .get("metrics")
452        .or_else(|| val.get("results"))
453        .unwrap_or(val);
454
455    if let Some(obj) = metrics.as_object() {
456        if include_errors {
457            if let Some(errors) = obj.get("errors").and_then(|v| v.as_array()).map(|arr| {
458                arr.iter()
459                    .filter_map(|e| e.as_str().map(|s| s.to_string()))
460                    .collect::<Vec<_>>()
461            }) {
462                for err in errors {
463                    push_error_unique(results, err);
464                }
465            }
466        }
467
468        if obj.contains_key("gpus") || obj.contains_key("gpu_model") {
469            let gpus = if let Some(arr) = obj.get("gpus").and_then(|v| v.as_array()) {
470                arr.iter()
471                    .filter_map(|g| g.as_object())
472                    .enumerate()
473                    .filter_map(|(idx, g)| {
474                        let model = g
475                            .get("gpu_model")
476                            .and_then(|v| v.as_str())
477                            .unwrap_or("Unknown");
478                        if is_integrated_gpu_model(model) {
479                            return None;
480                        }
481                        let gpu_index = g
482                            .get("index")
483                            .and_then(|v| v.as_u64())
484                            .or_else(|| g.get("local_index").and_then(|v| v.as_u64()))
485                            .unwrap_or(idx as u64) as usize;
486                        Some(GpuInfo {
487                            index: gpu_index,
488                            model: model.into(),
489                            vram_gb: g.get("vram_gb").and_then(|v| v.as_f64()).unwrap_or(0.0),
490                            compute_units: g
491                                .get("compute_units")
492                                .and_then(|v| v.as_u64())
493                                .unwrap_or(0) as u32,
494                            tensor_cores: g
495                                .get("tensor_cores")
496                                .and_then(|v| v.as_bool())
497                                .unwrap_or(false),
498                            temperature_c: g.get("temperature_c").and_then(|v| v.as_f64()),
499                            power_watts: g.get("power_watts").and_then(|v| v.as_f64()),
500                            utilization_percent: g
501                                .get("utilization_percent")
502                                .and_then(|v| v.as_f64()),
503                            memory_percent: g.get("memory_percent").and_then(|v| v.as_f64()),
504                            sclk_mhz: g.get("sclk_mhz").and_then(|v| v.as_u64()).map(|v| v as u32),
505                            mclk_mhz: g.get("mclk_mhz").and_then(|v| v.as_u64()).map(|v| v as u32),
506                        })
507                    })
508                    .collect()
509            } else {
510                let model = obj
511                    .get("gpu_model")
512                    .and_then(|v| v.as_str())
513                    .unwrap_or("Unknown");
514                if is_integrated_gpu_model(model) {
515                    Vec::new()
516                } else {
517                    vec![GpuInfo {
518                        index: obj.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize,
519                        model: model.into(),
520                        vram_gb: obj.get("vram_gb").and_then(|v| v.as_f64()).unwrap_or(0.0),
521                        compute_units: obj
522                            .get("compute_units")
523                            .and_then(|v| v.as_u64())
524                            .unwrap_or(0) as u32,
525                        tensor_cores: obj
526                            .get("tensor_cores")
527                            .and_then(|v| v.as_bool())
528                            .unwrap_or(false),
529                        temperature_c: obj.get("temperature_c").and_then(|v| v.as_f64()),
530                        power_watts: obj.get("power_watts").and_then(|v| v.as_f64()),
531                        utilization_percent: obj
532                            .get("utilization_percent")
533                            .and_then(|v| v.as_f64()),
534                        memory_percent: obj.get("memory_percent").and_then(|v| v.as_f64()),
535                        sclk_mhz: obj
536                            .get("sclk_mhz")
537                            .and_then(|v| v.as_u64())
538                            .map(|v| v as u32),
539                        mclk_mhz: obj
540                            .get("mclk_mhz")
541                            .and_then(|v| v.as_u64())
542                            .map(|v| v as u32),
543                    }]
544                }
545            };
546            results.gpus = gpus;
547        }
548
549        if obj.contains_key("hbm_peak_gb_s") {
550            results.memory_bandwidth = Some(MemoryBandwidthData {
551                hbm_peak_gb_s: obj
552                    .get("hbm_peak_gb_s")
553                    .and_then(|v| v.as_f64())
554                    .unwrap_or(0.0),
555                system_peak_gb_s: obj
556                    .get("system_peak_gb_s")
557                    .and_then(|v| v.as_f64())
558                    .unwrap_or(0.0),
559                hbm_ratio: obj.get("hbm_ratio").and_then(|v| v.as_f64()).unwrap_or(0.0),
560                hbm_samples: obj
561                    .get("hbm_samples_gbps")
562                    .map(to_f64_vec)
563                    .unwrap_or_default(),
564                system_samples: obj
565                    .get("system_samples_gbps")
566                    .map(to_f64_vec)
567                    .unwrap_or_default(),
568            });
569        }
570
571        if obj.contains_key("fp16_tflops") {
572            results.tensor_core = Some(TensorCoreData {
573                fp16_tflops: obj
574                    .get("fp16_tflops")
575                    .and_then(|v| v.as_f64())
576                    .unwrap_or(0.0),
577                bf16_tflops: obj
578                    .get("bf16_tflops")
579                    .and_then(|v| v.as_f64())
580                    .unwrap_or(0.0),
581                fp32_tflops: obj
582                    .get("fp32_tflops")
583                    .and_then(|v| v.as_f64())
584                    .unwrap_or(0.0),
585                fp16_samples: obj
586                    .get("fp16_samples_tflops")
587                    .map(to_f64_vec)
588                    .unwrap_or_default(),
589            });
590        }
591
592        if obj.contains_key("gemm_gflops") || obj.contains_key("convolution_gflops") {
593            results.pytorch = Some(PyTorchData {
594                gemm_gflops: obj
595                    .get("gemm_gflops")
596                    .and_then(|v| v.as_f64())
597                    .unwrap_or(0.0),
598                convolution_gflops: obj
599                    .get("convolution_gflops")
600                    .and_then(|v| v.as_f64())
601                    .unwrap_or(0.0),
602                autograd_overhead_percent: obj
603                    .get("autograd_overhead_percent")
604                    .and_then(|v| v.as_f64())
605                    .unwrap_or(0.0),
606                gemm_samples: obj
607                    .get("gemm_samples_gflops")
608                    .map(to_f64_vec)
609                    .unwrap_or_default(),
610                conv_samples: obj
611                    .get("conv_samples_gflops")
612                    .map(to_f64_vec)
613                    .unwrap_or_default(),
614            });
615        }
616
617        if obj.contains_key("flash_attention_speed") || obj.contains_key("standard_attention_speed")
618        {
619            results.flash_attention = Some(FlashAttentionData {
620                standard_attention_speed: obj
621                    .get("standard_attention_speed")
622                    .and_then(|v| v.as_f64())
623                    .unwrap_or(0.0),
624                flash_attention_speed: obj
625                    .get("flash_attention_speed")
626                    .and_then(|v| v.as_f64())
627                    .unwrap_or(0.0),
628                speedup: obj.get("speedup").and_then(|v| v.as_f64()).unwrap_or(0.0),
629                memory_savings_gb: obj
630                    .get("memory_savings_gb")
631                    .and_then(|v| v.as_f64())
632                    .unwrap_or(0.0),
633                standard_samples: obj
634                    .get("standard_samples_tok_s")
635                    .map(to_f64_vec)
636                    .unwrap_or_default(),
637                flash_samples: obj
638                    .get("flash_samples_tok_s")
639                    .map(to_f64_vec)
640                    .unwrap_or_default(),
641            });
642        }
643
644        if obj.contains_key("throughput_tokens_per_sec") {
645            let model_name = obj
646                .get("model")
647                .and_then(|v| v.as_str())
648                .unwrap_or("Unknown")
649                .to_string();
650            let model_format = obj
651                .get("model_format")
652                .and_then(|v| v.as_str())
653                .map(|s| s.to_string())
654                .unwrap_or_else(|| {
655                    if model_name.to_ascii_lowercase().ends_with(".gguf") {
656                        "gguf".to_string()
657                    } else {
658                        "safetensors".to_string()
659                    }
660                });
661            results.vllm = Some(VllmData {
662                throughput_tokens_per_sec: obj
663                    .get("throughput_tokens_per_sec")
664                    .and_then(|v| v.as_f64())
665                    .unwrap_or(0.0),
666                latency_ms: obj
667                    .get("latency_ms")
668                    .and_then(|v| v.as_f64())
669                    .unwrap_or(0.0),
670                model: model_name,
671                model_format,
672                throughput_samples: obj
673                    .get("throughput_samples")
674                    .map(to_f64_vec)
675                    .unwrap_or_default(),
676            });
677            let throughput = obj
678                .get("throughput_tokens_per_sec")
679                .and_then(|v| v.as_f64())
680                .unwrap_or(0.0);
681            if throughput <= 0.0 {
682                if let Some(reason) = obj
683                    .get("reason")
684                    .and_then(|v| v.as_str())
685                    .map(|s| s.trim())
686                    .filter(|s| !s.is_empty())
687                {
688                    push_error_unique(results, format!("vLLM benchmark degraded: {reason}"));
689                } else {
690                    push_error_unique(results, "vLLM benchmark degraded: zero throughput");
691                }
692            }
693        }
694
695        if obj.contains_key("throughput_samples_per_sec") {
696            results.deepspeed = Some(DeepspeedData {
697                throughput_samples_per_sec: obj
698                    .get("throughput_samples_per_sec")
699                    .and_then(|v| v.as_f64())
700                    .unwrap_or(0.0),
701                avg_latency_ms: obj
702                    .get("avg_latency_ms")
703                    .and_then(|v| v.as_f64())
704                    .unwrap_or(0.0),
705                stage: obj.get("stage").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
706                accelerator: obj
707                    .get("accelerator")
708                    .and_then(|v| v.as_str())
709                    .unwrap_or("rocm")
710                    .to_string(),
711                samples: obj.get("samples").map(to_f64_vec).unwrap_or_default(),
712            });
713        }
714
715        if obj.contains_key("megatron_throughput_samples_per_sec") {
716            results.megatron = Some(MegatronData {
717                throughput_samples_per_sec: obj
718                    .get("megatron_throughput_samples_per_sec")
719                    .and_then(|v| v.as_f64())
720                    .unwrap_or(0.0),
721                avg_latency_ms: obj
722                    .get("megatron_avg_latency_ms")
723                    .and_then(|v| v.as_f64())
724                    .unwrap_or(0.0),
725                backend: obj
726                    .get("megatron_backend")
727                    .and_then(|v| v.as_str())
728                    .unwrap_or("megatron")
729                    .to_string(),
730                samples: obj
731                    .get("megatron_samples")
732                    .map(to_f64_vec)
733                    .unwrap_or_default(),
734            });
735        }
736
737        if obj.contains_key("ort_version") {
738            results.onnx = Some(OnnxData {
739                ort_version: obj
740                    .get("ort_version")
741                    .and_then(|v| v.as_str())
742                    .unwrap_or("unknown")
743                    .to_string(),
744                provider: obj
745                    .get("provider")
746                    .and_then(|v| v.as_str())
747                    .unwrap_or("CPU")
748                    .to_string(),
749                providers_available: obj
750                    .get("providers_available")
751                    .and_then(|v| v.as_array())
752                    .map(|arr| {
753                        arr.iter()
754                            .filter_map(|x| x.as_str().map(|s| s.to_string()))
755                            .collect()
756                    })
757                    .unwrap_or_default(),
758                graph_opt_level: obj
759                    .get("graph_opt_level")
760                    .and_then(|v| v.as_str())
761                    .unwrap_or("ORI_DISABLE_ALL")
762                    .to_string(),
763                session_create_ms: obj
764                    .get("session_create_ms")
765                    .and_then(|v| v.as_f64())
766                    .unwrap_or(0.0),
767                inference_latency_p50_ms: obj
768                    .get("inference_latency_p50_ms")
769                    .and_then(|v| v.as_f64())
770                    .unwrap_or(0.0),
771                inference_latency_p95_ms: obj
772                    .get("inference_latency_p95_ms")
773                    .and_then(|v| v.as_f64())
774                    .unwrap_or(0.0),
775                inference_latency_p99_ms: obj
776                    .get("inference_latency_p99_ms")
777                    .and_then(|v| v.as_f64())
778                    .unwrap_or(0.0),
779                throughput_inf_per_sec: obj
780                    .get("throughput_inf_per_sec")
781                    .and_then(|v| v.as_f64())
782                    .unwrap_or(0.0),
783                peak_rss_mb: obj
784                    .get("peak_rss_mb")
785                    .and_then(|v| v.as_f64())
786                    .unwrap_or(0.0),
787                quantized_supported: obj
788                    .get("quantized_supported")
789                    .and_then(|v| v.as_bool())
790                    .unwrap_or(false),
791                inference_samples: obj
792                    .get("inference_samples")
793                    .map(to_f64_vec)
794                    .unwrap_or_default(),
795            });
796        }
797
798        // llama-cpp benchmark: keys like prefill_512_tps_gpu0, decode_128_tps_gpu0
799        if obj.keys().any(|k| k.contains("_tps_gpu")) {
800            let mut gpus = Vec::new();
801            let mut prefill = Vec::new();
802            let mut decode = Vec::new();
803
804            // Collect unique GPU indices from metric keys
805            let mut gpu_indices: Vec<usize> = obj
806                .keys()
807                .filter_map(|k| {
808                    k.rsplit("_gpu")
809                        .next()
810                        .and_then(|s| s.parse::<usize>().ok())
811                })
812                .collect();
813            gpu_indices.sort();
814            gpu_indices.dedup();
815
816            for gpu_idx in &gpu_indices {
817                gpus.push(LlamaCppGpu {
818                    name: format!("GPU {}", gpu_idx),
819                    arch: String::new(),
820                    vram_gb: 0.0,
821                });
822            }
823
824            for (key, value) in obj.iter() {
825                let tps = value.as_f64().unwrap_or(0.0);
826                let gpu_idx = key
827                    .rsplit("_gpu")
828                    .next()
829                    .and_then(|s| s.parse::<usize>().ok())
830                    .unwrap_or(0);
831
832                if key.starts_with("prefill_") && !key.contains("stddev") {
833                    // prefill_{context}_tps_gpu{idx}
834                    let context = key
835                        .trim_start_matches("prefill_")
836                        .trim_end_matches(&format!("_tps_gpu{}", gpu_idx))
837                        .parse::<u32>()
838                        .ok();
839                    prefill.push(LlamaCppPerfPoint {
840                        gpu: gpu_idx,
841                        context,
842                        n_gen: None,
843                        avg_tps: tps,
844                        stddev: obj
845                            .get(&key.replace("_tps_", "_stddev_tps_"))
846                            .and_then(|v| v.as_f64())
847                            .unwrap_or(0.0),
848                    });
849                } else if key.starts_with("decode_") && !key.contains("stddev") {
850                    // decode_{n_gen}_tps_gpu{idx}
851                    let n_gen = key
852                        .trim_start_matches("decode_")
853                        .trim_end_matches(&format!("_tps_gpu{}", gpu_idx))
854                        .parse::<u32>()
855                        .ok();
856                    decode.push(LlamaCppPerfPoint {
857                        gpu: gpu_idx,
858                        context: None,
859                        n_gen,
860                        avg_tps: tps,
861                        stddev: obj
862                            .get(&key.replace("_tps_", "_stddev_tps_"))
863                            .and_then(|v| v.as_f64())
864                            .unwrap_or(0.0),
865                    });
866                }
867            }
868
869            // Sort by GPU index then context/n_gen
870            prefill.sort_by(|a, b| a.gpu.cmp(&b.gpu).then(a.context.cmp(&b.context)));
871            decode.sort_by(|a, b| a.gpu.cmp(&b.gpu).then(a.n_gen.cmp(&b.n_gen)));
872
873            results.llama_cpp = Some(LlamaCppData {
874                gpus,
875                prefill,
876                decode,
877                model: String::new(),
878                rocm_version: String::new(),
879            });
880        }
881    }
882}
883
884fn extract_json(content: &str) -> String {
885    extract_benchmark_json_string(content).unwrap_or_default()
886}
887
888impl BenchmarkResults {
889    fn empty() -> Self {
890        BenchmarkResults {
891            gpus: Vec::new(),
892            memory_bandwidth: None,
893            tensor_core: None,
894            pytorch: None,
895            llama_cpp: None,
896            flash_attention: None,
897            vllm: None,
898            deepspeed: None,
899            megatron: None,
900            onnx: None,
901            errors: Vec::new(),
902            baseline: None,
903        }
904    }
905}
906
907/// Render the benchmark results page
908pub fn render_benchmark_page(
909    frame: &mut Frame,
910    area: Rect,
911    results: &BenchmarkResults,
912    tab_index: usize,
913) {
914    let tabs = Tabs::new(vec![
915        Line::from("GPU"),
916        Line::from("Memory"),
917        Line::from("Tensor Core"),
918        Line::from("PyTorch"),
919        Line::from("Flash Attn"),
920        Line::from("vLLM"),
921        Line::from("DeepSpeed"),
922        Line::from("Megatron"),
923        Line::from("ONNX"),
924        Line::from("LLaMA"),
925    ])
926    .select(tab_index)
927    .style(Style::default().fg(Color::Cyan))
928    .highlight_style(
929        Style::default()
930            .add_modifier(Modifier::BOLD)
931            .fg(Color::Yellow),
932    );
933
934    let chunks = Layout::default()
935        .direction(Direction::Vertical)
936        .constraints([Constraint::Length(3), Constraint::Min(10)])
937        .split(area);
938
939    frame.render_widget(tabs, chunks[0]);
940
941    match tab_index {
942        0 => render_gpu_tab(frame, chunks[1], results),
943        1 => render_memory_tab(frame, chunks[1], results),
944        2 => render_tensor_core_tab(frame, chunks[1], results),
945        3 => render_pytorch_tab(frame, chunks[1], results),
946        4 => render_flash_attention_tab(frame, chunks[1], results),
947        5 => render_vllm_tab(frame, chunks[1], results),
948        6 => render_deepspeed_tab(frame, chunks[1], results),
949        7 => render_megatron_tab(frame, chunks[1], results),
950        8 => render_onnx_tab(frame, chunks[1], results),
951        9 => render_llama_cpp_tab(frame, chunks[1], results),
952        _ => render_gpu_tab(frame, chunks[1], results),
953    }
954}
955
956fn render_gpu_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
957    let mut content = String::from("GPU Information & Performance Summary\n\n");
958
959    if results.gpus.is_empty() {
960        content.push_str("No GPU data available\n\nRun benchmarks to see GPU information");
961    } else {
962        for gpu in &results.gpus {
963            content.push_str(&format!(
964                "GPU {}: {}\nVRAM: {:.1} GB\nCompute Units: {}\nTensor Cores: {}\n",
965                gpu.index,
966                gpu.model,
967                gpu.vram_gb,
968                gpu.compute_units,
969                if gpu.tensor_cores { "Yes" } else { "No" }
970            ));
971
972            if let (Some(temp), Some(power)) = (gpu.temperature_c, gpu.power_watts) {
973                content.push_str(&format!(
974                    "Temperature: {:.1}°C | Power: {:.1}W\n",
975                    temp, power
976                ));
977            }
978
979            if let (Some(util), Some(mem)) = (gpu.utilization_percent, gpu.memory_percent) {
980                content.push_str(&format!(
981                    "GPU Utilization: {:.1}% | Memory Usage: {:.1}%\n",
982                    util, mem
983                ));
984            }
985
986            if let (Some(sclk), Some(mclk)) = (gpu.sclk_mhz, gpu.mclk_mhz) {
987                content.push_str(&format!(
988                    "GFX Clock: {} MHz | Memory Clock: {} MHz\n",
989                    sclk, mclk
990                ));
991            }
992
993            content.push_str("---\n\n");
994        }
995
996        content.push_str("Best Observed Performance:\n");
997        let mut has_perf = false;
998
999        if let Some(mem) = &results.memory_bandwidth {
1000            content.push_str(&format!(
1001                "  • Memory Bandwidth: {:.1} GB/s\n",
1002                mem.hbm_peak_gb_s
1003            ));
1004            has_perf = true;
1005        }
1006        if let Some(tc) = &results.tensor_core {
1007            content.push_str(&format!(
1008                "  • Tensor Cores (FP16): {:.1} TFLOPS\n",
1009                tc.fp16_tflops
1010            ));
1011            has_perf = true;
1012        }
1013        if let Some(torch) = &results.pytorch {
1014            content.push_str(&format!(
1015                "  • PyTorch GEMM: {:.1} GFLOPS\n",
1016                torch.gemm_gflops
1017            ));
1018            has_perf = true;
1019        }
1020        if let Some(llama) = &results.llama_cpp {
1021            content.push_str(&format!(
1022                "  • LLaMA.cpp: {} on {} GPU(s)\n",
1023                llama.model,
1024                llama.gpus.len()
1025            ));
1026            has_perf = true;
1027        }
1028        if let Some(fa) = &results.flash_attention {
1029            content.push_str(&format!(
1030                "  • Flash Attention: {:.1} tok/s ({:.2}x speedup)\n",
1031                fa.flash_attention_speed, fa.speedup
1032            ));
1033            has_perf = true;
1034        }
1035        if let Some(v) = &results.vllm {
1036            if v.throughput_tokens_per_sec > 0.0 {
1037                content.push_str(&format!(
1038                    "  • vLLM Throughput: {:.1} tok/s\n",
1039                    v.throughput_tokens_per_sec
1040                ));
1041                has_perf = true;
1042            } else {
1043                content.push_str("  • vLLM: Benchmark degraded (see Recent errors)\n");
1044            }
1045        }
1046        if let Some(ds) = &results.deepspeed {
1047            if ds.throughput_samples_per_sec > 0.0 {
1048                content.push_str(&format!(
1049                    "  • DeepSpeed Throughput: {:.1} samples/s\n",
1050                    ds.throughput_samples_per_sec
1051                ));
1052                has_perf = true;
1053            }
1054        }
1055        if let Some(meg) = &results.megatron {
1056            if meg.throughput_samples_per_sec > 0.0 {
1057                content.push_str(&format!(
1058                    "  • Megatron Throughput: {:.1} samples/s\n",
1059                    meg.throughput_samples_per_sec
1060                ));
1061                has_perf = true;
1062            }
1063        }
1064        if let Some(onnx) = &results.onnx {
1065            if onnx.throughput_inf_per_sec > 0.0 {
1066                content.push_str(&format!(
1067                    "  • ONNX Runtime: {:.1} inf/s ({}, {})\n",
1068                    onnx.throughput_inf_per_sec, onnx.provider, onnx.ort_version
1069                ));
1070                has_perf = true;
1071            }
1072        }
1073
1074        if !has_perf {
1075            content.push_str("  (No performance benchmarks run yet)\n");
1076        }
1077        content.push('\n');
1078    }
1079
1080    if !results.errors.is_empty() {
1081        content.push_str("Recent errors:\n");
1082        for err in &results.errors {
1083            content.push_str(&format!("- {}\n", err));
1084        }
1085    }
1086
1087    let paragraph = Paragraph::new(content)
1088        .alignment(Alignment::Left)
1089        .block(Block::default().title("GPU Details").borders(Borders::ALL));
1090
1091    frame.render_widget(paragraph, area);
1092}
1093
1094fn render_memory_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1095    if let Some(mem) = &results.memory_bandwidth {
1096        let chunks = Layout::default()
1097            .direction(Direction::Vertical)
1098            .constraints([Constraint::Length(4), Constraint::Min(7)])
1099            .split(area);
1100
1101        let mut summary = format!(
1102            "HBM Peak: {:.1} GB/s | System Peak: {:.1} GB/s | Ratio: {:.2}x",
1103            mem.hbm_peak_gb_s, mem.system_peak_gb_s, mem.hbm_ratio
1104        );
1105
1106        if let Some(baseline) = &results.baseline {
1107            if let Some(b_mem) = &baseline.memory_bandwidth {
1108                let diff =
1109                    ((mem.hbm_peak_gb_s - b_mem.hbm_peak_gb_s) / b_mem.hbm_peak_gb_s) * 100.0;
1110                summary.push_str(&format!(
1111                    " (Baseline: {:.1} GB/s | {:+.1}%)",
1112                    b_mem.hbm_peak_gb_s, diff
1113                ));
1114            }
1115        }
1116
1117        frame.render_widget(
1118            Paragraph::new(summary)
1119                .alignment(Alignment::Left)
1120                .style(Style::default().fg(if results.baseline.is_some() {
1121                    Color::Green
1122                } else {
1123                    Color::White
1124                }))
1125                .block(
1126                    Block::default()
1127                        .borders(Borders::ALL)
1128                        .title("Summary & Comparison"),
1129                ),
1130            chunks[0],
1131        );
1132
1133        let hbm_data = prepare_data(&mem.hbm_samples);
1134        let system_data = prepare_data(&mem.system_samples);
1135        let mut datasets = Vec::new();
1136        if !hbm_data.is_empty() {
1137            datasets.push(series_dataset("HBM", &hbm_data, Color::Cyan));
1138        }
1139        if !system_data.is_empty() {
1140            datasets.push(series_dataset("System", &system_data, Color::Yellow));
1141        }
1142        let y_values: Vec<f64> = mem
1143            .hbm_samples
1144            .iter()
1145            .chain(mem.system_samples.iter())
1146            .cloned()
1147            .collect();
1148        let (y0, y1) = y_bounds(&y_values);
1149        let x_bounds = [0.0, (mem.hbm_samples.len().max(1) - 1) as f64];
1150
1151        frame.render_widget(
1152            Chart::new(datasets)
1153                .block(
1154                    Block::default()
1155                        .borders(Borders::ALL)
1156                        .title("Memory Bandwidth Performance"),
1157                )
1158                .x_axis(
1159                    Axis::default()
1160                        .title("Buffer Size (MB)")
1161                        .style(Style::default().fg(Color::Gray))
1162                        .bounds(x_bounds)
1163                        .labels(vec![
1164                            Line::from("64"),
1165                            Line::from("128"),
1166                            Line::from("256"),
1167                            Line::from("512"),
1168                        ]),
1169                )
1170                .y_axis(
1171                    Axis::default()
1172                        .title("GB/s")
1173                        .style(Style::default().fg(Color::Gray))
1174                        .bounds([y0, y1])
1175                        .labels(vec![
1176                            Line::from(format!("{:.0}", y0)),
1177                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1178                            Line::from(format!("{:.0}", y1)),
1179                        ]),
1180                ),
1181            chunks[1],
1182        );
1183    } else {
1184        frame.render_widget(
1185            Paragraph::new("No memory data available\n\nRun benchmarks to see memory bandwidth")
1186                .alignment(Alignment::Left)
1187                .block(
1188                    Block::default()
1189                        .title("Memory Bandwidth")
1190                        .borders(Borders::ALL),
1191                ),
1192            area,
1193        );
1194    }
1195}
1196
1197fn render_tensor_core_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1198    if let Some(tc) = &results.tensor_core {
1199        let chunks = Layout::default()
1200            .direction(Direction::Vertical)
1201            .constraints([Constraint::Length(3), Constraint::Min(7)])
1202            .split(area);
1203
1204        let summary = format!(
1205            "FP16: {:.1} | BF16: {:.1} | FP32: {:.1} TFLOPS",
1206            tc.fp16_tflops, tc.bf16_tflops, tc.fp32_tflops
1207        );
1208        frame.render_widget(
1209            Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1210            chunks[0],
1211        );
1212
1213        let fp16_data = prepare_data(&tc.fp16_samples);
1214        let mut datasets = Vec::new();
1215        if !fp16_data.is_empty() {
1216            datasets.push(series_dataset("FP16 sweep", &fp16_data, Color::Magenta));
1217        }
1218        let (y0, y1) = y_bounds(&tc.fp16_samples);
1219        let x_bounds = [0.0, (tc.fp16_samples.len().max(1) - 1) as f64];
1220
1221        frame.render_widget(
1222            Chart::new(datasets)
1223                .block(
1224                    Block::default()
1225                        .borders(Borders::ALL)
1226                        .title("Tensor Core Performance"),
1227                )
1228                .x_axis(
1229                    Axis::default()
1230                        .title("Matrix Size (N x N)")
1231                        .style(Style::default().fg(Color::Gray))
1232                        .bounds(x_bounds)
1233                        .labels(vec![
1234                            Line::from("512"),
1235                            Line::from("1024"),
1236                            Line::from("2048"),
1237                        ]),
1238                )
1239                .y_axis(
1240                    Axis::default()
1241                        .title("TFLOPS")
1242                        .style(Style::default().fg(Color::Gray))
1243                        .bounds([y0, y1])
1244                        .labels(vec![
1245                            Line::from(format!("{:.0}", y0)),
1246                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1247                            Line::from(format!("{:.0}", y1)),
1248                        ]),
1249                ),
1250            chunks[1],
1251        );
1252    } else {
1253        frame.render_widget(
1254            Paragraph::new("No Tensor Core data available\n\nRun benchmarks to see performance")
1255                .alignment(Alignment::Left)
1256                .block(
1257                    Block::default()
1258                        .title("Tensor Core Performance")
1259                        .borders(Borders::ALL),
1260                ),
1261            area,
1262        );
1263    }
1264}
1265
1266fn render_pytorch_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1267    if let Some(torch) = &results.pytorch {
1268        let chunks = Layout::default()
1269            .direction(Direction::Vertical)
1270            .constraints([Constraint::Length(3), Constraint::Min(7)])
1271            .split(area);
1272
1273        let summary = format!(
1274            "GEMM: {:.1} GFLOPS | Conv: {:.1} GFLOPS | Autograd overhead: {:.1}%",
1275            torch.gemm_gflops, torch.convolution_gflops, torch.autograd_overhead_percent
1276        );
1277        frame.render_widget(
1278            Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1279            chunks[0],
1280        );
1281
1282        let gemm_data = prepare_data(&torch.gemm_samples);
1283        let conv_data = prepare_data(&torch.conv_samples);
1284        let mut datasets = Vec::new();
1285        if !gemm_data.is_empty() {
1286            datasets.push(series_dataset("GEMM", &gemm_data, Color::Cyan));
1287        }
1288        if !conv_data.is_empty() {
1289            datasets.push(series_dataset("Conv", &conv_data, Color::Yellow));
1290        }
1291        let y_values: Vec<f64> = torch
1292            .gemm_samples
1293            .iter()
1294            .chain(torch.conv_samples.iter())
1295            .cloned()
1296            .collect();
1297        let (y0, y1) = y_bounds(&y_values);
1298        let x_bounds = [0.0, (torch.gemm_samples.len().max(1) - 1) as f64];
1299
1300        frame.render_widget(
1301            Chart::new(datasets)
1302                .block(
1303                    Block::default()
1304                        .borders(Borders::ALL)
1305                        .title("PyTorch Kernel Performance"),
1306                )
1307                .x_axis(
1308                    Axis::default()
1309                        .title("Benchmark Trial")
1310                        .style(Style::default().fg(Color::Gray))
1311                        .bounds(x_bounds)
1312                        .labels(vec![
1313                            Line::from("Trial 1"),
1314                            Line::from("Trial 2"),
1315                            Line::from("Trial 3"),
1316                        ]),
1317                )
1318                .y_axis(
1319                    Axis::default()
1320                        .title("GFLOPS")
1321                        .style(Style::default().fg(Color::Gray))
1322                        .bounds([y0, y1])
1323                        .labels(vec![
1324                            Line::from(format!("{:.0}", y0)),
1325                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1326                            Line::from(format!("{:.0}", y1)),
1327                        ]),
1328                ),
1329            chunks[1],
1330        );
1331    } else {
1332        frame.render_widget(
1333            Paragraph::new("No PyTorch data available\n\nRun benchmarks to see performance")
1334                .alignment(Alignment::Left)
1335                .block(
1336                    Block::default()
1337                        .title("PyTorch Performance")
1338                        .borders(Borders::ALL),
1339                ),
1340            area,
1341        );
1342    }
1343}
1344
1345fn render_flash_attention_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1346    if let Some(fa) = &results.flash_attention {
1347        let chunks = Layout::default()
1348            .direction(Direction::Vertical)
1349            .constraints([Constraint::Length(3), Constraint::Min(7)])
1350            .split(area);
1351
1352        let summary = format!(
1353            "Standard: {:.1} tok/s | Flash: {:.1} tok/s | Speedup: {:.2}x | Saved: {:.2} GB",
1354            fa.standard_attention_speed, fa.flash_attention_speed, fa.speedup, fa.memory_savings_gb
1355        );
1356        frame.render_widget(
1357            Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1358            chunks[0],
1359        );
1360
1361        let standard_data = prepare_data(&fa.standard_samples);
1362        let flash_data = prepare_data(&fa.flash_samples);
1363        let mut datasets = Vec::new();
1364        if !standard_data.is_empty() {
1365            datasets.push(series_dataset("Standard", &standard_data, Color::Yellow));
1366        }
1367        if !flash_data.is_empty() {
1368            datasets.push(series_dataset("Flash", &flash_data, Color::Green));
1369        }
1370        let y_values: Vec<f64> = fa
1371            .standard_samples
1372            .iter()
1373            .chain(fa.flash_samples.iter())
1374            .cloned()
1375            .collect();
1376        let (y0, y1) = y_bounds(&y_values);
1377        let x_bounds = [0.0, (fa.flash_samples.len().max(1) - 1) as f64];
1378
1379        frame.render_widget(
1380            Chart::new(datasets)
1381                .block(
1382                    Block::default()
1383                        .borders(Borders::ALL)
1384                        .title("Flash Attention Speedup"),
1385                )
1386                .x_axis(
1387                    Axis::default()
1388                        .title("Sequence Length")
1389                        .style(Style::default().fg(Color::Gray))
1390                        .bounds(x_bounds)
1391                        .labels(vec![
1392                            Line::from("128"),
1393                            Line::from("256"),
1394                            Line::from("512"),
1395                            Line::from("1024"),
1396                        ]),
1397                )
1398                .y_axis(
1399                    Axis::default()
1400                        .title("Tokens/s")
1401                        .style(Style::default().fg(Color::Gray))
1402                        .bounds([y0, y1])
1403                        .labels(vec![
1404                            Line::from(format!("{:.0}", y0)),
1405                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1406                            Line::from(format!("{:.0}", y1)),
1407                        ]),
1408                ),
1409            chunks[1],
1410        );
1411    } else {
1412        frame.render_widget(
1413            Paragraph::new("No Flash Attention data available\n\nRun benchmarks to see comparison")
1414                .alignment(Alignment::Left)
1415                .block(
1416                    Block::default()
1417                        .title("Flash Attention vs Standard")
1418                        .borders(Borders::ALL),
1419                ),
1420            area,
1421        );
1422    }
1423}
1424
1425fn render_llama_cpp_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1426    if let Some(llama) = &results.llama_cpp {
1427        let chunks = Layout::default()
1428            .direction(Direction::Vertical)
1429            .constraints([
1430                Constraint::Length(5),
1431                Constraint::Length(8),
1432                Constraint::Min(8),
1433            ])
1434            .split(area);
1435
1436        let summary = format!(
1437            "Model: {} | ROCm: {} | GPUs: {}",
1438            llama.model,
1439            llama.rocm_version,
1440            llama.gpus.len()
1441        );
1442        frame.render_widget(
1443            Paragraph::new(summary).block(Block::default().borders(Borders::ALL).title("Summary")),
1444            chunks[0],
1445        );
1446
1447        let gpu_lines = llama
1448            .gpus
1449            .iter()
1450            .enumerate()
1451            .map(|(idx, gpu)| {
1452                Line::from(format!(
1453                    "GPU {}: {} | {} | {:.1} GB",
1454                    idx, gpu.name, gpu.arch, gpu.vram_gb
1455                ))
1456            })
1457            .collect::<Vec<_>>();
1458        frame.render_widget(
1459            Paragraph::new(Text::from(gpu_lines))
1460                .block(Block::default().borders(Borders::ALL).title("GPU Info")),
1461            chunks[1],
1462        );
1463
1464        let mut body = String::from("Prefill Throughput (tokens/sec)\n");
1465        for gpu in &llama.gpus {
1466            body.push_str(&format!("GPU: {} ({})\n", gpu.name, gpu.arch));
1467            for context in [512_u32, 2048_u32, 8192_u32] {
1468                let value = llama
1469                    .prefill
1470                    .iter()
1471                    .find(|p| {
1472                        p.gpu
1473                            == llama
1474                                .gpus
1475                                .iter()
1476                                .position(|g| g.name == gpu.name)
1477                                .unwrap_or(0)
1478                            && p.context == Some(context)
1479                    })
1480                    .map(|p| p.avg_tps)
1481                    .unwrap_or(0.0);
1482                body.push_str(&format!("  {}: {:.2}\n", context, value));
1483            }
1484            body.push('\n');
1485        }
1486        body.push_str("Decode Throughput (tokens/sec)\n");
1487        for gpu in &llama.gpus {
1488            body.push_str(&format!("GPU: {} ({})\n", gpu.name, gpu.arch));
1489            {
1490                let n_gen = 128_u32;
1491                let value = llama
1492                    .decode
1493                    .iter()
1494                    .find(|p| {
1495                        p.gpu
1496                            == llama
1497                                .gpus
1498                                .iter()
1499                                .position(|g| g.name == gpu.name)
1500                                .unwrap_or(0)
1501                            && p.n_gen == Some(n_gen)
1502                    })
1503                    .map(|p| p.avg_tps)
1504                    .unwrap_or(0.0);
1505                body.push_str(&format!("  {}: {:.2}\n", n_gen, value));
1506            }
1507        }
1508        frame.render_widget(
1509            Paragraph::new(body).block(
1510                Block::default()
1511                    .borders(Borders::ALL)
1512                    .title("Throughput Tables"),
1513            ),
1514            chunks[2],
1515        );
1516    } else {
1517        frame.render_widget(
1518            Paragraph::new("No LLaMA.cpp data available\n\nRun llama-cpp benchmark")
1519                .alignment(Alignment::Left)
1520                .block(
1521                    Block::default()
1522                        .title("LLaMA.cpp Performance")
1523                        .borders(Borders::ALL),
1524                ),
1525            area,
1526        );
1527    }
1528}
1529
1530fn render_vllm_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1531    if let Some(v) = &results.vllm {
1532        let chunks = Layout::default()
1533            .direction(Direction::Vertical)
1534            .constraints([Constraint::Length(4), Constraint::Min(7)])
1535            .split(area);
1536
1537        let mut summary = format!(
1538            "Model: {} ({}) | Throughput: {:.1} tok/s | Latency: {:.1} ms",
1539            v.model, v.model_format, v.throughput_tokens_per_sec, v.latency_ms
1540        );
1541
1542        if let Some(baseline) = &results.baseline {
1543            if let Some(b_vllm) = &baseline.vllm {
1544                if b_vllm.throughput_tokens_per_sec > 0.0 && v.throughput_tokens_per_sec > 0.0 {
1545                    let diff = ((v.throughput_tokens_per_sec - b_vllm.throughput_tokens_per_sec)
1546                        / b_vllm.throughput_tokens_per_sec)
1547                        * 100.0;
1548                    summary.push_str(&format!(
1549                        " (Baseline: {:.1} | {:+.1}%)",
1550                        b_vllm.throughput_tokens_per_sec, diff
1551                    ));
1552                }
1553            }
1554        }
1555
1556        let mut summary_text = Text::from(summary);
1557        if v.throughput_tokens_per_sec == 0.0 {
1558            summary_text.lines.push(Line::from(""));
1559            summary_text.lines.push(Line::from(vec![
1560                Span::styled("⚠ WARNING: ", Style::default().fg(Color::Yellow)),
1561                Span::raw(
1562                    "Benchmark produced no results. Ensure a tiny safetensors model or local tiny GGUF model is configured.",
1563                ),
1564            ]));
1565
1566            // Check for vLLM specific errors
1567            for err in &results.errors {
1568                let lowered = err.to_ascii_lowercase();
1569                if lowered.contains("vllm")
1570                    || lowered.contains("safetensors model")
1571                    || lowered.contains("torch.cuda")
1572                    || lowered.contains("hip gpu")
1573                    || lowered.contains("engine core initialization failed")
1574                {
1575                    summary_text.lines.push(Line::from(vec![
1576                        Span::styled("ERROR: ", Style::default().fg(Color::Red)),
1577                        Span::raw(err),
1578                    ]));
1579                }
1580            }
1581        }
1582
1583        frame.render_widget(
1584            Paragraph::new(summary_text).block(
1585                Block::default()
1586                    .borders(Borders::ALL)
1587                    .title("Summary & Comparison"),
1588            ),
1589            chunks[0],
1590        );
1591
1592        let throughput_data = prepare_data(&v.throughput_samples);
1593        let mut datasets = Vec::new();
1594        if !throughput_data.is_empty() {
1595            datasets.push(series_dataset("Throughput", &throughput_data, Color::Cyan));
1596        }
1597        let (y0, y1) = y_bounds(&v.throughput_samples);
1598        let x_bounds = [0.0, (v.throughput_samples.len().max(1) - 1) as f64];
1599
1600        frame.render_widget(
1601            Chart::new(datasets)
1602                .block(
1603                    Block::default()
1604                        .borders(Borders::ALL)
1605                        .title("vLLM Throughput Performance"),
1606                )
1607                .x_axis(
1608                    Axis::default()
1609                        .title("Benchmark Trial")
1610                        .style(Style::default().fg(Color::Gray))
1611                        .bounds(x_bounds)
1612                        .labels(vec![
1613                            Line::from("T1"),
1614                            Line::from(format!("T{}", v.throughput_samples.len() / 2 + 1)),
1615                            Line::from(format!("T{}", v.throughput_samples.len())),
1616                        ]),
1617                )
1618                .y_axis(
1619                    Axis::default()
1620                        .title("Tokens/s")
1621                        .style(Style::default().fg(Color::Gray))
1622                        .bounds([y0, y1])
1623                        .labels(vec![
1624                            Line::from(format!("{:.0}", y0)),
1625                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1626                            Line::from(format!("{:.0}", y1)),
1627                        ]),
1628                ),
1629            chunks[1],
1630        );
1631    } else {
1632        frame.render_widget(
1633            Paragraph::new("No vLLM data available\n\nInstall vLLM and rerun benchmarks")
1634                .alignment(Alignment::Left)
1635                .block(
1636                    Block::default()
1637                        .title("vLLM Performance")
1638                        .borders(Borders::ALL),
1639                ),
1640            area,
1641        );
1642    }
1643}
1644
1645fn render_deepspeed_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1646    if let Some(ds) = &results.deepspeed {
1647        let chunks = Layout::default()
1648            .direction(Direction::Vertical)
1649            .constraints([Constraint::Length(4), Constraint::Min(7)])
1650            .split(area);
1651
1652        let mut summary = format!(
1653            "Accelerator: {} | ZeRO Stage: {} | Throughput: {:.1} samples/s | Latency: {:.1} ms",
1654            ds.accelerator, ds.stage, ds.throughput_samples_per_sec, ds.avg_latency_ms
1655        );
1656
1657        if let Some(baseline) = &results.baseline {
1658            if let Some(b_ds) = &baseline.deepspeed {
1659                if b_ds.throughput_samples_per_sec > 0.0 {
1660                    let diff = ((ds.throughput_samples_per_sec - b_ds.throughput_samples_per_sec)
1661                        / b_ds.throughput_samples_per_sec)
1662                        * 100.0;
1663                    summary.push_str(&format!(
1664                        " (Baseline: {:.1} | {:+.1}%)",
1665                        b_ds.throughput_samples_per_sec, diff
1666                    ));
1667                }
1668            }
1669        }
1670
1671        frame.render_widget(
1672            Paragraph::new(summary).block(
1673                Block::default()
1674                    .borders(Borders::ALL)
1675                    .title("Summary & Comparison"),
1676            ),
1677            chunks[0],
1678        );
1679
1680        let ds_data = prepare_data(&ds.samples);
1681        let mut datasets = Vec::new();
1682        if !ds_data.is_empty() {
1683            datasets.push(series_dataset("DeepSpeed", &ds_data, Color::White));
1684        }
1685        let (y0, y1) = y_bounds(&ds.samples);
1686        let x_bounds = [0.0, (ds.samples.len().max(1) - 1) as f64];
1687
1688        frame.render_widget(
1689            Chart::new(datasets)
1690                .block(
1691                    Block::default()
1692                        .borders(Borders::ALL)
1693                        .title("DeepSpeed Throughput Performance"),
1694                )
1695                .x_axis(
1696                    Axis::default()
1697                        .title("Benchmark Trial")
1698                        .style(Style::default().fg(Color::Gray))
1699                        .bounds(x_bounds)
1700                        .labels(vec![
1701                            Line::from("T1"),
1702                            Line::from(format!("T{}", ds.samples.len() / 2 + 1)),
1703                            Line::from(format!("T{}", ds.samples.len())),
1704                        ]),
1705                )
1706                .y_axis(
1707                    Axis::default()
1708                        .title("Samples/s")
1709                        .style(Style::default().fg(Color::Gray))
1710                        .bounds([y0, y1])
1711                        .labels(vec![
1712                            Line::from(format!("{:.0}", y0)),
1713                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1714                            Line::from(format!("{:.0}", y1)),
1715                        ]),
1716                ),
1717            chunks[1],
1718        );
1719    } else {
1720        frame.render_widget(
1721            Paragraph::new("No DeepSpeed data available\n\nRun DeepSpeed Performance benchmark")
1722                .alignment(Alignment::Left)
1723                .block(
1724                    Block::default()
1725                        .title("DeepSpeed Performance")
1726                        .borders(Borders::ALL),
1727                ),
1728            area,
1729        );
1730    }
1731}
1732
1733fn render_megatron_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1734    if let Some(meg) = &results.megatron {
1735        let chunks = Layout::default()
1736            .direction(Direction::Vertical)
1737            .constraints([Constraint::Length(4), Constraint::Min(7)])
1738            .split(area);
1739
1740        let mut summary = format!(
1741            "Backend: {} | Throughput: {:.1} samples/s | Latency: {:.1} ms",
1742            meg.backend, meg.throughput_samples_per_sec, meg.avg_latency_ms
1743        );
1744
1745        if let Some(baseline) = &results.baseline {
1746            if let Some(b_meg) = &baseline.megatron {
1747                if b_meg.throughput_samples_per_sec > 0.0 {
1748                    let diff = ((meg.throughput_samples_per_sec
1749                        - b_meg.throughput_samples_per_sec)
1750                        / b_meg.throughput_samples_per_sec)
1751                        * 100.0;
1752                    summary.push_str(&format!(
1753                        " (Baseline: {:.1} | {:+.1}%)",
1754                        b_meg.throughput_samples_per_sec, diff
1755                    ));
1756                }
1757            }
1758        }
1759
1760        frame.render_widget(
1761            Paragraph::new(summary).block(
1762                Block::default()
1763                    .borders(Borders::ALL)
1764                    .title("Summary & Comparison"),
1765            ),
1766            chunks[0],
1767        );
1768
1769        let series = prepare_data(&meg.samples);
1770        let mut datasets = Vec::new();
1771        if !series.is_empty() {
1772            datasets.push(series_dataset("Megatron", &series, Color::LightBlue));
1773        }
1774        let (y0, y1) = y_bounds(&meg.samples);
1775        let x_bounds = [0.0, (meg.samples.len().max(1) - 1) as f64];
1776
1777        frame.render_widget(
1778            Chart::new(datasets)
1779                .block(
1780                    Block::default()
1781                        .borders(Borders::ALL)
1782                        .title("Megatron Throughput Performance"),
1783                )
1784                .x_axis(
1785                    Axis::default()
1786                        .title("Benchmark Trial")
1787                        .style(Style::default().fg(Color::Gray))
1788                        .bounds(x_bounds)
1789                        .labels(vec![
1790                            Line::from("T1"),
1791                            Line::from(format!("T{}", meg.samples.len() / 2 + 1)),
1792                            Line::from(format!("T{}", meg.samples.len())),
1793                        ]),
1794                )
1795                .y_axis(
1796                    Axis::default()
1797                        .title("Samples/s")
1798                        .style(Style::default().fg(Color::Gray))
1799                        .bounds([y0, y1])
1800                        .labels(vec![
1801                            Line::from(format!("{:.0}", y0)),
1802                            Line::from(format!("{:.0}", (y0 + y1) / 2.0)),
1803                            Line::from(format!("{:.0}", y1)),
1804                        ]),
1805                ),
1806            chunks[1],
1807        );
1808    } else {
1809        frame.render_widget(
1810            Paragraph::new("No Megatron data available\n\nRun Megatron-LM Performance benchmark")
1811                .alignment(Alignment::Left)
1812                .block(
1813                    Block::default()
1814                        .title("Megatron-LM Performance")
1815                        .borders(Borders::ALL),
1816                ),
1817            area,
1818        );
1819    }
1820}
1821
1822fn render_onnx_tab(frame: &mut Frame, area: Rect, results: &BenchmarkResults) {
1823    if let Some(onnx) = &results.onnx {
1824        let chunks = Layout::default()
1825            .direction(Direction::Vertical)
1826            .constraints([Constraint::Length(6), Constraint::Min(7)])
1827            .split(area);
1828
1829        let mut summary = format!(
1830            "ORT {} | Provider: {} | Opt: {}\nThroughput: {:.1} inf/s | Latency p50: {:.2} ms p95: {:.2} ms p99: {:.2} ms",
1831            onnx.ort_version,
1832            onnx.provider,
1833            onnx.graph_opt_level,
1834            onnx.throughput_inf_per_sec,
1835            onnx.inference_latency_p50_ms,
1836            onnx.inference_latency_p95_ms,
1837            onnx.inference_latency_p99_ms,
1838        );
1839
1840        if onnx.quantized_supported {
1841            summary.push_str(" | Quantized: supported");
1842        }
1843
1844        if onnx.peak_rss_mb > 0.0 {
1845            summary.push_str(&format!(" | Peak RSS: {:.0} MB", onnx.peak_rss_mb));
1846        }
1847
1848        if let Some(baseline) = &results.baseline {
1849            if let Some(b_onnx) = &baseline.onnx {
1850                if b_onnx.throughput_inf_per_sec > 0.0 {
1851                    let diff = ((onnx.throughput_inf_per_sec - b_onnx.throughput_inf_per_sec)
1852                        / b_onnx.throughput_inf_per_sec)
1853                        * 100.0;
1854                    summary.push_str(&format!(
1855                        "\nBaseline: {:.1} inf/s ({:+.1}%) | Session create: {:.1} ms",
1856                        b_onnx.throughput_inf_per_sec, diff, onnx.session_create_ms,
1857                    ));
1858                }
1859            }
1860        }
1861
1862        if !onnx.providers_available.is_empty() {
1863            summary.push_str(&format!(
1864                "\nAvailable providers: {}",
1865                onnx.providers_available.join(", ")
1866            ));
1867        }
1868
1869        frame.render_widget(
1870            Paragraph::new(summary).block(
1871                Block::default()
1872                    .borders(Borders::ALL)
1873                    .title("ONNX Runtime Summary"),
1874            ),
1875            chunks[0],
1876        );
1877
1878        let series = prepare_data(&onnx.inference_samples);
1879        let mut datasets = Vec::new();
1880        if !series.is_empty() {
1881            datasets.push(series_dataset("ONNX Inference", &series, Color::LightGreen));
1882        }
1883        let (y0, y1) = y_bounds(&onnx.inference_samples);
1884        let x_bounds = [0.0, (onnx.inference_samples.len().max(1) - 1) as f64];
1885
1886        frame.render_widget(
1887            Chart::new(datasets)
1888                .block(
1889                    Block::default()
1890                        .borders(Borders::ALL)
1891                        .title("ONNX Inference Latency per Trial"),
1892                )
1893                .x_axis(
1894                    Axis::default()
1895                        .title("Benchmark Trial")
1896                        .style(Style::default().fg(Color::Gray))
1897                        .bounds(x_bounds)
1898                        .labels(vec![
1899                            Line::from("T1"),
1900                            Line::from(format!("T{}", onnx.inference_samples.len() / 2 + 1)),
1901                            Line::from(format!("T{}", onnx.inference_samples.len())),
1902                        ]),
1903                )
1904                .y_axis(
1905                    Axis::default()
1906                        .title("ms")
1907                        .style(Style::default().fg(Color::Gray))
1908                        .bounds([y0, y1])
1909                        .labels(vec![
1910                            Line::from(format!("{:.2}", y0)),
1911                            Line::from(format!("{:.2}", (y0 + y1) / 2.0)),
1912                            Line::from(format!("{:.2}", y1)),
1913                        ]),
1914                ),
1915            chunks[1],
1916        );
1917    } else {
1918        frame.render_widget(
1919            Paragraph::new("No ONNX Runtime data available\n\nRun ONNX Performance benchmark")
1920                .alignment(Alignment::Left)
1921                .block(
1922                    Block::default()
1923                        .title("ONNX Runtime Performance")
1924                        .borders(Borders::ALL),
1925                ),
1926            area,
1927        );
1928    }
1929}
1930
1931fn series_dataset<'a>(name: &'a str, data: &'a [(f64, f64)], color: Color) -> Dataset<'a> {
1932    Dataset::default()
1933        .name(name)
1934        .marker(symbols::Marker::Braille)
1935        .style(Style::default().fg(color))
1936        .graph_type(GraphType::Line)
1937        .data(data)
1938}
1939
1940fn prepare_data(values: &[f64]) -> Vec<(f64, f64)> {
1941    values
1942        .iter()
1943        .enumerate()
1944        .map(|(i, v)| (i as f64, *v))
1945        .collect()
1946}
1947
1948fn y_bounds(values: &[f64]) -> (f64, f64) {
1949    let max = values.iter().cloned().fold(0.0_f64, f64::max);
1950    if max == 0.0 {
1951        (0.0, 1.0)
1952    } else {
1953        (0.0, (max * 1.2).ceil())
1954    }
1955}
1956
1957pub fn export_benchmark_report_html(
1958    results: &BenchmarkResults,
1959    output_path: Option<&Path>,
1960) -> Result<PathBuf, String> {
1961    let report_path = resolve_report_path(output_path)?;
1962    if let Some(parent) = report_path.parent() {
1963        fs::create_dir_all(parent).map_err(|e| {
1964            format!(
1965                "failed to create report directory {}: {}",
1966                parent.display(),
1967                e
1968            )
1969        })?;
1970    }
1971
1972    let generated_at = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
1973    let report_data = serde_json::json!({
1974        "generated_at": generated_at,
1975        "summary": build_summary_rows(results),
1976        "metric_rows": build_metric_rows(results),
1977        "sample_rows": build_sample_rows(results),
1978        "errors": results.errors,
1979        "gpus": results.gpus.iter().map(|g| serde_json::json!({
1980            "index": g.index,
1981            "model": g.model,
1982            "vram_gb": g.vram_gb,
1983            "compute_units": g.compute_units,
1984            "tensor_cores": g.tensor_cores,
1985            "temperature_c": g.temperature_c,
1986            "power_watts": g.power_watts,
1987            "utilization_percent": g.utilization_percent,
1988            "memory_percent": g.memory_percent
1989        })).collect::<Vec<_>>(),
1990        "charts": {
1991            "memory_hbm": results.memory_bandwidth.as_ref().map(|m| m.hbm_samples.clone()).unwrap_or_default(),
1992            "memory_system": results.memory_bandwidth.as_ref().map(|m| m.system_samples.clone()).unwrap_or_default(),
1993            "tensor_fp16": results.tensor_core.as_ref().map(|t| t.fp16_samples.clone()).unwrap_or_default(),
1994            "pytorch_gemm": results.pytorch.as_ref().map(|p| p.gemm_samples.clone()).unwrap_or_default(),
1995            "pytorch_conv": results.pytorch.as_ref().map(|p| p.conv_samples.clone()).unwrap_or_default(),
1996            "llama_cpp": results.llama_cpp.as_ref().map(|l| l.prefill.iter().map(|p| p.avg_tps).collect::<Vec<_>>()).unwrap_or_default(),
1997            "flash_standard": results.flash_attention.as_ref().map(|f| f.standard_samples.clone()).unwrap_or_default(),
1998            "flash_optimized": results.flash_attention.as_ref().map(|f| f.flash_samples.clone()).unwrap_or_default(),
1999            "vllm": results.vllm.as_ref().map(|v| v.throughput_samples.clone()).unwrap_or_default(),
2000            "deepspeed": results.deepspeed.as_ref().map(|d| d.samples.clone()).unwrap_or_default(),
2001            "megatron": results.megatron.as_ref().map(|m| m.samples.clone()).unwrap_or_default(),
2002            "onnx": results.onnx.as_ref().map(|o| o.inference_samples.clone()).unwrap_or_default()
2003        },
2004        "key_metrics": {
2005            "memory_hbm_peak": results.memory_bandwidth.as_ref().map(|m| m.hbm_peak_gb_s).unwrap_or(0.0),
2006            "tensor_fp16_tflops": results.tensor_core.as_ref().map(|t| t.fp16_tflops).unwrap_or(0.0),
2007            "pytorch_gemm_gflops": results.pytorch.as_ref().map(|p| p.gemm_gflops).unwrap_or(0.0),
2008            "llama_cpp_model": results.llama_cpp.as_ref().map(|l| l.model.clone()).unwrap_or_default(),
2009            "flash_speedup": results.flash_attention.as_ref().map(|f| f.speedup).unwrap_or(0.0),
2010            "vllm_tps": results.vllm.as_ref().map(|v| v.throughput_tokens_per_sec).unwrap_or(0.0),
2011            "deepspeed_sps": results.deepspeed.as_ref().map(|d| d.throughput_samples_per_sec).unwrap_or(0.0),
2012            "megatron_sps": results.megatron.as_ref().map(|m| m.throughput_samples_per_sec).unwrap_or(0.0),
2013            "onnx_inf_s": results.onnx.as_ref().map(|o| o.throughput_inf_per_sec).unwrap_or(0.0),
2014            "onnx_p50_ms": results.onnx.as_ref().map(|o| o.inference_latency_p50_ms).unwrap_or(0.0),
2015            "onnx_provider": results.onnx.as_ref().map(|o| o.provider.clone()).unwrap_or_default(),
2016            "onnx_ort_version": results.onnx.as_ref().map(|o| o.ort_version.clone()).unwrap_or_default()
2017        }
2018    });
2019    let report_json = serde_json::to_string_pretty(&report_data)
2020        .map_err(|e| format!("json serialization failed: {}", e))?;
2021
2022    let html = format!(
2023        r###"<!doctype html>
2024<html lang="en">
2025<head>
2026  <meta charset="utf-8" />
2027  <meta name="viewport" content="width=device-width, initial-scale=1" />
2028  <title>Rusty-Stack Benchmark Report</title>
2029  <style>
2030    :root {{
2031      --bg-0: #0b1220;
2032      --bg-1: #111a2d;
2033      --bg-2: #18243a;
2034      --card: #0f1728;
2035      --text: #e8edf9;
2036      --muted: #9fb0d0;
2037      --accent-a: #27d7c6;
2038      --accent-b: #74a7ff;
2039      --accent-c: #ffa24d;
2040      --ok: #3ddc84;
2041      --warn: #ffd166;
2042      --err: #ff6b6b;
2043      --line: #2a3a57;
2044    }}
2045    * {{ box-sizing: border-box; }}
2046    body {{
2047      margin: 0;
2048      font-family: "Space Grotesk", "Sora", "Segoe UI", sans-serif;
2049      color: var(--text);
2050      background:
2051        radial-gradient(900px 420px at 10% -10%, #1e365f 0%, transparent 60%),
2052        radial-gradient(700px 380px at 100% 0%, #23334f 0%, transparent 60%),
2053        linear-gradient(160deg, var(--bg-0), var(--bg-1), var(--bg-2));
2054      min-height: 100vh;
2055      padding: 24px;
2056    }}
2057    .wrap {{
2058      max-width: 1440px;
2059      margin: 0 auto;
2060      animation: fadeIn 420ms ease-out;
2061    }}
2062    .hero {{
2063      border: 1px solid var(--line);
2064      border-radius: 18px;
2065      padding: 20px 24px;
2066      background: linear-gradient(160deg, rgba(39, 215, 198, 0.08), rgba(116, 167, 255, 0.06), rgba(255, 162, 77, 0.08));
2067      backdrop-filter: blur(4px);
2068    }}
2069    .hero h1 {{
2070      margin: 0;
2071      font-size: 30px;
2072      letter-spacing: 0.2px;
2073    }}
2074    .meta {{
2075      margin-top: 10px;
2076      color: var(--muted);
2077      font-size: 14px;
2078    }}
2079    .grid {{
2080      margin-top: 20px;
2081      display: grid;
2082      gap: 14px;
2083      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
2084    }}
2085    .card {{
2086      border: 1px solid var(--line);
2087      border-radius: 14px;
2088      background: rgba(8, 14, 24, 0.75);
2089      padding: 14px;
2090      transform: translateY(10px);
2091      opacity: 0;
2092      animation: rise 400ms ease forwards;
2093    }}
2094    .k {{
2095      color: var(--muted);
2096      font-size: 12px;
2097      text-transform: uppercase;
2098      letter-spacing: 0.7px;
2099    }}
2100    .v {{
2101      margin-top: 8px;
2102      font-size: 28px;
2103      font-weight: 700;
2104      color: var(--accent-a);
2105    }}
2106    .section {{
2107      margin-top: 18px;
2108      border: 1px solid var(--line);
2109      border-radius: 14px;
2110      background: rgba(8, 14, 24, 0.72);
2111      padding: 14px;
2112    }}
2113    h2 {{
2114      margin: 0 0 10px 0;
2115      font-size: 20px;
2116      color: #f3f7ff;
2117    }}
2118    table {{
2119      width: 100%;
2120      border-collapse: collapse;
2121      font-size: 13px;
2122    }}
2123    th, td {{
2124      border-bottom: 1px solid #243452;
2125      padding: 8px 10px;
2126      text-align: left;
2127      vertical-align: top;
2128    }}
2129    th {{
2130      color: #bfccdf;
2131      font-weight: 600;
2132    }}
2133    .charts {{
2134      display: grid;
2135      gap: 14px;
2136      grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
2137    }}
2138    .plot {{
2139      border: 1px solid #253659;
2140      border-radius: 12px;
2141      padding: 10px;
2142      background: #0a1322;
2143    }}
2144    .plot h3 {{
2145      margin: 0 0 8px 0;
2146      font-size: 15px;
2147      color: #dce8ff;
2148    }}
2149    canvas {{
2150      width: 100%;
2151      height: 220px;
2152      border-radius: 8px;
2153      background: linear-gradient(180deg, rgba(26,41,66,0.35), rgba(11,18,32,0.65));
2154    }}
2155    .chip {{
2156      display: inline-block;
2157      padding: 3px 8px;
2158      border-radius: 999px;
2159      font-size: 12px;
2160      border: 1px solid #38517a;
2161      color: #b8c9e7;
2162      margin-right: 6px;
2163      margin-bottom: 6px;
2164    }}
2165    .err li {{
2166      margin-bottom: 6px;
2167      color: #ffd4d4;
2168    }}
2169    @keyframes fadeIn {{
2170      from {{ opacity: 0; }}
2171      to {{ opacity: 1; }}
2172    }}
2173    @keyframes rise {{
2174      from {{ opacity: 0; transform: translateY(10px); }}
2175      to {{ opacity: 1; transform: translateY(0); }}
2176    }}
2177    @media (max-width: 900px) {{
2178      .charts {{ grid-template-columns: 1fr; }}
2179      body {{ padding: 12px; }}
2180    }}
2181  </style>
2182</head>
2183<body>
2184  <div class="wrap">
2185    <div class="hero">
2186      <h1>Rusty-Stack Performance Report</h1>
2187      <div class="meta">Generated: <span id="generatedAt"></span></div>
2188      <div class="meta">Profile: Tiny safetensors/gguf benchmark policy enabled for model-based tests.</div>
2189    </div>
2190
2191    <div class="grid" id="metricCards"></div>
2192
2193    <div class="section">
2194      <h2>Summary Table</h2>
2195      <table id="summaryTable">
2196        <thead><tr><th>Component</th><th>Status</th><th>Primary Metric</th><th>Detail</th></tr></thead>
2197        <tbody></tbody>
2198      </table>
2199    </div>
2200
2201    <div class="section">
2202      <h2>Detailed Metrics</h2>
2203      <table id="metricsTable">
2204        <thead><tr><th>Component</th><th>Metric</th><th>Value</th><th>Unit</th></tr></thead>
2205        <tbody></tbody>
2206      </table>
2207    </div>
2208
2209    <div class="section">
2210      <h2>Sample Data (Text Summary)</h2>
2211      <table id="samplesTable">
2212        <thead><tr><th>Benchmark</th><th>Series</th><th>Samples</th></tr></thead>
2213        <tbody></tbody>
2214      </table>
2215    </div>
2216
2217    <div class="section">
2218      <h2>GPU Inventory</h2>
2219      <div id="gpuChips"></div>
2220      <table id="gpuTable">
2221        <thead><tr><th>GPU</th><th>VRAM (GB)</th><th>CUs</th><th>Tensor Cores</th><th>Utilization (%)</th></tr></thead>
2222        <tbody></tbody>
2223      </table>
2224    </div>
2225
2226    <div class="section">
2227      <h2>Benchmark Graphs</h2>
2228      <div class="charts">
2229        <div class="plot"><h3>Memory Bandwidth</h3><canvas id="chartMemory"></canvas></div>
2230        <div class="plot"><h3>Tensor Core FP16</h3><canvas id="chartTensor"></canvas></div>
2231        <div class="plot"><h3>PyTorch (GEMM/Conv)</h3><canvas id="chartPytorch"></canvas></div>
2232        <div class="plot"><h3>LLaMA.cpp Prefill</h3><canvas id="chartLlamaCpp"></canvas></div>
2233        <div class="plot"><h3>Flash Attention</h3><canvas id="chartFlash"></canvas></div>
2234        <div class="plot"><h3>vLLM Throughput</h3><canvas id="chartVllm"></canvas></div>
2235        <div class="plot"><h3>DeepSpeed Throughput</h3><canvas id="chartDeepspeed"></canvas></div>
2236        <div class="plot"><h3>Megatron Throughput</h3><canvas id="chartMegatron"></canvas></div>
2237        <div class="plot"><h3>ONNX Inference Latency</h3><canvas id="chartOnnx"></canvas></div>
2238      </div>
2239    </div>
2240
2241    <div class="section">
2242      <h2>Errors / Warnings</h2>
2243      <ul class="err" id="errors"></ul>
2244    </div>
2245  </div>
2246
2247  <script>
2248    const reportData = {report_json};
2249    document.getElementById("generatedAt").textContent = reportData.generated_at;
2250
2251    const cards = [
2252      ["HBM Peak (GB/s)", reportData.key_metrics.memory_hbm_peak],
2253      ["FP16 TFLOPS", reportData.key_metrics.tensor_fp16_tflops],
2254      ["PyTorch GEMM (GFLOPS)", reportData.key_metrics.pytorch_gemm_gflops],
2255      ["LLaMA.cpp Model", reportData.key_metrics.llama_cpp_model],
2256      ["Flash Speedup (x)", reportData.key_metrics.flash_speedup],
2257      ["vLLM tok/s", reportData.key_metrics.vllm_tps],
2258      ["DeepSpeed samp/s", reportData.key_metrics.deepspeed_sps],
2259      ["Megatron samp/s", reportData.key_metrics.megatron_sps],
2260      ["ONNX inf/s", reportData.key_metrics.onnx_inf_s]
2261    ];
2262    const metricCards = document.getElementById("metricCards");
2263    cards.forEach((entry, idx) => {{
2264      const c = document.createElement("div");
2265      c.className = "card";
2266      c.style.animationDelay = `${{idx * 35}}ms`;
2267      c.innerHTML = `<div class="k">${{entry[0]}}</div><div class="v">${{Number(entry[1] || 0).toFixed(2)}}</div>`;
2268      metricCards.appendChild(c);
2269    }});
2270
2271    const tbody = document.querySelector("#summaryTable tbody");
2272    (reportData.summary || []).forEach(row => {{
2273      const tr = document.createElement("tr");
2274      tr.innerHTML = `<td>${{row.component}}</td><td>${{row.status}}</td><td>${{row.metric}}</td><td>${{row.detail}}</td>`;
2275      tbody.appendChild(tr);
2276    }});
2277
2278    const metricsTbody = document.querySelector("#metricsTable tbody");
2279    (reportData.metric_rows || []).forEach(row => {{
2280      const tr = document.createElement("tr");
2281      tr.innerHTML = `<td>${{row.component}}</td><td>${{row.metric}}</td><td>${{Number(row.value || 0).toFixed(2)}}</td><td>${{row.unit}}</td>`;
2282      metricsTbody.appendChild(tr);
2283    }});
2284
2285    const samplesTbody = document.querySelector("#samplesTable tbody");
2286    (reportData.sample_rows || []).forEach(row => {{
2287      const tr = document.createElement("tr");
2288      tr.innerHTML = `<td>${{row.benchmark}}</td><td>${{row.series}}</td><td>${{(row.samples || []).map(v => Number(v).toFixed(2)).join(", ") || "-"}}</td>`;
2289      samplesTbody.appendChild(tr);
2290    }});
2291
2292    const gpuChips = document.getElementById("gpuChips");
2293    const gpuTbody = document.querySelector("#gpuTable tbody");
2294    (reportData.gpus || []).forEach(g => {{
2295      const chip = document.createElement("span");
2296      chip.className = "chip";
2297      chip.textContent = `GPU ${{g.index}} • ${{g.model}}`;
2298      gpuChips.appendChild(chip);
2299
2300      const tr = document.createElement("tr");
2301      tr.innerHTML = `<td>${{g.model}}</td><td>${{Number(g.vram_gb||0).toFixed(2)}}</td><td>${{g.compute_units}}</td><td>${{g.tensor_cores ? "yes" : "no"}}</td><td>${{g.utilization_percent ?? "-"}}</td>`;
2302      gpuTbody.appendChild(tr);
2303    }});
2304
2305    const errNode = document.getElementById("errors");
2306    if ((reportData.errors || []).length === 0) {{
2307      const li = document.createElement("li");
2308      li.textContent = "No benchmark errors were recorded.";
2309      errNode.appendChild(li);
2310    }} else {{
2311      reportData.errors.forEach(e => {{
2312        const li = document.createElement("li");
2313        li.textContent = e;
2314        errNode.appendChild(li);
2315      }});
2316    }}
2317
2318    function drawMultiLine(canvasId, series) {{
2319      const canvas = document.getElementById(canvasId);
2320      if (!canvas) return;
2321      const ctx = canvas.getContext("2d");
2322      const dpr = window.devicePixelRatio || 1;
2323      const w = canvas.clientWidth;
2324      const h = canvas.clientHeight;
2325      canvas.width = Math.max(1, Math.floor(w * dpr));
2326      canvas.height = Math.max(1, Math.floor(h * dpr));
2327      ctx.scale(dpr, dpr);
2328      ctx.clearRect(0, 0, w, h);
2329
2330      const all = series.flatMap(s => s.values || []);
2331      if (all.length === 0) {{
2332        ctx.fillStyle = "#9fb0d0";
2333        ctx.font = "14px sans-serif";
2334        ctx.fillText("No data", 16, 24);
2335        return;
2336      }}
2337      const maxV = Math.max(...all, 1);
2338      const minV = Math.min(...all, 0);
2339      const pad = 34;
2340      const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad;
2341      const yRange = Math.max(maxV - minV, 1e-9);
2342      const yTicks = 4;
2343      const plotY = (v, t=1) => y0 - ((v - minV) / yRange) * (y0 - y1) * t;
2344
2345      let start = null;
2346      function frame(ts) {{
2347        if (!start) start = ts;
2348        const t = Math.min(1, (ts - start) / 900);
2349        ctx.clearRect(0, 0, w, h);
2350
2351        // Grid and axes with numeric labels
2352        ctx.font = "11px Space Grotesk, Segoe UI, sans-serif";
2353        ctx.fillStyle = "#9fb0d0";
2354        ctx.strokeStyle = "rgba(115,140,180,0.22)";
2355        ctx.lineWidth = 1;
2356        for (let i = 0; i <= yTicks; i++) {{
2357          const yv = minV + (yRange * i / yTicks);
2358          const py = plotY(yv);
2359          ctx.beginPath();
2360          ctx.moveTo(x0, py);
2361          ctx.lineTo(x1, py);
2362          ctx.stroke();
2363          ctx.fillText(Number(yv).toFixed(2), 4, py + 3);
2364        }}
2365
2366        const maxLen = Math.max(1, ...series.map(s => (s.values || []).length));
2367        const xTicks = Math.min(4, Math.max(1, maxLen - 1));
2368        for (let i = 0; i <= xTicks; i++) {{
2369          const xi = maxLen === 1 ? 0 : Math.round(((maxLen - 1) * i) / xTicks);
2370          const px = maxLen === 1 ? (x0 + x1) / 2 : x0 + (xi / (maxLen - 1)) * (x1 - x0);
2371          ctx.beginPath();
2372          ctx.moveTo(px, y0);
2373          ctx.lineTo(px, y0 + 4);
2374          ctx.stroke();
2375          ctx.fillText(`S${{xi + 1}}`, px - 9, y0 + 16);
2376        }}
2377
2378        ctx.strokeStyle = "rgba(115,140,180,0.35)";
2379        ctx.lineWidth = 1;
2380        ctx.beginPath();
2381        ctx.moveTo(x0, y0); ctx.lineTo(x1, y0); ctx.lineTo(x1, y1); ctx.stroke();
2382
2383        series.forEach((s, si) => {{
2384          const vals = s.values || [];
2385          if (vals.length < 1) return;
2386          ctx.strokeStyle = s.color;
2387          ctx.lineWidth = 2;
2388          ctx.beginPath();
2389          vals.forEach((v, i) => {{
2390            const px = vals.length === 1 ? (x0 + x1) / 2 : x0 + (i / (vals.length - 1)) * (x1 - x0);
2391            const py = plotY(v, t);
2392            if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
2393          }});
2394          ctx.stroke();
2395
2396          // Data points
2397          ctx.fillStyle = s.color;
2398          vals.forEach((v, i) => {{
2399            const px = vals.length === 1 ? (x0 + x1) / 2 : x0 + (i / (vals.length - 1)) * (x1 - x0);
2400            const py = plotY(v, t);
2401            ctx.beginPath();
2402            ctx.arc(px, py, 2.5, 0, Math.PI * 2);
2403            ctx.fill();
2404          }});
2405
2406          // Simple legend
2407          const ly = y1 + 14 + si * 14;
2408          ctx.fillStyle = s.color;
2409          ctx.fillRect(x1 - 120, ly - 8, 10, 10);
2410          ctx.fillStyle = "#dce8ff";
2411          ctx.fillText(s.name, x1 - 105, ly);
2412        }});
2413        if (t < 1) requestAnimationFrame(frame);
2414      }}
2415      requestAnimationFrame(frame);
2416    }}
2417
2418    drawMultiLine("chartMemory", [
2419      {{name: "HBM", color: "#27d7c6", values: reportData.charts.memory_hbm}},
2420      {{name: "System", color: "#ffd166", values: reportData.charts.memory_system}}
2421    ]);
2422    drawMultiLine("chartTensor", [{{name: "FP16", color: "#74a7ff", values: reportData.charts.tensor_fp16}}]);
2423    drawMultiLine("chartPytorch", [
2424      {{name: "GEMM", color: "#3ddc84", values: reportData.charts.pytorch_gemm}},
2425      {{name: "Conv", color: "#ffa24d", values: reportData.charts.pytorch_conv}}
2426    ]);
2427    drawMultiLine("chartLlamaCpp", [
2428      {{name: "LLaMA.cpp", color: "#74a7ff", values: reportData.charts.llama_cpp}}
2429    ]);
2430    drawMultiLine("chartFlash", [
2431      {{name: "Standard", color: "#8a99b6", values: reportData.charts.flash_standard}},
2432      {{name: "Optimized", color: "#27d7c6", values: reportData.charts.flash_optimized}}
2433    ]);
2434    drawMultiLine("chartVllm", [{{name: "vLLM", color: "#74a7ff", values: reportData.charts.vllm}}]);
2435    drawMultiLine("chartDeepspeed", [{{name: "DeepSpeed", color: "#ffd166", values: reportData.charts.deepspeed}}]);
2436    drawMultiLine("chartMegatron", [{{name: "Megatron", color: "#ff8fab", values: reportData.charts.megatron}}]);
2437    drawMultiLine("chartOnnx", [{{name: "ONNX Inference", color: "#90ee90", values: reportData.charts.onnx}}]);
2438  </script>
2439</body>
2440</html>"###
2441    );
2442
2443    fs::write(&report_path, html)
2444        .map_err(|e| format!("failed to write report {}: {}", report_path.display(), e))?;
2445    Ok(report_path)
2446}
2447
2448fn resolve_report_path(output_path: Option<&Path>) -> Result<PathBuf, String> {
2449    if let Some(path) = output_path {
2450        return Ok(path.to_path_buf());
2451    }
2452    if let Ok(path) = std::env::var("MLSTACK_BENCHMARK_REPORT_PATH") {
2453        let trimmed = path.trim();
2454        if !trimmed.is_empty() {
2455            return Ok(PathBuf::from(trimmed));
2456        }
2457    }
2458    let base = std::env::var("HOME")
2459        .map(|h| Path::new(&h).join(".mlstack").join("reports"))
2460        .unwrap_or_else(|_| PathBuf::from("/tmp/mlstack/reports"));
2461    let file = format!(
2462        "benchmark_report_{}.html",
2463        Local::now().format("%Y%m%d_%H%M%S")
2464    );
2465    Ok(base.join(file))
2466}
2467
2468fn build_summary_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2469    let mut rows = Vec::new();
2470
2471    rows.push(serde_json::json!({
2472        "component": "GPU",
2473        "status": if results.gpus.is_empty() { "missing" } else { "ok" },
2474        "metric": format!("{} GPU(s)", results.gpus.len()),
2475        "detail": results.gpus.iter().map(|g| g.model.clone()).collect::<Vec<_>>().join(" | ")
2476    }));
2477
2478    if let Some(m) = &results.memory_bandwidth {
2479        rows.push(serde_json::json!({
2480            "component": "Memory Bandwidth",
2481            "status": "ok",
2482            "metric": format!("{:.2} GB/s", m.hbm_peak_gb_s),
2483            "detail": format!("System {:.2} GB/s, ratio {:.2}x", m.system_peak_gb_s, m.hbm_ratio)
2484        }));
2485    }
2486    if let Some(t) = &results.tensor_core {
2487        rows.push(serde_json::json!({
2488            "component": "Tensor Core",
2489            "status": "ok",
2490            "metric": format!("{:.2} TFLOPS", t.fp16_tflops),
2491            "detail": format!("BF16 {:.2} TFLOPS, FP32 {:.2} TFLOPS", t.bf16_tflops, t.fp32_tflops)
2492        }));
2493    }
2494    if let Some(p) = &results.pytorch {
2495        rows.push(serde_json::json!({
2496            "component": "PyTorch",
2497            "status": "ok",
2498            "metric": format!("{:.2} GFLOPS", p.gemm_gflops),
2499            "detail": format!("Conv {:.2} GFLOPS, autograd overhead {:.2}%", p.convolution_gflops, p.autograd_overhead_percent)
2500        }));
2501    }
2502    if let Some(l) = &results.llama_cpp {
2503        rows.push(serde_json::json!({
2504            "component": "LLaMA.cpp",
2505            "status": "ok",
2506            "metric": format!("{} GPUs", l.gpus.len()),
2507            "detail": format!("Model {} | ROCm {}", l.model, l.rocm_version)
2508        }));
2509    }
2510    if let Some(f) = &results.flash_attention {
2511        rows.push(serde_json::json!({
2512            "component": "Flash Attention",
2513            "status": "ok",
2514            "metric": format!("{:.2} tok/s", f.flash_attention_speed),
2515            "detail": format!("Standard {:.2} tok/s, speedup {:.2}x, saved {:.3} GB", f.standard_attention_speed, f.speedup, f.memory_savings_gb)
2516        }));
2517    }
2518    if let Some(v) = &results.vllm {
2519        rows.push(serde_json::json!({
2520            "component": "vLLM",
2521            "status": if v.throughput_tokens_per_sec > 0.0 { "ok" } else { "degraded" },
2522            "metric": format!("{:.2} tok/s", v.throughput_tokens_per_sec),
2523            "detail": format!("Model: {} ({}), latency {:.2} ms", v.model, v.model_format, v.latency_ms)
2524        }));
2525    }
2526    if let Some(d) = &results.deepspeed {
2527        rows.push(serde_json::json!({
2528            "component": "DeepSpeed",
2529            "status": if d.throughput_samples_per_sec > 0.0 { "ok" } else { "degraded" },
2530            "metric": format!("{:.2} samples/s", d.throughput_samples_per_sec),
2531            "detail": format!("{} stage {}, latency {:.2} ms", d.accelerator, d.stage, d.avg_latency_ms)
2532        }));
2533    }
2534    if let Some(m) = &results.megatron {
2535        rows.push(serde_json::json!({
2536            "component": "Megatron-LM",
2537            "status": if m.throughput_samples_per_sec > 0.0 { "ok" } else { "degraded" },
2538            "metric": format!("{:.2} samples/s", m.throughput_samples_per_sec),
2539            "detail": format!("backend {}, latency {:.2} ms", m.backend, m.avg_latency_ms)
2540        }));
2541    }
2542    if let Some(o) = &results.onnx {
2543        rows.push(serde_json::json!({
2544            "component": "ONNX Runtime",
2545            "status": if o.throughput_inf_per_sec > 0.0 { "ok" } else { "degraded" },
2546            "metric": format!("{:.2} inf/s", o.throughput_inf_per_sec),
2547            "detail": format!("{} v{}, provider {}, latency p50 {:.2} ms p95 {:.2} ms p99 {:.2} ms", o.ort_version, "", o.provider, o.inference_latency_p50_ms, o.inference_latency_p95_ms, o.inference_latency_p99_ms)
2548        }));
2549    }
2550
2551    rows
2552}
2553
2554fn build_metric_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2555    let mut rows = Vec::new();
2556
2557    if let Some(m) = &results.memory_bandwidth {
2558        rows.push(serde_json::json!({"component":"Memory","metric":"HBM Peak","value":m.hbm_peak_gb_s,"unit":"GB/s"}));
2559        rows.push(serde_json::json!({"component":"Memory","metric":"System Peak","value":m.system_peak_gb_s,"unit":"GB/s"}));
2560        rows.push(serde_json::json!({"component":"Memory","metric":"HBM Ratio","value":m.hbm_ratio,"unit":"x"}));
2561    }
2562    if let Some(t) = &results.tensor_core {
2563        rows.push(serde_json::json!({"component":"Tensor Core","metric":"FP16","value":t.fp16_tflops,"unit":"TFLOPS"}));
2564        rows.push(serde_json::json!({"component":"Tensor Core","metric":"BF16","value":t.bf16_tflops,"unit":"TFLOPS"}));
2565        rows.push(serde_json::json!({"component":"Tensor Core","metric":"FP32","value":t.fp32_tflops,"unit":"TFLOPS"}));
2566    }
2567    if let Some(p) = &results.pytorch {
2568        rows.push(serde_json::json!({"component":"PyTorch","metric":"GEMM","value":p.gemm_gflops,"unit":"GFLOPS"}));
2569        rows.push(serde_json::json!({"component":"PyTorch","metric":"Convolution","value":p.convolution_gflops,"unit":"GFLOPS"}));
2570        rows.push(serde_json::json!({"component":"PyTorch","metric":"Autograd Overhead","value":p.autograd_overhead_percent,"unit":"%"}));
2571    }
2572    if let Some(l) = &results.llama_cpp {
2573        rows.push(serde_json::json!({"component":"LLaMA.cpp","metric":"Prefill Tokens/s","value":l.prefill.iter().map(|p| p.avg_tps).sum::<f64>(),"unit":"t/s"}));
2574        rows.push(serde_json::json!({"component":"LLaMA.cpp","metric":"Decode Tokens/s","value":l.decode.iter().map(|p| p.avg_tps).sum::<f64>(),"unit":"t/s"}));
2575    }
2576    if let Some(f) = &results.flash_attention {
2577        rows.push(serde_json::json!({"component":"Flash Attention","metric":"Standard","value":f.standard_attention_speed,"unit":"tok/s"}));
2578        rows.push(serde_json::json!({"component":"Flash Attention","metric":"Flash","value":f.flash_attention_speed,"unit":"tok/s"}));
2579        rows.push(serde_json::json!({"component":"Flash Attention","metric":"Speedup","value":f.speedup,"unit":"x"}));
2580    }
2581    if let Some(v) = &results.vllm {
2582        rows.push(serde_json::json!({"component":"vLLM","metric":"Throughput","value":v.throughput_tokens_per_sec,"unit":"tok/s"}));
2583        rows.push(serde_json::json!({"component":"vLLM","metric":"Latency","value":v.latency_ms,"unit":"ms"}));
2584    }
2585    if let Some(d) = &results.deepspeed {
2586        rows.push(serde_json::json!({"component":"DeepSpeed","metric":"Throughput","value":d.throughput_samples_per_sec,"unit":"samples/s"}));
2587        rows.push(serde_json::json!({"component":"DeepSpeed","metric":"Latency","value":d.avg_latency_ms,"unit":"ms"}));
2588    }
2589    if let Some(m) = &results.megatron {
2590        rows.push(serde_json::json!({"component":"Megatron","metric":"Throughput","value":m.throughput_samples_per_sec,"unit":"samples/s"}));
2591        rows.push(serde_json::json!({"component":"Megatron","metric":"Latency","value":m.avg_latency_ms,"unit":"ms"}));
2592    }
2593    if let Some(o) = &results.onnx {
2594        rows.push(serde_json::json!({"component":"ONNX","metric":"Throughput","value":o.throughput_inf_per_sec,"unit":"inf/s"}));
2595        rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p50","value":o.inference_latency_p50_ms,"unit":"ms"}));
2596        rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p95","value":o.inference_latency_p95_ms,"unit":"ms"}));
2597        rows.push(serde_json::json!({"component":"ONNX","metric":"Latency p99","value":o.inference_latency_p99_ms,"unit":"ms"}));
2598        rows.push(serde_json::json!({"component":"ONNX","metric":"Session Create","value":o.session_create_ms,"unit":"ms"}));
2599        rows.push(serde_json::json!({"component":"ONNX","metric":"Peak RSS","value":o.peak_rss_mb,"unit":"MB"}));
2600    }
2601
2602    rows
2603}
2604
2605fn build_sample_rows(results: &BenchmarkResults) -> Vec<serde_json::Value> {
2606    let mut rows = Vec::new();
2607
2608    if let Some(m) = &results.memory_bandwidth {
2609        rows.push(
2610            serde_json::json!({"benchmark":"Memory","series":"HBM GB/s","samples":m.hbm_samples}),
2611        );
2612        rows.push(serde_json::json!({"benchmark":"Memory","series":"System GB/s","samples":m.system_samples}));
2613    }
2614    if let Some(t) = &results.tensor_core {
2615        rows.push(serde_json::json!({"benchmark":"Tensor Core","series":"FP16 TFLOPS","samples":t.fp16_samples}));
2616    }
2617    if let Some(p) = &results.pytorch {
2618        rows.push(serde_json::json!({"benchmark":"PyTorch","series":"GEMM GFLOPS","samples":p.gemm_samples}));
2619        rows.push(serde_json::json!({"benchmark":"PyTorch","series":"Conv GFLOPS","samples":p.conv_samples}));
2620    }
2621    if let Some(l) = &results.llama_cpp {
2622        rows.push(serde_json::json!({"benchmark":"LLaMA.cpp","series":"Prefill t/s","samples":l.prefill.iter().map(|p| p.avg_tps).collect::<Vec<_>>()}));
2623        rows.push(serde_json::json!({"benchmark":"LLaMA.cpp","series":"Decode t/s","samples":l.decode.iter().map(|p| p.avg_tps).collect::<Vec<_>>()}));
2624    }
2625    if let Some(f) = &results.flash_attention {
2626        rows.push(serde_json::json!({"benchmark":"Flash Attention","series":"Standard tok/s","samples":f.standard_samples}));
2627        rows.push(serde_json::json!({"benchmark":"Flash Attention","series":"Flash tok/s","samples":f.flash_samples}));
2628    }
2629    if let Some(v) = &results.vllm {
2630        rows.push(serde_json::json!({"benchmark":"vLLM","series":"Throughput tok/s","samples":v.throughput_samples}));
2631    }
2632    if let Some(d) = &results.deepspeed {
2633        rows.push(serde_json::json!({"benchmark":"DeepSpeed","series":"Throughput samples/s","samples":d.samples}));
2634    }
2635    if let Some(m) = &results.megatron {
2636        rows.push(serde_json::json!({"benchmark":"Megatron","series":"Throughput samples/s","samples":m.samples}));
2637    }
2638    if let Some(o) = &results.onnx {
2639        rows.push(serde_json::json!({"benchmark":"ONNX","series":"Inference latency ms","samples":o.inference_samples}));
2640    }
2641
2642    rows
2643}