From 6d31142258aa58afcf2741b2fa1ffb02a9dae2f3 Mon Sep 17 00:00:00 2001 From: Mark Poliakov Date: Mon, 30 Mar 2026 12:23:25 +0300 Subject: [PATCH] sysutils: add cpu/memory information to top --- kernel/libk/src/fs/sysfs/attribute/integer.rs | 206 ++++++++++++++++ kernel/libk/src/fs/sysfs/attribute/mod.rs | 4 + kernel/libk/src/task/sched.rs | 77 +++++- kernel/src/init.rs | 4 +- userspace/sysutils/src/top.rs | 222 ++++++++++++++++-- 5 files changed, 486 insertions(+), 27 deletions(-) create mode 100644 kernel/libk/src/fs/sysfs/attribute/integer.rs diff --git a/kernel/libk/src/fs/sysfs/attribute/integer.rs b/kernel/libk/src/fs/sysfs/attribute/integer.rs new file mode 100644 index 00000000..d6706e38 --- /dev/null +++ b/kernel/libk/src/fs/sysfs/attribute/integer.rs @@ -0,0 +1,206 @@ +use core::{any::Any, marker::PhantomData, sync::atomic::AtomicBool}; + +use alloc::{sync::Arc, vec::Vec}; +use libk_util::sync::spin_rwlock::IrqSafeRwLock; +use yggdrasil_abi::{ + error::Error, + io::{FileMode, OpenOptions}, +}; + +use crate::{ + fs::sysfs::{attribute::Attribute, object::KObject}, + vfs::{CommonImpl, Filename, InstanceData, Metadata, Node, NodeFlags, NodeRef, RegularImpl}, +}; + +pub trait IntegerAttributeValue: Sized + Send + Sync + 'static { + fn from_raw(data: &[u8]) -> Result; + fn into_raw(&self, format: IntegerAttributeFormat) -> Result, Error>; +} + +pub enum IntegerAttributeFormat { + Decimal, + Hex, + Octal, +} + +pub trait IntegerAttributeOps: Sync + Send + 'static { + type Data: Send + 'static = (); + + const WRITEABLE: bool = false; + const NAME: &'static str; + const FORMAT: IntegerAttributeFormat = IntegerAttributeFormat::Decimal; + + fn read(state: &Self::Data) -> Result { + let _ = state; + Err(Error::NotImplemented) + } + fn write(state: &Self::Data, value: N) -> Result<(), Error> { + let _ = state; + let _ = value; + Err(Error::ReadOnly) + } +} + +pub struct IntegerAttribute>( + PhantomData<(V, N)>, +); + +struct IntegerAttributeNode> { + object: Arc>, + _pd: PhantomData<(V, N)>, +} + +struct IntegerAttributeState { + value: IrqSafeRwLock>, + #[allow(unused)] + modified: AtomicBool, +} + +macro_rules! impl_integer_value { + ($($ty:ty),+ $(,)?) => { + $( + impl IntegerAttributeValue for $ty { + fn from_raw(_data: &[u8]) -> Result { + todo!("IntegerAttributeValue::from_raw()") + } + fn into_raw(&self, format: IntegerAttributeFormat) -> Result, Error> { + let string = match format { + IntegerAttributeFormat::Decimal => alloc::format!("{self}\n"), + IntegerAttributeFormat::Hex => alloc::format!("{self:#x}\n"), + IntegerAttributeFormat::Octal => alloc::format!("{self:#o}\n") + }; + Ok(string.into_bytes()) + } + } + )+ + }; +} + +impl_integer_value!(u8, u16, u32, u64, i8, i16, i32, i64); + +impl> CommonImpl + for IntegerAttributeNode +{ + fn size(&self, _node: &NodeRef) -> Result { + Ok(0) + } + + fn as_any(&self) -> &dyn Any { + self as _ + } +} + +impl> RegularImpl + for IntegerAttributeNode +{ + fn open( + &self, + _node: &NodeRef, + opts: OpenOptions, + ) -> Result<(u64, Option), Error> { + if opts.contains(OpenOptions::WRITE) { + // && !V::WRITEABLE { + return Err(Error::ReadOnly); + } + + let value = V::read(self.object.data())?.into_raw(V::FORMAT)?; + + let instance = IntegerAttributeState { + value: IrqSafeRwLock::new(value), + modified: AtomicBool::new(false), + }; + + Ok((0, Some(Arc::new(instance)))) + } + + fn close(&self, _node: &NodeRef, _instance: Option<&InstanceData>) -> Result<(), Error> { + Ok(()) + // if V::WRITEABLE { + // let instance = instance.ok_or(Error::InvalidFile)?; + // let instance = instance + // .downcast_ref::() + // .ok_or(Error::InvalidFile)?; + + // if instance.modified.load(Ordering::Acquire) { + // let value = instance.value.read(); + // let value_str = + // core::str::from_utf8(&value[..]).map_err(|_| Error::InvalidArgument)?; + + // // Trim whitespace and newlines + // V::write(&self.object.data, value_str.trim())?; + // } + // } + + // Ok(()) + } + + fn read( + &self, + _node: &NodeRef, + instance: Option<&InstanceData>, + pos: u64, + buf: &mut [u8], + ) -> Result { + let instance = instance.ok_or(Error::InvalidFile)?; + let instance = instance + .downcast_ref::() + .ok_or(Error::InvalidFile)?; + + let value = instance.value.read(); + let len = value.len(); + if pos >= len as u64 { + return Ok(0); + } + let pos = pos as usize; + let amount = (len - pos).min(buf.len()); + buf[..amount].copy_from_slice(&value[pos..pos + amount]); + + Ok(amount) + } + + fn write( + &self, + _node: &NodeRef, + _instance: Option<&InstanceData>, + _pos: u64, + _buf: &[u8], + ) -> Result { + todo!("Integer attribute write") + } + + fn truncate(&self, _node: &NodeRef, _new_size: u64) -> Result<(), Error> { + Ok(()) + } +} + +impl> From for IntegerAttribute { + fn from(_value: V) -> Self { + Self(PhantomData) + } +} + +impl> Attribute + for IntegerAttribute +{ + fn instantiate(&self, parent: &Arc>) -> Result, Error> { + let mode = match V::WRITEABLE { + false => FileMode::new(0o444), + true => FileMode::new(0o644), + }; + + Ok(Node::regular( + IntegerAttributeNode { + object: parent.clone(), + _pd: PhantomData::<(V, N)>, + }, + NodeFlags::IN_MEMORY_PROPS, + Some(Metadata::now_root(mode, 0)), + None, + )) + } + + // TODO implement this properly + fn name(&self) -> &Filename { + unsafe { Filename::from_str_unchecked(V::NAME) } + } +} diff --git a/kernel/libk/src/fs/sysfs/attribute/mod.rs b/kernel/libk/src/fs/sysfs/attribute/mod.rs index 5617f452..0704da65 100644 --- a/kernel/libk/src/fs/sysfs/attribute/mod.rs +++ b/kernel/libk/src/fs/sysfs/attribute/mod.rs @@ -6,6 +6,7 @@ use crate::vfs::{Filename, NodeRef}; use super::object::KObject; mod bytes; +mod integer; mod string; pub trait Attribute: Sync + Send { @@ -14,4 +15,7 @@ pub trait Attribute: Sync + Send { } pub use bytes::{BytesAttribute, BytesAttributeOps}; +pub use integer::{ + IntegerAttribute, IntegerAttributeFormat, IntegerAttributeOps, IntegerAttributeValue, +}; pub use string::{StringAttribute, StringAttributeOps}; diff --git a/kernel/libk/src/task/sched.rs b/kernel/libk/src/task/sched.rs index a6aa3521..a0328c7f 100644 --- a/kernel/libk/src/task/sched.rs +++ b/kernel/libk/src/task/sched.rs @@ -11,9 +11,14 @@ use kernel_arch::{ task::{Scheduler, TaskContext}, }; use libk_util::{OneTimeInit, sync::IrqGuard}; -use yggdrasil_abi::time::SystemTime; +use yggdrasil_abi::{error::Error, time::SystemTime}; use crate::{ + fs::sysfs::{ + self, + attribute::{IntegerAttribute, IntegerAttributeOps}, + object::KObject, + }, task::{TaskContextImpl, ThreadId, ThreadState, thread::Thread}, time::monotonic_time, }; @@ -131,7 +136,7 @@ impl CpuQueue { &self.stats.idle }; - let t = delta.as_millis() as u64; + let t = delta.as_nanos() as u64; self.stats.total.fetch_add(t, Ordering::Relaxed); counter.fetch_add(t, Ordering::Relaxed); @@ -254,6 +259,74 @@ impl Scheduler for CpuQueue { } } +pub fn setup_sysfs() { + fn register_cpu_node(cpu: usize, node: Arc>) { + static KERNEL_SCHED: OneTimeInit>> = OneTimeInit::new(); + + let Ok(kernel_sched) = KERNEL_SCHED.or_try_init_with(|| { + let kernel = sysfs::kernel().ok_or(Error::DoesNotExist)?; + let node = KObject::new(()); + kernel.add_object("sched", node.clone())?; + Ok(node) + }) else { + return; + }; + + let name = alloc::format!("{cpu}"); + kernel_sched.add_object(name, node).ok(); + } + + struct CpuIdle; + struct CpuKernel; + struct CpuUser; + struct CpuTotal; + + impl IntegerAttributeOps for CpuIdle { + type Data = usize; + const NAME: &'static str = "idle"; + + fn read(state: &Self::Data) -> Result { + Ok(stats(*state).idle.load(Ordering::Acquire)) + } + } + + impl IntegerAttributeOps for CpuTotal { + type Data = usize; + const NAME: &'static str = "total"; + + fn read(state: &Self::Data) -> Result { + Ok(stats(*state).total.load(Ordering::Acquire)) + } + } + + impl IntegerAttributeOps for CpuUser { + type Data = usize; + const NAME: &'static str = "user"; + + fn read(state: &Self::Data) -> Result { + Ok(stats(*state).user.load(Ordering::Acquire)) + } + } + + impl IntegerAttributeOps for CpuKernel { + type Data = usize; + const NAME: &'static str = "kernel"; + + fn read(state: &Self::Data) -> Result { + Ok(stats(*state).kernel.load(Ordering::Acquire)) + } + } + + for cpu in 0..ArchitectureImpl::cpu_count() { + let node = KObject::new(cpu); + node.add_attribute(IntegerAttribute::from(CpuIdle)).ok(); + node.add_attribute(IntegerAttribute::from(CpuTotal)).ok(); + node.add_attribute(IntegerAttribute::from(CpuUser)).ok(); + node.add_attribute(IntegerAttribute::from(CpuKernel)).ok(); + register_cpu_node(cpu, node); + } +} + pub fn stats(index: usize) -> &'static SchedulerStats { &QUEUES.get()[index].stats } diff --git a/kernel/src/init.rs b/kernel/src/init.rs index c96c0c19..4d3e78ea 100644 --- a/kernel/src/init.rs +++ b/kernel/src/init.rs @@ -6,7 +6,7 @@ use libk::{ device::display::console, fs::devfs, random, - task::{binary::LoadOptions, process::Process, runtime, thread::Thread}, + task::{binary::LoadOptions, process::Process, runtime, sched, thread::Thread}, vfs::{IoContext, NodeRef, OwnedFilename, impls::fn_hardlink}, }; use memfs::MemoryFilesystem; @@ -69,6 +69,8 @@ pub fn kinit() -> Result<(), Error> { runtime::spawn(ygg_driver_usb::bus::bus_handler())?; runtime::spawn(console::flush_consoles_task()).ok(); + sched::setup_sysfs(); + devfs::root() .add_child( OwnedFilename::new("tty").unwrap(), diff --git a/userspace/sysutils/src/top.rs b/userspace/sysutils/src/top.rs index 1c6609a4..15914dc7 100644 --- a/userspace/sysutils/src/top.rs +++ b/userspace/sysutils/src/top.rs @@ -1,7 +1,7 @@ -#![feature(yggdrasil_os)] +#![feature(yggdrasil_os, rustc_private)] use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fs, io, os::fd::AsRawFd, path::{Path, PathBuf}, @@ -10,13 +10,16 @@ use std::{ use cross::io::{Poll, PollEvent, TimerFd}; use libterm::{Term, TermKey}; +use libutil::fmt::FormatSize; use ratatui::{ Frame, Terminal, - layout::{Constraint, Layout}, + layout::{Constraint, Layout, Rect}, style::{Color, Style}, + symbols, text::{Line, Span}, - widgets::{Cell, Paragraph, Row, Table}, + widgets::{Cell, LineGauge, Paragraph, Row, Table}, }; +use runtime::{abi::system, rt::system::SystemMemoryStats}; struct ProcessInfo { pid: u32, @@ -31,10 +34,64 @@ struct ThreadInfo { struct State { processes: Vec, + memory: Option, expanded: HashSet, + cpu_load: HashMap, + cpu_stats: HashMap, selection: usize, } +struct CpuStats { + total_ns: u64, + idle_ns: u64, +} + +impl CpuStats { + fn read_ns>(path: P) -> Option { + fs::read_to_string(path).ok()?.trim().parse().ok() + } + + fn read(index: usize) -> Self { + let path = PathBuf::from("/sys/kernel/sched").join(format!("{index}")); + let total_ns = Self::read_ns(path.join("total")).unwrap_or_default(); + let idle_ns = Self::read_ns(path.join("idle")).unwrap_or_default(); + Self { total_ns, idle_ns } + } +} + +fn memory_info() -> io::Result { + runtime::rt::system::get_system_info!(system::MemoryUsage).map_err(io::Error::from) +} + +fn cpu_stats(stats: &mut HashMap, load: &mut HashMap) { + if stats.is_empty() { + let Ok(dir) = fs::read_dir("/sys/kernel/sched") else { + return; + }; + for entry in dir { + let Ok(entry) = entry else { continue }; + let filename = entry.file_name(); + let Some(filename) = filename.to_str() else { + continue; + }; + let Ok(index) = filename.parse() else { + continue; + }; + let data = CpuStats::read(index); + stats.insert(index, data); + } + return; + } + + for (cpu, stats) in stats.iter_mut() { + let new_stats = CpuStats::read(*cpu); + let idle_ns = new_stats.idle_ns.wrapping_sub(stats.idle_ns); + let total_ns = new_stats.total_ns.wrapping_sub(stats.total_ns).max(idle_ns); + load.insert(*cpu, CpuStats { total_ns, idle_ns }); + *stats = new_stats; + } +} + fn list_ids>(path: P) -> io::Result> { let mut list = vec![]; for entry in fs::read_dir(path)? { @@ -88,11 +145,16 @@ fn threads>(pid_path: P) -> io::Result> { impl State { fn new() -> io::Result { let processes = processes()?; - Ok(Self { + let mut this = Self { processes, + memory: None, + cpu_load: HashMap::new(), + cpu_stats: HashMap::new(), expanded: HashSet::new(), selection: 0, - }) + }; + this.refresh()?; + Ok(this) } fn next(&mut self) { @@ -134,6 +196,8 @@ impl State { .processes .get(self.selection) .map(|process| process.pid); + cpu_stats(&mut self.cpu_stats, &mut self.cpu_load); + self.memory = memory_info().ok(); self.processes = processes()?; let new_selection = selected_pid .and_then(|pid| self.processes.iter().position(|process| process.pid == pid)); @@ -142,22 +206,10 @@ impl State { Ok(()) } - fn render(&self, f: &mut Frame) { - const KEYS: &[(&str, &str)] = &[ - (" j ", "Down"), - (" k ", "Up"), - (" Enter ", "Toggle"), - (" K ", "Kill"), - ]; - - let rects = Layout::default() - .constraints([Constraint::Percentage(99), Constraint::Min(1)].as_ref()) - .split(f.area()); - + fn render_processes(&self, frame: &mut Frame, area: Rect) { let normal_style = Style::default(); let selected_style = Style::default().bg(Color::Cyan).fg(Color::Black); let hint_style = Style::default().bg(Color::Blue).fg(Color::White); - let key_style = Style::default().bg(Color::Cyan).fg(Color::Black); let rows = self.processes.iter().enumerate().flat_map(|(i, process)| { let style = if i == self.selection { @@ -213,6 +265,20 @@ impl State { .highlight_symbol(">>") .header(header); + frame.render_widget(table, area); + } + + fn render_keybar(&self, frame: &mut Frame, area: Rect) { + const KEYS: &[(&str, &str)] = &[ + (" j ", "Down"), + (" k ", "Up"), + (" Enter ", "Toggle"), + (" K ", "Kill"), + ]; + + let hint_style = Style::default().bg(Color::Blue).fg(Color::White); + let key_style = Style::default().bg(Color::Cyan).fg(Color::Black); + let mut status_spans = vec![]; for (i, &(key, hint)) in KEYS.iter().enumerate() { if i != 0 { @@ -221,14 +287,122 @@ impl State { status_spans.push(Span::styled(key, key_style)); status_spans.push(Span::styled(hint, hint_style)); } - let status = Paragraph::new(vec![Line::from(status_spans)]); - f.render_widget(table, rects[0]); - f.render_widget(status, rects[1]); + let keybar = Paragraph::new(vec![Line::from(status_spans)]); + + frame.render_widget(keybar, area); + } + + fn render_stats_header(&self, frame: &mut Frame, area: Rect) { + let cpu_count = self.cpu_stats.len().max(self.cpu_load.len()); + let cpu_col_count = frame.area().width as usize / 32; + let cpu_row_count = cpu_count.div_ceil(cpu_col_count); + + let row_layout = vec![Constraint::Min(1); 1 + cpu_row_count]; + let row_rects = Layout::vertical(&row_layout).split(area); + let mut cpu_constraints = vec![]; + let mut cpu_row_rects = vec![]; + for _ in 0..cpu_col_count { + cpu_constraints.push(Constraint::Fill(1)); + cpu_constraints.push(Constraint::Min(1)); + } + for row_rect in &row_rects[1..] { + cpu_row_rects.push( + Layout::horizontal(&cpu_constraints) + .spacing(1) + .split(*row_rect), + ); + } + + let memory_rects = Layout::horizontal(&[Constraint::Fill(1), Constraint::Min(16)]) + .spacing(1) + .split(row_rects[0]); + // let rects = Layout::horizontal(&[Constraint::Fill(1), Constraint::Min(16)]) + // .spacing(1) + // .split(area); + // let row_layout = vec![Constraint::Min(1); 1 + cpu_count]; + // let bar_rects = Layout::vertical(&row_layout).split(rects[0]); + // let label_rects = Layout::vertical(&row_layout).split(rects[1]); + + // Memory gauge + let (memory_total, memory_used) = match self.memory.as_ref() { + Some(memory) => (memory.total_usable_pages, memory.allocated_pages), + None => (1, 0), + }; + let usage = memory_used as f64 / memory_total as f64; + let fill_style = match usage { + u if u >= 0.85 => Color::from_u32(0x00FF0000), + u if u >= 0.65 => Color::from_u32(0x00FF8800), + u if u >= 0.45 => Color::from_u32(0x00FFFF00), + _ => Color::from_u32(0x0000FF00), + }; + let memory_used = memory_used * 4096; + let memory_total = memory_total * 4096; + let gauge = LineGauge::default() + .label("Mem") + .filled_style(fill_style) + .unfilled_symbol(" ") + .filled_symbol(symbols::block::FULL) + .ratio(usage); + let label = Paragraph::new(format!( + "{}/{}", + FormatSize::default(memory_used as u64), + FormatSize::default(memory_total as u64) + )); + frame.render_widget(gauge, memory_rects[0]); + frame.render_widget(label, memory_rects[1]); + + // CPU gauges + for cpu in 0..cpu_count { + let row = cpu / cpu_col_count; + let col = cpu % cpu_col_count; + let gauge_rect = cpu_row_rects[row][col * 2 + 0]; + let label_rect = cpu_row_rects[row][col * 2 + 1]; + let load = match self.cpu_load.get(&cpu) { + Some(load) => 1.0 - load.idle_ns as f64 / load.total_ns as f64, + None => 0.0, + }; + let gauge = LineGauge::default() + .label(format!("{cpu}")) + .filled_style(Color::Yellow) + .unfilled_symbol(" ") + .filled_symbol(symbols::block::FULL) + .ratio(load); + let label = Paragraph::new(format!("{:<3.1}%", load * 100.0)); + + frame.render_widget(gauge, gauge_rect); + frame.render_widget(label, label_rect); + } + } + + fn render(&self, f: &mut Frame) { + let cpus_per_row = f.area().width as usize / 32; + let cpu_row_count = self + .cpu_load + .len() + .max(self.cpu_stats.len()) + .div_ceil(cpus_per_row); + + let rects = Layout::default() + .constraints( + [ + Constraint::Min(1 + cpu_row_count as u16), + Constraint::Percentage(99), + Constraint::Min(1), + ] + .as_ref(), + ) + .split(f.area()); + + self.render_stats_header(f, rects[0]); + self.render_processes(f, rects[1]); + self.render_keybar(f, rects[2]); } } fn run() -> io::Result<()> { + const INTERVAL: Duration = Duration::from_millis(250); + let mut state = State::new()?; let mut poll = Poll::new()?; @@ -243,14 +417,14 @@ fn run() -> io::Result<()> { let mut term = Terminal::new(term)?; - timer.start(Duration::from_secs(1))?; + timer.start(INTERVAL)?; while running { term.draw(|f| state.render(f))?; match poll.wait(None)? { PollEvent::Ready(fd) if fd == timer.as_raw_fd() => { - timer.start(Duration::from_secs(1))?; + timer.start(INTERVAL)?; state.refresh()?; } PollEvent::Ready(fd) if fd == term_fd => {