1pub(crate) mod ratatui_backend;
7
8use crate::{arch::x86_64::vga, shell::ShellError, shell_println};
9use alloc::{format, string::String, vec, vec::Vec};
10use core::sync::atomic::Ordering;
11use ratatui::{
12 layout::{Constraint, Direction, Layout},
13 style::{Color, Modifier, Style},
14 widgets::{Block, Borders, Cell, Gauge, Paragraph, Row, Table, TableState},
15 Terminal,
16};
17pub(crate) use ratatui_backend::Strat9RatatuiBackend;
18
19const TOP_REFRESH_TICKS: u64 = 10; const MAX_CPU_GAUGES: usize = 8;
21
22#[derive(Clone)]
23struct TaskRowData {
24 pid: String,
25 name: String,
26 state: &'static str,
27 priority: String,
28 ticks: u64,
29}
30
31#[derive(Clone)]
32struct SiloRowData {
33 sid: String,
34 name: String,
35 state: String,
36 tasks: String,
37 label: String,
38}
39
40#[derive(Clone)]
41struct StrateRowData {
42 name: String,
43 silos: String,
44}
45
46struct TopSnapshot {
47 cpu_count: usize,
48 total_pages: usize,
49 used_pages: usize,
50 tasks: Vec<TaskRowData>,
51 silos: Vec<SiloRowData>,
52 strates: Vec<StrateRowData>,
53 scheduler: crate::process::SchedulerStateSnapshot,
54}
55
56#[derive(Clone, Copy)]
57struct CpuUsageWindow {
58 per_cpu_ratio: [f64; crate::arch::x86_64::percpu::MAX_CPUS],
59 avg_ratio: f64,
60}
61
62#[derive(Clone, Copy)]
63struct SchedulerMetricsWindow {
64 rt_ratio: f64,
65 fair_ratio: f64,
66 idle_ratio: f64,
67 switch_delta: u64,
68 preempt_delta: u64,
69 steal_in_delta: u64,
70 steal_out_delta: u64,
71}
72
73fn collect_silos_from_proc_scheme() -> Option<(Vec<SiloRowData>, Vec<StrateRowData>)> {
74 let fd = crate::vfs::open("/proc/silos", crate::vfs::OpenFlags::READ).ok()?;
75 let bytes = match crate::vfs::read_all(fd) {
76 Ok(b) => b,
77 Err(_) => {
78 let _ = crate::vfs::close(fd);
79 return None;
80 }
81 };
82 let _ = crate::vfs::close(fd);
83 let body = core::str::from_utf8(&bytes).ok()?;
84
85 let mut silos = Vec::new();
86 let mut strate_index: Vec<(String, Vec<String>)> = Vec::new();
87 for (line_idx, line) in body.lines().enumerate() {
88 if line_idx == 0 || line.is_empty() {
89 continue;
90 }
91 let mut fields = line.split('\t');
92 let sid = fields.next()?;
93 let state = fields.next()?;
94 let tasks = fields.next()?;
95 let _mem_used = fields.next()?;
96 let _mem_min = fields.next()?;
97 let _mem_max = fields.next()?;
98 let _gfx_flags = fields.next()?;
99 let _gfx_sessions = fields.next()?;
100 let _gfx_ttl = fields.next()?;
101 let label = fields.next()?;
102 let name = fields.next()?;
103
104 let strate_name = if label != "-" && !label.is_empty() {
105 String::from(label)
106 } else {
107 String::from(name)
108 };
109 if let Some((_, belongs)) = strate_index
110 .iter_mut()
111 .find(|(entry_name, _)| *entry_name == strate_name)
112 {
113 if !belongs.iter().any(|x| x == name) {
114 belongs.push(String::from(name));
115 }
116 } else {
117 strate_index.push((strate_name, vec![String::from(name)]));
118 }
119
120 silos.push(SiloRowData {
121 sid: String::from(sid),
122 name: String::from(name),
123 state: String::from(state),
124 tasks: String::from(tasks),
125 label: String::from(label),
126 });
127 }
128
129 silos.sort_by_key(|s| s.sid.parse::<u32>().unwrap_or(u32::MAX));
130 strate_index.sort_by(|a, b| a.0.cmp(&b.0));
131
132 let mut strates = Vec::with_capacity(strate_index.len());
133 for (name, belongs) in strate_index {
134 let mut silos_csv = String::new();
135 for (i, silo_name) in belongs.iter().enumerate() {
136 if i != 0 {
137 silos_csv.push_str(", ");
138 }
139 silos_csv.push_str(silo_name);
140 }
141 strates.push(StrateRowData {
142 name,
143 silos: silos_csv,
144 });
145 }
146
147 Some((silos, strates))
148}
149
150fn collect_snapshot() -> TopSnapshot {
152 let cpu_count = crate::arch::x86_64::percpu::cpu_count();
153 let (total_pages, used_pages) = {
154 let guard = crate::memory::buddy::get_allocator().lock();
155 guard.as_ref().map(|a| a.page_totals()).unwrap_or((0, 0))
156 };
157
158 let mut tasks = Vec::new();
159 if let Some(all_tasks) = crate::process::get_all_tasks() {
160 for task in all_tasks {
161 let state = unsafe { *task.state.get() };
162 let state_str = match state {
163 crate::process::TaskState::Ready => "Ready",
164 crate::process::TaskState::Running => "Running",
165 crate::process::TaskState::Blocked => "Blocked",
166 crate::process::TaskState::Dead => "Dead",
167 };
168 tasks.push(TaskRowData {
169 pid: format!("{}", task.pid),
170 name: String::from(task.name),
171 state: state_str,
172 priority: format!("{:?}", task.priority),
173 ticks: task.ticks.load(Ordering::Relaxed),
174 });
175 }
176 }
177
178 tasks.sort_by(|a, b| b.ticks.cmp(&a.ticks));
180
181 let (silos, strates) = collect_silos_from_proc_scheme().unwrap_or_else(|| {
182 let mut silos = Vec::new();
183 let mut strate_index: Vec<(String, Vec<String>)> = Vec::new();
184 let mut silo_snapshots = crate::silo::list_silos_snapshot();
185 silo_snapshots.sort_by_key(|s| s.id);
186 for s in silo_snapshots {
187 let label = s.strate_label.unwrap_or_default();
188 let strate_name = if !label.is_empty() {
189 label.clone()
190 } else {
191 s.name.clone()
192 };
193 if let Some((_, belongs)) = strate_index
194 .iter_mut()
195 .find(|(name, _)| *name == strate_name)
196 {
197 if !belongs.iter().any(|x| x == &s.name) {
198 belongs.push(s.name.clone());
199 }
200 } else {
201 strate_index.push((strate_name, vec![s.name.clone()]));
202 }
203 silos.push(SiloRowData {
204 sid: format!("{}", s.id),
205 name: s.name,
206 state: format!("{:?}", s.state),
207 tasks: format!("{}", s.task_count),
208 label: if label.is_empty() {
209 String::from("-")
210 } else {
211 label
212 },
213 });
214 }
215
216 strate_index.sort_by(|a, b| a.0.cmp(&b.0));
217 let mut strates = Vec::with_capacity(strate_index.len());
218 for (name, belongs) in strate_index {
219 let mut silos_csv = String::new();
220 for (i, silo_name) in belongs.iter().enumerate() {
221 if i != 0 {
222 silos_csv.push_str(", ");
223 }
224 silos_csv.push_str(silo_name);
225 }
226 strates.push(StrateRowData {
227 name,
228 silos: silos_csv,
229 });
230 }
231
232 (silos, strates)
233 });
234
235 TopSnapshot {
236 cpu_count,
237 total_pages,
238 used_pages,
239 tasks,
240 silos,
241 strates,
242 scheduler: crate::process::scheduler_state_snapshot(),
243 }
244}
245
246fn compute_cpu_usage_window(
248 prev: &crate::process::CpuUsageSnapshot,
249 now: &crate::process::CpuUsageSnapshot,
250) -> CpuUsageWindow {
251 let cpu_count = now.cpu_count.min(crate::arch::x86_64::percpu::MAX_CPUS);
252 let mut ratios = [0.0f64; crate::arch::x86_64::percpu::MAX_CPUS];
253 let mut sum = 0.0;
254
255 for i in 0..cpu_count {
256 let delta_total = now.total_ticks[i].saturating_sub(prev.total_ticks[i]);
257 let delta_idle = now.idle_ticks[i].saturating_sub(prev.idle_ticks[i]);
258 let ratio = if delta_total == 0 {
259 0.0
260 } else {
261 let busy = delta_total.saturating_sub(delta_idle);
262 (busy as f64 / delta_total as f64).clamp(0.0, 1.0)
263 };
264 ratios[i] = ratio;
265 sum += ratio;
266 }
267
268 CpuUsageWindow {
269 per_cpu_ratio: ratios,
270 avg_ratio: if cpu_count == 0 {
271 0.0
272 } else {
273 (sum / cpu_count as f64).clamp(0.0, 1.0)
274 },
275 }
276}
277
278fn compute_scheduler_metrics_window(
280 prev: &crate::process::SchedulerMetricsSnapshot,
281 now: &crate::process::SchedulerMetricsSnapshot,
282) -> SchedulerMetricsWindow {
283 let cpu_count = now.cpu_count.min(crate::arch::x86_64::percpu::MAX_CPUS);
284 let mut rt_delta = 0u64;
285 let mut fair_delta = 0u64;
286 let mut idle_delta = 0u64;
287 let mut switch_delta = 0u64;
288 let mut preempt_delta = 0u64;
289 let mut steal_in_delta = 0u64;
290 let mut steal_out_delta = 0u64;
291 for i in 0..cpu_count {
292 rt_delta = rt_delta
293 .saturating_add(now.rt_runtime_ticks[i].saturating_sub(prev.rt_runtime_ticks[i]));
294 fair_delta = fair_delta
295 .saturating_add(now.fair_runtime_ticks[i].saturating_sub(prev.fair_runtime_ticks[i]));
296 idle_delta = idle_delta
297 .saturating_add(now.idle_runtime_ticks[i].saturating_sub(prev.idle_runtime_ticks[i]));
298 switch_delta =
299 switch_delta.saturating_add(now.switch_count[i].saturating_sub(prev.switch_count[i]));
300 preempt_delta = preempt_delta
301 .saturating_add(now.preempt_count[i].saturating_sub(prev.preempt_count[i]));
302 steal_in_delta = steal_in_delta
303 .saturating_add(now.steal_in_count[i].saturating_sub(prev.steal_in_count[i]));
304 steal_out_delta = steal_out_delta
305 .saturating_add(now.steal_out_count[i].saturating_sub(prev.steal_out_count[i]));
306 }
307 let total = rt_delta
308 .saturating_add(fair_delta)
309 .saturating_add(idle_delta);
310 let to_ratio = |v: u64| {
311 if total == 0 {
312 0.0
313 } else {
314 (v as f64 / total as f64).clamp(0.0, 1.0)
315 }
316 };
317 SchedulerMetricsWindow {
318 rt_ratio: to_ratio(rt_delta),
319 fair_ratio: to_ratio(fair_delta),
320 idle_ratio: to_ratio(idle_delta),
321 switch_delta,
322 preempt_delta,
323 steal_in_delta,
324 steal_out_delta,
325 }
326}
327
328fn scheduler_runtime_lines(
330 s: &crate::process::SchedulerStateSnapshot,
331 w: &SchedulerMetricsWindow,
332) -> (String, String, String) {
333 let line1 = format!(
334 "Win: RT {:>3}% | FAIR {:>3}% | IDLE {:>3}% | sw {} | pre {} | st+ {} | st- {}",
335 (w.rt_ratio * 100.0) as u16,
336 (w.fair_ratio * 100.0) as u16,
337 (w.idle_ratio * 100.0) as u16,
338 w.switch_delta,
339 w.preempt_delta,
340 w.steal_in_delta,
341 w.steal_out_delta
342 );
343 let line2 = format!(
344 "Cfg: init={} blocked={} pick=[{},{},{}] steal=[{},{}]",
345 s.initialized,
346 s.blocked_tasks,
347 s.pick_order[0].as_str(),
348 s.pick_order[1].as_str(),
349 s.pick_order[2].as_str(),
350 s.steal_order[0].as_str(),
351 s.steal_order[1].as_str()
352 );
353 let cpu_count = s.cpu_count.min(crate::arch::x86_64::percpu::MAX_CPUS);
354 let line3 = if cpu_count == 0 {
355 String::from("CPU: n/a")
356 } else {
357 let c0 = format!(
358 "cpu0 cur={} rq={}/{}/{} nr={}",
359 s.current_task[0], s.rq_rt[0], s.rq_fair[0], s.rq_idle[0], s.need_resched[0]
360 );
361 if cpu_count == 1 {
362 format!("CPU: {}", c0)
363 } else {
364 let c1 = format!(
365 "cpu1 cur={} rq={}/{}/{} nr={}",
366 s.current_task[1], s.rq_rt[1], s.rq_fair[1], s.rq_idle[1], s.need_resched[1]
367 );
368 format!("CPU: {} | {}", c0, c1)
369 }
370 };
371 (line1, line2, line3)
372}
373
374pub fn cmd_top(_args: &[alloc::string::String]) -> Result<(), ShellError> {
376 if !vga::is_available() {
377 shell_println!("Error: 'top' requires a graphical framebuffer console.");
378 return Ok(());
379 }
380
381 let was_db = vga::double_buffer_mode();
383 vga::set_double_buffer_mode(true);
384 let backend = Strat9RatatuiBackend::new().map_err(|_| ShellError::ExecutionFailed)?;
385 let mut terminal = Terminal::new(backend).map_err(|_| ShellError::ExecutionFailed)?;
386 terminal.clear().map_err(|_| ShellError::ExecutionFailed)?;
387
388 let mut last_refresh_tick = crate::process::scheduler::ticks();
389 let boot_tick = last_refresh_tick;
390 let mut prev_cpu_sample = crate::process::cpu_usage_snapshot();
391 let mut prev_sched_sample = crate::process::scheduler_metrics_snapshot();
392 let mut selected_task: usize = 0;
393
394 loop {
395 let ticks = crate::process::scheduler::ticks();
396
397 if let Some(ch) = crate::arch::x86_64::keyboard::read_char() {
399 match ch {
400 b'q' | 0x1B | 0x03 => break,
401 crate::arch::x86_64::keyboard::KEY_UP => {
402 selected_task = selected_task.saturating_sub(1);
403 }
404 crate::arch::x86_64::keyboard::KEY_DOWN => {
405 selected_task = selected_task.saturating_add(1);
406 }
407 _ => {}
408 }
409 }
410
411 if ticks.saturating_sub(last_refresh_tick) < TOP_REFRESH_TICKS {
413 crate::process::yield_task();
414 continue;
415 }
416 last_refresh_tick = ticks;
417 let snapshot = collect_snapshot();
418 let cpu_sample = crate::process::cpu_usage_snapshot();
419 let cpu_window = compute_cpu_usage_window(&prev_cpu_sample, &cpu_sample);
420 prev_cpu_sample = cpu_sample;
421 let sched_sample = crate::process::scheduler_metrics_snapshot();
422 let sched_window = compute_scheduler_metrics_window(&prev_sched_sample, &sched_sample);
423 prev_sched_sample = sched_sample;
424 let mem_ratio = if snapshot.total_pages > 0 {
425 (snapshot.used_pages as f64) / (snapshot.total_pages as f64)
426 } else {
427 0.0
428 };
429
430 let rows: Vec<Row> = snapshot
431 .tasks
432 .iter()
433 .map(|task| {
434 Row::new(vec![
435 Cell::from(task.pid.as_str()),
436 Cell::from(task.name.as_str()),
437 Cell::from(task.state),
438 Cell::from(task.priority.as_str()),
439 Cell::from(format!("{}", task.ticks)),
440 ])
441 })
442 .collect();
443 let row_count = rows.len();
444 if row_count == 0 {
445 selected_task = 0;
446 } else if selected_task >= row_count {
447 selected_task = row_count - 1;
448 }
449 let mut table_state = TableState::default();
450 if row_count > 0 {
451 table_state.select(Some(selected_task));
452 }
453
454 let uptime_secs = ticks.saturating_sub(boot_tick) / 100;
455
456 let frame_started = vga::begin_frame();
457 terminal
458 .draw(|frame| {
459 let title_style = Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD);
460 let primary_text = Style::default().fg(Color::White);
461 let muted_text = Style::default().fg(Color::Gray);
462 let header_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD);
463
464 let area = frame.area();
465 let vertical = Layout::default()
466 .direction(Direction::Vertical)
467 .constraints([
468 Constraint::Length(2),
469 Constraint::Length(3),
470 Constraint::Length(5),
471 Constraint::Length(6),
472 Constraint::Min(8),
473 Constraint::Length(1),
474 ])
475 .split(area);
476
477 let title = Paragraph::new("Strat9 System Monitor (Ratatui no_std)")
478 .style(title_style)
479 .block(Block::default().borders(Borders::BOTTOM).title("Top"));
480 frame.render_widget(title, vertical[0]);
481
482 let stats_line = Paragraph::new(format!(
483 "CPUs: {} | Tasks: {} | Silos: {} | Strates: {} | CPU(avg): {:>3}% | Uptime: {}s",
484 snapshot.cpu_count,
485 snapshot.tasks.len(),
486 snapshot.silos.len(),
487 snapshot.strates.len(),
488 (cpu_window.avg_ratio * 100.0) as u16,
489 uptime_secs
490 ))
491 .style(primary_text)
492 .block(Block::default().borders(Borders::BOTTOM).title("Stats"));
493 frame.render_widget(stats_line, vertical[1]);
494
495 let (sched_line1, sched_line2, sched_line3) =
496 scheduler_runtime_lines(&snapshot.scheduler, &sched_window);
497 let sched_line = Paragraph::new(format!(
498 "{}\n{}\n{}",
499 sched_line1, sched_line2, sched_line3
500 ))
501 .style(primary_text)
502 .block(Block::default().borders(Borders::BOTTOM).title("Scheduler"));
503 frame.render_widget(sched_line, vertical[2]);
504
505 let cpu_split = Layout::default()
506 .direction(Direction::Horizontal)
507 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
508 .split(vertical[3]);
509
510 let mem_gauge = Gauge::default()
511 .block(
512 Block::default()
513 .borders(Borders::TOP | Borders::BOTTOM)
514 .title(format!(
515 "Memory {} / {} pages",
516 snapshot.used_pages, snapshot.total_pages
517 )),
518 )
519 .gauge_style(Style::default().fg(Color::Blue))
520 .use_unicode(false)
521 .ratio(mem_ratio.clamp(0.0, 1.0))
522 .label(format!("{:.1}%", mem_ratio * 100.0));
523 frame.render_widget(mem_gauge, cpu_split[0]);
524
525 let cpu_gauge_count = snapshot.cpu_count.min(MAX_CPU_GAUGES);
526 if cpu_gauge_count > 0 {
527 let mut constraints = Vec::with_capacity(cpu_gauge_count);
528 for _ in 0..cpu_gauge_count {
529 constraints.push(Constraint::Length(1));
530 }
531 let cpu_rows = Layout::default()
532 .direction(Direction::Vertical)
533 .constraints(constraints)
534 .split(cpu_split[1]);
535
536 for i in 0..cpu_gauge_count {
537 let ratio = cpu_window.per_cpu_ratio[i];
538 let gauge = Gauge::default()
539 .block(Block::default().title(format!("CPU{}", i)).borders(Borders::NONE))
540 .gauge_style(Style::default().fg(Color::Green))
541 .use_unicode(false)
542 .ratio(ratio)
543 .label(format!("{:>3}%", (ratio * 100.0) as u16));
544 frame.render_widget(gauge, cpu_rows[i]);
545 }
546 }
547
548 let main_split = Layout::default()
549 .direction(Direction::Horizontal)
550 .constraints([Constraint::Percentage(64), Constraint::Percentage(36)])
551 .split(vertical[4]);
552
553 let task_table = Table::new(
554 rows.iter().cloned(),
555 [
556 Constraint::Length(5), Constraint::Min(18), Constraint::Length(9), Constraint::Length(8), Constraint::Length(10), ],
562 )
563 .header(
564 Row::new(vec!["PID", "Name", "State", "Prio", "Ticks"]).style(header_style),
565 )
566 .column_spacing(1)
567 .style(primary_text)
568 .row_highlight_style(
569 Style::default()
570 .bg(Color::White)
571 .fg(Color::Black)
572 .add_modifier(Modifier::BOLD),
573 )
574 .block(
575 Block::default()
576 .borders(Borders::TOP)
577 .title("Tasks (sorted by ticks)"),
578 );
579 frame.render_stateful_widget(task_table, main_split[0], &mut table_state);
580
581 let right_split = Layout::default()
582 .direction(Direction::Vertical)
583 .constraints([Constraint::Percentage(52), Constraint::Percentage(48)])
584 .split(main_split[1]);
585
586 let silo_rows: Vec<Row> = snapshot
587 .silos
588 .iter()
589 .map(|s| {
590 Row::new(vec![
591 Cell::from(s.sid.as_str()),
592 Cell::from(s.name.as_str()),
593 Cell::from(s.state.as_str()),
594 Cell::from(s.tasks.as_str()),
595 Cell::from(s.label.as_str()),
596 ])
597 })
598 .collect();
599 let silo_table = Table::new(
600 silo_rows,
601 [
602 Constraint::Length(5),
603 Constraint::Length(10),
604 Constraint::Length(8),
605 Constraint::Length(5),
606 Constraint::Min(8),
607 ],
608 )
609 .header(
610 Row::new(vec!["SID", "Name", "State", "T", "Label"]).style(
611 Style::default().fg(Color::LightGreen).add_modifier(Modifier::BOLD),
612 ),
613 )
614 .column_spacing(1)
615 .style(primary_text)
616 .block(Block::default().borders(Borders::TOP).title("Silos"));
617 frame.render_widget(silo_table, right_split[0]);
618
619 let strate_rows: Vec<Row> = snapshot
620 .strates
621 .iter()
622 .map(|s| Row::new(vec![Cell::from(s.name.as_str()), Cell::from(s.silos.as_str())]))
623 .collect();
624 let strate_table = Table::new(
625 strate_rows,
626 [Constraint::Length(12), Constraint::Min(10)],
627 )
628 .header(
629 Row::new(vec!["Strate", "BelongsTo"]).style(
630 Style::default().fg(Color::LightCyan).add_modifier(Modifier::BOLD),
631 ),
632 )
633 .column_spacing(1)
634 .style(primary_text)
635 .block(Block::default().borders(Borders::TOP).title("Strates"));
636 frame.render_widget(strate_table, right_split[1]);
637
638 let footer = Paragraph::new("[Up/Down] Select process | [q|Esc] Exit")
639 .style(muted_text)
640 .block(Block::default().borders(Borders::TOP));
641 frame.render_widget(footer, vertical[5]);
642 })
643 .map_err(|_| ShellError::ExecutionFailed)?;
644
645 if frame_started {
646 vga::end_frame();
647 } else {
648 vga::present();
649 }
650
651 crate::process::yield_task();
652 }
653
654 vga::set_double_buffer_mode(was_db);
656 crate::shell::output::clear_screen();
657 vga::set_text_cursor(0, 0);
658 shell_println!("Top exited.");
659 Ok(())
660}