use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
symbols,
text::{Line, Span, Text},
widgets::{Block, Borders, Gauge, Paragraph, Sparkline},
};
use crate::{
app::{App, AppColors, History, HistoryExt},
metric_key::{ClusterId, MetricKey},
metrics,
modules::soc::SocInfo,
units,
};
const CLUSTER_SPACING: u16 = 1; const SPARKLINE_HEIGHT: u16 = 3;
const SPARKLINE_MAX_OVERSHOOT: f32 = 1.05; const GAUGE_HEIGHT: u16 = 2;
const PKG_TEXT_HEIGHT: u16 = 1;
pub(crate) fn draw_overview_tab(f: &mut Frame, app: &App, area: Rect) {
let metrics = match &app.metrics {
Some(metrics) => metrics,
None => return,
};
let num_clusters_blocks = (num_blocks_for(metrics.e_clusters.len())
+ num_blocks_for(metrics.p_clusters.len())
+ num_blocks_for(metrics.s_clusters.len())) as u16;
let cls_block_height = GAUGE_HEIGHT + SPARKLINE_HEIGHT;
let cpu_block_height =
cls_block_height * num_clusters_blocks + (num_clusters_blocks - 1) * CLUSTER_SPACING;
let gpu_block_height = GAUGE_HEIGHT + SPARKLINE_HEIGHT;
let pkg_block_height = PKG_TEXT_HEIGHT + SPARKLINE_HEIGHT;
let mem_block_height = GAUGE_HEIGHT + SPARKLINE_HEIGHT;
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2 + cpu_block_height), Constraint::Length(2 + gpu_block_height), Constraint::Length(2 + pkg_block_height), Constraint::Length(2 + mem_block_height), Constraint::Min(0),
])
.split(area);
let cpu_area = vertical_chunks[0];
let gpu_area = vertical_chunks[1];
let pkg_area = vertical_chunks[2];
let mem_area = vertical_chunks[3];
draw_cpu_clusters_usage_block(f, metrics, &app.history, &app.colors, cpu_area);
draw_gpu_ane_usage_block(
f,
metrics,
&app.soc_info,
&app.history,
&app.colors,
gpu_area,
);
draw_pkg_thm_usage_block(f, metrics, &app.history, &app.colors, pkg_area);
draw_mem_usage_block(f, metrics, &app.history, &app.colors, mem_area);
}
fn draw_cpu_clusters_usage_block(
f: &mut Frame,
metrics: &metrics::Metrics,
history: &History,
colors: &AppColors,
area: Rect,
) {
let num_cluster_blocks = num_blocks_for(metrics.e_clusters.len())
+ num_blocks_for(metrics.p_clusters.len())
+ num_blocks_for(metrics.s_clusters.len());
let sig = history.get_or_default(&MetricKey::CpuPowerW);
let title = "CPU Clusters";
let title_with_power = format!(
" {title}: {} (peak: {}) ",
units::watts2(metrics.consumption.cpu_w),
units::watts2(sig.peak)
);
let block = Block::default()
.title(title_with_power)
.borders(Borders::ALL);
f.render_widget(block, area);
let constraints = (0..num_cluster_blocks)
.map(|k| {
Constraint::Length(
GAUGE_HEIGHT
+ SPARKLINE_HEIGHT
+ if k < num_cluster_blocks - 1 {
CLUSTER_SPACING
} else {
0
},
)
}) .collect::<Vec<_>>();
let cpu_cluster_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.margin(1)
.split(area);
let mut clu_area_iter = cpu_cluster_chunks.iter();
for (chunk_idx, clu_slice) in metrics.e_clusters.chunks(2).enumerate() {
let area = clu_area_iter
.next()
.expect("layout: expected area for E-cluster block");
match clu_slice.len() {
1 => {
let cluster = &clu_slice[0];
let cluster_id = ClusterId::efficiency((chunk_idx * 2) as u8);
draw_cluster_overall_metrics(f, cluster, cluster_id, history, colors, *area);
}
2 => {
let (left_cluster, right_cluster) = (&clu_slice[0], &clu_slice[1]);
let left_id = ClusterId::efficiency((chunk_idx * 2) as u8);
let right_id = ClusterId::efficiency((chunk_idx * 2 + 1) as u8);
draw_cluster_pair_overall_metrics(
f,
left_cluster,
left_id,
right_cluster,
right_id,
history,
colors,
*area,
);
}
_ => unreachable!(),
}
}
for (chunk_idx, clu_slice) in metrics.p_clusters.chunks(2).enumerate() {
let area = clu_area_iter
.next()
.expect("layout: expected area for P-cluster block");
match clu_slice.len() {
1 => {
let cluster = &clu_slice[0];
let cluster_id = ClusterId::performance((chunk_idx * 2) as u8);
draw_cluster_overall_metrics(f, cluster, cluster_id, history, colors, *area);
}
2 => {
let (left_cluster, right_cluster) = (&clu_slice[0], &clu_slice[1]);
let left_id = ClusterId::performance((chunk_idx * 2) as u8);
let right_id = ClusterId::performance((chunk_idx * 2 + 1) as u8);
draw_cluster_pair_overall_metrics(
f,
left_cluster,
left_id,
right_cluster,
right_id,
history,
colors,
*area,
);
}
_ => unreachable!(),
}
}
for (chunk_idx, clu_slice) in metrics.s_clusters.chunks(2).enumerate() {
let area = clu_area_iter
.next()
.expect("layout: expected area for S-cluster block");
match clu_slice.len() {
1 => {
let cluster = &clu_slice[0];
let cluster_id = ClusterId::super_core((chunk_idx * 2) as u8);
draw_cluster_overall_metrics(f, cluster, cluster_id, history, colors, *area);
}
2 => {
let (left_cluster, right_cluster) = (&clu_slice[0], &clu_slice[1]);
let left_id = ClusterId::super_core((chunk_idx * 2) as u8);
let right_id = ClusterId::super_core((chunk_idx * 2 + 1) as u8);
draw_cluster_pair_overall_metrics(
f,
left_cluster,
left_id,
right_cluster,
right_id,
history,
colors,
*area,
);
}
_ => unreachable!(),
}
}
}
fn draw_cluster_overall_metrics(
f: &mut Frame,
cluster: &metrics::ClusterMetrics,
cluster_id: ClusterId,
history: &History,
colors: &AppColors,
area: Rect,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(GAUGE_HEIGHT),
Constraint::Length(SPARKLINE_HEIGHT),
Constraint::Max(CLUSTER_SPACING),
])
.split(area);
let top_area = chunks[0];
let bottom_area = chunks[1];
let sig = history.get_or_default(&MetricKey::ClusterActivePercent(cluster_id));
let title = format!(
"{}: {} @ {} (peak: {})",
cluster.name,
units::percent1(cluster.active_ratio() * 100.0),
units::mhz(cluster.freq_mhz),
units::percent1(sig.peak)
);
let gauge = Gauge::default()
.block(Block::default().title(title))
.gauge_style(Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()))
.ratio(cluster.active_ratio() as f64);
f.render_widget(gauge, top_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(bottom_area.width as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(sparkline, bottom_area);
}
#[allow(clippy::too_many_arguments)]
fn draw_cluster_pair_overall_metrics(
f: &mut Frame,
left_cluster: &metrics::ClusterMetrics,
left_id: ClusterId,
right_cluster: &metrics::ClusterMetrics,
right_id: ClusterId,
history: &History,
colors: &AppColors,
area: Rect,
) {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Ratio(1, 2),
Constraint::Length(2), Constraint::Ratio(1, 2),
])
.split(area);
let left_area = horizontal_chunks[0];
let right_area = horizontal_chunks[2];
draw_cluster_overall_metrics(f, left_cluster, left_id, history, colors, left_area);
draw_cluster_overall_metrics(f, right_cluster, right_id, history, colors, right_area);
}
fn draw_gpu_ane_usage_block(
f: &mut Frame,
metrics: &metrics::Metrics,
soc_info: &SocInfo,
history: &History,
colors: &AppColors,
area: Rect,
) {
let block = Block::default().title(" GPU & ANE ").borders(Borders::ALL);
f.render_widget(block, area);
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Ratio(1, 2),
Constraint::Length(2), Constraint::Ratio(1, 2),
])
.margin(1)
.split(area);
let left_area = horizontal_chunks[0];
let right_area = horizontal_chunks[2];
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Length(9)])
.split(left_area);
let top_left_area = left_chunks[0];
let bottom_left_area = left_chunks[1];
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Length(9)])
.split(right_area);
let top_right_area = right_chunks[0];
let bottom_right_area = right_chunks[1];
let gpu = &metrics.gpu;
let sig = history.get_or_default(&MetricKey::GpuActivePercent);
let sig_gpu_power = history.get_or_default(&MetricKey::GpuPowerW);
let title = format!(
"GPU: {} @ {} | {} (peak: {} | {})",
units::percent1(gpu.active_ratio * 100.0),
units::mhz(gpu.freq_mhz),
units::watts2(metrics.consumption.gpu_w),
units::percent1(sig.peak),
units::watts2(sig_gpu_power.peak)
);
let gauge = Gauge::default()
.block(Block::default().title(title))
.gauge_style(
Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()),
)
.ratio(gpu.active_ratio);
f.render_widget(gauge, top_left_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(bottom_left_area.width as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(sparkline, bottom_left_area);
let ane_active_ratio = metrics.consumption.ane_w as f64 / soc_info.max_ane_w;
let sig = history.get_or_default(&MetricKey::AneActivePercent);
let sig_ane_power = history.get_or_default(&MetricKey::AnePowerW);
let title = format!(
"ANE: {} | {} (peak: {} | {})",
units::percent1(ane_active_ratio * 100.0),
units::watts2(metrics.consumption.ane_w),
units::percent1(sig.peak),
units::watts2(sig_ane_power.peak)
);
let gauge = Gauge::default()
.block(Block::default().title(title))
.gauge_style(Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()))
.ratio(ane_active_ratio);
f.render_widget(gauge, top_right_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(bottom_right_area.width as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(sparkline, bottom_right_area);
}
fn draw_pkg_thm_usage_block(
f: &mut Frame,
metrics: &metrics::Metrics,
history: &History,
colors: &AppColors,
area: Rect,
) {
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Ratio(7, 10), Constraint::Ratio(3, 10)])
.split(area);
let pkg_area = horizontal_chunks[0];
let thr_area = horizontal_chunks[1];
draw_package_power_block(f, metrics, history, colors, pkg_area);
draw_thermal_pressure_block(f, metrics, colors, thr_area);
}
fn draw_mem_usage_block(
f: &mut Frame,
metrics: &metrics::Metrics,
history: &History,
colors: &AppColors,
area: Rect,
) {
let block = Block::default()
.title(" Memory & SWAP ")
.borders(Borders::ALL);
f.render_widget(block, area);
let horizontal_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Ratio(1, 2),
Constraint::Length(2), Constraint::Ratio(1, 2),
])
.margin(1)
.split(area);
let left_area = horizontal_chunks[0];
let right_area = horizontal_chunks[2];
let left_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Length(9)])
.split(left_area);
let top_left_area = left_chunks[0];
let bottom_left_area = left_chunks[1];
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(2), Constraint::Length(9)])
.split(right_area);
let top_right_area = right_chunks[0];
let bottom_right_area = right_chunks[1];
let mem = &metrics.memory;
{
let sig = history.get_or_default(&MetricKey::RamUsageBytes);
let ram_usage_ratio = mem.ram_usage_ratio();
let title = format!(
"Memory Used: {} = {} / {} (peak: {} = {})",
units::percent1(ram_usage_ratio * 100.0),
units::bibytes1(mem.ram_used as f64),
units::bibytes1(mem.ram_total as f64),
units::percent1(sig.peak / mem.ram_total as f32 * 100.0),
units::bibytes1(sig.peak),
);
let gauge = Gauge::default()
.block(Block::default().title(title))
.gauge_style(
Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()),
)
.ratio(ram_usage_ratio);
f.render_widget(gauge, top_left_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(bottom_left_area.width as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(sparkline, bottom_left_area);
}
{
let sig = history.get_or_default(&MetricKey::SwapUsageBytes);
let swap_usage_ratio = mem.swap_usage_ratio();
let title = format!(
"SWAP: {} = {} / {} (peak: {})",
units::percent1(swap_usage_ratio * 100.0),
units::bibytes1(mem.swap_used as f64),
units::bibytes1(mem.swap_total as f64),
units::bibytes1(sig.peak),
);
let gauge = Gauge::default()
.block(Block::default().title(title))
.gauge_style(
Style::default().fg(colors.gauge_fg()).bg(colors.gauge_bg()),
)
.ratio(swap_usage_ratio);
f.render_widget(gauge, top_right_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(bottom_right_area.width as usize))
.max((SPARKLINE_MAX_OVERSHOOT * sig.max) as u64);
f.render_widget(sparkline, bottom_right_area);
}
}
fn draw_package_power_block(
f: &mut Frame,
metrics: &metrics::Metrics,
history: &History,
colors: &AppColors,
area: Rect,
) {
let block = Block::default().title(" Package ").borders(Borders::ALL);
f.render_widget(block, area);
let vertical_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Length(SPARKLINE_HEIGHT)])
.margin(1)
.split(area);
let title_area = vertical_chunks[0];
let sparkline_area = vertical_chunks[1];
let sig = history.get_or_default(&MetricKey::PackagePowerW);
let title = format!(
"CPU+GPU+ANE: {} (peak: {})",
units::watts2(metrics.consumption.package_w),
units::watts2(sig.peak)
);
let text = Paragraph::new(Text::from(title));
f.render_widget(text, title_area);
let sparkline = Sparkline::default()
.style(
Style::default()
.fg(colors.history_fg())
.bg(colors.history_bg()),
)
.bar_set(symbols::bar::NINE_LEVELS)
.data(sig.as_slice_last_n(sparkline_area.width as usize))
.max(sig.max as u64);
f.render_widget(sparkline, sparkline_area);
}
fn draw_thermal_pressure_block(
f: &mut Frame,
metrics: &metrics::Metrics,
colors: &AppColors,
area: Rect,
) {
let color = match metrics.thermal_pressure.as_str() {
"Nominal" => colors.accent(),
_ => Color::Yellow,
};
let text = Line::from(vec![
Span::raw("Pressure: "),
Span::styled(&metrics.thermal_pressure, Style::default().fg(color)),
]);
let paragraph =
Paragraph::new(text).block(Block::default().title(" Thermals ").borders(Borders::ALL));
f.render_widget(paragraph, area);
}
fn num_blocks_for(count: usize) -> usize {
(count as f32 / 2.0).ceil() as usize
}