diff --git a/local/recipes/system/redbear-power/source/src/acpi.rs b/local/recipes/system/redbear-power/source/src/acpi.rs index b8c86e5bc4..49e98b3fa5 100644 --- a/local/recipes/system/redbear-power/source/src/acpi.rs +++ b/local/recipes/system/redbear-power/source/src/acpi.rs @@ -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 { // Redox exposes the CPU count via the sys:cpu scheme file // (kernel/src/scheme/sys/cpu.rs) as "CPUs: N\n...". /dev/cpu/ does diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 85c18ecd8c..67812a3e0e 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -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, + rapl_pp0_prev: Option<(u64, std::time::Instant)>, + pub pp1_power_w: Option, + rapl_pp1_prev: Option<(u64, std::time::Instant)>, + pub dram_power_w: Option, + 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, pub current_tab: TabId, pub bench_start_time: Option, + 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> = 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) { diff --git a/local/recipes/system/redbear-power/source/src/collector.rs b/local/recipes/system/redbear-power/source/src/collector.rs new file mode 100644 index 0000000000..eab07f5417 --- /dev/null +++ b/local/recipes/system/redbear-power/source/src/collector.rs @@ -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::(), &mask) == 0 + } + #[cfg(not(target_os = "linux"))] + { + false + } +} + +pub fn collect(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 = vec![]; + let stats = collect(&mut data, |_, _| {}); + assert_eq!(stats.thread_count, 0); + } +} diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index 91f0a17eae..6cac5ae19b 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -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, ); diff --git a/local/recipes/system/redbear-power/source/src/msr.rs b/local/recipes/system/redbear-power/source/src/msr.rs index 02634657f5..3c4a34d8a4 100644 --- a/local/recipes/system/redbear-power/source/src/msr.rs +++ b/local/recipes/system/redbear-power/source/src/msr.rs @@ -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 0–255: + /// 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 { + 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", + } + } } \ No newline at end of file diff --git a/local/recipes/system/redbear-power/source/src/process.rs b/local/recipes/system/redbear-power/source/src/process.rs index b222f68c41..17c5b9e471 100644 --- a/local/recipes/system/redbear-power/source/src/process.rs +++ b/local/recipes/system/redbear-power/source/src/process.rs @@ -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>, + pub sched_policy: String, } impl ProcessInfo { @@ -834,6 +835,7 @@ fn parse_stat_line(line: &str) -> Option { 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 { 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()) diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index a91379a4d8..0ff292a656 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -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 = 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 = 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, 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 = 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, ); diff --git a/local/recipes/system/redbear-power/source/src/sched.rs b/local/recipes/system/redbear-power/source/src/sched.rs new file mode 100644 index 0000000000..83543906df --- /dev/null +++ b/local/recipes/system/redbear-power/source/src/sched.rs @@ -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, + pub contexts_created: Option, + pub contexts_running: Option, + pub contexts_blocked: Option, + pub per_cpu_irqs: Vec, + pub total_irqs: Option, + pub per_cpu_switches: Vec, + pub per_cpu_steals: Vec, + pub per_cpu_queue_depth: Vec, +} + +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 + /// cpu0 ... + /// IRQs ... + /// boot_time: + /// context_switches: + /// contexts_created: + /// contexts_running: + /// contexts_blocked: + /// ``` + 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 ..." + let nums: Vec = 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 " — context switches since boot. + if let Some(rest) = trimmed.strip_prefix("ctxt ") { + stats.context_switches = rest.trim().parse().ok(); + } + // "intr ..." — 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]); + } +} diff --git a/local/sources/kernel b/local/sources/kernel index e812356cf0..c6a5b7a1ad 160000 --- a/local/sources/kernel +++ b/local/sources/kernel @@ -1 +1 @@ -Subproject commit e812356cf0a8927557999d89dfe227d6d91b01e6 +Subproject commit c6a5b7a1ad87d031ef8c458ab0a35ebe5e735744 diff --git a/local/sources/relibc b/local/sources/relibc index 9774052fd1..26595f1624 160000 --- a/local/sources/relibc +++ b/local/sources/relibc @@ -1 +1 @@ -Subproject commit 9774052fd11afdda3562d03421b7e417c97cf6e3 +Subproject commit 26595f1624563770e8d9f8a516cd8f74b4c0407d