pub(super) mod controller;
const MIB: u64 = 1024 * 1024;
pub(super) const IDLE_BALLOON_HEADROOM: u64 = 256 * MIB;
pub(super) const IDLE_BALLOON_FLOOR: u64 = 384 * MIB;
pub(super) const IDLE_BUSY_LOADAVG: f64 = 1.0;
pub(super) const GUEST_STATS_TIMEOUT_SECS: u64 = 3;
pub(super) const SHRINK_STEP: u64 = 2 * 1024 * MIB;
pub(super) fn next_step(current: u64, final_target: u64) -> u64 {
current.saturating_sub(SHRINK_STEP).max(final_target)
}
#[derive(Debug, Clone, Copy)]
pub(super) struct GuestStats {
pub total: u64,
pub available: u64,
pub loadavg1: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum EntryDecision {
Shrink(u64),
NotIdle,
Keep,
}
pub(super) fn idle_target(stats: GuestStats, full: u64) -> u64 {
let used = stats.total.saturating_sub(stats.available);
used.saturating_add(IDLE_BALLOON_HEADROOM)
.clamp(IDLE_BALLOON_FLOOR, full)
}
pub(super) fn entry_decision(stats: Option<GuestStats>, full: u64) -> EntryDecision {
let Some(stats) = stats else {
return EntryDecision::Keep;
};
if stats.total == 0 {
return EntryDecision::Keep;
}
if stats.loadavg1 >= IDLE_BUSY_LOADAVG {
return EntryDecision::NotIdle;
}
let target = idle_target(stats, full);
if target >= full {
EntryDecision::Keep
} else {
EntryDecision::Shrink(target)
}
}
#[cfg(test)]
mod tests {
use super::*;
const GIB: u64 = 1024 * MIB;
const FULL: u64 = 16 * GIB;
fn stats(total: u64, available: u64) -> GuestStats {
GuestStats {
total,
available,
loadavg1: 0.1,
}
}
#[test]
fn idle_target_tracks_guest_usage() {
let t = idle_target(stats(FULL, 4 * GIB), FULL);
assert_eq!(t, 12 * GIB + IDLE_BALLOON_HEADROOM);
}
#[test]
fn idle_target_clamps_to_floor() {
let t = idle_target(stats(FULL, FULL - 100 * MIB), FULL);
assert_eq!(t, IDLE_BALLOON_FLOOR);
}
#[test]
fn idle_target_clamps_to_full() {
let t = idle_target(stats(FULL, 128 * MIB), FULL);
assert_eq!(t, FULL);
}
#[test]
fn idle_target_saturates_on_inconsistent_stats() {
let t = idle_target(stats(4 * GIB, 8 * GIB), FULL);
assert_eq!(t, IDLE_BALLOON_FLOOR);
}
#[test]
fn entry_without_stats_never_shrinks() {
assert_eq!(entry_decision(None, FULL), EntryDecision::Keep);
}
#[test]
fn entry_shrinks_to_usage_aware_target() {
let d = entry_decision(Some(stats(FULL, 12 * GIB)), FULL);
assert_eq!(d, EntryDecision::Shrink(4 * GIB + IDLE_BALLOON_HEADROOM));
}
#[test]
fn entry_with_no_reclaimable_memory_keeps() {
let d = entry_decision(Some(stats(FULL, 100 * MIB)), FULL);
assert_eq!(d, EntryDecision::Keep);
}
#[test]
fn next_step_descends_by_step_size() {
assert_eq!(next_step(16 * GIB, 4 * GIB), 14 * GIB);
assert_eq!(next_step(14 * GIB, 4 * GIB), 12 * GIB);
}
#[test]
fn next_step_clamps_at_final_target() {
assert_eq!(next_step(5 * GIB, 4 * GIB), 4 * GIB);
assert_eq!(next_step(4 * GIB, 4 * GIB), 4 * GIB);
}
#[test]
fn entry_zero_total_is_not_a_reading() {
let d = entry_decision(Some(stats(0, 0)), FULL);
assert_eq!(d, EntryDecision::Keep);
}
#[test]
fn entry_busy_guest_is_not_idle() {
let busy = GuestStats {
total: FULL,
available: 12 * GIB,
loadavg1: 2.5,
};
assert_eq!(entry_decision(Some(busy), FULL), EntryDecision::NotIdle);
}
}