use crate::{
style::*, AppTreeItem, InspectorModel, KeyCode, NodoAppTreeNode, TreeLocation,
TreeLocationLevel, TreeLocationPos, PAGE_SCROLL_STEP,
};
use nodo::codelet::Transition;
use nodo_runtime::proto::nodo as nodo_pb;
use ratatui::{
layout::{Constraint, Layout},
prelude::{Alignment, Buffer, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{Cell, Row, StatefulWidget, Table, Widget},
};
use regex::Regex;
use std::{cell::RefCell, cmp::Ordering, sync::Arc};
pub struct StatisticsController {
pub model: Arc<RefCell<InspectorModel>>,
}
impl StatisticsController {
pub fn on_key(&mut self, key: KeyCode) {
match key {
KeyCode::Down => self.select_next(),
KeyCode::Up => self.select_previous(),
KeyCode::PageDown => self.on_page_down(),
KeyCode::PageUp => self.on_page_up(),
KeyCode::Enter => self.toggle_expand(),
_ => {}
}
}
fn select_next(&mut self) {
self.model.borrow_mut().report_table_state.select_next();
}
fn select_previous(&mut self) {
self.model.borrow_mut().report_table_state.select_previous();
}
fn on_page_down(&mut self) {
for _ in 0..PAGE_SCROLL_STEP {
self.select_next()
}
}
fn on_page_up(&mut self) {
for _ in 0..PAGE_SCROLL_STEP {
self.select_previous()
}
}
fn toggle_expand(&mut self) {
self.model.borrow_mut().tree.on_toggle_expand_selected();
}
}
pub struct StatisticsView {
pub model: Arc<RefCell<InspectorModel>>,
}
impl Widget for &StatisticsView {
fn render(self, area: Rect, buf: &mut Buffer) {
let chunks = Layout::default()
.constraints([Constraint::Percentage(100)].as_ref())
.split(area);
const BASE_LEN: usize = 70;
let mut model = self.model.borrow_mut();
fn tree_view_sort<'a>(a: &AppTreeItem<'a>, b: &AppTreeItem<'a>) -> Ordering {
let card = |x| (x * 1000.) as i64;
match (a, b) {
(AppTreeItem::Worker(a), AppTreeItem::Worker(b)) => card(a.step_duration)
.cmp(&card(b.step_duration))
.then_with(|| a.name.cmp(&b.name)),
(AppTreeItem::Sequence(a), AppTreeItem::Sequence(b)) => card(a.step_duration)
.cmp(&card(b.step_duration))
.then_with(|| a.name.cmp(&b.name)),
(AppTreeItem::Node(a), AppTreeItem::Node(b)) => card(a.step_duration())
.cmp(&card(b.step_duration()))
.then_with(|| a.info.node_name.cmp(&b.info.node_name)),
_ => unreachable!(),
}
.reverse()
}
fn tree_view_recurse<'a>(a: &AppTreeItem<'a>) -> bool {
match a {
AppTreeItem::Worker(a) => a.is_expanded,
AppTreeItem::Sequence(a) => a.is_expanded,
AppTreeItem::Node(_) => true,
}
}
let tree_structure_f = |loc: &TreeLocation| -> &'static str {
match loc.level {
TreeLocationLevel::Node(0) => {
if loc.is_expanded {
"+ "
} else {
"- "
}
}
TreeLocationLevel::Node(1) => {
if loc.is_expanded {
"├─+ "
} else {
"├─- "
}
}
TreeLocationLevel::Leaf => match loc.pos {
TreeLocationPos::First | TreeLocationPos::Mid => "| ├─",
TreeLocationPos::Last => "| └─",
},
_ => unreachable!(),
}
};
let tree_node_f = |loc: &TreeLocation, name, step_duration| {
Row::new(vec![
Cell::from(Line::from(vec![
Span::from(tree_structure_f(loc)),
Span::styled(name, STYLE_COLOR_SCHEDULE_NAME),
Span::from(format!(" {}", "─".repeat(2 * BASE_LEN))),
])),
Cell::from("─".repeat(4)),
Cell::from("─".repeat(2 * BASE_LEN)),
Cell::from("─".repeat(10)),
Cell::from(align_right(
format_total_duration(step_duration, model.tree.step_duration, 0.35)
.patch_style(Style::default().add_modifier(Modifier::REVERSED)),
)),
Cell::from("─".repeat(10)),
Cell::from("─".repeat(10)),
Cell::from("─".repeat(10)),
Cell::from("─".repeat(4 * BASE_LEN)),
])
};
let tree_leaf_f = |loc: &TreeLocation, node: &NodoAppTreeNode| {
let transition = &node.statistics.transitions[Transition::Step.index()];
Row::new(vec![
Cell::from(Line::from(vec![
Span::from(tree_structure_f(loc)),
Span::styled(format!(" {}", node.info.node_name), STYLE_COLOR_NODE_NAME),
])),
Cell::from(format_node_id(node.info.id)),
Cell::from(format_status(&node.status)),
Cell::from(align_right(format_skip_percent(transition))),
Cell::from(align_right(format_total_duration(
transition.duration.unwrap().total,
model.tree.step_duration,
0.10,
))),
Cell::from(align_right(format_step_duration(transition))),
Cell::from(align_right(format_step_count(transition))),
Cell::from(align_right(format_period(transition))),
Cell::from(Text::from(format_typename(&node.info.typename))),
])
};
let mut combined_rows: Vec<_> = Vec::new();
for (loc, item) in model.tree.tree_iter(tree_view_sort, tree_view_recurse) {
match item {
AppTreeItem::Worker(sched) => {
combined_rows.push(tree_node_f(&loc, sched.name.clone(), sched.step_duration));
}
AppTreeItem::Sequence(seq) => {
combined_rows.push(tree_node_f(&loc, seq.name.clone(), seq.step_duration));
}
AppTreeItem::Node(node) => {
combined_rows.push(tree_leaf_f(&loc, node));
}
}
}
let selection = model.report_table_state.selected();
model.tree.tree_entry_select(selection);
let combined_table = Table::new(
combined_rows,
&[
Constraint::Fill(2), Constraint::Length(4), Constraint::Fill(2), Constraint::Length(8), Constraint::Length(10), Constraint::Length(10), Constraint::Length(10), Constraint::Length(10), Constraint::Fill(4), ],
)
.header(
Row::new(vec![
"Codelet".into(),
align_right("ID".into()),
"Status".into(),
align_right("Skip%".into()),
align_right("Time".into()),
align_right("Step".into()),
align_right("Count".into()),
align_right("Period".into()),
"Type".into(),
])
.style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
),
)
.row_highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.style(Color::Yellow);
StatefulWidget::render(
combined_table,
chunks[0],
buf,
&mut model.report_table_state,
);
}
}
fn align_right(span: Span<'_>) -> Text<'_> {
Text::from(span).alignment(Alignment::Right)
}
fn align_left(span: Span<'_>) -> Text<'_> {
Text::from(span).alignment(Alignment::Left)
}
fn format_status(status: &nodo_pb::NodeStatus) -> Line<'static> {
let lcs: nodo_pb::LifecycleStatus = status
.lifecycle_status
.try_into()
.unwrap_or(nodo_pb::LifecycleStatus::Unspecified);
let lcs_color = lifecycle_status_color(lcs);
let mut spans = vec![Span::styled(format!("{:?}", lcs), lcs_color)];
spans.push(Span::styled("[", lcs_color));
let status_style = if status.is_skipped {
Style::default().fg(STYLE_COLOR_WARN)
} else {
Style::default().fg(STYLE_COLOR_OK)
};
spans.push(Span::styled(status.label.clone(), status_style));
spans.push(Span::styled("]", lcs_color));
Line::from(spans)
}
pub(crate) fn lifecycle_status_color(lcs: nodo_pb::LifecycleStatus) -> Color {
match lcs {
nodo_pb::LifecycleStatus::Unspecified => STYLE_COLOR_ERR,
nodo_pb::LifecycleStatus::Inactive => STYLE_COLOR_UNAVAILABLE,
nodo_pb::LifecycleStatus::Starting => STYLE_COLOR_WARN,
nodo_pb::LifecycleStatus::Running => STYLE_COLOR_OK,
nodo_pb::LifecycleStatus::Pausing => STYLE_COLOR_WARN,
nodo_pb::LifecycleStatus::Paused => STYLE_COLOR_WARN,
nodo_pb::LifecycleStatus::Resuming => STYLE_COLOR_WARN,
nodo_pb::LifecycleStatus::Stopping => STYLE_COLOR_WARN,
nodo_pb::LifecycleStatus::Error => STYLE_COLOR_ERR,
}
}
fn format_skip_percent(u: &nodo_pb::TransitionStatistics) -> Span<'static> {
if u.skipped_count == 0 {
Span::styled(format!("{:>6}", "None"), STYLE_COLOR_UNAVAILABLE)
} else {
let p = {
let total = u.skipped_count + u.duration.unwrap().count;
if total == 0 {
0.
} else {
u.skipped_count as f32 / total as f32
}
};
let color = if p < 0.8 {
STYLE_COLOR_VALUE_DEFAULT
} else if p < 0.9 {
STYLE_COLOR_WARN
} else {
STYLE_COLOR_ERR
};
Span::styled(format!("{:>5.1}%", p * 100.), color)
}
}
fn format_total_duration(x: f32, overall_total: f32, threshold: f32) -> Span<'static> {
let p = x / overall_total;
let color = if p > threshold {
STYLE_COLOR_ERR
} else if p > 0.1 * threshold {
STYLE_COLOR_WARN
} else {
STYLE_COLOR_VALUE_DEFAULT
};
Span::styled(format!("{:>7.1}s", x), color)
}
fn average_ms(sample: &nodo_pb::StatisticsSamples) -> Option<f32> {
(sample.count > 0).then(|| 1000.0 * sample.total / sample.count as f32)
}
fn format_step_duration(u: &nodo_pb::TransitionStatistics) -> Span<'static> {
if let (Some(x), Some(period)) = (
u.duration.as_ref().and_then(|d| average_ms(d)),
u.period.as_ref().and_then(|d| average_ms(d)),
) {
let p = x / period;
let color = if p > 0.5 {
STYLE_COLOR_ERR
} else if p > 0.20 {
STYLE_COLOR_WARN
} else {
STYLE_COLOR_VALUE_DEFAULT
};
Span::styled(format!("{:>5.1} ms", x), color)
} else {
Span::styled(format!("{:>8}", "None"), STYLE_COLOR_UNAVAILABLE)
}
}
fn format_step_count(u: &nodo_pb::TransitionStatistics) -> Span<'static> {
let x = u.duration.unwrap().count;
Span::styled(format!("{:>8}", x), STYLE_COLOR_VALUE_DEFAULT)
}
fn format_period(u: &nodo_pb::TransitionStatistics) -> Span<'static> {
if let Some(x) = average_ms(u.period.as_ref().unwrap()) {
Span::styled(format!("{:>6.1} ms", x), STYLE_COLOR_VALUE_DEFAULT)
} else {
Span::styled(format!("{:>8}", "Never"), STYLE_COLOR_UNAVAILABLE)
}
}
fn format_node_id(id: u32) -> Span<'static> {
Span::styled(format!("{:>4}", id), STYLE_COLOR_TYPENAME)
}
fn format_typename<'a>(input: &str) -> Line<'a> {
let regex = Regex::new(r"(?P<namespace>(?:[a-zA-Z_][a-zA-Z0-9_]*::)+)?(?P<typename>[a-zA-Z_][a-zA-Z0-9_]*)?(?P<generics><.+>)?")
.unwrap();
let mut spans = Vec::new();
if let Some(captures) = regex.captures(input) {
if let Some(namespace) = captures.name("namespace") {
spans.push(Span::styled(
namespace.as_str().to_string(),
Style::default().fg(STYLE_COLOR_DISABLED),
));
}
if let Some(typename) = captures.name("typename") {
spans.push(Span::styled(
typename.as_str().to_string(),
Style::default().fg(STYLE_COLOR_TYPENAME),
));
}
if let Some(generics) = captures.name("generics") {
spans.push(Span::styled(
generics.as_str().to_string(),
Style::default().fg(STYLE_COLOR_DISABLED),
));
}
}
spans.into()
}