use std::time::Duration;
use anyhow::Result;
use prost::Message as _;
use tokio::io::AsyncWrite;
use arcbox_protocol::agent::memory_pressure_event::Reason;
use arcbox_protocol::agent::{MemoryPressureEvent, WatchMemoryPressureRequest};
use crate::memory_pressure::{
PSI_WINDOW_US, PressureDetector, PressureSignal, PressureThresholds, parse_mem_available,
parse_workingset_refaults, psi_stall_threshold_us,
};
use crate::rpc::{MessageType, write_message};
const SAMPLE_INTERVAL: Duration = Duration::from_secs(1);
const DEFAULT_WINDOW: Duration = Duration::from_secs(300);
const DEFAULT_KEEPALIVE: Duration = Duration::from_secs(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PsiEvent {
Stall,
Tick,
Lost,
}
struct PsiTrigger {
events: tokio::sync::mpsc::Receiver<PsiEvent>,
}
impl PsiTrigger {
fn open(stall_us: u32, tick: Duration) -> std::io::Result<Self> {
use std::io::Write;
use std::os::unix::io::AsRawFd;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/proc/pressure/memory")?;
let trigger = format!("full {stall_us} {PSI_WINDOW_US}\0");
file.write_all(trigger.as_bytes())?;
let (tx, rx) = tokio::sync::mpsc::channel(4);
let tick_ms = i32::try_from(tick.as_millis()).unwrap_or(i32::MAX);
std::thread::spawn(move || {
let fd = file.as_raw_fd();
loop {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLPRI,
revents: 0,
};
let n = unsafe { libc::poll(&mut pfd, 1, tick_ms) };
if n < 0
&& std::io::Error::last_os_error().kind() == std::io::ErrorKind::Interrupted
{
continue;
}
let event = match n {
0 => PsiEvent::Tick,
n if n > 0 && pfd.revents & libc::POLLERR == 0 => PsiEvent::Stall,
_ => PsiEvent::Lost,
};
if tx.blocking_send(event).is_err() || event == PsiEvent::Lost {
return;
}
}
});
Ok(Self { events: rx })
}
async fn next(&mut self) -> PsiEvent {
self.events.recv().await.unwrap_or(PsiEvent::Lost)
}
}
fn read_sample() -> Option<(u64, u64)> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let vmstat = std::fs::read_to_string("/proc/vmstat").ok()?;
Some((
parse_mem_available(&meminfo)?,
parse_workingset_refaults(&vmstat)?,
))
}
pub(super) async fn handle_watch_memory_pressure<S>(
stream: &mut S,
req: WatchMemoryPressureRequest,
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 keepalive = if req.keepalive_ms == 0 {
DEFAULT_KEEPALIVE
} else {
Duration::from_millis(u64::from(req.keepalive_ms))
};
let mut detector = PressureDetector::new(PressureThresholds {
min_available_bytes: req.min_available_bytes,
max_refault_rate: req.max_refault_rate,
});
tracing::info!(
trace_id = %trace_id,
window_secs = window.as_secs(),
min_available_bytes = req.min_available_bytes,
max_refault_rate = req.max_refault_rate,
"Memory pressure watch opened"
);
write_event(stream, trace_id, Reason::Keepalive, 0, 0).await?;
let started = tokio::time::Instant::now();
let mut last_keepalive = started;
let mut previous: Option<(tokio::time::Instant, u64)> = None;
let mut warned_unreadable = false;
let mut ticker = tokio::time::interval(SAMPLE_INTERVAL);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
let now = ticker.tick().await;
if started.elapsed() >= window {
write_event(stream, trace_id, Reason::WindowElapsed, 0, 0).await?;
return Ok(());
}
let Some((available, refaults)) = read_sample() else {
if !warned_unreadable {
warned_unreadable = true;
tracing::warn!(trace_id = %trace_id, "memory pressure signals unreadable");
}
if now.duration_since(last_keepalive) >= keepalive {
last_keepalive = now;
write_event(stream, trace_id, Reason::Keepalive, 0, 0).await?;
}
continue;
};
let was_armed = detector.is_armed();
let refault_rate = match previous {
Some((at, count)) => {
let elapsed = now.duration_since(at).as_secs_f64();
if elapsed > 0.0 {
(refaults.saturating_sub(count) as f64 / elapsed) as u64
} else {
0
}
}
None => 0,
};
previous = Some((now, refaults));
let signal = detector.evaluate(available, refault_rate);
if !was_armed && detector.is_armed() {
tracing::info!(
trace_id = %trace_id,
available_bytes = available,
elapsed_secs = started.elapsed().as_secs(),
"memory pressure detector armed (guest settled after shrink)"
);
last_keepalive = now;
write_event(stream, trace_id, Reason::Settled, available, refault_rate).await?;
let stall_us = psi_stall_threshold_us(req.psi_full_stall_us);
match PsiTrigger::open(stall_us, keepalive) {
Ok(psi) => {
tracing::info!(
trace_id = %trace_id,
stall_us,
"PSI trigger armed; sampling paused"
);
return watch_armed_psi(
stream,
psi,
req.min_available_bytes,
window,
started,
trace_id,
)
.await;
}
Err(e) => {
tracing::info!(
trace_id = %trace_id,
"PSI unavailable ({e}); staying on the sampling path"
);
}
}
}
match signal {
Some(signal) => {
let reason = match signal {
PressureSignal::LowAvailable => Reason::LowAvailable,
PressureSignal::RefaultSpike => Reason::RefaultSpike,
};
tracing::info!(
trace_id = %trace_id,
available_bytes = available,
refault_rate,
reason = ?reason,
"Memory pressure detected"
);
last_keepalive = now;
write_event(stream, trace_id, reason, available, refault_rate).await?;
}
None if now.duration_since(last_keepalive) >= keepalive => {
last_keepalive = now;
write_event(stream, trace_id, Reason::Keepalive, available, refault_rate).await?;
}
None => {}
}
}
}
async fn watch_armed_psi<S>(
stream: &mut S,
mut psi: PsiTrigger,
min_available_bytes: u64,
window: Duration,
started: tokio::time::Instant,
trace_id: &str,
) -> Result<()>
where
S: AsyncWrite + Unpin,
{
loop {
if started.elapsed() >= window {
write_event(stream, trace_id, Reason::WindowElapsed, 0, 0).await?;
return Ok(());
}
match psi.next().await {
PsiEvent::Stall => {
let available = read_sample().map_or(0, |(available, _)| available);
tracing::info!(
trace_id = %trace_id,
available_bytes = available,
"Memory pressure detected (PSI full-stall trigger)"
);
write_event(stream, trace_id, Reason::LowAvailable, available, 0).await?;
}
PsiEvent::Tick => {
let Some((available, _)) = read_sample() else {
write_event(stream, trace_id, Reason::Keepalive, 0, 0).await?;
continue;
};
if min_available_bytes > 0 && available < min_available_bytes {
tracing::info!(
trace_id = %trace_id,
available_bytes = available,
"Memory pressure detected (available floor, PSI mode)"
);
write_event(stream, trace_id, Reason::LowAvailable, available, 0).await?;
} else {
write_event(stream, trace_id, Reason::Keepalive, available, 0).await?;
}
}
PsiEvent::Lost => {
anyhow::bail!("PSI trigger lost");
}
}
}
}
async fn write_event<S>(
stream: &mut S,
trace_id: &str,
reason: Reason,
available_bytes: u64,
refault_rate: u64,
) -> Result<()>
where
S: AsyncWrite + Unpin,
{
let event = MemoryPressureEvent {
reason: reason as i32,
available_bytes,
refault_rate,
};
write_message(
stream,
MessageType::MemoryPressureEvent,
trace_id,
&event.encode_to_vec(),
)
.await
}