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