Skip to main content

strat9_kernel/shell/commands/top/
mod.rs

1//! Top command with Ratatui no_std backend.
2//!
3//! This command keeps Chevron shell as default UX and only uses Ratatui while `top`
4//! is running.
5
6pub(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; // 100ms at 100Hz
20const 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
150/// Performs the collect snapshot operation.
151fn 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    // Top-like behavior: most CPU-consumed tasks first.
179    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
246/// Performs the compute cpu usage window operation.
247fn 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
278/// Performs the compute scheduler metrics window operation.
279fn 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
328/// Performs the scheduler runtime lines operation.
329fn 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
374/// Top command main loop
375pub 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    // Switch to double buffering for flicker-free updates.
382    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        // Keep input responsive even between render ticks.
398        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        // Refresh every 100ms with a 100Hz timer.
412        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),  // PID
557                        Constraint::Min(18),    // Name (takes remaining width)
558                        Constraint::Length(9),  // State
559                        Constraint::Length(8),  // Prio
560                        Constraint::Length(10), // Ticks
561                    ],
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    // Clean exit.
655    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}