use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::row::{CpuTicks, Row, RowFlags};
use crate::system::{CpuTimes, DiskCounters, MemInfo, NetCounters, SwapInfo, SystemSnapshot};
use crate::table::Cache;
use crate::Pid;
#[cfg(target_os = "linux")]
use super::delayacct::{DelayAcct, DelayStats};
use super::scan;
use super::LinuxPlatform;
const PAR_THRESHOLD: usize = 512;
const CACHE_SLACK: usize = 4;
#[derive(Copy, Clone)]
struct PrevRow {
starttime: u64,
proc_ticks: u64,
}
pub struct Sampler {
root: PathBuf,
clk_tck: u64,
page_size: u64,
cache: Cache,
prev_cpu: Vec<CpuTimes>,
prev_rows: HashMap<Pid, PrevRow>,
prev_net: Option<(NetCounters, std::time::Instant)>,
prev_disk: Option<(DiskCounters, std::time::Instant)>,
pub use_rayon: bool,
show_threads: bool,
read_smaps: bool,
read_gpu: bool,
#[cfg(target_os = "linux")]
read_delayacct: bool,
#[cfg(target_os = "linux")]
delayacct: Option<DelayAcct>,
#[cfg(target_os = "linux")]
prev_delay: HashMap<Pid, (u64, DelayStats)>,
}
impl Default for Sampler {
fn default() -> Self {
Self::new(Path::new("/proc"))
}
}
impl Sampler {
pub fn new(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
clk_tck: LinuxPlatform::clk_tck(),
page_size: LinuxPlatform::page_size(),
cache: Cache::new(),
prev_cpu: Vec::new(),
prev_rows: HashMap::new(),
prev_net: None,
prev_disk: None,
use_rayon: true,
show_threads: false,
read_smaps: false,
read_gpu: false,
#[cfg(target_os = "linux")]
read_delayacct: false,
#[cfg(target_os = "linux")]
delayacct: None,
#[cfg(target_os = "linux")]
prev_delay: HashMap::new(),
}
}
#[cfg(target_os = "linux")]
fn delay_for(&mut self, pid: Pid, starttime: u64) -> Option<DelayStats> {
let acct = match self.delayacct {
Some(ref mut a) => a,
None => self.delayacct.insert(DelayAcct::open()?),
};
let stats = acct.read(pid)?;
if let Some(&(prev_start, _)) = self.prev_delay.get(&pid) {
if prev_start != starttime {
self.prev_delay.remove(&pid);
}
}
self.prev_delay.insert(pid, (starttime, stats));
Some(stats)
}
pub fn clk_tck(&self) -> u64 {
self.clk_tck
}
fn sample_inner(&mut self) -> (SystemSnapshot, Vec<Row>) {
let at = std::time::Instant::now();
let cpu = scan::read_proc_stat(&self.root);
let cpu_delta: Vec<CpuTimes> = if self.prev_cpu.len() == cpu.len() && !cpu.is_empty() {
cpu.iter()
.zip(&self.prev_cpu)
.map(|(n, p)| n.delta(p))
.collect()
} else {
vec![CpuTimes::default(); cpu.len()]
};
let mut mem = MemInfo::default();
let mut swap = SwapInfo::default();
scan::read_meminfo(&self.root, &mut mem, &mut swap);
let load = scan::read_loadavg(&self.root).unwrap_or([0.0; 3]);
let uptime = scan::read_uptime(&self.root).unwrap_or(0);
let psi = scan::read_psi(&self.root);
let zram = scan::read_zram(&self.root);
if let Some(percent) = scan::read_zswap_max_percent(&self.root) {
mem.zswap_max = mem.total.saturating_mul(percent.min(100)) / 100;
}
let net = scan::read_net_counters(&self.root);
let disk = scan::read_disk_counters(&self.root);
let net_rate = match self.prev_net {
Some((prev, at_prev)) => net.per_sec(&prev, at.duration_since(at_prev).as_secs_f64()),
None => crate::system::Rate::default(),
};
let disk_rate = match self.prev_disk {
Some((prev, at_prev)) => {
disk.per_sec(&prev, at.duration_since(at_prev).as_secs_f64())
}
None => crate::system::Rate::default(),
};
self.prev_net = Some((net, at));
self.prev_disk = Some((disk, at));
let pids = scan::list_pids(&self.root).unwrap_or_default();
let mut rows = self.read_rows(&pids, &cpu_delta, uptime, mem.total);
if self.show_threads {
let threads = self.read_threads(&pids, &cpu_delta);
rows.extend(threads);
}
self.prev_cpu = cpu.clone();
self.cache.retain_live(
&rows
.iter()
.map(|r| (r.pid, r.starttime))
.collect::<Vec<_>>(),
);
if self.cache.len() > rows.len().saturating_mul(CACHE_SLACK) + 64 {
self.cache.clear();
}
let snapshot = SystemSnapshot {
at,
cpu,
cpu_delta,
mem,
swap,
load,
uptime,
clk_tck: self.clk_tck,
page_size: self.page_size,
net,
net_rate,
disk,
disk_rate,
tasks: task_stats(&rows),
psi,
zram,
};
(snapshot, rows)
}
fn read_threads(&mut self, pids: &[Pid], cpu_delta: &[CpuTimes]) -> Vec<Row> {
let root = self.root.clone();
let page_size = self.page_size;
let total_delta = cpu_delta.first().map(|d| d.total()).unwrap_or(0) as f32;
let mut pairs: Vec<(Pid, Pid)> = Vec::new();
for &pid in pids {
let Ok(tids) = scan::list_tids(&root, pid) else {
continue;
};
for tid in tids {
if tid != pid {
pairs.push((pid, tid));
}
}
}
if pairs.is_empty() {
return Vec::new();
}
let read_one = |&(pid, tid): &(Pid, Pid)| {
scan::read_thread_stat(&root, pid, tid).map(|(comm, fields)| (pid, tid, comm, fields))
};
let stats: Vec<(Pid, Pid, crate::row::CommBuf, super::parse::StatFields)> =
if self.use_rayon && pairs.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
pairs.par_iter().filter_map(read_one).collect()
} else {
pairs.iter().filter_map(read_one).collect()
};
let mut out = Vec::with_capacity(stats.len());
for (pid, tid, comm, fields) in stats {
let starttime = fields.starttime;
let mut row = Row::default_for_test(tid);
row.pid = tid;
row.thread_id = Some(tid as u64);
row.tgid = pid;
row.is_thread = true;
row.ppid = pid;
row.comm = comm;
let prev_thread_ticks = match self.prev_rows.get(&tid) {
Some(p) if p.starttime == starttime => p.proc_ticks,
_ => 0,
};
self.prev_rows.insert(
tid,
PrevRow {
starttime,
proc_ticks: fields.cpu_ticks(),
},
);
scan::apply_stat(&mut row, &fields, page_size);
let delta = fields.cpu_ticks().saturating_sub(prev_thread_ticks) as f32;
row.percent_cpu = if total_delta > 0.0 {
delta * 100.0 / total_delta
} else {
0.0
};
let stable = scan::read_identity(&self.root, pid, starttime, &mut self.cache);
row.uid = stable.uid;
row.cmdline = stable.cmdline.clone();
row.exe = stable.exe.clone();
row.cwd = stable.cwd.clone();
row.cgroup = stable.cgroup.clone();
#[cfg(target_os = "linux")]
{
row.container = stable.container.clone();
row.container_runtime = stable.container_runtime.clone();
}
row.flags = row
.flags
.insert(RowFlags::USERLAND_THREAD)
.set(RowFlags::STALE_EXE, stable.exe_deleted);
row.nlwp = 1;
out.push(row);
}
out
}
fn read_rows(
&mut self,
pids: &[Pid],
cpu_delta: &[CpuTimes],
uptime: u64,
mem_total: u64,
) -> Vec<Row> {
let root = &self.root;
let page_size = self.page_size;
let stats: Vec<(Pid, crate::row::CommBuf, super::parse::StatFields)> =
if self.use_rayon && pids.len() >= PAR_THRESHOLD {
use rayon::prelude::*;
pids.par_iter()
.filter_map(|&pid| scan::read_stat(root, pid).map(|(c, f)| (pid, c, f)))
.collect()
} else {
pids.iter()
.filter_map(|&pid| scan::read_stat(root, pid).map(|(c, f)| (pid, c, f)))
.collect()
};
let mut out = Vec::with_capacity(stats.len());
let mut next_prev = HashMap::with_capacity(stats.len());
let total_delta = cpu_delta.first().map(|d| d.total()).unwrap_or(0) as f32;
for (pid, comm, fields) in stats {
let starttime = fields.starttime;
let mut row = Row::default_for_test(pid);
let prev_proc_ticks = match self.prev_rows.get(&pid) {
Some(p) if p.starttime == starttime => p.proc_ticks,
_ => 0,
};
next_prev.insert(
pid,
PrevRow {
starttime,
proc_ticks: fields.cpu_ticks(),
},
);
scan::apply_stat(&mut row, &fields, page_size);
row.comm = comm;
#[cfg(target_os = "linux")]
if self.read_smaps {
if let Some(rollup) = super::fsops::read_smaps_rollup(&self.root, pid) {
row.pss = Some(rollup.pss);
row.epss = Some(rollup.epss());
row.private = Some(rollup.private());
row.swap_bytes = Some(rollup.swap);
}
}
#[cfg(target_os = "linux")]
if self.read_gpu {
if let Some(gpu) = super::scan::read_gpu_usage(&self.root, pid) {
row.gpu_percent = gpu.percent();
row.gpu_ns = Some(gpu.gpu_ns);
}
}
#[cfg(target_os = "linux")]
if self.read_delayacct {
if let Some(stats) = self.delay_for(pid, starttime) {
row.cpu_delay_percent = stats.cpu_percent();
row.io_delay_percent = stats.blkio_percent();
row.swap_delay_percent = stats.swapin_percent();
}
}
let d_proc = fields.cpu_ticks().saturating_sub(prev_proc_ticks) as f32;
row.percent_cpu = if total_delta > 0.0 {
d_proc * 100.0 / total_delta
} else {
0.0
};
let stable = scan::read_identity(&self.root, pid, starttime, &mut self.cache);
row.uid = stable.uid;
row.flags = row
.flags
.set(RowFlags::KERNEL_THREAD, stable.is_kernel_thread);
row.cmdline = stable.cmdline;
row.exe = stable.exe;
row.cwd = stable.cwd;
row.cgroup = stable.cgroup;
row.flags = row
.flags
.set(RowFlags::STALE_EXE, stable.exe_deleted);
#[cfg(target_os = "linux")]
{
row.container = stable.container;
row.container_runtime = stable.container_runtime;
}
row.elapsed = if self.clk_tck > 0 {
uptime.saturating_sub(starttime.checked_div(self.clk_tck).unwrap_or(0))
} else {
0
};
row.percent_mem = if mem_total > 0 {
row.res as f32 * 100.0 / mem_total as f32
} else {
0.0
};
row.ticks = CpuTicks {
proc: fields.cpu_ticks(),
total: total_delta as u64,
};
row.prev_ticks = CpuTicks {
proc: prev_proc_ticks,
total: 0,
};
out.push(row);
}
self.prev_rows = next_prev;
#[cfg(target_os = "linux")]
{
let live: std::collections::HashSet<Pid> =
out.iter().filter(|r| !r.is_thread).map(|r| r.pid).collect();
self.prev_delay.retain(|pid, _| live.contains(pid));
}
out
}
}
fn task_stats(rows: &[Row]) -> crate::system::TaskStats {
let procs = rows.iter().filter(|r| !r.is_thread);
let mut stats = crate::system::TaskStats::default();
for row in procs {
stats.total += 1;
if row.state == b'R' {
stats.running += 1;
}
stats.threads += row.nlwp.max(1) as usize;
}
stats
}
impl crate::PlatformSampler for Sampler {
fn sample(&mut self) -> (SystemSnapshot, Vec<Row>) {
self.sample_inner()
}
fn cache_len(&self) -> usize {
self.cache.len()
}
fn backend_name() -> &'static str {
"linux (/proc)"
}
fn set_scan_options(&mut self, opts: crate::ScanOptions) {
self.show_threads = opts.threads;
self.read_smaps = opts.smaps;
self.read_gpu = opts.gpu;
#[cfg(target_os = "linux")]
{
self.read_delayacct = opts.delayacct;
if !opts.delayacct {
self.delayacct = None;
self.prev_delay.clear();
}
}
}
fn supports_threads() -> bool {
true
}
}
#[cfg(all(test, target_os = "linux"))]
mod platform_trait_tests {
use crate::PlatformSampler;
#[test]
fn the_linux_sampler_satisfies_the_shared_trait() {
assert!(<crate::Sampler as PlatformSampler>::backend_name().contains("linux"));
let mut s = crate::Sampler::default();
let (_snap, rows) = PlatformSampler::sample(&mut s);
assert!(!rows.is_empty(), "/proc must yield processes");
assert_eq!(PlatformSampler::cache_len(&s), <crate::Sampler>::cache_len(&s));
}
}