use std::time::Duration;
use anyhow::Result;
use prost::Message as _;
use tokio::io::AsyncWrite;
use arcbox_protocol::agent::{ContainerStats, MachineStats, WatchStatsRequest};
use crate::rpc::{MessageType, write_message};
use crate::stats::{
is_container_id, parse_cgroup_cpu_usage_usec, parse_cgroup_io_bytes, parse_cgroup_memory_max,
parse_cpu_ticks, parse_diskstats, parse_first_pid, parse_loadavg1, parse_meminfo,
parse_net_dev, parse_psi_full_avg10, parse_u64_file, parse_uptime_ms,
};
const DOCKER_CGROUP_ROOT: &str = "/sys/fs/cgroup/docker";
const DEFAULT_WINDOW: Duration = Duration::from_secs(300);
const DEFAULT_INTERVAL: Duration = Duration::from_millis(1000);
const MIN_INTERVAL: Duration = Duration::from_millis(200);
pub(super) async fn handle_watch_stats<S>(
stream: &mut S,
req: WatchStatsRequest,
trace_id: &str,
) -> Result<()>
where
S: AsyncWrite + Unpin,
{
let window = if req.timeout_ms == 0 {
DEFAULT_WINDOW
} else {
Duration::from_millis(u64::from(req.timeout_ms))
};
let interval = if req.interval_ms == 0 {
DEFAULT_INTERVAL
} else {
Duration::from_millis(u64::from(req.interval_ms)).max(MIN_INTERVAL)
};
tracing::info!(
trace_id = %trace_id,
window_secs = window.as_secs(),
interval_ms = interval.as_millis(),
"stats watch opened"
);
let started = tokio::time::Instant::now();
let mut warned_unreadable = false;
let mut ticker = tokio::time::interval(interval);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
ticker.tick().await;
if started.elapsed() >= window {
return Ok(());
}
match read_machine_stats() {
Some(sample) => {
write_message(
stream,
MessageType::MachineStats,
trace_id,
&sample.encode_to_vec(),
)
.await?;
}
None if !warned_unreadable => {
warned_unreadable = true;
tracing::warn!(trace_id = %trace_id, "machine stats unreadable; skipping ticks");
}
None => {}
}
}
}
fn read_machine_stats() -> Option<MachineStats> {
let stat = std::fs::read_to_string("/proc/stat").ok()?;
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let loadavg = std::fs::read_to_string("/proc/loadavg").ok()?;
let uptime = std::fs::read_to_string("/proc/uptime").ok()?;
let diskstats = std::fs::read_to_string("/proc/diskstats").unwrap_or_default();
let net_dev = std::fs::read_to_string("/proc/net/dev").unwrap_or_default();
let psi = std::fs::read_to_string("/proc/pressure/memory").ok();
let (cpu_busy_ticks, cpu_total_ticks) = parse_cpu_ticks(&stat)?;
let (memory_total_bytes, memory_available_bytes) = parse_meminfo(&meminfo)?;
let (disk_read_bytes, disk_written_bytes) = parse_diskstats(&diskstats);
let (net_rx_bytes, net_tx_bytes) = parse_net_dev(&net_dev);
Some(MachineStats {
monotonic_ms: parse_uptime_ms(&uptime)?,
cpu_busy_ticks,
cpu_total_ticks,
online_cpus: std::thread::available_parallelism().map_or(0, |n| n.get() as u32),
loadavg1: parse_loadavg1(&loadavg)?,
memory_total_bytes,
memory_available_bytes,
memory_psi_full_avg10: psi
.as_deref()
.and_then(parse_psi_full_avg10)
.unwrap_or(-1.0),
disk_read_bytes,
disk_written_bytes,
net_rx_bytes,
net_tx_bytes,
containers: read_container_stats(),
})
}
fn read_container_stats() -> Vec<ContainerStats> {
let Ok(entries) = std::fs::read_dir(DOCKER_CGROUP_ROOT) else {
return Vec::new();
};
let mut containers = Vec::new();
for entry in entries.flatten() {
let Ok(name) = entry.file_name().into_string() else {
continue;
};
if !is_container_id(&name) {
continue;
}
let dir = entry.path();
let read = |leaf: &str| std::fs::read_to_string(dir.join(leaf)).unwrap_or_default();
let (disk_read_bytes, disk_written_bytes) = parse_cgroup_io_bytes(&read("io.stat"));
let (net_rx_bytes, net_tx_bytes) = container_network(&read("cgroup.procs"));
containers.push(ContainerStats {
id: name,
name: String::new(),
cpu_usage_usec: parse_cgroup_cpu_usage_usec(&read("cpu.stat")).unwrap_or(0),
memory_current_bytes: parse_u64_file(&read("memory.current")).unwrap_or(0),
memory_limit_bytes: parse_cgroup_memory_max(&read("memory.max")),
disk_read_bytes,
disk_written_bytes,
pids: parse_u64_file(&read("pids.current")).unwrap_or(0) as u32,
net_rx_bytes,
net_tx_bytes,
});
}
containers
}
fn container_network(cgroup_procs: &str) -> (u64, u64) {
let Some(pid) = parse_first_pid(cgroup_procs) else {
return (0, 0);
};
if shares_agent_netns(pid) {
return (0, 0);
}
match std::fs::read_to_string(format!("/proc/{pid}/net/dev")) {
Ok(net_dev) => parse_net_dev(&net_dev),
Err(_) => (0, 0),
}
}
fn shares_agent_netns(pid: u32) -> bool {
let Ok(agent_ns) = std::fs::read_link("/proc/self/ns/net") else {
return false;
};
std::fs::read_link(format!("/proc/{pid}/ns/net")).is_ok_and(|ns| ns == agent_ns)
}