use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::swizzle::PageTemperature;
static COOLING_SCANS_TOTAL: AtomicU64 = AtomicU64::new(0);
static PAGES_COOLED_TOTAL: AtomicU64 = AtomicU64::new(0);
static PAGES_REHEATED_TOTAL: AtomicU64 = AtomicU64::new(0);
static PAGES_EVICTED_TOTAL: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CoolingMetricsSnapshot {
pub cooling_scans_total: u64,
pub pages_cooled_total: u64,
pub pages_reheated_total: u64,
pub pages_evicted_total: u64,
}
impl fmt::Display for CoolingMetricsSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"cooling_scans={} pages_cooled={} pages_reheated={} pages_evicted={}",
self.cooling_scans_total,
self.pages_cooled_total,
self.pages_reheated_total,
self.pages_evicted_total,
)
}
}
#[must_use]
pub fn cooling_metrics_snapshot() -> CoolingMetricsSnapshot {
CoolingMetricsSnapshot {
cooling_scans_total: COOLING_SCANS_TOTAL.load(Ordering::Relaxed),
pages_cooled_total: PAGES_COOLED_TOTAL.load(Ordering::Relaxed),
pages_reheated_total: PAGES_REHEATED_TOTAL.load(Ordering::Relaxed),
pages_evicted_total: PAGES_EVICTED_TOTAL.load(Ordering::Relaxed),
}
}
pub fn reset_cooling_metrics() {
COOLING_SCANS_TOTAL.store(0, Ordering::Relaxed);
PAGES_COOLED_TOTAL.store(0, Ordering::Relaxed);
PAGES_REHEATED_TOTAL.store(0, Ordering::Relaxed);
PAGES_EVICTED_TOTAL.store(0, Ordering::Relaxed);
}
#[derive(Debug, Clone, Copy)]
pub struct CoolingConfig {
pub cooling_threshold: u32,
}
impl Default for CoolingConfig {
fn default() -> Self {
Self {
cooling_threshold: 2,
}
}
}
#[derive(Debug, Clone)]
struct PageState {
temperature: PageTemperature,
access_count: u32,
frame_addr: u64,
}
pub struct CoolingStateMachine {
config: CoolingConfig,
pages: Mutex<HashMap<u64, PageState>>,
pinned_roots: Mutex<HashSet<u64>>,
}
impl CoolingStateMachine {
pub fn new(config: CoolingConfig) -> Self {
Self {
config,
pages: Mutex::new(HashMap::new()),
pinned_roots: Mutex::new(HashSet::new()),
}
}
pub fn register_page(&self, page_id: u64) {
let mut pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
pages.entry(page_id).or_insert(PageState {
temperature: PageTemperature::Cold,
access_count: 0,
frame_addr: 0,
});
}
pub fn pin_root(&self, page_id: u64) {
self.register_page(page_id);
self.pinned_roots
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(page_id);
}
#[must_use]
pub fn is_pinned(&self, page_id: u64) -> bool {
self.pinned_roots
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains(&page_id)
}
#[allow(clippy::significant_drop_tightening)]
pub fn load_page(&self, page_id: u64, frame_addr: u64) -> bool {
let mut pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
let entry = pages.entry(page_id).or_insert(PageState {
temperature: PageTemperature::Cold,
access_count: 0,
frame_addr: 0,
});
match entry.temperature {
PageTemperature::Cold => {
entry.temperature = PageTemperature::Hot;
entry.frame_addr = frame_addr;
entry.access_count = 1;
true
}
PageTemperature::Cooling => {
entry.temperature = PageTemperature::Hot;
entry.frame_addr = frame_addr;
entry.access_count = 1;
PAGES_REHEATED_TOTAL.fetch_add(1, Ordering::Relaxed);
false
}
PageTemperature::Hot => {
entry.access_count += 1;
false
}
}
}
pub fn access_page(&self, page_id: u64) {
let mut pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = pages.get_mut(&page_id) {
entry.access_count += 1;
if entry.temperature == PageTemperature::Cooling {
entry.temperature = PageTemperature::Hot;
PAGES_REHEATED_TOTAL.fetch_add(1, Ordering::Relaxed);
}
}
}
pub fn evict_page(&self, page_id: u64) -> Result<(), &'static str> {
if self.is_pinned(page_id) {
return Err("page is a pinned root");
}
let mut pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
if let Some(entry) = pages.get_mut(&page_id) {
match entry.temperature {
PageTemperature::Cooling => {
entry.temperature = PageTemperature::Cold;
entry.frame_addr = 0;
entry.access_count = 0;
PAGES_EVICTED_TOTAL.fetch_add(1, Ordering::Relaxed);
Ok(())
}
PageTemperature::Hot => Err("page is HOT; must cool first"),
PageTemperature::Cold => Err("page is already COLD"),
}
} else {
Err("page not registered")
}
}
pub fn run_cooling_scan(&self) -> CoolingScanResult {
COOLING_SCANS_TOTAL.fetch_add(1, Ordering::Relaxed);
let mut pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
let pinned = self.pinned_roots.lock().unwrap_or_else(|e| e.into_inner());
let mut scanned = 0u32;
let mut cooled = 0u32;
for (pid, entry) in pages.iter_mut() {
scanned += 1;
if pinned.contains(pid) {
entry.access_count = 0;
continue;
}
if entry.temperature == PageTemperature::Hot
&& entry.access_count < self.config.cooling_threshold
{
entry.temperature = PageTemperature::Cooling;
cooled += 1;
PAGES_COOLED_TOTAL.fetch_add(1, Ordering::Relaxed);
}
entry.access_count = 0;
}
drop(pages);
CoolingScanResult {
pages_scanned: scanned,
pages_cooled: cooled,
}
}
#[must_use]
pub fn temperature(&self, page_id: u64) -> Option<PageTemperature> {
self.pages
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&page_id)
.map(|e| e.temperature)
}
#[must_use]
pub fn frame_addr(&self, page_id: u64) -> Option<u64> {
self.pages
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&page_id)
.and_then(|e| {
if e.temperature == PageTemperature::Cold {
None
} else {
Some(e.frame_addr)
}
})
}
#[must_use]
pub fn temperature_counts(&self) -> TemperatureCounts {
let pages = self.pages.lock().unwrap_or_else(|e| e.into_inner());
let mut counts = TemperatureCounts::default();
for entry in pages.values() {
match entry.temperature {
PageTemperature::Hot => counts.hot += 1,
PageTemperature::Cooling => counts.cooling += 1,
PageTemperature::Cold => counts.cold += 1,
}
}
drop(pages);
counts
}
#[must_use]
pub fn tracked_count(&self) -> usize {
self.pages.lock().unwrap_or_else(|e| e.into_inner()).len()
}
#[must_use]
pub fn pinned_count(&self) -> usize {
self.pinned_roots
.lock()
.unwrap_or_else(|e| e.into_inner())
.len()
}
}
#[allow(clippy::missing_fields_in_debug)]
impl fmt::Debug for CoolingStateMachine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let counts = self.temperature_counts();
f.debug_struct("CoolingStateMachine")
.field("config", &self.config)
.field("hot", &counts.hot)
.field("cooling", &counts.cooling)
.field("cold", &counts.cold)
.field("pinned", &self.pinned_count())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CoolingScanResult {
pub pages_scanned: u32,
pub pages_cooled: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TemperatureCounts {
pub hot: usize,
pub cooling: usize,
pub cold: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_lifecycle() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.register_page(1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
csm.load_page(1, 0x1000);
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
let result = csm.run_cooling_scan();
assert_eq!(result.pages_cooled, 1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
csm.evict_page(1).expect("evict should succeed");
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
}
#[test]
fn load_page_returns_true_only_on_first_cold_to_hot_transition() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.register_page(1);
assert!(csm.load_page(1, 0x1000));
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
csm.run_cooling_scan();
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
assert!(!csm.load_page(1, 0x2000));
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
assert!(!csm.load_page(1, 0x3000));
}
#[test]
fn re_heat_on_access() {
let csm = CoolingStateMachine::new(CoolingConfig {
cooling_threshold: 2,
});
csm.register_page(1);
csm.load_page(1, 0x1000);
csm.run_cooling_scan();
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
csm.access_page(1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
}
#[test]
fn access_page_is_noop_for_unregistered_and_does_not_reheat_cold() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.access_page(999);
assert_eq!(csm.tracked_count(), 0);
assert_eq!(csm.temperature(999), None);
csm.register_page(1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
csm.access_page(1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
}
#[test]
fn pinned_root_never_cools() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.pin_root(1);
csm.load_page(1, 0x1000);
for _ in 0..10 {
csm.run_cooling_scan();
}
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
assert!(csm.is_pinned(1));
}
#[test]
fn cannot_evict_hot_page() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.register_page(1);
csm.load_page(1, 0x1000);
let err = csm.evict_page(1).unwrap_err();
assert_eq!(err, "page is HOT; must cool first");
}
#[test]
fn cannot_evict_pinned_root() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.pin_root(1);
csm.load_page(1, 0x1000);
csm.run_cooling_scan();
let err = csm.evict_page(1).unwrap_err();
assert_eq!(err, "page is a pinned root");
}
#[test]
fn evict_page_distinguishes_unregistered_from_already_cold() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
assert_eq!(csm.evict_page(99).unwrap_err(), "page not registered");
csm.register_page(1);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
assert_eq!(csm.evict_page(1).unwrap_err(), "page is already COLD");
csm.load_page(1, 0x1000);
csm.run_cooling_scan();
csm.evict_page(1).expect("a cooling page should evict");
assert_eq!(csm.temperature(1), Some(PageTemperature::Cold));
assert_eq!(csm.evict_page(1).unwrap_err(), "page is already COLD");
}
#[test]
fn run_cooling_scan_keeps_frequently_accessed_hot_pages_hot() {
let csm = CoolingStateMachine::new(CoolingConfig {
cooling_threshold: 3,
});
csm.register_page(1);
csm.load_page(1, 0x1000); csm.access_page(1); csm.access_page(1);
let result = csm.run_cooling_scan();
assert_eq!(csm.temperature(1), Some(PageTemperature::Hot));
assert_eq!(result.pages_cooled, 0);
assert_eq!(result.pages_scanned, 1);
let result2 = csm.run_cooling_scan();
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
assert_eq!(result2.pages_cooled, 1);
}
#[test]
fn cooling_tracker_counts_frame_addr_and_multi_page_scan() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.register_page(1);
csm.load_page(1, 0x1000);
csm.register_page(2);
csm.load_page(2, 0x2000);
csm.pin_root(3);
csm.load_page(3, 0x3000);
assert_eq!(csm.tracked_count(), 3);
assert_eq!(csm.pinned_count(), 1);
assert!(csm.is_pinned(3) && !csm.is_pinned(1));
assert_eq!(csm.frame_addr(1), Some(0x1000));
assert_eq!(csm.frame_addr(2), Some(0x2000));
assert!(
csm.temperature(999).is_none(),
"unregistered page has no temperature"
);
let result = csm.run_cooling_scan();
assert_eq!(
result.pages_cooled, 2,
"two unpinned pages cool; the pinned root does not"
);
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
assert_eq!(csm.temperature(2), Some(PageTemperature::Cooling));
assert_eq!(csm.temperature(3), Some(PageTemperature::Hot));
assert!(csm.evict_page(1).is_ok());
assert!(csm.evict_page(3).is_err());
}
#[test]
fn frame_addr_is_none_for_cold_and_unregistered_pages() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
assert_eq!(csm.frame_addr(99), None);
csm.register_page(1);
assert_eq!(csm.frame_addr(1), None);
csm.load_page(1, 0x1000);
assert_eq!(csm.frame_addr(1), Some(0x1000));
csm.run_cooling_scan();
assert_eq!(csm.temperature(1), Some(PageTemperature::Cooling));
assert_eq!(csm.frame_addr(1), Some(0x1000));
csm.evict_page(1).expect("a cooling page evicts");
assert_eq!(csm.frame_addr(1), None);
}
#[test]
fn temperature_counts_buckets_pages_by_thermal_state() {
let csm = CoolingStateMachine::new(CoolingConfig::default());
csm.register_page(1);
csm.register_page(2);
csm.register_page(3);
csm.load_page(1, 0x1000); csm.load_page(2, 0x2000); csm.run_cooling_scan(); csm.load_page(1, 0x1000);
let counts = csm.temperature_counts();
assert_eq!(counts.hot, 1);
assert_eq!(counts.cooling, 1);
assert_eq!(counts.cold, 1);
assert_eq!(
counts.hot + counts.cooling + counts.cold,
csm.tracked_count()
);
}
}