use std::time::Instant;
#[derive(Copy, Clone, Default, Debug)]
pub struct CpuTimes {
pub user: u64,
pub nice: u64,
pub system: u64,
pub idle: u64,
pub iowait: u64,
pub irq: u64,
pub softirq: u64,
pub steal: u64,
pub guest: u64,
pub guest_nice: u64,
}
impl CpuTimes {
pub fn total(&self) -> u64 {
self.user
+ self.nice
+ self.system
+ self.idle
+ self.iowait
+ self.irq
+ self.softirq
+ self.steal
+ self.guest
+ self.guest_nice
}
pub fn busy(&self) -> u64 {
self.total().saturating_sub(self.idle)
}
pub fn delta(&self, prev: &CpuTimes) -> CpuTimes {
CpuTimes {
user: self.user.saturating_sub(prev.user),
nice: self.nice.saturating_sub(prev.nice),
system: self.system.saturating_sub(prev.system),
idle: self.idle.saturating_sub(prev.idle),
iowait: self.iowait.saturating_sub(prev.iowait),
irq: self.irq.saturating_sub(prev.irq),
softirq: self.softirq.saturating_sub(prev.softirq),
steal: self.steal.saturating_sub(prev.steal),
guest: self.guest.saturating_sub(prev.guest),
guest_nice: self.guest_nice.saturating_sub(prev.guest_nice),
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct MemoryClass {
pub label: &'static str,
pub bytes: u64,
pub counts_as_used: bool,
pub counts_as_cache: bool,
pub counts_as_total: bool,
pub color_slot: u8,
}
#[derive(Clone, Default, Debug)]
pub struct MemInfo {
pub total: u64,
pub available: u64,
pub used: u64,
pub free: u64,
pub classes: Vec<MemoryClass>,
pub buffers: u64,
pub cached: u64,
pub sreclaimable: u64,
pub shmem: u64,
pub wired: u64,
pub compressed: u64,
pub active: u64,
pub inactive: u64,
pub purgeable: u64,
pub speculative: u64,
pub zswap: u64,
pub zswapped: u64,
pub zswap_max: u64,
pub huge_pages_total: u64,
pub huge_pages_free: u64,
pub huge_pages_rsvd: u64,
pub huge_pages_surplus: u64,
pub huge_page_size: u64,
pub laundry: u64,
pub file_cache: u64,
pub managed_pages: u64,
pub internal: u64,
}
impl MemInfo {
pub fn used_from_classes(&self) -> u64 {
self.classes
.iter()
.filter(|c| c.counts_as_used)
.map(|c| c.bytes)
.sum()
}
pub fn cache_from_classes(&self) -> u64 {
self.classes
.iter()
.filter(|c| c.counts_as_cache)
.map(|c| c.bytes)
.sum()
}
pub fn finish_linux(&mut self) {
let used_diff = self.free + self.cached + self.sreclaimable + self.buffers;
self.used = if self.total >= used_diff {
self.total - used_diff
} else {
self.total.saturating_sub(self.free)
};
self.cached = self.cached + self.sreclaimable - self.shmem;
self.available = if self.available != 0 {
self.available.min(self.total)
} else {
self.free
};
self.classes = vec![
MemoryClass {
label: "used",
bytes: self.used,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 0,
},
MemoryClass {
label: "buffers",
bytes: self.buffers,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 1,
},
MemoryClass {
label: "cache",
bytes: self.cached,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 2,
},
MemoryClass {
label: "free",
bytes: self.free,
counts_as_used: false,
counts_as_cache: false,
counts_as_total: true,
color_slot: 5,
},
];
}
pub fn finish_darwin(&mut self) {
let wired = self.wired.min(self.total);
let compressed = self.compressed.min(self.total - wired);
let internal = self.internal.min(self.total - wired - compressed);
let purgeable = self.purgeable.min(internal);
let active = internal - purgeable;
self.used = wired + compressed + active;
let free = self.free.min(self.total - self.used - purgeable);
let cache = self.total - self.used - purgeable - free;
self.available = self.total - self.used;
self.classes = vec![
MemoryClass {
label: "wired",
bytes: wired,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 0,
},
MemoryClass {
label: "active",
bytes: active,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 1,
},
MemoryClass {
label: "compressed",
bytes: compressed,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 2,
},
MemoryClass {
label: "purgeable",
bytes: purgeable,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 4,
},
MemoryClass {
label: "cache",
bytes: cache,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 3,
},
MemoryClass {
label: "free",
bytes: free,
counts_as_used: false,
counts_as_cache: false,
counts_as_total: true,
color_slot: 5,
},
];
}
pub fn finish_freebsd(&mut self) {
let wired = self.wired.min(self.total);
let active = self.active.min(self.total - wired);
let laundry = self.laundry.min(self.total - wired - active);
let used = wired + active + laundry;
let inactive = self.inactive.min(self.total - used);
let file_cache = self.file_cache.min(self.total - used - inactive);
let free = self.free.min(self.total - used - inactive - file_cache);
self.used = used;
self.cached = inactive + file_cache;
self.available = (free + self.cached).min(self.total);
self.classes = vec![
MemoryClass {
label: "wired",
bytes: wired,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 0,
},
MemoryClass {
label: "active",
bytes: active,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 1,
},
MemoryClass {
label: "laundry",
bytes: laundry,
counts_as_used: true,
counts_as_cache: false,
counts_as_total: true,
color_slot: 2,
},
MemoryClass {
label: "inactive",
bytes: inactive,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 3,
},
MemoryClass {
label: "cache",
bytes: file_cache,
counts_as_used: false,
counts_as_cache: true,
counts_as_total: true,
color_slot: 4,
},
MemoryClass {
label: "free",
bytes: free,
counts_as_used: false,
counts_as_cache: false,
counts_as_total: true,
color_slot: 5,
},
];
}
pub fn huge_used_bytes(&self) -> u64 {
let used_pages = self.huge_pages_total.saturating_sub(self.huge_pages_free);
used_pages.saturating_mul(self.huge_page_size)
}
pub fn huge_total_bytes(&self) -> u64 {
self.huge_pages_total.saturating_mul(self.huge_page_size)
}
pub fn huge_pages_configured(&self) -> bool {
self.huge_pages_total > 0 && self.huge_page_size > 0
}
pub fn compute_used(&mut self) {
self.finish_linux();
}
pub fn is_consistent(&self) -> bool {
self.used <= self.total && self.available <= self.total
}
pub fn is_exactly_partitioned(&self) -> bool {
self.used <= self.total && self.used + self.available == self.total
}
}
#[derive(Copy, Clone, Default, Debug)]
pub struct TaskStats {
pub total: usize,
pub running: usize,
pub threads: usize,
}
#[derive(Copy, Clone, Default, Debug)]
pub struct SwapInfo {
pub total: u64,
pub free: u64,
pub used: u64,
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct NetCounters {
pub rx_bytes: u64,
pub tx_bytes: u64,
}
impl NetCounters {
pub fn delta(&self, prev: &NetCounters) -> NetCounters {
NetCounters {
rx_bytes: self.rx_bytes.saturating_sub(prev.rx_bytes),
tx_bytes: self.tx_bytes.saturating_sub(prev.tx_bytes),
}
}
pub fn per_sec(&self, prev: &NetCounters, secs: f64) -> Rate {
let d = self.delta(prev);
Rate::from_bytes(d.rx_bytes, d.tx_bytes, secs)
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct DiskCounters {
pub read_bytes: u64,
pub write_bytes: u64,
}
impl DiskCounters {
pub fn delta(&self, prev: &DiskCounters) -> DiskCounters {
DiskCounters {
read_bytes: self.read_bytes.saturating_sub(prev.read_bytes),
write_bytes: self.write_bytes.saturating_sub(prev.write_bytes),
}
}
pub fn per_sec(&self, prev: &DiskCounters, secs: f64) -> Rate {
let d = self.delta(prev);
Rate::from_bytes(d.read_bytes, d.write_bytes, secs)
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct Rate {
pub inbound: f64,
pub outbound: f64,
}
impl Rate {
fn from_bytes(inbound: u64, outbound: u64, secs: f64) -> Self {
if secs <= 0.0 || secs.is_nan() {
return Self::default();
}
Self {
inbound: inbound as f64 / secs,
outbound: outbound as f64 / secs,
}
}
pub fn total(&self) -> f64 {
self.inbound + self.outbound
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct PsiLine {
pub avg10: f32,
pub avg60: f32,
pub avg300: f32,
pub total_us: u64,
}
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct PsiResource {
pub present: bool,
pub some: PsiLine,
pub full: PsiLine,
pub has_full: bool,
}
#[derive(Copy, Clone, Default, Debug, PartialEq)]
pub struct Psi {
pub cpu: PsiResource,
pub memory: PsiResource,
pub io: PsiResource,
pub irq: PsiResource,
}
impl Psi {
pub fn any_present(&self) -> bool {
self.cpu.present || self.memory.present || self.io.present || self.irq.present
}
pub fn rows(&self) -> [(&'static str, PsiResource); 4] {
[
("CPU", self.cpu),
("MEM", self.memory),
("IO", self.io),
("IRQ", self.irq),
]
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct ZramDevice {
pub name: String,
pub orig_data_size: u64,
pub compr_data_size: u64,
pub mem_used_total: u64,
}
impl ZramDevice {
pub fn ratio(&self) -> f32 {
if self.compr_data_size == 0 {
1.0
} else {
self.orig_data_size as f32 / self.compr_data_size as f32
}
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct ZramInfo {
pub devices: Vec<ZramDevice>,
}
impl ZramInfo {
pub fn present(&self) -> bool {
!self.devices.is_empty()
}
pub fn orig_total(&self) -> u64 {
self.devices.iter().map(|d| d.orig_data_size).sum()
}
pub fn compr_total(&self) -> u64 {
self.devices.iter().map(|d| d.compr_data_size).sum()
}
pub fn mem_total(&self) -> u64 {
self.devices.iter().map(|d| d.mem_used_total).sum()
}
pub fn ratio(&self) -> f32 {
let compr = self.compr_total();
if compr == 0 {
1.0
} else {
self.orig_total() as f32 / compr as f32
}
}
}
#[derive(Clone, Debug)]
pub struct SystemSnapshot {
pub at: Instant,
pub cpu: Vec<CpuTimes>,
pub cpu_delta: Vec<CpuTimes>,
pub mem: MemInfo,
pub swap: SwapInfo,
pub load: [f64; 3],
pub uptime: u64,
pub clk_tck: u64,
pub page_size: u64,
pub net: NetCounters,
pub net_rate: Rate,
pub disk: DiskCounters,
pub disk_rate: Rate,
pub tasks: TaskStats,
pub psi: Option<Psi>,
pub zram: ZramInfo,
}
impl Default for SystemSnapshot {
fn default() -> Self {
Self {
at: Instant::now(),
cpu: Vec::new(),
cpu_delta: Vec::new(),
mem: MemInfo::default(),
swap: SwapInfo::default(),
load: [0.0; 3],
uptime: 0,
clk_tck: 100,
page_size: 4096,
net: NetCounters::default(),
net_rate: Rate::default(),
disk: DiskCounters::default(),
disk_rate: Rate::default(),
tasks: TaskStats::default(),
psi: None,
zram: ZramInfo::default(),
}
}
}
impl SystemSnapshot {
pub fn cpu_count(&self) -> usize {
self.cpu.len().saturating_sub(1)
}
pub fn cpu_usage(&self) -> f32 {
let d = match self.cpu_delta.first() {
Some(d) => d,
None => return f32::NAN,
};
let total = d.total();
if total == 0 {
return 0.0;
}
d.busy() as f32 / total as f32
}
pub fn mem_usage(&self) -> f32 {
if self.mem.total == 0 {
return f32::NAN;
}
self.mem.used as f32 / self.mem.total as f32
}
pub fn swap_usage(&self) -> f32 {
if self.swap.total == 0 {
return 0.0;
}
self.swap.used as f32 / self.swap.total as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delta_is_tick_wise() {
let a = CpuTimes {
user: 10,
idle: 90,
..Default::default()
};
let b = CpuTimes {
user: 30,
idle: 170,
..Default::default()
};
let d = b.delta(&a);
assert_eq!(d.user, 20);
assert_eq!(d.idle, 80);
assert_eq!(d.total(), 100);
assert_eq!(d.busy(), 20);
}
#[test]
fn delta_never_wraps() {
let a = CpuTimes {
user: 30,
..Default::default()
};
let b = CpuTimes {
user: 10,
..Default::default()
};
assert_eq!(b.delta(&a).user, 0);
}
#[test]
fn linux_accounting_matches_free() {
let mut mem = MemInfo {
total: 1000,
free: 100,
buffers: 50,
cached: 200,
sreclaimable: 100,
shmem: 20,
available: 400,
..Default::default()
};
mem.finish_linux();
assert_eq!(mem.used, 1000 - 100 - 200 - 100 - 50);
assert_eq!(mem.cached, 280);
assert!(mem.is_consistent(), "{mem:?}");
}
#[test]
fn linux_accounting_keeps_the_invariant_on_bad_input() {
let mut mem = MemInfo {
total: 100,
available: 500,
free: 10,
..Default::default()
};
mem.finish_linux();
assert!(mem.is_consistent(), "{mem:?}");
assert!(mem.used <= mem.total);
assert!(mem.available <= mem.total);
}
#[test]
fn darwin_used_matches_htop_341() {
const PAGE: u64 = 16 * 1024;
let mut mem = MemInfo {
total: 16 * 1024 * 1024 * 1024,
internal: 301_497 * PAGE,
active: 219_954 * PAGE,
inactive: 216_885 * PAGE,
speculative: 2_022 * PAGE,
wired: 136_897 * PAGE,
compressed: 429_207 * PAGE,
purgeable: 0,
free: 4_007 * PAGE,
..Default::default()
};
mem.finish_darwin();
let htop_used_excl_compressor = 301_497 * PAGE + 136_897 * PAGE;
let htop_compressed = 429_207 * PAGE;
assert_eq!(
mem.used,
htop_used_excl_compressor + htop_compressed,
"gander's used must equal htop's USED + COMPRESSED"
);
assert_eq!(mem.used_from_classes(), mem.used);
assert!(mem.is_exactly_partitioned());
assert_eq!(
mem.used - (mem.internal - mem.purgeable) - mem.wired,
htop_compressed,
"the remainder after active+wired is the compressor"
);
assert!(mem.used < mem.total);
let labels: Vec<&str> = mem.classes.iter().map(|c| c.label).collect();
assert_eq!(
labels,
vec![
"wired",
"active",
"compressed",
"purgeable",
"cache",
"free"
]
);
}
#[test]
fn darwin_free_segment_is_not_the_compressor() {
const PAGE: u64 = 16 * 1024;
let mut mem = MemInfo {
total: 16 * 1024 * 1024 * 1024,
internal: 301_497 * PAGE,
wired: 136_897 * PAGE,
compressed: 429_207 * PAGE,
purgeable: 0,
free: 4_007 * PAGE,
..Default::default()
};
mem.finish_darwin();
let free_class = mem
.classes
.iter()
.find(|c| c.label == "free")
.expect("a free class is always present");
assert!(
free_class.bytes < 1024 * 1024 * 1024,
"free segment is {} MiB, which is the compressor leaking in",
free_class.bytes / (1024 * 1024)
);
}
#[test]
fn darwin_bar_segments_partition_total() {
for (internal, wired, compressed, purgeable) in [
(0u64, 0u64, 0u64, 0u64),
(500, 300, 200, 0),
(500, 300, 200, 700), (400, 300, 300, 0), (4000, 3000, 3000, 0), ] {
let mut mem = MemInfo {
total: 1000,
internal,
wired,
compressed,
purgeable,
..Default::default()
};
mem.finish_darwin();
let classes: u64 = mem
.classes
.iter()
.filter(|c| c.counts_as_total)
.map(|c| c.bytes)
.sum();
assert_eq!(classes, mem.total, "{mem:?}");
assert!(mem.is_exactly_partitioned(), "{mem:?}");
assert!(mem.used <= mem.total, "{mem:?}");
}
}
#[test]
fn zero_delta_is_not_nan() {
let snap = SystemSnapshot {
cpu_delta: vec![CpuTimes::default()],
..Default::default()
};
assert_eq!(snap.cpu_usage(), 0.0);
}
}