sysutils: add cpu/memory information to top

This commit is contained in:
2026-03-30 12:23:25 +03:00
parent d7df44b1d9
commit 6d31142258
5 changed files with 486 additions and 27 deletions
@@ -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<Self, Error>;
fn into_raw(&self, format: IntegerAttributeFormat) -> Result<Vec<u8>, Error>;
}
pub enum IntegerAttributeFormat {
Decimal,
Hex,
Octal,
}
pub trait IntegerAttributeOps<N: IntegerAttributeValue>: 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<N, Error> {
let _ = state;
Err(Error::NotImplemented)
}
fn write(state: &Self::Data, value: N) -> Result<(), Error> {
let _ = state;
let _ = value;
Err(Error::ReadOnly)
}
}
pub struct IntegerAttribute<N: IntegerAttributeValue, V: IntegerAttributeOps<N>>(
PhantomData<(V, N)>,
);
struct IntegerAttributeNode<N: IntegerAttributeValue, V: IntegerAttributeOps<N>> {
object: Arc<KObject<V::Data>>,
_pd: PhantomData<(V, N)>,
}
struct IntegerAttributeState {
value: IrqSafeRwLock<Vec<u8>>,
#[allow(unused)]
modified: AtomicBool,
}
macro_rules! impl_integer_value {
($($ty:ty),+ $(,)?) => {
$(
impl IntegerAttributeValue for $ty {
fn from_raw(_data: &[u8]) -> Result<Self, Error> {
todo!("IntegerAttributeValue::from_raw()")
}
fn into_raw(&self, format: IntegerAttributeFormat) -> Result<Vec<u8>, 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<N: IntegerAttributeValue, V: IntegerAttributeOps<N>> CommonImpl
for IntegerAttributeNode<N, V>
{
fn size(&self, _node: &NodeRef) -> Result<u64, Error> {
Ok(0)
}
fn as_any(&self) -> &dyn Any {
self as _
}
}
impl<N: IntegerAttributeValue, V: IntegerAttributeOps<N>> RegularImpl
for IntegerAttributeNode<N, V>
{
fn open(
&self,
_node: &NodeRef,
opts: OpenOptions,
) -> Result<(u64, Option<InstanceData>), 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::<StringAttributeState>()
// .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<usize, Error> {
let instance = instance.ok_or(Error::InvalidFile)?;
let instance = instance
.downcast_ref::<IntegerAttributeState>()
.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<usize, Error> {
todo!("Integer attribute write")
}
fn truncate(&self, _node: &NodeRef, _new_size: u64) -> Result<(), Error> {
Ok(())
}
}
impl<N: IntegerAttributeValue, V: IntegerAttributeOps<N>> From<V> for IntegerAttribute<N, V> {
fn from(_value: V) -> Self {
Self(PhantomData)
}
}
impl<N: IntegerAttributeValue, V: IntegerAttributeOps<N>> Attribute<V::Data>
for IntegerAttribute<N, V>
{
fn instantiate(&self, parent: &Arc<KObject<V::Data>>) -> Result<Arc<Node>, 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) }
}
}
@@ -6,6 +6,7 @@ use crate::vfs::{Filename, NodeRef};
use super::object::KObject; use super::object::KObject;
mod bytes; mod bytes;
mod integer;
mod string; mod string;
pub trait Attribute<D>: Sync + Send { pub trait Attribute<D>: Sync + Send {
@@ -14,4 +15,7 @@ pub trait Attribute<D>: Sync + Send {
} }
pub use bytes::{BytesAttribute, BytesAttributeOps}; pub use bytes::{BytesAttribute, BytesAttributeOps};
pub use integer::{
IntegerAttribute, IntegerAttributeFormat, IntegerAttributeOps, IntegerAttributeValue,
};
pub use string::{StringAttribute, StringAttributeOps}; pub use string::{StringAttribute, StringAttributeOps};
+75 -2
View File
@@ -11,9 +11,14 @@ use kernel_arch::{
task::{Scheduler, TaskContext}, task::{Scheduler, TaskContext},
}; };
use libk_util::{OneTimeInit, sync::IrqGuard}; use libk_util::{OneTimeInit, sync::IrqGuard};
use yggdrasil_abi::time::SystemTime; use yggdrasil_abi::{error::Error, time::SystemTime};
use crate::{ use crate::{
fs::sysfs::{
self,
attribute::{IntegerAttribute, IntegerAttributeOps},
object::KObject,
},
task::{TaskContextImpl, ThreadId, ThreadState, thread::Thread}, task::{TaskContextImpl, ThreadId, ThreadState, thread::Thread},
time::monotonic_time, time::monotonic_time,
}; };
@@ -131,7 +136,7 @@ impl CpuQueue {
&self.stats.idle &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); self.stats.total.fetch_add(t, Ordering::Relaxed);
counter.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<KObject<usize>>) {
static KERNEL_SCHED: OneTimeInit<Arc<KObject<()>>> = 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<u64> for CpuIdle {
type Data = usize;
const NAME: &'static str = "idle";
fn read(state: &Self::Data) -> Result<u64, Error> {
Ok(stats(*state).idle.load(Ordering::Acquire))
}
}
impl IntegerAttributeOps<u64> for CpuTotal {
type Data = usize;
const NAME: &'static str = "total";
fn read(state: &Self::Data) -> Result<u64, Error> {
Ok(stats(*state).total.load(Ordering::Acquire))
}
}
impl IntegerAttributeOps<u64> for CpuUser {
type Data = usize;
const NAME: &'static str = "user";
fn read(state: &Self::Data) -> Result<u64, Error> {
Ok(stats(*state).user.load(Ordering::Acquire))
}
}
impl IntegerAttributeOps<u64> for CpuKernel {
type Data = usize;
const NAME: &'static str = "kernel";
fn read(state: &Self::Data) -> Result<u64, Error> {
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 { pub fn stats(index: usize) -> &'static SchedulerStats {
&QUEUES.get()[index].stats &QUEUES.get()[index].stats
} }
+3 -1
View File
@@ -6,7 +6,7 @@ use libk::{
device::display::console, device::display::console,
fs::devfs, fs::devfs,
random, 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}, vfs::{IoContext, NodeRef, OwnedFilename, impls::fn_hardlink},
}; };
use memfs::MemoryFilesystem; use memfs::MemoryFilesystem;
@@ -69,6 +69,8 @@ pub fn kinit() -> Result<(), Error> {
runtime::spawn(ygg_driver_usb::bus::bus_handler())?; runtime::spawn(ygg_driver_usb::bus::bus_handler())?;
runtime::spawn(console::flush_consoles_task()).ok(); runtime::spawn(console::flush_consoles_task()).ok();
sched::setup_sysfs();
devfs::root() devfs::root()
.add_child( .add_child(
OwnedFilename::new("tty").unwrap(), OwnedFilename::new("tty").unwrap(),
+198 -24
View File
@@ -1,7 +1,7 @@
#![feature(yggdrasil_os)] #![feature(yggdrasil_os, rustc_private)]
use std::{ use std::{
collections::HashSet, collections::{HashMap, HashSet},
fs, io, fs, io,
os::fd::AsRawFd, os::fd::AsRawFd,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -10,13 +10,16 @@ use std::{
use cross::io::{Poll, PollEvent, TimerFd}; use cross::io::{Poll, PollEvent, TimerFd};
use libterm::{Term, TermKey}; use libterm::{Term, TermKey};
use libutil::fmt::FormatSize;
use ratatui::{ use ratatui::{
Frame, Terminal, Frame, Terminal,
layout::{Constraint, Layout}, layout::{Constraint, Layout, Rect},
style::{Color, Style}, style::{Color, Style},
symbols,
text::{Line, Span}, text::{Line, Span},
widgets::{Cell, Paragraph, Row, Table}, widgets::{Cell, LineGauge, Paragraph, Row, Table},
}; };
use runtime::{abi::system, rt::system::SystemMemoryStats};
struct ProcessInfo { struct ProcessInfo {
pid: u32, pid: u32,
@@ -31,10 +34,64 @@ struct ThreadInfo {
struct State { struct State {
processes: Vec<ProcessInfo>, processes: Vec<ProcessInfo>,
memory: Option<SystemMemoryStats>,
expanded: HashSet<u32>, expanded: HashSet<u32>,
cpu_load: HashMap<usize, CpuStats>,
cpu_stats: HashMap<usize, CpuStats>,
selection: usize, selection: usize,
} }
struct CpuStats {
total_ns: u64,
idle_ns: u64,
}
impl CpuStats {
fn read_ns<P: AsRef<Path>>(path: P) -> Option<u64> {
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<SystemMemoryStats> {
runtime::rt::system::get_system_info!(system::MemoryUsage).map_err(io::Error::from)
}
fn cpu_stats(stats: &mut HashMap<usize, CpuStats>, load: &mut HashMap<usize, CpuStats>) {
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<P: AsRef<Path>>(path: P) -> io::Result<Vec<u32>> { fn list_ids<P: AsRef<Path>>(path: P) -> io::Result<Vec<u32>> {
let mut list = vec![]; let mut list = vec![];
for entry in fs::read_dir(path)? { for entry in fs::read_dir(path)? {
@@ -88,11 +145,16 @@ fn threads<P: AsRef<Path>>(pid_path: P) -> io::Result<Vec<ThreadInfo>> {
impl State { impl State {
fn new() -> io::Result<Self> { fn new() -> io::Result<Self> {
let processes = processes()?; let processes = processes()?;
Ok(Self { let mut this = Self {
processes, processes,
memory: None,
cpu_load: HashMap::new(),
cpu_stats: HashMap::new(),
expanded: HashSet::new(), expanded: HashSet::new(),
selection: 0, selection: 0,
}) };
this.refresh()?;
Ok(this)
} }
fn next(&mut self) { fn next(&mut self) {
@@ -134,6 +196,8 @@ impl State {
.processes .processes
.get(self.selection) .get(self.selection)
.map(|process| process.pid); .map(|process| process.pid);
cpu_stats(&mut self.cpu_stats, &mut self.cpu_load);
self.memory = memory_info().ok();
self.processes = processes()?; self.processes = processes()?;
let new_selection = selected_pid let new_selection = selected_pid
.and_then(|pid| self.processes.iter().position(|process| process.pid == pid)); .and_then(|pid| self.processes.iter().position(|process| process.pid == pid));
@@ -142,22 +206,10 @@ impl State {
Ok(()) Ok(())
} }
fn render(&self, f: &mut Frame) { fn render_processes(&self, frame: &mut Frame, area: Rect) {
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());
let normal_style = Style::default(); let normal_style = Style::default();
let selected_style = Style::default().bg(Color::Cyan).fg(Color::Black); let selected_style = Style::default().bg(Color::Cyan).fg(Color::Black);
let hint_style = Style::default().bg(Color::Blue).fg(Color::White); 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 rows = self.processes.iter().enumerate().flat_map(|(i, process)| {
let style = if i == self.selection { let style = if i == self.selection {
@@ -213,6 +265,20 @@ impl State {
.highlight_symbol(">>") .highlight_symbol(">>")
.header(header); .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![]; let mut status_spans = vec![];
for (i, &(key, hint)) in KEYS.iter().enumerate() { for (i, &(key, hint)) in KEYS.iter().enumerate() {
if i != 0 { if i != 0 {
@@ -221,14 +287,122 @@ impl State {
status_spans.push(Span::styled(key, key_style)); status_spans.push(Span::styled(key, key_style));
status_spans.push(Span::styled(hint, hint_style)); status_spans.push(Span::styled(hint, hint_style));
} }
let status = Paragraph::new(vec![Line::from(status_spans)]);
f.render_widget(table, rects[0]); let keybar = Paragraph::new(vec![Line::from(status_spans)]);
f.render_widget(status, rects[1]);
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<()> { fn run() -> io::Result<()> {
const INTERVAL: Duration = Duration::from_millis(250);
let mut state = State::new()?; let mut state = State::new()?;
let mut poll = Poll::new()?; let mut poll = Poll::new()?;
@@ -243,14 +417,14 @@ fn run() -> io::Result<()> {
let mut term = Terminal::new(term)?; let mut term = Terminal::new(term)?;
timer.start(Duration::from_secs(1))?; timer.start(INTERVAL)?;
while running { while running {
term.draw(|f| state.render(f))?; term.draw(|f| state.render(f))?;
match poll.wait(None)? { match poll.wait(None)? {
PollEvent::Ready(fd) if fd == timer.as_raw_fd() => { PollEvent::Ready(fd) if fd == timer.as_raw_fd() => {
timer.start(Duration::from_secs(1))?; timer.start(INTERVAL)?;
state.refresh()?; state.refresh()?;
} }
PollEvent::Ready(fd) if fd == term_fd => { PollEvent::Ready(fd) if fd == term_fd => {