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:
@@ -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 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<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]);
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule local/sources/kernel updated: e812356cf0...c6a5b7a1ad
+1
-1
Submodule local/sources/relibc updated: 9774052fd1...26595f1624
Reference in New Issue
Block a user