use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Gauge, Paragraph, Sparkline},
Frame,
};
use trueno_gpu::monitor::{ComputeDevice, PressureLevel};
use crate::{App, StressTestVerdict};
pub(crate) fn render_stress_tab(f: &mut Frame, app: &App, area: Rect) {
if app.stress_running {
render_stress_running(f, app, area);
} else {
render_stress_idle(f, app, area);
}
}
fn render_stress_idle(f: &mut Frame, app: &App, area: Rect) {
let block =
Block::default().title(" Stress Test Mode (TRUENO-SPEC-025) ").borders(Borders::ALL);
let mut text = vec![
Line::from(""),
Line::from(vec![
Span::raw(" Status: "),
Span::styled("IDLE", Style::default().fg(Color::DarkGray)),
]),
Line::from(""),
Line::from(Span::styled(
" Hardware Detected:",
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::raw(" CPU: "),
Span::styled(app.cpu.device_name(), Style::default().fg(Color::White)),
Span::styled(
format!(" ({} cores)", num_cpus::get()),
Style::default().fg(Color::DarkGray),
),
]),
];
for (i, gpu) in app.gpus.iter().enumerate() {
text.push(Line::from(vec![
Span::raw(format!(" GPU{}: ", i)),
Span::styled(&gpu.info.name, Style::default().fg(Color::Magenta)),
]));
}
text.extend(vec![
Line::from(""),
Line::from(Span::styled(
" Stress Test Will:",
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::raw(" "),
Span::styled("CPU:", Style::default().fg(Color::Yellow)),
Span::raw(format!(" {} threads doing FP math (sin/cos/sqrt)", num_cpus::get())),
]),
Line::from(vec![
Span::raw(" "),
Span::styled("MEM:", Style::default().fg(Color::Yellow)),
Span::raw(" Allocate 512MB, touch every page"),
]),
]);
for (i, gpu) in app.gpus.iter().enumerate() {
text.push(Line::from(vec![
Span::raw(" "),
Span::styled(format!("GPU{}:", i), Style::default().fg(Color::Yellow)),
Span::raw(format!(" {} - H2D/D2H transfers (1M x f32)", gpu.info.name)),
]));
}
if let Some(ref report) = app.stress_report {
let verdict_color = match report.verdict {
StressTestVerdict::Pass => Color::Green,
StressTestVerdict::PassWithNotes => Color::Yellow,
StressTestVerdict::Fail => Color::Red,
};
text.extend(vec![
Line::from(""),
Line::from(Span::styled(
" Stress Test Report (renacer):",
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(vec![
Span::raw(" Verdict: "),
Span::styled(
format!("{}", report.verdict),
Style::default().fg(verdict_color).add_modifier(Modifier::BOLD),
),
]),
Line::from(vec![
Span::raw(" Duration: "),
Span::styled(
format!("{}s", report.duration_secs),
Style::default().fg(Color::White),
),
Span::raw(" | Workers: "),
Span::styled(
format!("{} CPU + {} GPU", report.cpu_workers, report.gpu_workers),
Style::default().fg(Color::White),
),
]),
Line::from(""),
Line::from(vec![
Span::raw(" Peak CPU: "),
Span::styled(
format!(
"{:.2} M ops/sec ({:.1}%)",
report.peak_cpu_ops as f64 / 1_000_000.0,
report.peak_cpu_util
),
Style::default().fg(Color::Cyan),
),
]),
Line::from(vec![
Span::raw(" Peak MEM: "),
Span::styled(
format!(
"{:.2} M pages/sec ({:.1}%)",
report.peak_mem_ops as f64 / 1_000_000.0,
report.peak_ram_util
),
Style::default().fg(Color::Magenta),
),
]),
]);
if report.peak_gpu_ops > 0 {
text.push(Line::from(vec![
Span::raw(" Peak GPU: "),
Span::styled(
format!(
"{:.2} G xfers/sec ({:.1}% VRAM)",
report.peak_gpu_ops as f64 / 1_000_000_000.0,
report.peak_vram_util
),
Style::default().fg(Color::Yellow),
),
]));
}
if !report.recommendations.is_empty() {
text.push(Line::from(""));
text.push(Line::from(Span::styled(
" Recommendations:",
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
)));
for rec in &report.recommendations {
text.push(Line::from(vec![
Span::raw(" \u{2022} "),
Span::styled(rec, Style::default().fg(Color::DarkGray)),
]));
}
}
}
text.extend(vec![
Line::from(""),
Line::from(""),
Line::from(Span::styled(
" >>> Press 's' to START stress test <<<",
Style::default().fg(Color::Green).add_modifier(Modifier::BOLD),
)),
]);
let paragraph = Paragraph::new(text).block(block);
f.render_widget(paragraph, area);
}
fn render_stress_running(f: &mut Frame, app: &App, area: Rect) {
let has_gpu = !app.gpu_workers.is_empty();
let mut constraints = vec![
Constraint::Length(3), Constraint::Length(3), Constraint::Length(4), Constraint::Length(3), Constraint::Length(4), ];
if has_gpu {
constraints.push(Constraint::Length(3)); constraints.push(Constraint::Length(4)); for _ in 0..app.gpu_vram_history.len() {
constraints.push(Constraint::Length(4)); }
}
constraints.push(Constraint::Min(3));
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.margin(1)
.split(area);
let elapsed = app.stress_start.map(|s| s.elapsed().as_secs()).unwrap_or(0);
let status_text = format!(
" STRESS TEST RUNNING | {}s | {} CPU + {} GPU workers | 's' to STOP ",
elapsed,
app.cpu_workers.len(),
app.gpu_workers.len()
);
let status = Paragraph::new(status_text)
.style(Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD))
.block(Block::default().borders(Borders::ALL));
f.render_widget(status, chunks[0]);
let cpu_mops = app.cpu_ops_per_sec as f64 / 1_000_000.0;
let cpu_pct = ((cpu_mops / 100.0) * 100.0).min(100.0) as u16;
let cpu_gauge = Gauge::default()
.block(
Block::default()
.title(format!(
" CPU: {:.1} M ops/sec (peak: {:.1}) ",
cpu_mops,
app.peak_cpu_ops as f64 / 1_000_000.0
))
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(Color::Cyan))
.percent(cpu_pct)
.label(format!("{:.2} M ops/sec", cpu_mops));
f.render_widget(cpu_gauge, chunks[1]);
let cpu_sparkline = Sparkline::default()
.block(Block::default().title(" CPU History ").borders(Borders::ALL))
.data(&app.cpu_ops_history)
.style(Style::default().fg(Color::Cyan));
f.render_widget(cpu_sparkline, chunks[2]);
let mem_mops = app.mem_ops_per_sec as f64 / 1_000_000.0;
let mem_pct = ((mem_mops / 10.0) * 100.0).min(100.0) as u16;
let mem_gauge = Gauge::default()
.block(
Block::default()
.title(format!(
" MEM: {:.1} M pages/sec (peak: {:.1}) ",
mem_mops,
app.peak_mem_ops as f64 / 1_000_000.0
))
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(Color::Magenta))
.percent(mem_pct)
.label(format!("{:.2} M pages/sec", mem_mops));
f.render_widget(mem_gauge, chunks[3]);
let mem_sparkline = Sparkline::default()
.block(Block::default().title(" Memory History ").borders(Borders::ALL))
.data(&app.mem_ops_history)
.style(Style::default().fg(Color::Magenta));
f.render_widget(mem_sparkline, chunks[4]);
let stats_idx = if has_gpu {
let gpu_gops = app.gpu_ops_per_sec as f64 / 1_000_000_000.0;
let gpu_pct = ((gpu_gops / 10.0) * 100.0).min(100.0) as u16; let gpu_gauge = Gauge::default()
.block(
Block::default()
.title(format!(
" GPU: {:.2} G transfers/sec (peak: {:.2}) ",
gpu_gops,
app.peak_gpu_ops as f64 / 1_000_000_000.0
))
.borders(Borders::ALL),
)
.gauge_style(Style::default().fg(Color::Yellow))
.percent(gpu_pct)
.label(format!("{:.2} G xfers/sec", gpu_gops));
f.render_widget(gpu_gauge, chunks[5]);
let gpu_sparkline = Sparkline::default()
.block(Block::default().title(" GPU History ").borders(Borders::ALL))
.data(&app.gpu_ops_history)
.style(Style::default().fg(Color::Yellow));
f.render_widget(gpu_sparkline, chunks[6]);
let mut next_chunk = 7;
for (i, vram_history) in app.gpu_vram_history.iter().enumerate() {
let gpu_name = app.gpus.get(i).map(|g| g.info.name.as_str()).unwrap_or("GPU");
let vram_sparkline = Sparkline::default()
.block(
Block::default()
.title(format!(" GPU{} VRAM % ({}) ", i, gpu_name))
.borders(Borders::ALL),
)
.data(vram_history)
.max(100)
.style(Style::default().fg(Color::Magenta));
f.render_widget(vram_sparkline, chunks[next_chunk]);
next_chunk += 1;
}
next_chunk
} else {
5
};
let cpu_util = app.cpu.compute_utilization().unwrap_or(0.0);
let mem_pct_used = app.memory.ram_usage_percent();
let mut stats = vec![Line::from(vec![
Span::styled("System: ", Style::default().fg(Color::DarkGray)),
Span::styled(
format!("CPU {:.0}%", cpu_util),
Style::default().fg(if cpu_util > 90.0 { Color::Red } else { Color::Green }),
),
Span::raw(" | "),
Span::styled(
format!("RAM {:.0}%", mem_pct_used),
Style::default().fg(if mem_pct_used > 80.0 { Color::Red } else { Color::Green }),
),
Span::raw(" | Pressure: "),
Span::styled(
format!("{}", app.memory.pressure_level),
Style::default().fg(match app.memory.pressure_level {
PressureLevel::Ok => Color::Green,
PressureLevel::Elevated => Color::Yellow,
PressureLevel::Warning => Color::Rgb(255, 165, 0),
PressureLevel::Critical => Color::Red,
}),
),
])];
for (i, gpu) in app.gpus.iter().enumerate() {
stats.push(Line::from(vec![
Span::styled(format!("GPU{} VRAM: ", i), Style::default().fg(Color::DarkGray)),
Span::styled(
format!("{:.1}%", gpu.vram_percent),
Style::default().fg(if gpu.vram_percent > 90.0 {
Color::Red
} else {
Color::Green
}),
),
Span::styled(
format!(" ({:.1}/{:.1} GB)", gpu.vram_used_gb, gpu.vram_total_gb),
Style::default().fg(Color::DarkGray),
),
]));
}
let stats_block = Paragraph::new(stats)
.block(Block::default().title(" System Impact ").borders(Borders::ALL));
f.render_widget(stats_block, chunks[stats_idx]);
}