229 lines
6.3 KiB
Rust
229 lines
6.3 KiB
Rust
//! Count-Min Sketch for temperature tracking.
|
|
//!
|
|
//! Tracks per-block access frequency to drive tier promotion/demotion.
|
|
//! Spec defaults: width=1024, depth=4, 8-bit saturating counters.
|
|
|
|
use alloc::vec;
|
|
use alloc::vec::Vec;
|
|
|
|
/// Count-Min Sketch for access frequency estimation.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CountMinSketch {
|
|
/// Counter matrix: `counters[row][col]`, each row uses a different hash.
|
|
pub counters: Vec<Vec<u8>>,
|
|
/// Number of counters per row.
|
|
pub width: usize,
|
|
/// Number of hash functions (rows).
|
|
pub depth: usize,
|
|
/// Total number of increment operations (for aging decisions).
|
|
pub total_accesses: u64,
|
|
}
|
|
|
|
/// Default width (counters per row).
|
|
pub const DEFAULT_WIDTH: usize = 1024;
|
|
|
|
/// Default depth (hash functions / rows).
|
|
pub const DEFAULT_DEPTH: usize = 4;
|
|
|
|
/// Aging trigger: halve all counters every 2^16 accesses.
|
|
const AGING_INTERVAL: u64 = 1 << 16;
|
|
|
|
impl CountMinSketch {
|
|
/// Create a new sketch with the given width and depth.
|
|
pub fn new(width: usize, depth: usize) -> Self {
|
|
Self {
|
|
counters: vec![vec![0u8; width]; depth],
|
|
width,
|
|
depth,
|
|
total_accesses: 0,
|
|
}
|
|
}
|
|
|
|
/// Create a sketch with default parameters (w=1024, d=4).
|
|
pub fn default_sketch() -> Self {
|
|
Self::new(DEFAULT_WIDTH, DEFAULT_DEPTH)
|
|
}
|
|
|
|
/// Increment the count for `block_id` using saturating addition.
|
|
///
|
|
/// Updates all `depth` hash rows with `min(counter + 1, 255)`.
|
|
pub fn increment(&mut self, block_id: u64) {
|
|
for row in 0..self.depth {
|
|
let idx = self.hash(block_id, row) % self.width;
|
|
self.counters[row][idx] = self.counters[row][idx].saturating_add(1);
|
|
}
|
|
self.total_accesses = self.total_accesses.wrapping_add(1);
|
|
}
|
|
|
|
/// Estimate the access count for `block_id`.
|
|
///
|
|
/// Returns the minimum across all hash rows (Count-Min guarantee:
|
|
/// estimate >= true count, with bounded overestimation).
|
|
pub fn estimate(&self, block_id: u64) -> u8 {
|
|
let mut min_val = u8::MAX;
|
|
for row in 0..self.depth {
|
|
let idx = self.hash(block_id, row) % self.width;
|
|
min_val = min_val.min(self.counters[row][idx]);
|
|
}
|
|
min_val
|
|
}
|
|
|
|
/// Age (decay) all counters by right-shifting by 1 (halving).
|
|
///
|
|
/// This ensures the sketch tracks *recent* access patterns rather
|
|
/// than cumulative history.
|
|
pub fn age(&mut self) {
|
|
for row in &mut self.counters {
|
|
for counter in row.iter_mut() {
|
|
*counter >>= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns true if aging should be triggered (every 2^16 accesses).
|
|
pub fn should_age(&self) -> bool {
|
|
self.total_accesses > 0 && self.total_accesses.is_multiple_of(AGING_INTERVAL)
|
|
}
|
|
|
|
/// Memory footprint in bytes (counters only, excluding struct overhead).
|
|
pub fn memory_bytes(&self) -> usize {
|
|
self.width * self.depth
|
|
}
|
|
|
|
/// Hash function using FNV-1a style multiplicative hashing.
|
|
///
|
|
/// Each row uses a different seed to produce independent hash values.
|
|
fn hash(&self, block_id: u64, row: usize) -> usize {
|
|
// FNV-1a inspired: mix block_id with row-dependent seed.
|
|
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
|
|
const FNV_PRIME: u64 = 0x100000001b3;
|
|
|
|
let seed = HASH_SEEDS[row % HASH_SEEDS.len()];
|
|
let mut h = FNV_OFFSET ^ seed;
|
|
let bytes = block_id.to_le_bytes();
|
|
for &b in &bytes {
|
|
h ^= b as u64;
|
|
h = h.wrapping_mul(FNV_PRIME);
|
|
}
|
|
h as usize
|
|
}
|
|
}
|
|
|
|
/// Seeds for hash functions (one per row).
|
|
const HASH_SEEDS: [u64; 8] = [
|
|
0x517cc1b727220a95,
|
|
0x6c62272e07bb0142,
|
|
0x44c6b90e0f294e41,
|
|
0x3b9f7a3e2d8f1c5b,
|
|
0x7e4a1b3c5d6f8a9e,
|
|
0x1a2b3c4d5e6f7089,
|
|
0x9f8e7d6c5b4a3210,
|
|
0xdeadbeefcafebabe,
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn new_sketch_all_zeros() {
|
|
let s = CountMinSketch::new(64, 4);
|
|
for row in &s.counters {
|
|
for &c in row {
|
|
assert_eq!(c, 0);
|
|
}
|
|
}
|
|
assert_eq!(s.total_accesses, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn estimate_ge_true_count() {
|
|
let mut s = CountMinSketch::new(256, 4);
|
|
let block = 42u64;
|
|
|
|
for _ in 0..10 {
|
|
s.increment(block);
|
|
}
|
|
let est = s.estimate(block);
|
|
assert!(est >= 10, "estimate {est} should be >= true count 10");
|
|
}
|
|
|
|
#[test]
|
|
fn increment_saturates_at_255() {
|
|
let mut s = CountMinSketch::new(64, 2);
|
|
let block = 1u64;
|
|
|
|
for _ in 0..300 {
|
|
s.increment(block);
|
|
}
|
|
let est = s.estimate(block);
|
|
assert_eq!(est, 255);
|
|
}
|
|
|
|
#[test]
|
|
fn aging_halves_counters() {
|
|
let mut s = CountMinSketch::new(64, 2);
|
|
let block = 7u64;
|
|
|
|
for _ in 0..100 {
|
|
s.increment(block);
|
|
}
|
|
let before = s.estimate(block);
|
|
s.age();
|
|
let after = s.estimate(block);
|
|
|
|
// After aging, counts should be approximately halved.
|
|
assert!(after <= before, "aging should not increase count");
|
|
assert!(
|
|
after >= before / 2 - 1,
|
|
"aging should halve: before={before}, after={after}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn should_age_at_power_of_two() {
|
|
let mut s = CountMinSketch::new(64, 2);
|
|
// Not at boundary
|
|
s.total_accesses = 100;
|
|
assert!(!s.should_age());
|
|
|
|
// At 2^16
|
|
s.total_accesses = 1 << 16;
|
|
assert!(s.should_age());
|
|
|
|
// At 2 * 2^16
|
|
s.total_accesses = 2 << 16;
|
|
assert!(s.should_age());
|
|
}
|
|
|
|
#[test]
|
|
fn different_blocks_independent() {
|
|
let mut s = CountMinSketch::new(1024, 4);
|
|
|
|
for _ in 0..50 {
|
|
s.increment(100);
|
|
}
|
|
for _ in 0..10 {
|
|
s.increment(200);
|
|
}
|
|
|
|
let est_100 = s.estimate(100);
|
|
let est_200 = s.estimate(200);
|
|
assert!(est_100 >= 50);
|
|
assert!(est_200 >= 10);
|
|
assert!(est_100 > est_200);
|
|
}
|
|
|
|
#[test]
|
|
fn memory_bytes() {
|
|
let s = CountMinSketch::new(1024, 4);
|
|
assert_eq!(s.memory_bytes(), 4096);
|
|
}
|
|
|
|
#[test]
|
|
fn unseen_block_is_zero() {
|
|
let s = CountMinSketch::new(1024, 4);
|
|
assert_eq!(s.estimate(999), 0);
|
|
}
|
|
}
|