[Add] UI optimizations, Graphs, More console commands

This commit is contained in:
Alex Emmet 2026-02-20 20:24:11 +01:00
commit 997c567c80
13 changed files with 424 additions and 115 deletions

View file

@ -1,5 +1,4 @@
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::{KeyCode, KeyEvent};
use json::JsonValue;
use ratatui::{ use ratatui::{
Frame, Frame,
layout::Rect, layout::Rect,
@ -7,14 +6,15 @@ use ratatui::{
text::{Line, Span}, text::{Line, Span},
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Paragraph},
}; };
use uuid::Uuid;
use crate::{ use crate::{
ACTIVE_TASKS, ACTIVE_TASKS, RELOAD, SHUTDOWN,
data::communication::{CommunicationType, CommunicationValue, DataTypes}, data::communication::{CommunicationType, CommunicationValue},
gui::{ gui::{
elements::elements::{Element, InteractableElement, JoinableElement}, elements::elements::{Element, InteractableElement, JoinableElement},
interaction_result::InteractionResult, interaction_result::InteractionResult,
ui::{FPS, UI}, ui::FPS,
util::borders::draw_block_joins, util::borders::draw_block_joins,
}, },
log, log_cv, log, log_cv,
@ -22,11 +22,7 @@ use crate::{
users::{user_manager, user_profile::UserProfile}, users::{user_manager, user_profile::UserProfile},
util::file_util, util::file_util,
}; };
use std::{ use std::{any::Any, time::Duration};
any::Any,
sync::Arc,
time::{Duration, SystemTime},
};
pub struct ConsoleCard { pub struct ConsoleCard {
focused: bool, focused: bool,
@ -129,8 +125,12 @@ impl InteractableElement for ConsoleCard {
match key.code { match key.code {
KeyCode::Enter => { KeyCode::Enter => {
let command = self.content.clone(); let command = self.content.clone();
let id = Uuid::new_v4();
let task_id = format!("command_{}_{}", command, id);
ACTIVE_TASKS.insert(task_id.clone());
tokio::spawn(async move { tokio::spawn(async move {
run_command(&command).await; run_command(&command).await;
ACTIVE_TASKS.remove(&task_id);
}); });
self.content = "".to_string(); self.content = "".to_string();
InteractionResult::Handled InteractionResult::Handled
@ -171,66 +171,43 @@ pub async fn run_command(command: &str) {
["tasks"] => { ["tasks"] => {
let active_tasks: Vec<String> = let active_tasks: Vec<String> =
ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect(); ACTIVE_TASKS.clone().iter().map(|v| v.to_string()).collect();
log!("Active tasks: {:?}", active_tasks); let info = if *SHUTDOWN.read().await && *RELOAD.read().await {
"Rebooting, "
} else if *SHUTDOWN.read().await {
"Shutting , "
} else {
""
};
log!("{}Active tasks: {:?}", info, active_tasks);
} }
["fps"] => { ["fps"] => {
log!("FPS: {}", *FPS.read().await); let (fps, skips) = *FPS.read().await;
log!("{:.1} FPS with {:.1}% of attempts skipped", fps, skips);
} }
["help"] => {
log!("Available commands: tasks, fps, ping, user");
}
["help", "tasks"] => {
log!("Tasks command usage: tasks");
}
["help", "fps"] => {
log!("FPS command usage: fps");
}
["help", "ping"] => {
log!("Ping command usage: ping [time]");
}
["help", "user"] => {
log!("User command usage: user add <username> | user remove <username> | user list");
}
["ping"] => { ["ping"] => {
let time = 20; ping(20).await;
let now = SystemTime::now();
let conn = {
let guard = OMIKRON_CONNECTION.read().await;
guard.as_ref().cloned()
};
let conn = match conn {
Some(c) => c,
None => return,
};
let response_cv = conn
.await_response(
&CommunicationValue::new(CommunicationType::ping),
Some(Duration::from_secs(time)),
)
.await;
let elapsed = now.elapsed().unwrap_or(Duration::ZERO);
match response_cv {
Ok(response) => log_cv!(response.add_data(
DataTypes::get_time,
JsonValue::from(elapsed.as_millis() as i64)
)),
Err(err) => log!("Ping error: {:?}", err),
}
} }
["ping", time] => { ["ping", time] => {
let time = time.parse::<u64>().unwrap_or(20); let time = time.parse::<u64>().unwrap_or(20);
ping(time).await;
let conn = {
let guard = OMIKRON_CONNECTION.read().await;
guard.as_ref().cloned()
};
let conn = match conn {
Some(c) => c,
None => return,
};
let response_cv = conn
.await_response(
&CommunicationValue::new(CommunicationType::ping),
Some(Duration::from_secs(time)),
)
.await;
match response_cv {
Ok(response) => log_cv!(response),
Err(err) => log!("Ping error: {:?}", err),
}
} }
["user", "add", username] => { ["user", "add", username] => {
if let (Some(user), Some(_)) = user_manager::create_user(username).await { if let (Some(user), Some(_)) = user_manager::create_user(username).await {
@ -273,3 +250,28 @@ pub async fn run_command(command: &str) {
} }
} }
} }
pub async fn ping(time: u64) {
let time = time;
let conn = {
let guard = OMIKRON_CONNECTION.read().await;
guard.as_ref().cloned()
};
let conn = match conn {
Some(c) => c,
None => return,
};
let response_cv = conn
.await_response(
&CommunicationValue::new(CommunicationType::ping),
Some(Duration::from_secs(time)),
)
.await;
match response_cv {
Ok(response) => log_cv!(response),
Err(err) => log!("Ping error: {:?}", err),
}
}

View file

@ -0,0 +1,208 @@
use std::{any::Any, sync::Arc};
use crossterm::event::KeyEvent;
use ratatui::{
Frame,
layout::Rect,
style::{Color, Style},
widgets::{
Block, Borders,
canvas::{Canvas, Line},
},
};
use crate::{
APP_STATE,
gui::{
elements::elements::{Element, InteractableElement, JoinableElement},
interaction_result::InteractionResult,
ui::UI,
util::borders::draw_block_joins,
},
};
pub enum GRAPHS {
Ram,
Cpu,
Ping,
}
impl GRAPHS {
pub fn get_graph(&self) -> Vec<(f64, f64)> {
match self {
GRAPHS::Ram => APP_STATE.lock().unwrap().ram.clone(),
GRAPHS::Cpu => APP_STATE.lock().unwrap().cpu.clone(),
GRAPHS::Ping => APP_STATE.lock().unwrap().ping.clone(),
}
}
pub fn get_unit(&self) -> String {
match self {
GRAPHS::Ram => "MB".to_string(),
GRAPHS::Cpu => "%".to_string(),
GRAPHS::Ping => "ms".to_string(),
}
}
}
pub struct GraphCard {
ui: Arc<UI>,
graph_type: GRAPHS,
focused: bool,
pub title: String,
color: Color,
borders: Borders,
joins: Borders,
open: bool,
}
impl GraphCard {
pub fn new(ui: Arc<UI>, graph_type: GRAPHS, title: String) -> Self {
Self {
ui,
graph_type,
focused: false,
title,
color: Color::White,
borders: Borders::ALL,
joins: Borders::NONE,
open: false,
}
}
pub fn set_open(&mut self, open: bool) {
self.open = open;
}
}
impl Element for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn render(&self, f: &mut Frame, r: Rect) {
if self.open {
let graph = self.graph_type.get_graph();
let unit = self.graph_type.get_unit();
let min_x = graph.first().map(|(x, _)| *x).unwrap_or(0.0);
let max_x = graph.last().map(|(x, _)| *x).unwrap_or(100.0);
let min_y = graph
.iter()
.map(|(_, y)| *y)
.filter(|y| *y > 0.0)
.min_by(|a, b| a.total_cmp(b))
.unwrap_or(0.0);
let max_y = graph.iter().map(|(_, y)| *y).fold(f64::MIN, f64::max);
let block = Block::default()
.title(format!(
"─{}:─{}{}──{}/{}─MIN/MAX",
self.title,
graph.last().unwrap_or(&(0.0, 0.0)).1 as i64,
unit,
min_y as i64,
max_y as i64,
))
.borders(self.borders)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
let canvas = Canvas::default()
.block(block)
.x_bounds([min_x, max_x])
.y_bounds([0.0, 100.0])
.paint(|ctx| {
for (x, y) in &graph {
ctx.draw(&Line {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color: self.color,
});
}
});
f.render_widget(canvas, r);
} else {
let block = Block::default()
.title("")
.borders(self.borders)
.border_style(if self.focused {
Style::default().fg(Color::Yellow)
} else {
Style::default()
});
f.render_widget(block, r);
}
draw_block_joins(f, r, self.borders, self.joins);
}
}
impl JoinableElement for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &dyn Element {
self
}
fn as_element_mut(&mut self) -> &mut dyn Element {
self
}
fn set_borders(&mut self, borders: Borders) {
self.borders = borders;
}
fn set_joins(&mut self, joins: Borders) {
self.joins = joins;
}
}
impl InteractableElement for GraphCard {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn as_element(&self) -> &dyn Element {
self
}
fn as_element_mut(&mut self) -> &mut dyn Element {
self
}
fn interact(&mut self, _key: KeyEvent) -> InteractionResult {
InteractionResult::Handled
}
fn can_focus(&self) -> bool {
true
}
fn is_focused(&self) -> bool {
self.focused
}
fn focus(&mut self, f: bool) {
self.focused = f;
}
}

25
src/gui/elements/log_card.rs Normal file → Executable file
View file

@ -30,7 +30,7 @@ impl LogCard {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
focused: false, focused: false,
scroll: 0, scroll: 1,
borders: Borders::ALL, borders: Borders::ALL,
joins: Borders::NONE, joins: Borders::NONE,
} }
@ -51,7 +51,9 @@ impl Element for LogCard {
let lines: Vec<Line> = logs let lines: Vec<Line> = logs
.iter() .iter()
.map(|log| Line::from(Span::raw(log.line.clone())).style(log.color)) .map(|log| {
Line::from(Span::raw(log.line.clone())).style(Style::default().fg(log.color))
})
.collect(); .collect();
let block = Block::default() let block = Block::default()
@ -67,10 +69,17 @@ impl Element for LogCard {
Style::default() Style::default()
}); });
let inner_height = area.height.saturating_sub(2) as usize;
let total_lines = lines.len();
let base_scroll = total_lines.saturating_sub(inner_height) as u16;
let scroll = base_scroll.saturating_sub(self.scroll);
let paragraph = Paragraph::new(lines) let paragraph = Paragraph::new(lines)
.block(block) .block(block)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((self.scroll, 0)); .scroll((scroll, 0));
f.render_widget(paragraph, area); f.render_widget(paragraph, area);
draw_block_joins(f, area, self.borders, self.joins); draw_block_joins(f, area, self.borders, self.joins);
@ -121,13 +130,17 @@ impl InteractableElement for LogCard {
fn interact(&mut self, key: KeyEvent) -> InteractionResult { fn interact(&mut self, key: KeyEvent) -> InteractionResult {
match key.code { match key.code {
KeyCode::Char('J') | KeyCode::Char('j') => { KeyCode::Char('J') | KeyCode::Char('j') => {
if self.scroll > 0 { let state = APP_STATE.lock().unwrap();
self.scroll -= 1; let logs = state.get_logs();
if self.scroll < logs.len() as u16 {
self.scroll += 1;
} }
InteractionResult::Handled InteractionResult::Handled
} }
KeyCode::Char('K') | KeyCode::Char('k') => { KeyCode::Char('K') | KeyCode::Char('k') => {
self.scroll += 1; if self.scroll > 1 {
self.scroll -= 1;
}
InteractionResult::Handled InteractionResult::Handled
} }
_ => InteractionResult::Unhandled, _ => InteractionResult::Unhandled,

View file

@ -3,6 +3,7 @@ use crate::gui::ui::{UI, UNIQUE};
use crate::{RELOAD, SHUTDOWN}; use crate::{RELOAD, SHUTDOWN};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, poll, read};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
pub fn setup_input_handler(ui: Arc<UI>) { pub fn setup_input_handler(ui: Arc<UI>) {
@ -30,7 +31,7 @@ pub fn setup_input_handler(ui: Arc<UI>) {
if key_event.kind == KeyEventKind::Press { if key_event.kind == KeyEventKind::Press {
let uic = ui.clone(); let uic = ui.clone();
handle_input(key_event, uic).await; handle_input(key_event, uic).await;
*UNIQUE.write().await = true; UNIQUE.store(true, Ordering::Relaxed);
} }
} }
} }

View file

@ -1,6 +1,7 @@
pub mod elements { pub mod elements {
pub mod console_card; pub mod console_card;
pub mod elements; pub mod elements;
pub mod graph_card;
pub mod log_card; pub mod log_card;
} }
pub mod screens { pub mod screens {

View file

@ -2,6 +2,7 @@ use crate::gui::{
elements::{ elements::{
console_card::ConsoleCard, console_card::ConsoleCard,
elements::{InteractableElement, JoinableElement}, elements::{InteractableElement, JoinableElement},
graph_card::{GRAPHS, GraphCard},
log_card::LogCard, log_card::LogCard,
}, },
interaction_result::InteractionResult, interaction_result::InteractionResult,
@ -19,34 +20,49 @@ use ratatui::{
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
pub struct MainScreen { pub struct MainScreen {
ui: Arc<UI>,
elements: Vec<Box<dyn InteractableElement>>, elements: Vec<Box<dyn InteractableElement>>,
nav_grid: Vec<Vec<Option<usize>>>, nav_grid: Vec<Vec<Option<usize>>>,
selected_coords: (usize, usize), selected_coords: (usize, usize),
graphs_open: bool,
} }
impl MainScreen { impl MainScreen {
pub async fn new(ui: Arc<UI>) -> Self { pub async fn new(ui: Arc<UI>) -> Self {
let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new(); let mut elements: Vec<Box<dyn InteractableElement>> = Vec::new();
let mut nav_grid: Vec<Vec<Option<usize>>> = Vec::new();
let nav_grid = vec![
vec![Some(0), Some(2)],
vec![Some(1), Some(3)],
vec![None, Some(4)],
];
let mut log_card = LogCard::new(); let mut log_card = LogCard::new();
log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT)); log_card.set_borders(Borders::TOP.union(Borders::RIGHT).union(Borders::LEFT));
let mut console_card = ConsoleCard::new("Console", ""); let mut console_card = ConsoleCard::new("Console", "");
console_card.set_joins(Borders::TOP); console_card.set_joins(Borders::TOP);
elements.push(Box::new(log_card)); elements.push(Box::new(log_card));
elements.push(Box::new(console_card)); elements.push(Box::new(console_card));
nav_grid.push(vec![Some(0)]); let mut ram_graph = GraphCard::new(ui.clone(), GRAPHS::Ram, "RAM Usage".into());
nav_grid.push(vec![Some(1)]); ram_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
elements.push(Box::new(ram_graph));
let mut cpu_graph = GraphCard::new(ui.clone(), GRAPHS::Cpu, "CPU Usage".into());
cpu_graph.set_borders(Borders::TOP.union(Borders::LEFT).union(Borders::RIGHT));
cpu_graph.set_joins(Borders::TOP);
elements.push(Box::new(cpu_graph));
let mut ping_graph = GraphCard::new(ui.clone(), GRAPHS::Ping, "Ping".into());
ping_graph.set_joins(Borders::TOP);
elements.push(Box::new(ping_graph));
let graphs_open = true;
let mut screen = MainScreen { let mut screen = MainScreen {
ui,
elements, elements,
nav_grid, nav_grid,
selected_coords: (1, 0), selected_coords: (1, 0),
graphs_open,
}; };
screen.focus_current(); screen.focus_current();
screen screen
} }
@ -101,7 +117,6 @@ impl MainScreen {
_ => {} _ => {}
} }
// Clamp X to row length
if let Some(row) = self.nav_grid.get(y) { if let Some(row) = self.nav_grid.get(y) {
if x >= row.len() { if x >= row.len() {
x = row.len() - 1; x = row.len() - 1;
@ -129,10 +144,6 @@ impl Screen for MainScreen {
self self
} }
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, rect: Rect) { fn render(&self, f: &mut Frame, rect: Rect) {
let main_block = Block::default().title("Main").borders(Borders::ALL); let main_block = Block::default().title("Main").borders(Borders::ALL);
f.render_widget(main_block, rect); f.render_widget(main_block, rect);
@ -142,14 +153,42 @@ impl Screen for MainScreen {
horizontal: 1, horizontal: 1,
}); });
let chunks = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(inner); let graphs_width = if self.graphs_open { 30 } else { 2 };
let main_width = inner.width.saturating_sub(graphs_width);
let chunks = Layout::default()
.direction(ratatui::layout::Direction::Horizontal)
.constraints([
Constraint::Length(main_width),
Constraint::Length(graphs_width),
])
.split(inner);
let left_chunks =
Layout::vertical([Constraint::Min(0), Constraint::Length(3)]).split(chunks[0]);
if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) { if let Some(Some(index)) = self.nav_grid.get(0).and_then(|r| r.get(0)) {
self.elements[*index].as_element().render(f, chunks[0]); self.elements[*index].as_element().render(f, left_chunks[0]);
}
if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) {
self.elements[*index].as_element().render(f, left_chunks[1]);
} }
if let Some(Some(index)) = self.nav_grid.get(1).and_then(|r| r.get(0)) { let graph_elements: Vec<_> = self
self.elements[*index].as_element().render(f, chunks[1]); .elements
.iter()
.filter(|el| el.as_any().is::<GraphCard>())
.collect();
let graph_chunks = Layout::vertical(
graph_elements
.iter()
.map(|_| Constraint::Length(chunks[1].height / graph_elements.len() as u16))
.collect::<Vec<_>>(),
)
.split(chunks[1]);
for (el, area) in graph_elements.iter().zip(graph_chunks.iter()) {
el.as_element().render(f, *area);
} }
} }
@ -159,6 +198,15 @@ impl Screen for MainScreen {
KeyCode::Down => self.navigate(NavDirection::Down), KeyCode::Down => self.navigate(NavDirection::Down),
KeyCode::Left => self.navigate(NavDirection::Left), KeyCode::Left => self.navigate(NavDirection::Left),
KeyCode::Right => self.navigate(NavDirection::Right), KeyCode::Right => self.navigate(NavDirection::Right),
KeyCode::Enter | KeyCode::Char(' ') if self.selected_coords.1 == 1 => {
self.graphs_open = !self.graphs_open;
for element in self.elements.iter_mut() {
if let Some(graph) = element.as_any_mut().downcast_mut::<GraphCard>() {
graph.set_open(self.graphs_open);
}
}
return InteractionResult::Handled;
}
_ => { _ => {
let (y, x) = self.selected_coords; let (y, x) = self.selected_coords;
if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) { if let Some(Some(index)) = self.nav_grid.get(y).and_then(|r| r.get(x)) {

View file

@ -1,9 +1,9 @@
use std::{any::Any, sync::Arc}; use std::any::Any;
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use ratatui::{Frame, layout::Rect}; use ratatui::{Frame, layout::Rect};
use crate::gui::{interaction_result::InteractionResult, ui::UI}; use crate::gui::interaction_result::InteractionResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NavDirection { pub enum NavDirection {
@ -20,8 +20,6 @@ pub trait Screen: Send + Sync + Any {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any;
fn get_ui(&self) -> &Arc<UI>;
fn render(&self, f: &mut Frame, rect: Rect); fn render(&self, f: &mut Frame, rect: Rect);
fn handle_input(&mut self, event: KeyEvent) -> InteractionResult; fn handle_input(&mut self, event: KeyEvent) -> InteractionResult;
} }

View file

@ -51,10 +51,6 @@ impl Screen for TermsCheckerScreen {
self self
} }
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, size: Rect) { fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5; let mut needed_height = 5;

View file

@ -121,10 +121,6 @@ impl Screen for TermsUpdaterScreen {
self self
} }
fn get_ui(&self) -> &Arc<UI> {
&self.ui
}
fn render(&self, f: &mut Frame, size: Rect) { fn render(&self, f: &mut Frame, size: Rect) {
let mut needed_height = 5; let mut needed_height = 5;

0
src/gui/tui.rs Normal file
View file

View file

@ -9,16 +9,21 @@ use crossterm::event::KeyEvent;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use ratatui::{Terminal, backend::CrosstermBackend, init}; use ratatui::{Terminal, backend::CrosstermBackend, init};
use std::{ use std::{
collections::VecDeque,
io::Stdout, io::Stdout,
sync::{Arc, Mutex}, sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration, time::Duration,
}; };
use tokio::{sync::RwLock, time::Instant}; use tokio::{sync::RwLock, time::Instant};
/// UI state and rendering /// UI state and rendering
pub static UNIQUE: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true)); pub static UNIQUE: AtomicBool = AtomicBool::new(true);
pub static FPS: Lazy<RwLock<(f64, f64)>> = Lazy::new(|| RwLock::new((0.0, 0.0)));
pub static FPS: Lazy<RwLock<f64>> = Lazy::new(|| RwLock::new(0.0));
pub struct UI { pub struct UI {
pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>, pub terminal: Arc<Mutex<Terminal<CrosstermBackend<Stdout>>>>,
screen: Arc<RwLock<Option<Box<dyn Screen>>>>, screen: Arc<RwLock<Option<Box<dyn Screen>>>>,
@ -27,26 +32,71 @@ pub struct UI {
pub fn start_tui() -> Arc<UI> { pub fn start_tui() -> Arc<UI> {
let ui = Arc::new(UI::new()); let ui = Arc::new(UI::new());
let uic = ui.clone(); let uic = ui.clone();
tokio::spawn(async move {
ACTIVE_TASKS.insert("UI Renderer".to_string()); ACTIVE_TASKS.insert("UI Renderer".to_string());
tokio::spawn(async move {
let mut last_render = Instant::now(); let mut last_render = Instant::now();
let mut last: Vec<f64> = Vec::new();
let mut fps_samples: VecDeque<f64> = VecDeque::with_capacity(20);
let mut skip_samples: VecDeque<u16> = VecDeque::with_capacity(20);
let mut fps_sum = 0.0;
let mut skip_sum: u32 = 0;
let mut skipped = 0;
loop { loop {
if *SHUTDOWN.read().await { if *SHUTDOWN.read().await {
break; break;
} }
if *UNIQUE.read().await { if skipped > 5 || UNIQUE.load(Ordering::Relaxed) {
uic.render().await; uic.render().await;
skip_samples.push_back(skipped);
skip_sum += skipped as u32;
if skip_samples.len() > 20 {
if let Some(old) = skip_samples.pop_front() {
skip_sum -= old as u32;
}
}
skipped = 0;
let elapsed = last_render.elapsed().as_secs_f64(); let elapsed = last_render.elapsed().as_secs_f64();
if elapsed > 0.0 { if elapsed > 0.0 {
last.push(1.0 / elapsed); let fps = 1.0 / elapsed;
fps_samples.push_back(fps);
fps_sum += fps;
if fps_samples.len() > 20 {
if let Some(old) = fps_samples.pop_front() {
fps_sum -= old;
} }
if last.len() > 10 {
last.remove(0);
} }
*FPS.write().await = last.iter().sum::<f64>() / last.len() as f64; }
let avg_fps = if !fps_samples.is_empty() {
fps_sum / fps_samples.len() as f64
} else {
0.0
};
let avg_skips_percentage = if !skip_samples.is_empty() {
let avg_skipped = skip_sum as f64 / skip_samples.len() as f64;
let total_iterations = avg_skipped + 1.0;
(avg_skipped / total_iterations) * 100.0
} else {
0.0
};
*FPS.write().await = (avg_fps, avg_skips_percentage);
last_render = Instant::now(); last_render = Instant::now();
UNIQUE.store(false, Ordering::Relaxed);
} else {
skipped += 1;
} }
tokio::time::sleep(Duration::from_millis(16)).await; tokio::time::sleep(Duration::from_millis(16)).await;
} }

View file

@ -40,7 +40,7 @@ pub static SHUTDOWN: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(false));
pub static RELOAD: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true)); pub static RELOAD: Lazy<RwLock<bool>> = Lazy::new(|| RwLock::new(true));
pub static ACTIVE_TASKS: Lazy<DashSet<String>> = Lazy::new(|| DashSet::new()); pub static ACTIVE_TASKS: Lazy<DashSet<String>> = Lazy::new(|| DashSet::new());
#[tokio::main(flavor = "multi_thread", worker_threads = 8)] #[tokio::main(flavor = "multi_thread", worker_threads = 16)]
#[allow(unused_must_use, dead_code)] #[allow(unused_must_use, dead_code)]
async fn main() { async fn main() {
while *RELOAD.read().await { while *RELOAD.read().await {
@ -57,7 +57,7 @@ async fn main() {
if ACTIVE_TASKS.is_empty() { if ACTIVE_TASKS.is_empty() {
break; break;
} }
sleep(Duration::from_secs(1)).await; sleep(Duration::from_millis(100)).await;
} }
println!("You need to accept our End User Licence Agreement before launching!"); println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!"); println!("You can find this at 'agreements'!");
@ -176,7 +176,7 @@ async fn main() {
if !omikron.is_connected().await { if !omikron.is_connected().await {
break; break;
} }
sleep(Duration::from_secs(1)).await; sleep(Duration::from_millis(100)).await;
} }
} }
if *RELOAD.read().await { if *RELOAD.read().await {

View file

@ -2,7 +2,7 @@ use std::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io::Write, io::Write,
path::Path, path::Path,
sync::{OnceLock, mpsc}, sync::{OnceLock, atomic::Ordering, mpsc},
thread, thread,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
@ -122,9 +122,7 @@ pub fn log_internal_translated(
args: Vec<String>, args: Vec<String>,
) { ) {
if let Some(tx) = LOGGER.get() { if let Some(tx) = LOGGER.get() {
tokio::spawn(async move { UNIQUE.store(true, Ordering::Relaxed);
*UNIQUE.write().await = true;
});
let _ = tx.send(LogMessage { let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now() timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -142,9 +140,7 @@ pub fn log_internal_translated(
pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) {
if let Some(tx) = LOGGER.get() { if let Some(tx) = LOGGER.get() {
tokio::spawn(async move { UNIQUE.store(true, Ordering::Relaxed);
*UNIQUE.write().await = true;
});
let _ = tx.send(LogMessage { let _ = tx.send(LogMessage {
timestamp_ms: SystemTime::now() timestamp_ms: SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)