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;
mod bytes;
mod integer;
mod string;
pub trait Attribute<D>: Sync + Send {
@@ -14,4 +15,7 @@ pub trait Attribute<D>: Sync + Send {
}
pub use bytes::{BytesAttribute, BytesAttributeOps};
pub use integer::{
IntegerAttribute, IntegerAttributeFormat, IntegerAttributeOps, IntegerAttributeValue,
};
pub use string::{StringAttribute, StringAttributeOps};
+75 -2
View File
@@ -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<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 {
&QUEUES.get()[index].stats
}
+3 -1
View File
@@ -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(),
+198 -24
View File
@@ -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<ProcessInfo>,
memory: Option<SystemMemoryStats>,
expanded: HashSet<u32>,
cpu_load: HashMap<usize, CpuStats>,
cpu_stats: HashMap<usize, CpuStats>,
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>> {
let mut list = vec![];
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 {
fn new() -> io::Result<Self> {
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 => {