USB: P3 scheme service wrapper — redbear-ftdi registers /scheme/ttys/usbFTDI_<N>
Same pattern as acmd: FtdiScheme with Mutex<XhciEndpHandle>, SchemeSync impl, Socket::create() + register on Redox, stdout fallback on host/Linux.
This commit is contained in:
Generated
+4
-4
@@ -592,7 +592,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.18+rb0.2.5"
|
||||
version = "0.1.18+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -910,12 +910,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_installer"
|
||||
version = "0.2.42+rb0.2.5"
|
||||
version = "0.2.42+rb0.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arg_parser",
|
||||
"libc",
|
||||
"libredox 0.1.18+rb0.2.5",
|
||||
"libredox 0.1.18+rb0.3.0",
|
||||
"ring",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -924,7 +924,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.9.0+rb0.2.5"
|
||||
version = "0.9.0+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
@@ -14,6 +14,9 @@ xhcid = { path = "../../../../../local/sources/base/drivers/usb/xhcid" }
|
||||
common = { path = "../../../../../local/sources/base/drivers/common" }
|
||||
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] }
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
redox-scheme = { path = "../../../../../local/sources/redox-scheme" }
|
||||
|
||||
[patch.crates-io]
|
||||
redox_syscall = { path = "../../../../../local/sources/syscall" }
|
||||
libredox = { path = "../../../../../local/sources/libredox" }
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
//! Cross-referenced with Linux 7.1 `drivers/usb/serial/ftdi_sio.c`
|
||||
//! (2,876 lines). Implements the core FTDI protocol: reset, baud
|
||||
//! rate setting, flow control, and bidirectional bulk read/write.
|
||||
//!
|
||||
//! On Redox: registers /scheme/ttys/usbFTDI_<N> for serial console use.
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
mod scheme;
|
||||
|
||||
use std::{env, io, io::{Read, Write}, thread, time};
|
||||
|
||||
@@ -142,6 +147,28 @@ fn main() {
|
||||
log::info!("FTDI ready on {} port {} — bulk_in={} bulk_out={}",
|
||||
scheme, port_id, bulk_in_num, bulk_out_num);
|
||||
|
||||
// ── Scheme IPC path (Redox) ──
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
use redox_scheme::scheme::SchemeSync;
|
||||
use redox_scheme::{Socket, SignalBehavior};
|
||||
let scheme_name = format!("ttys/usbFTDI_{}", port_id);
|
||||
log::info!("FTDI: registering /scheme/{}", scheme_name);
|
||||
let socket = Socket::create().expect("FTDI: scheme socket");
|
||||
let mut scheme_state = redox_scheme::scheme::SchemeState::new();
|
||||
let mut ftdi_scheme = crate::scheme::FtdiScheme::new(dev.bulk_in, dev.bulk_out);
|
||||
socket.register(&scheme_name).expect("FTDI: scheme register");
|
||||
loop {
|
||||
let req = socket.next_request(SignalBehavior::Restart).expect("FTDI: scheme request");
|
||||
if let Err(e) = req.handle_scheme_mut(&mut ftdi_scheme, &mut scheme_state) {
|
||||
log::warn!("FTDI: scheme error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Stdout path (host/Linux testing) ──
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
let mut bulk_out = dev.bulk_out;
|
||||
let _writer = thread::spawn(move || {
|
||||
let mut buf = [0u8; 1024];
|
||||
@@ -173,4 +200,5 @@ fn main() {
|
||||
}
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use redox_scheme::scheme::SchemeSync;
|
||||
use redox_scheme::{CallerCtx, OpenResult};
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL};
|
||||
use syscall::flag::{MODE_FILE, O_RDWR, O_STAT};
|
||||
|
||||
use xhcid_interface::XhciEndpHandle;
|
||||
|
||||
pub struct FtdiScheme {
|
||||
bulk_in: Mutex<XhciEndpHandle>,
|
||||
bulk_out: Mutex<XhciEndpHandle>,
|
||||
open_count: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl FtdiScheme {
|
||||
pub fn new(bulk_in: XhciEndpHandle, bulk_out: XhciEndpHandle) -> Self {
|
||||
Self { bulk_in: Mutex::new(bulk_in), bulk_out: Mutex::new(bulk_out), open_count: Mutex::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeSync for FtdiScheme {
|
||||
fn openat(&self, id: usize, path: &str, _flags: usize, _ctx: &CallerCtx) -> Result<OpenResult> {
|
||||
if id != 1 { return Err(Error::new(EBADF)); }
|
||||
if path.is_empty() || path == "." || path == "/" {
|
||||
*self.open_count.lock().unwrap_or_else(|e| e.into_inner()) += 1;
|
||||
return Ok(OpenResult::ThisScheme { number: 1, flags: O_STAT | MODE_FILE });
|
||||
}
|
||||
let mut count = self.open_count.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let next_id = 2 + *count;
|
||||
*count += 1;
|
||||
Ok(OpenResult::OtherScheme { number: next_id, flags: O_RDWR })
|
||||
}
|
||||
fn read(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
if id < 2 || buf.is_empty() { return Err(Error::new(EBADF)); }
|
||||
let mut b = self.bulk_in.lock().unwrap_or_else(|e| e.into_inner());
|
||||
b.transfer_read(buf).map(|s| s.bytes_transferred as usize).map_err(|e| { log::warn!("FTDI scheme: read: {}", e); Error::new(EINVAL) })
|
||||
}
|
||||
fn write(&mut self, id: usize, buf: &[u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
if id < 2 || buf.is_empty() { return Err(Error::new(EBADF)); }
|
||||
let mut b = self.bulk_out.lock().unwrap_or_else(|e| e.into_inner());
|
||||
b.transfer_write(buf).map(|s| s.bytes_transferred as usize).map_err(|e| { log::warn!("FTDI scheme: write: {}", e); Error::new(EINVAL) })
|
||||
}
|
||||
fn fsync(&mut self, _id: usize, _ctx: &CallerCtx) -> Result<()> { Ok(()) }
|
||||
fn close(&self, id: usize, _ctx: &CallerCtx) -> Result<()> {
|
||||
if id >= 2 { *self.open_count.lock().unwrap_or_else(|e| e.into_inner()) = self.open_count.lock().unwrap_or_else(|e| e.into_inner()).saturating_sub(1); }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,9 @@ lto = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
libredox = { path = "../../../../../local/sources/libredox", features = ["call", "redox_syscall", "protocol"] }
|
||||
syscall = { package = "redox_syscall", path = "../../../../../local/sources/syscall" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libc = "0.2"
|
||||
libc = "0.2"
|
||||
|
||||
@@ -21,12 +21,14 @@ use crate::acpi::{
|
||||
};
|
||||
use crate::cpufreq::Cpufreq;
|
||||
use crate::cpuid::{self, CoreType, CpuId};
|
||||
use crate::cpuidle::CpuidleStats;
|
||||
use crate::msr::{
|
||||
IA32_PERF_CTL, PERF_CTL_STATE_MASK, PackageThermal, THERM_STATUS_CRITICAL,
|
||||
THERM_STATUS_POWER_LIMIT, THERM_STATUS_PROCHOT, THERM_STATUS_READOUT_VALID,
|
||||
THERM_STATUS_TEMP_MASK, read_current_perf_ctl, read_package_thermal_status,
|
||||
read_thermal_status, write_msr,
|
||||
};
|
||||
use crate::wakeup::WakeupStats;
|
||||
|
||||
fn read_load_avg() -> Option<(f64, f64, f64)> {
|
||||
let s = std::fs::read_to_string("/proc/loadavg").ok()?;
|
||||
@@ -156,6 +158,19 @@ pub struct App {
|
||||
/// `/proc/stat` (Linux). Read every tick — it's a single
|
||||
/// file read. Fields degrade to `None` when unavailable.
|
||||
pub sched_stats: crate::sched::SchedStats,
|
||||
/// Previous per-CPU scheduler time breakdown, used to render
|
||||
/// htop-style CPU state bars from cumulative counters.
|
||||
pub prev_sched_breakdown: Vec<crate::sched::CpuTimeBreakdown>,
|
||||
/// CPU idle (C-state) residency percentages per CPU, read from
|
||||
/// `/sys/devices/system/cpu/cpu*/cpuidle/state*/time`.
|
||||
pub cpuidle_stats: CpuidleStats,
|
||||
/// Total system interrupts from the previous refresh, used to
|
||||
/// compute wakeups per second.
|
||||
pub wakeup_prev_total: Option<u64>,
|
||||
/// System wakeups per second (and cumulative total interrupts).
|
||||
pub wakeup_stats: WakeupStats,
|
||||
/// Hide kernel threads in the Process tab.
|
||||
pub hide_kernel_threads: bool,
|
||||
pub load_avg: Option<(f64, f64, f64)>,
|
||||
pub poll_ms: u64,
|
||||
pub theme_idx: usize,
|
||||
@@ -464,6 +479,11 @@ impl App {
|
||||
process_sort: crate::process::SortMode::default(),
|
||||
process_filter: String::new(),
|
||||
sched_stats: crate::sched::SchedStats::default(),
|
||||
prev_sched_breakdown: Vec::new(),
|
||||
cpuidle_stats: CpuidleStats::default(),
|
||||
wakeup_prev_total: None,
|
||||
wakeup_stats: WakeupStats::default(),
|
||||
hide_kernel_threads: false,
|
||||
load_avg: read_load_avg(),
|
||||
poll_ms: 0,
|
||||
theme_idx: 0,
|
||||
@@ -532,6 +552,7 @@ impl App {
|
||||
app.show_full_cmdline = session.show_full_cmdline;
|
||||
app.process_narrow = session.process_narrow;
|
||||
app.poll_ms = session.refresh_ms;
|
||||
app.hide_kernel_threads = session.hide_kernel_threads;
|
||||
app
|
||||
}
|
||||
|
||||
@@ -555,6 +576,7 @@ impl App {
|
||||
show_full_cmdline: self.show_full_cmdline,
|
||||
process_narrow: self.process_narrow,
|
||||
refresh_ms: self.poll_ms,
|
||||
hide_kernel_threads: self.hide_kernel_threads,
|
||||
};
|
||||
session.save();
|
||||
}
|
||||
@@ -737,7 +759,23 @@ impl App {
|
||||
|
||||
// Scheduler stats (context switches, IRQs). One file read
|
||||
// — cheap enough for every tick.
|
||||
self.sched_stats = crate::sched::SchedStats::read();
|
||||
let next_sched_stats = crate::sched::SchedStats::read();
|
||||
if !next_sched_stats.per_cpu_time_breakdown.is_empty() {
|
||||
self.prev_sched_breakdown =
|
||||
std::mem::take(&mut self.sched_stats.per_cpu_time_breakdown);
|
||||
}
|
||||
self.sched_stats = next_sched_stats;
|
||||
|
||||
// CPU C-state residency from sysfs. Cheap on Linux; degrades
|
||||
// to empty stats when the sysfs tree is unavailable.
|
||||
self.cpuidle_stats = CpuidleStats::read();
|
||||
|
||||
// System-wide wakeups per second from /proc/stat interrupts.
|
||||
let dt = Duration::from_millis(self.poll_ms.max(1));
|
||||
if let Some(stats) = crate::wakeup::read_system_wakeups(self.wakeup_prev_total, dt) {
|
||||
self.wakeup_prev_total = Some(stats.system_total);
|
||||
self.wakeup_stats = stats;
|
||||
}
|
||||
|
||||
let pkg_temps: Vec<Option<u32>> = self
|
||||
.cpus
|
||||
@@ -1195,16 +1233,34 @@ impl App {
|
||||
/// Process tab renders this many rows and `process_cursor` is
|
||||
/// bounded to this count.
|
||||
pub(crate) fn visible_processes(&self) -> Vec<&crate::process::ProcessInfo> {
|
||||
self.processes
|
||||
.processes
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
self.process_filter.is_empty()
|
||||
|| p.comm
|
||||
.to_lowercase()
|
||||
.contains(&self.process_filter.to_lowercase())
|
||||
})
|
||||
.collect()
|
||||
self.visible_processes_with(self.hide_kernel_threads)
|
||||
}
|
||||
|
||||
pub(crate) fn visible_processes_with(
|
||||
&self,
|
||||
hide_kernel_threads: bool,
|
||||
) -> Vec<&crate::process::ProcessInfo> {
|
||||
crate::process::visible_processes(
|
||||
&self.processes.processes,
|
||||
&self.process_filter,
|
||||
hide_kernel_threads,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn toggle_kernel_threads(&mut self) {
|
||||
self.hide_kernel_threads = !self.hide_kernel_threads;
|
||||
}
|
||||
|
||||
pub fn swap_used_pct(&self) -> Option<f64> {
|
||||
self.meminfo.swap_used_pct()
|
||||
}
|
||||
|
||||
pub fn wakeups_per_second(&self) -> f64 {
|
||||
self.wakeup_stats.system_per_second
|
||||
}
|
||||
|
||||
pub fn cpuidle_stats(&self) -> &CpuidleStats {
|
||||
&self.cpuidle_stats
|
||||
}
|
||||
|
||||
/// Update all per-PID history maps (IO rate, CPU%, RSS) from
|
||||
@@ -1878,6 +1934,7 @@ mod tests {
|
||||
show_full_cmdline: app.show_full_cmdline,
|
||||
process_narrow: app.process_narrow,
|
||||
refresh_ms: app.poll_ms,
|
||||
hide_kernel_threads: app.hide_kernel_threads,
|
||||
};
|
||||
let serialized = toml::to_string(&session).unwrap();
|
||||
let parsed: crate::session::SessionState = toml::from_str(&serialized).unwrap();
|
||||
|
||||
@@ -28,17 +28,22 @@ unsafe extern "C" {
|
||||
}
|
||||
|
||||
fn try_pin_cpu(cpu_id: u32) -> bool {
|
||||
if cpu_id >= 64 {
|
||||
if cpu_id >= 128 {
|
||||
return false;
|
||||
}
|
||||
let mask: u64 = 1u64 << cpu_id;
|
||||
let bytes = mask.to_le_bytes();
|
||||
let mut mask = [0u64; 2];
|
||||
let idx = (cpu_id as usize) / 64;
|
||||
let bit = (cpu_id as usize) % 64;
|
||||
mask[idx] |= 1u64 << bit;
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[..8].copy_from_slice(&mask[0].to_le_bytes());
|
||||
bytes[8..].copy_from_slice(&mask[1].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
|
||||
sched_setaffinity(0, std::mem::size_of::<u64>(), &mask as *const u64) == 0
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
//! CPU idle residency stats from sysfs.
|
||||
//!
|
||||
//! Reads per-CPU C-state names and accumulated residency time from:
|
||||
//! `/sys/devices/system/cpu/cpu*/cpuidle/state*/{name,time}`.
|
||||
//! Missing paths degrade to empty stats.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct CStateResidency {
|
||||
pub name: String,
|
||||
pub time: u64,
|
||||
pub percentage: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct CpuidleStats {
|
||||
pub per_cpu: BTreeMap<u32, Vec<CStateResidency>>,
|
||||
}
|
||||
|
||||
impl CpuidleStats {
|
||||
pub fn read() -> Self {
|
||||
Self::read_from(Path::new("/sys/devices/system/cpu"))
|
||||
}
|
||||
|
||||
pub(crate) fn read_from(base: &Path) -> Self {
|
||||
let mut stats = Self::default();
|
||||
let Ok(entries) = fs::read_dir(base) else {
|
||||
return stats;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let Some(cpu_id) = parse_cpu_dir(&path) else {
|
||||
continue;
|
||||
};
|
||||
let cpuidle_dir = path.join("cpuidle");
|
||||
let mut states = read_cpu_states(&cpuidle_dir);
|
||||
normalize_percentages(&mut states);
|
||||
if !states.is_empty() {
|
||||
stats.per_cpu.insert(cpu_id, states);
|
||||
}
|
||||
}
|
||||
|
||||
stats
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_cpu_dir(path: &Path) -> Option<u32> {
|
||||
let name = path.file_name()?.to_str()?;
|
||||
name.strip_prefix("cpu")?.parse().ok()
|
||||
}
|
||||
|
||||
fn read_cpu_states(cpuidle_dir: &Path) -> Vec<CStateResidency> {
|
||||
let Ok(entries) = fs::read_dir(cpuidle_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut states = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let state_dir = entry.path();
|
||||
if !state_dir.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let name = read_trimmed(&state_dir.join("name"));
|
||||
let time = read_trimmed(&state_dir.join("time"));
|
||||
let (Some(name), Some(time)) = (name, time) else {
|
||||
continue;
|
||||
};
|
||||
let Some(time) = time.parse().ok() else {
|
||||
continue;
|
||||
};
|
||||
states.push(CStateResidency {
|
||||
name,
|
||||
time,
|
||||
percentage: 0.0,
|
||||
});
|
||||
}
|
||||
states.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
states
|
||||
}
|
||||
|
||||
fn read_trimmed(path: &Path) -> Option<String> {
|
||||
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn normalize_percentages(states: &mut [CStateResidency]) {
|
||||
let total: u64 = states.iter().map(|s| s.time).sum();
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
for state in states {
|
||||
state.percentage = state.time as f64 / total as f64;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn temp_dir() -> std::path::PathBuf {
|
||||
let mut dir = std::env::temp_dir();
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("time went backwards")
|
||||
.as_nanos();
|
||||
dir.push(format!("redbear-power-cpuidle-{unique}"));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_percentages_sums_to_one() {
|
||||
let mut states = vec![
|
||||
CStateResidency {
|
||||
name: "C1".into(),
|
||||
time: 25,
|
||||
percentage: 0.0,
|
||||
},
|
||||
CStateResidency {
|
||||
name: "C2".into(),
|
||||
time: 75,
|
||||
percentage: 0.0,
|
||||
},
|
||||
];
|
||||
normalize_percentages(&mut states);
|
||||
assert!((states[0].percentage - 0.25).abs() < f64::EPSILON);
|
||||
assert!((states[1].percentage - 0.75).abs() < f64::EPSILON);
|
||||
assert!((states.iter().map(|s| s.percentage).sum::<f64>() - 1.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_percentages_handles_zero_total() {
|
||||
let mut states = vec![CStateResidency {
|
||||
name: "C1".into(),
|
||||
time: 0,
|
||||
percentage: 0.0,
|
||||
}];
|
||||
normalize_percentages(&mut states);
|
||||
assert_eq!(states[0].percentage, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_from_unknown_layout_returns_empty() {
|
||||
let dir = temp_dir();
|
||||
let stats = CpuidleStats::read_from(&dir);
|
||||
assert!(stats.per_cpu.is_empty());
|
||||
let _ = fs::remove_dir_all(dir);
|
||||
}
|
||||
}
|
||||
@@ -95,10 +95,12 @@ fn do_kill(pid: u32, sig: i32) -> Result<(), String> {
|
||||
Err(std::io::Error::last_os_error().to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let _ = (pid, sig);
|
||||
Err("process killing not supported on this platform".into())
|
||||
let sig = sig as u32;
|
||||
libredox::call::kill(pid as usize, sig)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ mod collector;
|
||||
mod config;
|
||||
mod cpufreq;
|
||||
mod cpuid;
|
||||
mod cpuidle;
|
||||
mod dbus;
|
||||
mod dmi;
|
||||
mod graph;
|
||||
@@ -73,6 +74,7 @@ mod session;
|
||||
mod smart;
|
||||
mod storage;
|
||||
mod theme;
|
||||
mod wakeup;
|
||||
|
||||
use crate::app::{App, POLL_MS, TabId};
|
||||
use crate::graph::BrailleGraph;
|
||||
@@ -1237,6 +1239,17 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
));
|
||||
}
|
||||
Key::Char('K') if app.current_tab == TabId::Process => {
|
||||
app.toggle_kernel_threads();
|
||||
app.flash_status(format!(
|
||||
"kernel threads: {}",
|
||||
if app.hide_kernel_threads {
|
||||
"hidden"
|
||||
} else {
|
||||
"shown"
|
||||
}
|
||||
));
|
||||
}
|
||||
Key::Char('C') if app.current_tab == TabId::Process => {
|
||||
app.show_full_cmdline = !app.show_full_cmdline;
|
||||
app.flash_status(format!(
|
||||
|
||||
@@ -25,6 +25,10 @@ pub struct MemInfo {
|
||||
pub free_kib: u64,
|
||||
/// Total swap in KiB (0 if no swap).
|
||||
pub swap_total_kib: u64,
|
||||
/// Free swap in KiB.
|
||||
pub swap_free_kib: u64,
|
||||
/// Swap cached in KiB.
|
||||
pub swap_cached_kib: u64,
|
||||
/// Used swap in KiB.
|
||||
pub swap_used_kib: u64,
|
||||
/// True when at least one value was populated from a real source.
|
||||
@@ -62,6 +66,18 @@ impl MemInfo {
|
||||
}
|
||||
(self.swap_used_kib as f64 / self.swap_total_kib as f64) * 100.0
|
||||
}
|
||||
pub fn swap_used(&self) -> u64 {
|
||||
self.swap_total_kib
|
||||
.saturating_sub(self.swap_free_kib)
|
||||
.saturating_sub(self.swap_cached_kib)
|
||||
.max(self.swap_used_kib)
|
||||
}
|
||||
pub fn swap_used_pct(&self) -> Option<f64> {
|
||||
if self.swap_total_kib == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((self.swap_used() as f64 / self.swap_total_kib as f64) * 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read memory info. Tries Redox scheme first, then Linux `/proc/meminfo`.
|
||||
@@ -126,13 +142,23 @@ fn parse_meminfo_kv(s: &str, info: &mut MemInfo) -> bool {
|
||||
any = true;
|
||||
}
|
||||
"SwapFree" => {
|
||||
// derive SwapUsed = SwapTotal - SwapFree
|
||||
info.swap_used_kib = info.swap_total_kib.saturating_sub(v_kib);
|
||||
info.swap_free_kib = v_kib;
|
||||
any = true;
|
||||
}
|
||||
"SwapCached" => {
|
||||
info.swap_cached_kib = v_kib;
|
||||
any = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if info.swap_total_kib > 0 {
|
||||
let used = info
|
||||
.swap_total_kib
|
||||
.saturating_sub(info.swap_free_kib)
|
||||
.saturating_sub(info.swap_cached_kib);
|
||||
info.swap_used_kib = used;
|
||||
}
|
||||
// If MemTotal was parsed but MemFree wasn't, fall back to "used = total - free - buffers - cached"
|
||||
if info.used_kib == 0 && info.total_kib > 0 {
|
||||
info.used_kib = info
|
||||
@@ -256,3 +282,37 @@ pub fn format_uptime(secs: u64) -> String {
|
||||
format!("{s}s")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MemInfo, parse_meminfo_kv};
|
||||
|
||||
#[test]
|
||||
fn parses_swap_fields_and_computes_usage() {
|
||||
let mut info = MemInfo::default();
|
||||
let meminfo = "\
|
||||
MemTotal: 8192 kB
|
||||
MemFree: 4096 kB
|
||||
Buffers: 512 kB
|
||||
Cached: 1024 kB
|
||||
SwapTotal: 2048 kB
|
||||
SwapFree: 1536 kB
|
||||
SwapCached: 256 kB
|
||||
";
|
||||
|
||||
assert!(parse_meminfo_kv(meminfo, &mut info));
|
||||
assert_eq!(info.swap_total_kib, 2048);
|
||||
assert_eq!(info.swap_free_kib, 1536);
|
||||
assert_eq!(info.swap_cached_kib, 256);
|
||||
assert_eq!(info.swap_used(), 256);
|
||||
assert_eq!(info.swap_used_kib, 256);
|
||||
assert_eq!(info.swap_used_pct(), Some(12.5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_missing_swap_fields() {
|
||||
let info = MemInfo::default();
|
||||
assert_eq!(info.swap_used(), 0);
|
||||
assert_eq!(info.swap_used_pct(), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
//! this module we use `/proc/net/if_inet6` for IPv6 (one line per
|
||||
//! address, easy to parse) and read IPv4 from a simpler source.
|
||||
//!
|
||||
//! On Redox, no equivalent scheme exists yet, so `read()` returns an
|
||||
//! empty `NetInfo` and the render layer shows
|
||||
//! `(no network interfaces detected)`.
|
||||
//! On Redox, network configuration is exposed through `/scheme/netcfg/`.
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
@@ -128,6 +126,7 @@ fn read_ipv6_addrs(iface_name: &str) -> Vec<String> {
|
||||
addrs
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Read IPv4 addresses for a specific interface using libc getifaddrs.
|
||||
fn read_ipv4_addrs(iface_name: &str) -> Vec<String> {
|
||||
let mut addrs = Vec::new();
|
||||
@@ -156,6 +155,41 @@ fn read_ipv4_addrs(iface_name: &str) -> Vec<String> {
|
||||
addrs
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_trimmed(path: &Path) -> Option<String> {
|
||||
fs::read_to_string(path)
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_u64(path: &Path) -> Option<u64> {
|
||||
read_trimmed(path)?.parse::<u64>().ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_ipv4_addrs(iface_name: &str) -> Vec<String> {
|
||||
let path = Path::new("/scheme/netcfg/ifaces")
|
||||
.join(iface_name)
|
||||
.join("addr/list");
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
content
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| {
|
||||
line.split_once('/')
|
||||
.map(|(addr, _)| addr)
|
||||
.unwrap_or(line)
|
||||
.to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_interface(name: &str, path: &Path) -> Option<NetInterface> {
|
||||
Some(NetInterface {
|
||||
name: name.to_string(),
|
||||
@@ -179,9 +213,22 @@ fn read_interface(name: &str, path: &Path) -> Option<NetInterface> {
|
||||
}
|
||||
|
||||
impl NetInfo {
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_NET).is_dir()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
pub fn available() -> bool {
|
||||
Path::new("/scheme/netcfg/ifaces").is_dir()
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "redox")))]
|
||||
pub fn available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn read() -> Self {
|
||||
let Ok(dirs) = fs::read_dir(SYS_NET) else {
|
||||
return Self::default();
|
||||
@@ -199,6 +246,54 @@ impl NetInfo {
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Self { interfaces }
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
pub fn read() -> Self {
|
||||
let root = Path::new("/scheme/netcfg/ifaces");
|
||||
let Ok(dirs) = fs::read_dir(root) else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let mut interfaces = Vec::new();
|
||||
for entry in dirs.flatten() {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mac_address = read_trimmed(&path.join("mac"));
|
||||
let ipv4_addrs = read_ipv4_addrs(name);
|
||||
let operstate = read_trimmed(&path.join("state")).or_else(|| Some("up".to_string()));
|
||||
|
||||
interfaces.push(NetInterface {
|
||||
name: name.to_string(),
|
||||
operstate,
|
||||
speed_mbps: None,
|
||||
mac_address,
|
||||
mtu: None,
|
||||
rx_bytes: read_u64(&path.join("traffic/rx")).unwrap_or(0),
|
||||
tx_bytes: read_u64(&path.join("traffic/tx")).unwrap_or(0),
|
||||
rx_packets: 0,
|
||||
tx_packets: 0,
|
||||
rx_errors: 0,
|
||||
tx_errors: 0,
|
||||
rx_dropped: 0,
|
||||
tx_dropped: 0,
|
||||
ipv6_addrs: Vec::new(),
|
||||
ipv4_addrs,
|
||||
rx_kbps: 0.0,
|
||||
tx_kbps: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
interfaces.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Self { interfaces }
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "redox")))]
|
||||
pub fn read() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
/// Read interfaces and compute R/W throughput (KiB/s) for each
|
||||
/// based on delta of rx_bytes/tx_bytes vs previous read.
|
||||
pub fn read_with_throughput(prev: &NetInfo, dt_secs: f64) -> Self {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
use std::fs;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use libredox::protocol::ProcCall;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const MAX_PROCESSES: usize = 50;
|
||||
@@ -486,6 +488,26 @@ pub struct ProcessInfo {
|
||||
pub sched_policy: String,
|
||||
}
|
||||
|
||||
/// Return true when a process is a kernel thread.
|
||||
pub fn is_kernel_thread(proc: &ProcessInfo) -> bool {
|
||||
proc.ppid == 2 || proc.comm.starts_with('[')
|
||||
}
|
||||
|
||||
pub(crate) fn visible_processes<'a>(
|
||||
processes: &'a [ProcessInfo],
|
||||
process_filter: &str,
|
||||
hide_kernel_threads: bool,
|
||||
) -> Vec<&'a ProcessInfo> {
|
||||
let filter_lower = process_filter.to_lowercase();
|
||||
processes
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
(!hide_kernel_threads || !is_kernel_thread(p))
|
||||
&& (filter_lower.is_empty() || p.comm.to_lowercase().contains(&filter_lower))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl ProcessInfo {
|
||||
pub fn total_cpu_ticks(&self) -> u64 {
|
||||
self.utime.saturating_add(self.stime)
|
||||
@@ -1003,6 +1025,30 @@ impl ProcInfo {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn mock_process(pid: u32, ppid: u32, comm: &str) -> ProcessInfo {
|
||||
ProcessInfo {
|
||||
pid,
|
||||
ppid,
|
||||
comm: comm.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_kernel_thread_matches_ppid_2() {
|
||||
assert!(is_kernel_thread(&mock_process(42, 2, "kworker/0:1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_kernel_thread_matches_bracketed_comm() {
|
||||
assert!(is_kernel_thread(&mock_process(43, 1, "[kthreadd]")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_kernel_thread_ignores_regular_processes() {
|
||||
assert!(!is_kernel_thread(&mock_process(44, 1, "bash")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_memory_below_1kib() {
|
||||
assert_eq!(ProcessInfo::format_memory_kb(500), "500.0 KiB");
|
||||
@@ -2067,10 +2113,24 @@ pub fn set_nice(pid: u32, nice: i32) -> Result<(), String> {
|
||||
Err(std::io::Error::last_os_error().to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let _ = (pid, n);
|
||||
Err("setpriority not supported on this platform".into())
|
||||
let kernel_prio = 20 + n;
|
||||
let fd = libredox::call::open("/scheme/proc", 0, 0).map_err(|e| e.to_string())?;
|
||||
let result = libredox::call::call_wo(
|
||||
fd,
|
||||
&[],
|
||||
syscall::CallFlags::empty(),
|
||||
&[
|
||||
ProcCall::SetProcPriority as u64,
|
||||
pid as u64,
|
||||
kernel_prio as u64,
|
||||
],
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string());
|
||||
let close_result = libredox::call::close(fd).map_err(|e| e.to_string());
|
||||
result.and(close_result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2094,9 +2154,19 @@ pub fn set_affinity(pid: u32, cpus: &[u32]) -> Result<(), String> {
|
||||
Err(std::io::Error::last_os_error().to_string())
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let _ = (pid, cpus);
|
||||
Err("sched_setaffinity not supported on this platform".into())
|
||||
let mut mask = [0u64; 2];
|
||||
for &cpu in cpus {
|
||||
if (cpu as usize) < 128 {
|
||||
let idx = (cpu as usize) / 64;
|
||||
let bit = (cpu as usize) % 64;
|
||||
mask[idx] |= 1u64 << bit;
|
||||
}
|
||||
}
|
||||
let mut bytes = [0u8; 16];
|
||||
bytes[..8].copy_from_slice(&mask[0].to_le_bytes());
|
||||
bytes[8..].copy_from_slice(&mask[1].to_le_bytes());
|
||||
std::fs::write(format!("/proc/{pid}/sched-affinity"), bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,16 @@ pub fn render_header<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
};
|
||||
(label, style)
|
||||
});
|
||||
let swap_text = app
|
||||
.meminfo
|
||||
.swap_used_pct()
|
||||
.map(|p| format!("{p:.1}%"))
|
||||
.unwrap_or_else(|| "n/a".into());
|
||||
let wakeups_text = if app.wakeup_stats.system_per_second > 0.0 {
|
||||
format!("{:.0}/s", app.wakeup_stats.system_per_second)
|
||||
} else {
|
||||
"n/a".into()
|
||||
};
|
||||
let lines = vec![
|
||||
Line::from(vec![
|
||||
"Vendor: ".set_style(theme.label),
|
||||
@@ -229,6 +239,9 @@ pub fn render_header<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
" ".into(),
|
||||
"P-state source: ".set_style(theme.label),
|
||||
app.pss_source.as_str().into(),
|
||||
" ".into(),
|
||||
"Swap: ".set_style(theme.label),
|
||||
swap_text.set_style(theme.value),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"PkgTherm: ".set_style(theme.label),
|
||||
@@ -243,6 +256,9 @@ pub fn render_header<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
} else {
|
||||
"ok".set_style(theme.value_ok)
|
||||
},
|
||||
" ".into(),
|
||||
"Wakeups/s: ".set_style(theme.label),
|
||||
wakeups_text.set_style(theme.value),
|
||||
]),
|
||||
Line::from(vec![
|
||||
"SIMD: ".set_style(theme.label),
|
||||
@@ -552,6 +568,66 @@ pub fn render_system_panel<'a>(app: &'a App, focused: bool) -> Paragraph<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// htop/bottom-style per-CPU scheduler breakdown: cumulative time
|
||||
// deltas rendered as state bars with queue depth / steals overlay.
|
||||
let cpu_mix = crate::sched::time_breakdown_delta(
|
||||
&app.prev_sched_breakdown,
|
||||
&app.sched_stats.per_cpu_time_breakdown,
|
||||
);
|
||||
if !cpu_mix.is_empty() {
|
||||
lines.push(Line::from(vec!["CPU states: ".set_style(theme.label)]));
|
||||
for (idx, mix) in cpu_mix.iter().enumerate().take(app.cpus.len()) {
|
||||
let user = mix.user.clamp(0.0, 100.0);
|
||||
let system = mix.system.clamp(0.0, 100.0);
|
||||
let idle = mix.idle.clamp(0.0, 100.0);
|
||||
let io = mix.iowait.clamp(0.0, 100.0);
|
||||
let active = (100.0 - idle).clamp(0.0, 100.0);
|
||||
let bar = horizontal_bar(active as u8, 16);
|
||||
let qd = app
|
||||
.sched_stats
|
||||
.per_cpu_queue_depth
|
||||
.get(idx)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
let steals = app
|
||||
.sched_stats
|
||||
.per_cpu_steals
|
||||
.get(idx)
|
||||
.copied()
|
||||
.unwrap_or(0);
|
||||
lines.push(Line::from(vec![
|
||||
format!(" CPU{idx:<2} ").set_style(theme.label),
|
||||
format!("[{bar}] ").set_style(theme.value),
|
||||
format!(
|
||||
"u{:>4.1}% s{:>4.1}% i{:>4.1}% io{:>4.1}% ",
|
||||
user, system, idle, io
|
||||
)
|
||||
.set_style(theme.value),
|
||||
format!("qd={qd} steals={steals}").set_style(theme.value_off),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
// C-state residency (powertop-style idle stats). Shown per CPU so
|
||||
// the user can spot asymmetric package deep-sleep behaviour.
|
||||
if !app.cpuidle_stats.per_cpu.is_empty() {
|
||||
lines.push(Line::from(vec!["C-states: ".set_style(theme.label)]));
|
||||
for cpu_id in app.cpuidle_stats.per_cpu.keys().take(app.cpus.len()) {
|
||||
let mut parts: Vec<Span> = vec![format!(" CPU{cpu_id:<2} ").set_style(theme.label)];
|
||||
if let Some(states) = app.cpuidle_stats.per_cpu.get(cpu_id) {
|
||||
for s in states {
|
||||
let pct = (s.percentage * 100.0).clamp(0.0, 100.0) as u8;
|
||||
let bar = horizontal_bar(pct, 4);
|
||||
parts.push(
|
||||
format!("{}[{bar}]{:>4.1}% ", s.name, s.percentage * 100.0)
|
||||
.set_style(theme.value),
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(Line::from(parts));
|
||||
}
|
||||
}
|
||||
|
||||
if app.pkg_power_w.is_some()
|
||||
|| app.pp0_power_w.is_some()
|
||||
|| app.pp1_power_w.is_some()
|
||||
@@ -2174,7 +2250,7 @@ pub fn render_keybar<'a>(app: &'a App) -> Paragraph<'a> {
|
||||
let gov = format!(" g:{}", app.cpufreq.active.as_str());
|
||||
let interval = format!(" [/]:{}ms", app.poll_ms);
|
||||
let tab_hints: &str = if app.current_tab == crate::app::TabId::Process {
|
||||
" ↑↓:sel k:kill N:nice a:affinity l:files C:cmd o:sort i:invert F:filter y:tree Enter:detail"
|
||||
" ↑↓:sel k:kill N:nice a:affinity l:files C:cmd o:sort i:invert F:filter y:tree K:kernel Enter:detail"
|
||||
} else {
|
||||
" ↑↓:cpu p/P:±pstate m/M:min/max t:throttle r:refresh"
|
||||
};
|
||||
@@ -2297,6 +2373,7 @@ INTERACTIVE CONTROLS:
|
||||
[y] toggle process tree view (Process tab)
|
||||
[F] filter processes by name or PID; number alone jumps to PID (Process tab)
|
||||
[h] toggle narrow process view (Process tab)
|
||||
[K] toggle kernel-thread hiding (Process tab)
|
||||
[o] cycle process sort mode
|
||||
[i] invert sort direction
|
||||
[Space] fold/unfold process subtree (tree mode)
|
||||
|
||||
@@ -14,6 +14,31 @@
|
||||
|
||||
use std::fs;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CpuTimeBreakdown {
|
||||
pub user: u64,
|
||||
pub nice: u64,
|
||||
pub system: u64,
|
||||
pub idle: u64,
|
||||
pub iowait: u64,
|
||||
pub irq: u64,
|
||||
pub softirq: u64,
|
||||
pub steal: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct CpuUsagePct {
|
||||
pub total: f64,
|
||||
pub user: f64,
|
||||
pub nice: f64,
|
||||
pub system: f64,
|
||||
pub idle: f64,
|
||||
pub iowait: f64,
|
||||
pub irq: f64,
|
||||
pub softirq: f64,
|
||||
pub steal: f64,
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -29,6 +54,7 @@ pub struct SchedStats {
|
||||
pub per_cpu_switches: Vec<u64>,
|
||||
pub per_cpu_steals: Vec<u64>,
|
||||
pub per_cpu_queue_depth: Vec<u64>,
|
||||
pub per_cpu_time_breakdown: Vec<CpuTimeBreakdown>,
|
||||
}
|
||||
|
||||
impl SchedStats {
|
||||
@@ -108,6 +134,25 @@ impl SchedStats {
|
||||
let mut stats = Self::default();
|
||||
for line in data.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(rest) = trimmed.strip_prefix("cpu") {
|
||||
let mut parts = rest.split_whitespace();
|
||||
let cpu_id = parts.next();
|
||||
if cpu_id.is_some_and(|s| s.chars().all(|c| c.is_ascii_digit())) {
|
||||
let values: Vec<u64> = parts.map(|s| s.parse().unwrap_or(0)).collect();
|
||||
if values.len() >= 8 {
|
||||
stats.per_cpu_time_breakdown.push(CpuTimeBreakdown {
|
||||
user: values[0],
|
||||
nice: values[1],
|
||||
system: values[2],
|
||||
idle: values[3],
|
||||
iowait: values[4],
|
||||
irq: values[5],
|
||||
softirq: values[6],
|
||||
steal: values[7],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// "ctxt <number>" — context switches since boot.
|
||||
if let Some(rest) = trimmed.strip_prefix("ctxt ") {
|
||||
stats.context_switches = rest.trim().parse().ok();
|
||||
@@ -141,6 +186,56 @@ impl SchedStats {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn breakdown_delta(prev: &[CpuTimeBreakdown], next: &[CpuTimeBreakdown]) -> Vec<CpuUsagePct> {
|
||||
prev.iter()
|
||||
.zip(next.iter())
|
||||
.map(|(prev, next)| {
|
||||
let user = next.user.saturating_sub(prev.user);
|
||||
let nice = next.nice.saturating_sub(prev.nice);
|
||||
let system = next.system.saturating_sub(prev.system);
|
||||
let idle = next.idle.saturating_sub(prev.idle);
|
||||
let iowait = next.iowait.saturating_sub(prev.iowait);
|
||||
let irq = next.irq.saturating_sub(prev.irq);
|
||||
let softirq = next.softirq.saturating_sub(prev.softirq);
|
||||
let steal = next.steal.saturating_sub(prev.steal);
|
||||
let total = user + nice + system + idle + iowait + irq + softirq + steal;
|
||||
let pct = |v: u64| {
|
||||
if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(v as f64) * 100.0 / (total as f64)
|
||||
}
|
||||
};
|
||||
CpuUsagePct {
|
||||
total: if total == 0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0 - pct(idle) - pct(iowait)
|
||||
},
|
||||
user: pct(user),
|
||||
nice: pct(nice),
|
||||
system: pct(system),
|
||||
idle: pct(idle),
|
||||
iowait: pct(iowait),
|
||||
irq: pct(irq),
|
||||
softirq: pct(softirq),
|
||||
steal: pct(steal),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Convert cumulative scheduler time counters into per-CPU percentages.
|
||||
/// If the previous sample is missing or shorter than `next`, the missing
|
||||
/// entries are skipped and the returned vector is truncated to the
|
||||
/// shared prefix, mirroring the existing process of `zip`-based deltas.
|
||||
pub fn time_breakdown_delta(
|
||||
prev: &[CpuTimeBreakdown],
|
||||
next: &[CpuTimeBreakdown],
|
||||
) -> Vec<CpuUsagePct> {
|
||||
breakdown_delta(prev, next)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -227,4 +322,85 @@ total switches 3000 steals 7
|
||||
assert_eq!(stats.per_cpu_steals, vec![5, 2]);
|
||||
assert_eq!(stats.per_cpu_queue_depth, vec![3, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_linux_cpu_breakdowns() {
|
||||
let data = "\
|
||||
cpu 100 200 300 400 500 600 700 800
|
||||
cpu0 10 20 30 40 50 60 70 80
|
||||
cpu1 1 2 3 4 5 6 7 8
|
||||
ctxt 1
|
||||
";
|
||||
let stats = SchedStats::parse_linux(data);
|
||||
assert_eq!(stats.per_cpu_time_breakdown.len(), 2);
|
||||
assert_eq!(stats.per_cpu_time_breakdown[0].user, 10);
|
||||
assert_eq!(stats.per_cpu_time_breakdown[0].steal, 80);
|
||||
assert_eq!(stats.per_cpu_time_breakdown[1].nice, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn breakdown_delta_calculates_percentages() {
|
||||
let prev = vec![CpuTimeBreakdown {
|
||||
user: 10,
|
||||
nice: 0,
|
||||
system: 10,
|
||||
idle: 80,
|
||||
iowait: 0,
|
||||
irq: 0,
|
||||
softirq: 0,
|
||||
steal: 0,
|
||||
}];
|
||||
let next = vec![CpuTimeBreakdown {
|
||||
user: 30,
|
||||
nice: 10,
|
||||
system: 20,
|
||||
idle: 100,
|
||||
iowait: 5,
|
||||
irq: 5,
|
||||
softirq: 10,
|
||||
steal: 0,
|
||||
}];
|
||||
let delta = breakdown_delta(&prev, &next);
|
||||
assert_eq!(delta.len(), 1);
|
||||
let d = &delta[0];
|
||||
assert!((d.total - 68.75).abs() < 1e-9);
|
||||
assert!((d.user - 25.0).abs() < 1e-9);
|
||||
assert!((d.nice - 12.5).abs() < 1e-9);
|
||||
assert!((d.system - 12.5).abs() < 1e-9);
|
||||
assert!((d.idle - 25.0).abs() < 1e-9);
|
||||
assert!((d.iowait - 6.25).abs() < 1e-9);
|
||||
assert!((d.irq - 6.25).abs() < 1e-9);
|
||||
assert!((d.softirq - 12.5).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_breakdown_delta_tracks_cpu_state_mix() {
|
||||
let prev = vec![CpuTimeBreakdown {
|
||||
user: 100,
|
||||
nice: 10,
|
||||
system: 30,
|
||||
idle: 200,
|
||||
iowait: 20,
|
||||
irq: 4,
|
||||
softirq: 6,
|
||||
steal: 1,
|
||||
}];
|
||||
let next = vec![CpuTimeBreakdown {
|
||||
user: 130,
|
||||
nice: 10,
|
||||
system: 40,
|
||||
idle: 230,
|
||||
iowait: 20,
|
||||
irq: 8,
|
||||
softirq: 10,
|
||||
steal: 1,
|
||||
}];
|
||||
let pct = time_breakdown_delta(&prev, &next);
|
||||
assert_eq!(pct.len(), 1);
|
||||
assert!((pct[0].user - 38.461_538).abs() < 0.01);
|
||||
assert!((pct[0].system - 12.820_512).abs() < 0.01);
|
||||
assert!((pct[0].idle - 38.461_538).abs() < 0.01);
|
||||
assert!((pct[0].irq - 5.128_205).abs() < 0.01);
|
||||
assert!((pct[0].softirq - 5.128_205).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,40 @@ fn read_sysfs_i64(path: &Path) -> Option<i64> {
|
||||
read_sysfs(path)?.parse::<i64>().ok()
|
||||
}
|
||||
|
||||
pub fn read_msr(cpu: u32, msr: u32) -> Option<u64> {
|
||||
let path = format!("/scheme/sys/msr/{}/0x{:x}", cpu, msr);
|
||||
let mut data = [0u8; 8];
|
||||
let mut file = fs::File::open(path).ok()?;
|
||||
use std::io::Read;
|
||||
file.read_exact(&mut data).ok()?;
|
||||
Some(u64::from_le_bytes(data))
|
||||
}
|
||||
|
||||
pub fn read_cpu_temp_msr(cpu: u32) -> Option<f64> {
|
||||
const IA32_THERM_STATUS: u32 = 0x19c;
|
||||
const IA32_TEMPERATURE_TARGET: u32 = 0x1a2;
|
||||
|
||||
let therm_status = read_msr(cpu, IA32_THERM_STATUS)?;
|
||||
if therm_status & (1 << 31) == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let digital_readout = ((therm_status >> 16) & 0x7f) as f64;
|
||||
let tjmax = read_msr(cpu, IA32_TEMPERATURE_TARGET)
|
||||
.map(|raw| ((raw >> 16) & 0xff) as f64)
|
||||
.unwrap_or(100.0);
|
||||
Some(tjmax - digital_readout)
|
||||
}
|
||||
|
||||
fn read_first_cpu_temp_msr() -> Option<f64> {
|
||||
for cpu in 0..256 {
|
||||
if let Some(temp) = read_cpu_temp_msr(cpu) {
|
||||
return Some(temp);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Read all `*_input` files in the chip directory, grouped by prefix.
|
||||
fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
|
||||
let entries = match fs::read_dir(chip_dir) {
|
||||
@@ -153,10 +187,24 @@ fn read_chip_readings(chip_dir: &Path) -> Vec<SensorReading> {
|
||||
|
||||
impl SensorInfo {
|
||||
pub fn available() -> bool {
|
||||
Path::new(SYS_HWMON).is_dir()
|
||||
Path::new(SYS_HWMON).is_dir() || read_first_cpu_temp_msr().is_some()
|
||||
}
|
||||
pub fn read() -> Self {
|
||||
let Ok(dirs) = fs::read_dir(SYS_HWMON) else {
|
||||
if let Some(temp_c) = read_first_cpu_temp_msr() {
|
||||
return Self {
|
||||
chips: vec![HwmonChip {
|
||||
name: "msr".to_string(),
|
||||
path: PathBuf::from("/scheme/sys/msr"),
|
||||
readings: vec![SensorReading {
|
||||
kind: SensorKind::Temp,
|
||||
label: Some("Package".to_string()),
|
||||
raw_value: (temp_c * 1000.0).round() as i64,
|
||||
display_value: format!("{:.1} °C", temp_c),
|
||||
}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
return Self::default();
|
||||
};
|
||||
let mut chips = Vec::new();
|
||||
@@ -230,6 +278,13 @@ impl SensorInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
"msr" => {
|
||||
for r in &chip.readings {
|
||||
if r.kind == SensorKind::Temp && r.label.as_deref() == Some("Package") {
|
||||
return Some((r.raw_value / 1000) as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -357,4 +412,20 @@ mod tests {
|
||||
});
|
||||
assert_eq!(info.pkg_temp_c(0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pkg_temp_c_from_msr_package_sensor() {
|
||||
let mut info = SensorInfo::default();
|
||||
info.chips.push(HwmonChip {
|
||||
name: "msr".to_string(),
|
||||
path: PathBuf::from("/scheme/sys/msr"),
|
||||
readings: vec![SensorReading {
|
||||
kind: SensorKind::Temp,
|
||||
label: Some("Package".to_string()),
|
||||
raw_value: 47000,
|
||||
display_value: "47.0 °C".to_string(),
|
||||
}],
|
||||
});
|
||||
assert_eq!(info.pkg_temp_c(0), Some(47));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ pub struct SessionState {
|
||||
/// Last refresh interval in milliseconds (0 = not set, use config/default).
|
||||
#[serde(default)]
|
||||
pub refresh_ms: u64,
|
||||
/// Hide kernel threads in the Process tab.
|
||||
#[serde(default)]
|
||||
pub hide_kernel_threads: bool,
|
||||
}
|
||||
|
||||
impl Default for SessionState {
|
||||
@@ -65,6 +68,7 @@ impl Default for SessionState {
|
||||
show_full_cmdline: false,
|
||||
process_narrow: false,
|
||||
refresh_ms: 0,
|
||||
hide_kernel_threads: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,6 +182,7 @@ mod tests {
|
||||
show_full_cmdline: true,
|
||||
process_narrow: true,
|
||||
refresh_ms: 1_500,
|
||||
hide_kernel_threads: false,
|
||||
};
|
||||
let serialized = toml::to_string(&s).unwrap();
|
||||
let parsed: SessionState = toml::from_str(&serialized).unwrap();
|
||||
@@ -239,6 +244,7 @@ mod tests {
|
||||
show_full_cmdline: false,
|
||||
process_narrow: false,
|
||||
refresh_ms: 2_000,
|
||||
hide_kernel_threads: false,
|
||||
};
|
||||
let serialized = toml::to_string(&s).unwrap();
|
||||
// Mimic the temp+rename flow with a different name
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct WakeupStats {
|
||||
pub system_total: u64,
|
||||
pub system_per_second: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ProcessWakeupRate {
|
||||
pub pid: u32,
|
||||
pub wakeups: u64,
|
||||
pub wakeups_per_second: f64,
|
||||
}
|
||||
|
||||
fn dt_seconds(dt: Duration) -> f64 {
|
||||
let secs = dt.as_secs_f64();
|
||||
if secs > 0.0 { secs } else { 1.0 }
|
||||
}
|
||||
|
||||
fn parse_intr_total(data: &str) -> Option<u64> {
|
||||
data.lines().find_map(|line| {
|
||||
let trimmed = line.trim_start();
|
||||
let rest = trimmed.strip_prefix("intr ")?;
|
||||
rest.split_whitespace().next()?.parse::<u64>().ok()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read_system_wakeups(prev_total: Option<u64>, dt: Duration) -> Option<WakeupStats> {
|
||||
let data = fs::read_to_string("/proc/stat").ok()?;
|
||||
let total = parse_intr_total(&data)?;
|
||||
|
||||
let delta = prev_total.map_or(0, |prev| total.saturating_sub(prev));
|
||||
Some(WakeupStats {
|
||||
system_total: total,
|
||||
system_per_second: delta as f64 / dt_seconds(dt),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_wakeups_per_sec(
|
||||
prev: &[(u32, u64)],
|
||||
next: &[(u32, u64)],
|
||||
dt: Duration,
|
||||
) -> Vec<ProcessWakeupRate> {
|
||||
let dt = dt_seconds(dt);
|
||||
next.iter()
|
||||
.map(|&(pid, next_ctx)| {
|
||||
let prev_ctx = prev
|
||||
.iter()
|
||||
.find(|&&(p, _)| p == pid)
|
||||
.map(|&(_, c)| c)
|
||||
.unwrap_or(0);
|
||||
let wakeups = next_ctx.saturating_sub(prev_ctx);
|
||||
ProcessWakeupRate {
|
||||
pid,
|
||||
wakeups,
|
||||
wakeups_per_second: wakeups as f64 / dt,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn read_process_wakeups(pid: u32) -> Option<u64> {
|
||||
let path = format!("/proc/{pid}/status");
|
||||
let data = fs::read_to_string(path).ok()?;
|
||||
let mut voluntary = None;
|
||||
let mut nonvoluntary = None;
|
||||
|
||||
for line in data.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if let Some(rest) = trimmed.strip_prefix("voluntary_ctxt_switches:") {
|
||||
voluntary = rest.trim().parse::<u64>().ok();
|
||||
} else if let Some(rest) = trimmed.strip_prefix("nonvoluntary_ctxt_switches:") {
|
||||
nonvoluntary = rest.trim().parse::<u64>().ok();
|
||||
}
|
||||
}
|
||||
|
||||
Some(voluntary.unwrap_or(0) + nonvoluntary.unwrap_or(0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn system_delta_computation() {
|
||||
assert_eq!(
|
||||
parse_intr_total("cpu 1 2 3\nintr 12345 1 2 3\n"),
|
||||
Some(12345)
|
||||
);
|
||||
let stats = read_system_wakeups_from_str(
|
||||
"intr 12345 1 2 3\n",
|
||||
Some(12000),
|
||||
Duration::from_millis(500),
|
||||
);
|
||||
assert_eq!(stats.system_total, 12345);
|
||||
assert_eq!(stats.system_per_second, 690.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dt_fallback_uses_one_second() {
|
||||
let rates = process_wakeups_per_sec(&[(1, 10)], &[(1, 13)], Duration::ZERO);
|
||||
assert_eq!(rates[0].wakeups, 3);
|
||||
assert_eq!(rates[0].wakeups_per_second, 3.0);
|
||||
}
|
||||
|
||||
fn read_system_wakeups_from_str(
|
||||
data: &str,
|
||||
prev_total: Option<u64>,
|
||||
dt: Duration,
|
||||
) -> WakeupStats {
|
||||
let total = parse_intr_total(data).unwrap();
|
||||
let delta = prev_total.map_or(0, |prev| total.saturating_sub(prev));
|
||||
WakeupStats {
|
||||
system_total: total,
|
||||
system_per_second: delta as f64 / dt_seconds(dt),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user