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