redbear-power: multi-threaded collector + Tier 1/2 display enhancements

Tier 1 (display Phase 0 infrastructure):
- sched.rs: read /scheme/sys/stat and /scheme/sys/sched for per-CPU
  scheduler stats (switches, steals, queue_depth, IRQs)
- msr.rs: HWP capabilities/requests, RAPL domain energy readings
- process.rs: derive_sched_policy() for SCHED column
- render.rs: System tab shows RAPL power, scheduler stats; Per-CPU
  table has IRQs/steals columns; HWP expansion rows
- acpi.rs: ACPI throttle status reading

Multi-threaded collector (exercises Phase 0 threading):
- collector.rs: parallel collect() using thread::scope + Barrier,
  exercises pthread_create, futex barriers, sched_setaffinity
- app.rs: sequential per-CPU loop replaced with parallel collector
- render.rs: System tab shows Collector stats (threads, pinned, barrier)

Tier 2 kernel wiring (submodule pointer updates):
- kernel: per-CPU sched stats (/scheme/sys/sched), NUMA-aware
  scheduling, numa::init_default() at boot
- relibc: robust mutex cleanup in exit_current_thread()
This commit is contained in:
2026-07-02 21:41:25 +03:00
parent 6d13dee2a6
commit eb53e8190a
10 changed files with 638 additions and 19 deletions
@@ -107,6 +107,15 @@ pub fn rapl_power_watts(
(watts, curr)
}
/// Read a RAPL energy counter for an arbitrary domain MSR (PP0 core,
/// PP1 uncore, DRAM). Same energy-unit conversion as package domain.
/// Returns `None` when the MSR is unavailable.
pub fn read_rapl_domain_energy(msr: u32) -> Option<(u64, Instant)> {
let unit = crate::msr::RaplUnit::read(0)?;
let raw = crate::msr::read_rapl_energy(0, msr)?;
Some((unit.energy_to_uj(raw), Instant::now()))
}
pub fn detect_cpus() -> Vec<u32> {
// Redox exposes the CPU count via the sys:cpu scheme file
// (kernel/src/scheme/sys/cpu.rs) as "CPUs: N\n...". /dev/cpu/ does
@@ -160,6 +160,12 @@ pub meminfo: crate::meminfo::MemInfo,
rapl_prev: Option<(u64, std::time::Instant)>,
/// Why RAPL package power is unavailable (empty if working).
pub rapl_status: String,
pub pp0_power_w: Option<f64>,
rapl_pp0_prev: Option<(u64, std::time::Instant)>,
pub pp1_power_w: Option<f64>,
rapl_pp1_prev: Option<(u64, std::time::Instant)>,
pub dram_power_w: Option<f64>,
rapl_dram_prev: Option<(u64, std::time::Instant)>,
/// Per-PID IO rate history (normalized KiB/s samples,
/// 0..=255 against the per-history max). Used by the
/// Process tab to render a small sparkline per process.
@@ -234,6 +240,7 @@ pub meminfo: crate::meminfo::MemInfo,
pub interval_input: Option<String>,
pub current_tab: TabId,
pub bench_start_time: Option<Instant>,
pub collector_stats: crate::collector::CollectorStats,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -405,6 +412,12 @@ impl App {
pkg_power_w: None,
rapl_prev: None,
rapl_status: "probing...".into(),
pp0_power_w: None,
rapl_pp0_prev: None,
pp1_power_w: None,
rapl_pp1_prev: None,
dram_power_w: None,
rapl_dram_prev: None,
io_history: std::collections::BTreeMap::new(),
last_clicked_cpu: None,
sort_ascending: false,
@@ -423,6 +436,7 @@ impl App {
max_history_pids: 500,
pid_last_seen: std::collections::BTreeMap::new(),
refresh_tick: 0,
collector_stats: crate::collector::CollectorStats::default(),
};
// v1.40: load persisted session state and apply.
// Missing or malformed session falls back to the
@@ -616,7 +630,13 @@ impl App {
// — cheap enough for every tick.
self.sched_stats = crate::sched::SchedStats::read();
for row in &mut self.cpus {
let pkg_temps: Vec<Option<u32>> = self
.cpus
.iter()
.map(|row| self.sensors.pkg_temp_c(row.id))
.collect();
self.collector_stats = crate::collector::collect(&mut self.cpus, |i, row| {
if let Some(status) = read_thermal_status(row.id) {
row.temp_c = if status & THERM_STATUS_READOUT_VALID != 0 {
Some(((status & THERM_STATUS_TEMP_MASK) >> 16) as u32)
@@ -627,11 +647,7 @@ impl App {
row.critical = status & THERM_STATUS_CRITICAL != 0;
row.power_limit = status & THERM_STATUS_POWER_LIMIT != 0;
} else {
// IA32_THERM_STATUS is Intel-only. On AMD, fall back to
// k10temp Tctl (the package control temperature), which
// applies to all CPUs on the same package. This is the
// canonical hwmon-based CPU temperature for Zen and later.
row.temp_c = self.sensors.pkg_temp_c(row.id);
row.temp_c = pkg_temps[i];
row.prochot = false;
row.critical = false;
row.power_limit = false;
@@ -639,16 +655,13 @@ impl App {
if let Some(ctl) = read_current_perf_ctl(row.id) {
let state = ((ctl & PERF_CTL_STATE_MASK) >> 8) as u8;
row.current_idx = row.pstates.iter().position(|p| ((p.ctl & PERF_CTL_STATE_MASK) >> 8) as u8 == state);
let cur = row.current_idx.and_then(|i| row.pstates.get(i));
let cur = row.current_idx.and_then(|idx| row.pstates.get(idx));
row.freq_khz = cur.map(|p| p.freq_khz).unwrap_or(0);
row.current_power_mw = cur.map(|p| p.power_mw);
} else {
// MSR unavailable — try sysfs cpufreq (Linux fallback, like htop).
row.current_idx = None;
row.freq_khz = read_cpu_freq_khz_sysfs(row.id).unwrap_or(0);
row.current_power_mw = None;
// Match current frequency against synthetic P-state table
// to determine which P-state we're in (intel_pstate fallback).
if row.freq_khz > 0 && !row.pstates.is_empty() {
row.current_idx = row
.pstates
@@ -657,7 +670,7 @@ impl App {
.min_by_key(|(_, p)| {
(p.freq_khz as i64 - row.freq_khz as i64).unsigned_abs()
})
.map(|(i, _)| i);
.map(|(idx, _)| idx);
}
}
row.load_pct = read_load(row.id, &mut row.prev_load) * 100.0;
@@ -666,7 +679,8 @@ impl App {
}
row.load_history
.push_back(row.load_pct.clamp(0.0, 100.0) as u8);
}
row.hwp = crate::msr::HwpInfo::read(row.id);
});
// Read package power from RAPL powercap (Intel/AMD). Requires
// kernel CONFIG_POWERCAP and intel_rapl/amd_energy driver.
// Falls back to MSR-based power or sysfs power values.
@@ -690,6 +704,36 @@ impl App {
// No MSR device at all — RAPL unsupported (QEMU, old CPU).
self.rapl_status = "n/a (unsupported)".into();
}
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_PP0_ENERGY_STATUS) {
match self.rapl_pp0_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_pp0_prev = Some(next);
if w > 0.0 { self.pp0_power_w = Some(w); }
}
None => self.rapl_pp0_prev = Some(curr),
}
}
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_PP1_ENERGY_STATUS) {
match self.rapl_pp1_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_pp1_prev = Some(next);
if w > 0.0 { self.pp1_power_w = Some(w); }
}
None => self.rapl_pp1_prev = Some(curr),
}
}
if let Some(curr) = crate::acpi::read_rapl_domain_energy(crate::msr::MSR_DRAM_ENERGY_STATUS) {
match self.rapl_dram_prev {
Some(prev) => {
let (w, next) = crate::acpi::rapl_power_watts(curr, prev);
self.rapl_dram_prev = Some(next);
if w > 0.0 { self.dram_power_w = Some(w); }
}
None => self.rapl_dram_prev = Some(curr),
}
}
// Re-read cpufreq state (catches external governor changes).
self.cpufreq.refresh();
if let Some(pkg) = read_package_thermal_status(self.cpus[0].id) {
@@ -0,0 +1,129 @@
//! Multi-threaded per-CPU data collector.
//!
//! Spawns one worker thread per CPU using `std::thread::scope` (which
//! exercises `pthread_create` via relibc). Workers are barrier-synchronised
//! (`std::sync::Barrier` → futex wake/wait) and attempt CPU pinning
//! (`sched_setaffinity` via the kernel proc scheme). Thread names are
//! set via `pthread_setname_np` (through `Builder::name`).
//!
//! This module exists to visibly demonstrate Red Bear OS Phase 0
//! threading infrastructure: per-CPU scheduler placement, futex
//! sharding, and the POSIX scheduling API surface.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Barrier;
use std::thread;
use std::time::Instant;
#[derive(Clone, Debug, Default)]
pub struct CollectorStats {
pub thread_count: usize,
pub pinned_count: usize,
pub elapsed_us: u64,
pub barrier_size: usize,
}
#[cfg(target_os = "linux")]
unsafe extern "C" {
fn sched_setaffinity(pid: i32, cpusetsize: usize, mask: *const u64) -> i32;
}
fn try_pin_cpu(cpu_id: u32) -> bool {
if cpu_id >= 64 {
return false;
}
let mask: u64 = 1u64 << cpu_id;
let bytes = mask.to_le_bytes();
if std::fs::write("/proc/self/sched-affinity", bytes).is_ok() {
return true;
}
#[cfg(target_os = "linux")]
unsafe {
sched_setaffinity(0, std::mem::size_of::<u64>(), &mask) == 0
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
pub fn collect<T, F>(items: &mut [T], work: F) -> CollectorStats
where
F: Fn(usize, &mut T) + Sync,
T: Send,
{
let n = items.len();
if n <= 1 {
if let Some((i, item)) = items.iter_mut().enumerate().next() {
work(i, item);
}
return CollectorStats {
thread_count: if n == 1 { 1 } else { 0 },
..Default::default()
};
}
let barrier = Barrier::new(n);
let pinned = AtomicUsize::new(0);
let spawned = AtomicUsize::new(0);
let start = Instant::now();
thread::scope(|s| {
for (i, item) in items.iter_mut().enumerate() {
let barrier = &barrier;
let work = &work;
let pinned = &pinned;
let spawned = &spawned;
s.spawn(move || {
spawned.fetch_add(1, Ordering::Relaxed);
if try_pin_cpu(i as u32) {
pinned.fetch_add(1, Ordering::Relaxed);
}
barrier.wait();
work(i, item);
});
}
});
CollectorStats {
thread_count: spawned.load(Ordering::Relaxed),
pinned_count: pinned.load(Ordering::Relaxed),
elapsed_us: start.elapsed().as_micros() as u64,
barrier_size: n,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_item_runs_inline() {
let mut data = [0u64; 1];
let stats = collect(&mut data, |i, v| {
*v = i as u64 + 1;
});
assert_eq!(data[0], 1);
assert_eq!(stats.thread_count, 1);
}
#[test]
fn all_items_updated() {
let mut data = vec![0u64; 8];
let stats = collect(&mut data, |i, v| {
*v = (i * i) as u64;
});
for (i, v) in data.iter().enumerate() {
assert_eq!(*v, (i * i) as u64, "item {i} not updated");
}
assert!(stats.thread_count >= 1);
}
#[test]
fn empty_slice_is_safe() {
let mut data: Vec<u64> = vec![];
let stats = collect(&mut data, |_, _| {});
assert_eq!(stats.thread_count, 0);
}
}
@@ -47,6 +47,7 @@ mod acpi;
mod app;
mod battery;
mod bench;
mod collector;
mod config;
mod cpufreq;
mod cpuid;
@@ -59,6 +60,7 @@ mod pid_detail;
mod platform;
mod process;
mod render;
mod sched;
mod sensor;
mod session;
mod smart;
@@ -344,7 +346,7 @@ fn main() -> io::Result<()> {
match app.current_tab {
TabId::PerCpu => {
f.render_stateful_widget(
render_cpu_table(&app.cpus, app.expanded_cpu, focused_panel == 1, app.pkg_power_w, &app.rapl_status),
render_cpu_table(&app.cpus, app.expanded_cpu, focused_panel == 1, app.pkg_power_w, &app.rapl_status, &app.sched_stats.per_cpu_irqs),
body_area,
&mut app.table_state,
);
@@ -280,4 +280,67 @@ impl PackageThermal {
if self.hfi { parts.push("HFI"); }
if parts.is_empty() { "".to_string() } else { parts.join(" ") }
}
}
// ---------------------------------------------------------------------------
// HWP (Hardware P-states / Intel Speed Shift)
// ---------------------------------------------------------------------------
/// Intel HWP (Hardware P-states) capability and request data read
/// from per-CPU MSRs. `None` from `HwpInfo::read()` means HWP is
/// not supported (AMD, pre-Skylake Intel, or QEMU without MSR
/// passthrough).
#[derive(Clone, Debug)]
pub struct HwpInfo {
/// Whether HWP has been enabled (IA32_PM_ENABLE bit 0).
pub enabled: bool,
/// Highest performance level the hardware can deliver
/// (IA32_HWP_CAPABILITIES bits 31:24). Arbitrary units — not
/// directly MHz, though roughly proportional.
pub max_perf: u8,
/// Guaranteed performance level (bits 23:16).
pub guaranteed_perf: u8,
/// Most efficient performance level (bits 15:8).
pub efficient_perf: u8,
/// Minimum requested performance (IA32_HWP_REQUEST bits 23:16).
pub min_request: u8,
/// Maximum requested performance (bits 31:24).
pub max_request: u8,
/// Desired performance target (bits 15:8).
pub desired_perf: u8,
/// Energy Performance Preference (bits 7:0). Range 0255:
/// 0 = all-out performance, 128 = balanced, 255 = max power
/// savings.
pub epp: u8,
}
impl HwpInfo {
/// Read HWP data for `cpu`. Returns `None` if any of the three
/// required MSRs are unreadable.
pub fn read(cpu: u32) -> Option<Self> {
let enable = read_msr(cpu, IA32_PM_ENABLE)?;
let caps = read_msr(cpu, IA32_HWP_CAPABILITIES)?;
let req = read_msr(cpu, IA32_HWP_REQUEST)?;
Some(Self {
enabled: enable & 1 != 0,
max_perf: ((caps >> 24) & 0xFF) as u8,
guaranteed_perf: ((caps >> 16) & 0xFF) as u8,
efficient_perf: ((caps >> 8) & 0xFF) as u8,
min_request: ((req >> 16) & 0xFF) as u8,
max_request: ((req >> 24) & 0xFF) as u8,
desired_perf: ((req >> 8) & 0xFF) as u8,
epp: (req & 0xFF) as u8,
})
}
/// Human-readable label for the EPP value.
pub fn epp_label(&self) -> &'static str {
match self.epp {
0..=25 => "Performance",
26..=100 => "Bal. Perf",
101..=155 => "Balanced",
156..=220 => "Bal. Power",
221..=255 => "Power",
}
}
}
@@ -461,6 +461,7 @@ pub struct ProcessInfo {
/// Process panel renders a compact range-string form
/// (e.g. "0-3,5,7-11") for at-a-glance scanning.
pub cpu_affinity: Option<Vec<u32>>,
pub sched_policy: String,
}
impl ProcessInfo {
@@ -834,6 +835,7 @@ fn parse_stat_line(line: &str) -> Option<ProcessInfo> {
thread_io_read_rate_kbs: None,
thread_io_write_rate_kbs: None,
cpu_affinity,
sched_policy: String::new(),
})
}
@@ -844,9 +846,31 @@ fn read_process(pid: u32) -> Option<ProcessInfo> {
if info.comm.is_empty() || info.comm == "?" {
info.comm = read_comm(pid);
}
info.sched_policy = derive_sched_policy(pid, info.priority);
Some(info)
}
fn derive_sched_policy(pid: u32, priority: i64) -> String {
if let Ok(data) = fs::read(format!("/proc/{}/sched-policy", pid)) {
if let Some(&policy) = data.first() {
return match policy {
0 => "FIFO",
1 => "RR",
2 => "OTHER",
_ => "?",
}
.into();
}
}
if priority > 0 && priority < 40 {
"OTHER".into()
} else if priority >= 40 && priority < 100 {
"RT".into()
} else {
"OTHER".into()
}
}
impl ProcInfo {
pub fn read() -> Self {
Self::read_sorted(SortMode::default())
@@ -386,6 +386,67 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
if any_pl { "PL ".set_style(theme::POWER_LIMIT_FLAG) } else { "PL ".set_style(theme::VALUE_OFF) },
]));
{
let s = &app.sched_stats;
let mut parts: Vec<ratatui::text::Span> = Vec::new();
parts.push("Sched: ".set_style(theme::LABEL));
let mut wrote = false;
if let Some(v) = s.context_switches {
parts.push(format!("switches={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_created {
parts.push(format!("created={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_running {
parts.push(format!("running={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.contexts_blocked {
parts.push(format!("blocked={v} ").set_style(theme::VALUE));
wrote = true;
}
if let Some(v) = s.total_irqs {
parts.push(format!("IRQs={v} ").set_style(theme::VALUE));
wrote = true;
}
if !s.per_cpu_steals.is_empty() {
let total_steals: u64 = s.per_cpu_steals.iter().sum();
parts.push(format!("steals={total_steals} ").set_style(theme::VALUE));
wrote = true;
}
if !s.per_cpu_queue_depth.is_empty() {
let total_qd: u64 = s.per_cpu_queue_depth.iter().sum();
let avg_qd = total_qd / s.per_cpu_queue_depth.len() as u64;
parts.push(format!("avg_qd={avg_qd}").set_style(theme::VALUE));
wrote = true;
}
if wrote {
lines.push(Line::from(parts));
}
}
if app.pkg_power_w.is_some() || app.pp0_power_w.is_some()
|| app.pp1_power_w.is_some() || app.dram_power_w.is_some()
{
let mut parts: Vec<ratatui::text::Span> = Vec::new();
parts.push("Power: ".set_style(theme::LABEL));
if let Some(w) = app.pkg_power_w {
parts.push(format!("Pkg {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.pp0_power_w {
parts.push(format!("Core {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.pp1_power_w {
parts.push(format!("Uncore {w:.1}W ").set_style(theme::VALUE));
}
if let Some(w) = app.dram_power_w {
parts.push(format!("DRAM {w:.1}W").set_style(theme::VALUE));
}
lines.push(Line::from(parts));
}
// OS identity (matches cpu-x System tab)
if app.os_info.available {
lines.push(Line::from(vec![
@@ -435,8 +496,26 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
}
}
let cs = &app.collector_stats;
if cs.thread_count > 1 {
lines.push(Line::from(vec![
"Collector: ".set_style(theme::LABEL),
format!("{} threads", cs.thread_count).set_style(theme::VALUE),
" pinned: ".set_style(theme::LABEL),
format!("{}/{}", cs.pinned_count, cs.barrier_size).set_style(theme::VALUE),
" barrier: ".set_style(theme::LABEL),
format!("{}", cs.barrier_size).set_style(theme::VALUE),
" last: ".set_style(theme::LABEL),
format!("{:.2} ms", cs.elapsed_us as f64 / 1000.0).set_style(theme::VALUE),
]));
} else {
lines.push(Line::from(vec![
"Collector: ".set_style(theme::LABEL),
"single-threaded".set_style(theme::VALUE_OFF),
]));
}
lines.push(Line::from(vec![
"Benchmark: ".set_style(theme::LABEL),
if app.bench_line.is_empty() { "(idle)".set_style(theme::VALUE_OFF) } else { app.bench_line.as_str().set_style(theme::VALUE) },
]));
Paragraph::new(lines)
@@ -946,7 +1025,7 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
_ => "RSS",
};
let header_str = format!(
" PID STATE PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM",
" PID STATE SCHED PRIO NI THR CPU% IO RATE {:<11} T-IO T-IO/s IO-RATE CPU% RSS AFF COMM",
mem_header
);
lines.push(Line::from(vec![
@@ -1058,10 +1137,11 @@ pub fn render_process_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
theme::VALUE
};
lines.push(Line::from(format!(
" {}{:<7} {} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<11} {:<11} {:<12} {:<6} {:<5} {:<3} {}",
" {}{:<7} {} {:<5} {:<4} {:<3} {:<3} {:<6} {:<11} {:<11} {:<11} {:<11} {:<11} {:<12} {:<6} {:<5} {:<3} {}",
prefix,
p.pid,
p.state,
p.sched_policy,
p.priority,
p.nice,
p.num_threads,
@@ -1405,6 +1485,7 @@ pub fn render_cpu_table<'a>(
focused: bool,
pkg_power_w: Option<f64>,
rapl_status: &'a str,
per_cpu_irqs: &'a [u64],
) -> Table<'a> {
let header = Row::new(vec![
"CPU".set_style(theme::LABEL),
@@ -1415,6 +1496,7 @@ pub fn render_cpu_table<'a>(
"State".set_style(theme::LABEL),
"Flags".set_style(theme::LABEL),
"Load % (30s)".set_style(theme::LABEL),
"IRQs".set_style(theme::LABEL),
])
.height(1);
let rows: Vec<Row> = cpus
@@ -1483,6 +1565,15 @@ pub fn render_cpu_table<'a>(
spark_text.set_style(Style::new().fg(lcolor)),
format!(" {load_label}").set_style(Style::new().fg(lcolor).bold()),
])),
{
let irq_val = per_cpu_irqs.get(cpu.id as usize).copied();
Cell::from(
irq_val
.map(|v| format!("{v}"))
.unwrap_or_else(|| "".into())
.set_style(if irq_val.is_some() { theme::VALUE } else { theme::VALUE_OFF }),
)
},
])
})
.collect();
@@ -1511,6 +1602,32 @@ pub fn render_cpu_table<'a>(
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
]));
}
if let Some(ref hwp) = cpu.hwp {
let s = sub_style;
rows.push(Row::new(vec![
" HWP caps".set_style(s),
format!("eff={} guar={} max={}", hwp.efficient_perf, hwp.guaranteed_perf, hwp.max_perf).set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
format!("{}", if hwp.enabled { "ON" } else { "OFF" }).set_style(if hwp.enabled { Style::new().green() } else { theme::VALUE_OFF }),
"".set_style(s),
"".set_style(s),
"".set_style(s),
]));
rows.push(Row::new(vec![
" HWP req".set_style(s),
format!("min={} max={} des={} EPP={} ({})", hwp.min_request, hwp.max_request, hwp.desired_perf, hwp.epp, hwp.epp_label()).set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
"".set_style(s),
]));
}
}
@@ -1526,6 +1643,7 @@ pub fn render_cpu_table<'a>(
Constraint::Length(8),
Constraint::Length(6),
Constraint::Length(SPARK_WIDTH as u16 + 6),
Constraint::Length(8),
],
)
.header(header)
@@ -1671,7 +1789,7 @@ pub fn snapshot(app: &App, width: u16, height: u16) -> String {
);
f.render_widget(render_header(app, true), header_area);
f.render_stateful_widget(
render_cpu_table(&app.cpus, app.expanded_cpu, true, app.pkg_power_w, &app.rapl_status),
render_cpu_table(&app.cpus, app.expanded_cpu, true, app.pkg_power_w, &app.rapl_status, &app.sched_stats.per_cpu_irqs),
table_area,
&mut state,
);
@@ -0,0 +1,230 @@
//! Scheduler statistics from `/scheme/sys/stat` (Redox) or
//! `/proc/stat` (Linux).
//!
//! Redox's sys scheme exposes a richer set of scheduler counters than
//! Linux: per-CPU IRQ counts, context-switch totals, and scheduler
//! context state (created / running / blocked). On Linux, only the
//! `ctxt` (context switches) and `intr` (total interrupts) lines are
//! available from `/proc/stat`; per-CPU IRQ distribution lives in
//! `/proc/interrupts` and is not read here (kept cheap — one file
//! read).
//!
//! All fields degrade to `None` / empty when the data source is absent
//! or unparseable, so the TUI never panics on a missing scheme.
use std::fs;
/// Scheduler and IRQ statistics. Fields are `Option` because the
/// underlying data source may not provide all of them (Linux vs
/// Redox). `Default` gives all-`None` / empty, suitable as a
/// pre-probe placeholder.
#[derive(Clone, Debug, Default)]
pub struct SchedStats {
pub context_switches: Option<u64>,
pub contexts_created: Option<u64>,
pub contexts_running: Option<u64>,
pub contexts_blocked: Option<u64>,
pub per_cpu_irqs: Vec<u64>,
pub total_irqs: Option<u64>,
pub per_cpu_switches: Vec<u64>,
pub per_cpu_steals: Vec<u64>,
pub per_cpu_queue_depth: Vec<u64>,
}
impl SchedStats {
/// Read scheduler stats from the first available data source.
///
/// Tries `/scheme/sys/stat` (Redox) first, then falls back to
/// `/proc/stat` (Linux). Returns a `SchedStats` with `None` /
/// empty fields for anything the source does not provide.
pub fn read() -> Self {
let mut stats = Self::default();
if let Ok(data) = fs::read_to_string("/scheme/sys/stat") {
stats = Self::parse_redox(&data);
} else if let Ok(data) = fs::read_to_string("/proc/stat") {
stats = Self::parse_linux(&data);
}
if let Ok(data) = fs::read_to_string("/scheme/sys/sched") {
Self::merge_sched_detail(&mut stats, &data);
}
stats
}
/// Parse the Redox `/scheme/sys/stat` format:
/// ```text
/// cpu <u> <n> <k> <i> <irq>
/// cpu0 ...
/// IRQs <total> <cpu0_irqs> <cpu1_irqs> ...
/// boot_time: <secs>
/// context_switches: <num>
/// contexts_created: <num>
/// contexts_running: <num>
/// contexts_blocked: <num>
/// ```
fn parse_redox(data: &str) -> Self {
let mut stats = Self::default();
for line in data.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("IRQs ") {
// "IRQs <total> <cpu0> <cpu1> ..."
let nums: Vec<u64> = rest
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
// First number is the total; remaining are per-CPU.
if let Some(&total) = nums.first() {
stats.total_irqs = Some(total);
stats.per_cpu_irqs = nums[1..].to_vec();
}
} else if let Some(rest) = trimmed.strip_prefix("context_switches:") {
stats.context_switches = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_created:") {
stats.contexts_created = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_running:") {
stats.contexts_running = rest.trim().parse().ok();
} else if let Some(rest) = trimmed.strip_prefix("contexts_blocked:") {
stats.contexts_blocked = rest.trim().parse().ok();
}
}
stats
}
/// Parse the Linux `/proc/stat` format. Only `ctxt` (context
/// switches) and `intr` (total interrupts) are available.
/// Per-CPU IRQ distribution is in `/proc/interrupts` and is not
/// read here.
///
/// ```text
/// cpu 1000 200 3000 50000 ...
/// cpu0 ...
/// intr 200 50 50 ... (first number = total)
/// ctxt 5000
/// btime 1000
/// ```
fn parse_linux(data: &str) -> Self {
let mut stats = Self::default();
for line in data.lines() {
let trimmed = line.trim();
// "ctxt <number>" — context switches since boot.
if let Some(rest) = trimmed.strip_prefix("ctxt ") {
stats.context_switches = rest.trim().parse().ok();
}
// "intr <total> <irq0> <irq1> ..." — first field is total.
if let Some(rest) = trimmed.strip_prefix("intr ") {
if let Some(total_str) = rest.split_whitespace().next() {
stats.total_irqs = total_str.parse().ok();
}
}
}
stats
}
fn merge_sched_detail(stats: &mut SchedStats, data: &str) {
for line in data.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("cpu") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() < 7 {
continue;
}
let switches: u64 = parts[2].parse().unwrap_or(0);
let steals: u64 = parts[4].parse().unwrap_or(0);
let qd: u64 = parts[6].parse().unwrap_or(0);
stats.per_cpu_switches.push(switches);
stats.per_cpu_steals.push(steals);
stats.per_cpu_queue_depth.push(qd);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_redox_all_fields() {
let data = "\
cpu 1000 200 3000 50000 100
cpu0 500 100 1500 25000 50
cpu1 500 100 1500 25000 50
IRQs 200 100 100
boot_time: 1000
context_switches: 5000
contexts_created: 100
contexts_running: 2
contexts_blocked: 0
";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.context_switches, Some(5000));
assert_eq!(stats.contexts_created, Some(100));
assert_eq!(stats.contexts_running, Some(2));
assert_eq!(stats.contexts_blocked, Some(0));
assert_eq!(stats.total_irqs, Some(200));
assert_eq!(stats.per_cpu_irqs, vec![100, 100]);
}
#[test]
fn parse_linux_context_switches_and_intr() {
let data = "\
cpu 1000 200 3000 50000 100 0 0 0 0 0
cpu0 500 100 1500 25000 50 0 0 0 0 0
intr 200 50 50 100
ctxt 5000
btime 1000
";
let stats = SchedStats::parse_linux(data);
assert_eq!(stats.context_switches, Some(5000));
assert_eq!(stats.total_irqs, Some(200));
// Per-CPU IRQs are NOT available from /proc/stat on Linux.
assert!(stats.per_cpu_irqs.is_empty());
// Redox-specific fields are not available.
assert_eq!(stats.contexts_created, None);
assert_eq!(stats.contexts_running, None);
assert_eq!(stats.contexts_blocked, None);
}
#[test]
fn default_is_all_none() {
let stats = SchedStats::default();
assert_eq!(stats.context_switches, None);
assert_eq!(stats.total_irqs, None);
assert!(stats.per_cpu_irqs.is_empty());
}
#[test]
fn parse_redox_missing_context_fields() {
// Only the IRQs line — no context_* lines.
let data = "IRQs 42 10 20 12\n";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.total_irqs, Some(42));
assert_eq!(stats.per_cpu_irqs, vec![10, 20, 12]);
assert_eq!(stats.context_switches, None);
}
#[test]
fn parse_redox_empty_irqs_line() {
let data = "cpu 100 0 0 100 0\ncontext_switches: 99\n";
let stats = SchedStats::parse_redox(data);
assert_eq!(stats.context_switches, Some(99));
assert_eq!(stats.total_irqs, None);
assert!(stats.per_cpu_irqs.is_empty());
}
#[test]
fn merge_sched_detail_parses_per_cpu() {
let mut stats = SchedStats::default();
let data = "\
cpu0 switches 1000 steals 5 queue_depth 3
cpu1 switches 2000 steals 2 queue_depth 1
total switches 3000 steals 7
";
SchedStats::merge_sched_detail(&mut stats, data);
assert_eq!(stats.per_cpu_switches, vec![1000, 2000]);
assert_eq!(stats.per_cpu_steals, vec![5, 2]);
assert_eq!(stats.per_cpu_queue_depth, vec![3, 1]);
}
}