#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BlockKey(pub u64);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum Tier {
Tier0 = 0,
Tier1 = 1,
Tier2 = 2,
Tier3 = 3,
}
#[derive(Clone, Debug)]
pub struct BlockMeta {
pub ema_rate: f32,
pub access_window: u64,
pub last_access: u64,
pub access_count: u64,
pub current_tier: Tier,
pub tier_since: u64,
}
impl BlockMeta {
pub fn new(now: u64) -> Self {
Self {
ema_rate: 0.0,
access_window: 0,
last_access: now,
access_count: 0,
current_tier: Tier::Tier1,
tier_since: now,
}
}
}
#[derive(Clone, Debug)]
pub struct TierConfig {
pub alpha: f32,
pub tau: f32,
pub w_ema: f32,
pub w_pop: f32,
pub w_rec: f32,
pub t1: f32,
pub t2: f32,
pub t3: f32,
pub hysteresis: f32,
pub min_residency: u32,
pub max_delta_chain: u8,
pub block_bytes: usize,
pub tier1_byte_cap: Option<usize>,
pub warm_aggressive_threshold: Option<usize>,
}
impl Default for TierConfig {
fn default() -> Self {
Self {
alpha: 0.3,
tau: 100.0,
w_ema: 0.4,
w_pop: 0.3,
w_rec: 0.3,
t1: 0.7,
t2: 0.3,
t3: 0.1,
hysteresis: 0.05,
min_residency: 5,
max_delta_chain: 8,
block_bytes: 16384,
tier1_byte_cap: None,
warm_aggressive_threshold: None,
}
}
}
#[inline]
#[allow(dead_code)]
fn fast_exp_neg(x: f32) -> f32 {
if x < 0.0 {
return 1.0;
}
1.0 / (1.0 + x)
}
const LUT_SIZE: usize = 64;
const LUT_X_MAX: f32 = 8.0;
const EXP_LUT: [f32; LUT_SIZE + 1] = {
let mut table = [0.0f32; LUT_SIZE + 1];
let mut i = 0;
while i <= LUT_SIZE {
let x = -(i as f64) * (LUT_X_MAX as f64) / (LUT_SIZE as f64);
let v = const_exp(x);
table[i] = v as f32;
i += 1;
}
table
};
const fn const_exp(x: f64) -> f64 {
if x < 0.0 {
let pos = const_exp_pos(-x);
return 1.0 / pos;
}
const_exp_pos(x)
}
const fn const_exp_pos(x: f64) -> f64 {
let mut sum = 1.0f64;
let mut term = 1.0f64;
let mut k = 1u32;
while k <= 35 {
term *= x / (k as f64);
sum += term;
k += 1;
}
sum
}
#[inline]
fn fast_exp_neg_lut(x: f32) -> f32 {
if x <= 0.0 {
return 1.0;
}
if x >= LUT_X_MAX {
return EXP_LUT[LUT_SIZE];
}
let scaled = x * (LUT_SIZE as f32) / LUT_X_MAX;
let idx = scaled as usize; let frac = scaled - (idx as f32);
let lo = EXP_LUT[idx];
let hi = EXP_LUT[idx + 1];
lo + frac * (hi - lo)
}
pub fn compute_score(config: &TierConfig, now: u64, meta: &BlockMeta) -> f32 {
let ema_component = config.w_ema * meta.ema_rate.clamp(0.0, 1.0);
let pop = meta.access_window.count_ones() as f32 / 64.0;
let pop_component = config.w_pop * pop;
let dt = now.saturating_sub(meta.last_access) as f32;
let recency = fast_exp_neg_lut(dt / config.tau);
let rec_component = config.w_rec * recency;
ema_component + pop_component + rec_component
}
pub fn choose_tier(config: &TierConfig, now: u64, meta: &BlockMeta) -> Option<Tier> {
let ticks_in_tier = now.saturating_sub(meta.tier_since);
if ticks_in_tier < config.min_residency as u64 {
return None;
}
let score = compute_score(config, now, meta);
let current = meta.current_tier;
let raw_target = if score >= config.t1 {
Tier::Tier1
} else if score >= config.t2 {
Tier::Tier2
} else if score >= config.t3 {
Tier::Tier3
} else {
Tier::Tier3 };
if raw_target == current {
return None;
}
let h = config.hysteresis;
let transition_allowed = if raw_target < current {
let threshold = match raw_target {
Tier::Tier0 => return None, Tier::Tier1 => config.t1,
Tier::Tier2 => config.t2,
Tier::Tier3 => config.t3,
};
score > threshold + h
} else {
let threshold = match current {
Tier::Tier0 => return None,
Tier::Tier1 => config.t1,
Tier::Tier2 => config.t2,
Tier::Tier3 => return None, };
score < threshold - h
};
if transition_allowed {
Some(raw_target)
} else {
None
}
}
pub fn touch(config: &TierConfig, now: u64, meta: &mut BlockMeta) {
meta.ema_rate = config.alpha + (1.0 - config.alpha) * meta.ema_rate;
let elapsed = now.saturating_sub(meta.last_access);
if elapsed > 0 {
if elapsed >= 64 {
meta.access_window = 1;
} else {
meta.access_window = (meta.access_window << elapsed) | 1;
}
} else {
meta.access_window |= 1;
}
meta.last_access = now;
meta.access_count = meta.access_count.saturating_add(1);
}
pub fn tick_decay(config: &TierConfig, meta: &mut BlockMeta) {
meta.ema_rate *= 1.0 - config.alpha;
meta.access_window <<= 1;
}
#[derive(Debug, Default)]
pub struct MaintenanceResult {
pub upgrades: u32,
pub downgrades: u32,
pub evictions: u32,
pub bytes_freed: usize,
pub ops_used: u32,
}
#[derive(Debug)]
pub struct MigrationCandidate {
pub key: BlockKey,
pub current_tier: Tier,
pub target_tier: Tier,
pub score: f32,
}
pub fn select_candidates(
config: &TierConfig,
now: u64,
blocks: &[(BlockKey, &BlockMeta)],
) -> Vec<MigrationCandidate> {
let mut upgrades: Vec<MigrationCandidate> = Vec::new();
let mut downgrades: Vec<MigrationCandidate> = Vec::new();
for &(key, meta) in blocks {
if let Some(target) = choose_tier(config, now, meta) {
let score = compute_score(config, now, meta);
let candidate = MigrationCandidate {
key,
current_tier: meta.current_tier,
target_tier: target,
score,
};
if target < meta.current_tier {
upgrades.push(candidate);
} else {
downgrades.push(candidate);
}
}
}
upgrades.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(core::cmp::Ordering::Equal)
});
downgrades.sort_by(|a, b| {
a.score
.partial_cmp(&b.score)
.unwrap_or(core::cmp::Ordering::Equal)
});
upgrades.extend(downgrades);
upgrades
}
#[derive(Clone, Debug)]
pub struct ScoredPartition {
pub hot: Vec<usize>,
pub warm: Vec<usize>,
pub cold: Vec<usize>,
pub evict: Vec<usize>,
pub scores: Vec<f32>,
}
pub fn compute_scores_batch(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> Vec<f32> {
metas
.iter()
.map(|m| compute_score(config, now, m))
.collect()
}
pub fn choose_tiers_batch(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> Vec<Option<Tier>> {
metas.iter().map(|m| choose_tier(config, now, m)).collect()
}
pub fn score_and_partition(config: &TierConfig, now: u64, metas: &[BlockMeta]) -> ScoredPartition {
let scores = compute_scores_batch(config, now, metas);
let mut hot = Vec::new();
let mut warm = Vec::new();
let mut cold = Vec::new();
let mut evict = Vec::new();
for (i, &score) in scores.iter().enumerate() {
if score >= config.t1 {
hot.push(i);
} else if score >= config.t2 {
warm.push(i);
} else if score >= config.t3 {
cold.push(i);
} else {
evict.push(i);
}
}
ScoredPartition {
hot,
warm,
cold,
evict,
scores,
}
}
pub fn top_k_coldest(
config: &TierConfig,
now: u64,
metas: &[BlockMeta],
k: usize,
) -> Vec<(usize, f32)> {
let scores = compute_scores_batch(config, now, metas);
let mut indexed: Vec<(usize, f32)> = scores.into_iter().enumerate().collect();
if k < indexed.len() {
indexed.select_nth_unstable_by(k, |a, b| {
a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal)
});
indexed.truncate(k);
}
indexed.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal));
indexed
}
pub fn bits_for_tier(config: &TierConfig, tier: Tier, warm_bytes: usize) -> u8 {
match tier {
Tier::Tier0 => 0,
Tier::Tier1 => 8,
Tier::Tier2 => {
if let Some(threshold) = config.warm_aggressive_threshold {
if warm_bytes > threshold {
return 5;
}
}
7
}
Tier::Tier3 => 3,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn default_config() -> TierConfig {
TierConfig::default()
}
fn make_meta(
ema_rate: f32,
access_window: u64,
last_access: u64,
current_tier: Tier,
tier_since: u64,
) -> BlockMeta {
BlockMeta {
ema_rate,
access_window,
last_access,
access_count: 0,
current_tier,
tier_since,
}
}
#[test]
fn score_all_components_at_max() {
let cfg = default_config();
let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0);
let score = compute_score(&cfg, 100, &meta);
assert!((score - 1.0).abs() < 1e-4, "score={score}");
}
#[test]
fn score_all_components_at_zero() {
let cfg = default_config();
let meta = make_meta(0.0, 0, 0, Tier::Tier3, 0);
let score = compute_score(&cfg, 10_000, &meta);
assert!(score < 0.01, "score={score}");
}
#[test]
fn score_only_ema_contributes() {
let cfg = TierConfig {
w_ema: 1.0,
w_pop: 0.0,
w_rec: 0.0,
..default_config()
};
let meta = make_meta(0.75, 0, 0, Tier::Tier2, 0);
let score = compute_score(&cfg, 1000, &meta);
assert!((score - 0.75).abs() < 1e-6, "score={score}");
}
#[test]
fn score_only_popcount_contributes() {
let cfg = TierConfig {
w_ema: 0.0,
w_pop: 1.0,
w_rec: 0.0,
..default_config()
};
let meta = make_meta(0.0, 0x0000_FFFF_FFFF_0000, 0, Tier::Tier2, 0);
let pop = 0x0000_FFFF_FFFF_0000u64.count_ones() as f32 / 64.0;
let score = compute_score(&cfg, 1000, &meta);
assert!(
(score - pop).abs() < 1e-6,
"score={score}, expected pop={pop}"
);
}
#[test]
fn fast_exp_neg_monotonic() {
let mut prev = fast_exp_neg(0.0);
for i in 1..100 {
let x = i as f32 * 0.1;
let val = fast_exp_neg(x);
assert!(val <= prev, "not monotonic at x={x}");
assert!(val >= 0.0);
prev = val;
}
}
#[test]
fn fast_exp_neg_at_zero() {
assert!((fast_exp_neg(0.0) - 1.0).abs() < 1e-6);
}
#[test]
fn fast_exp_neg_negative_input() {
assert!((fast_exp_neg(-5.0) - 1.0).abs() < 1e-6);
}
#[test]
fn fast_exp_neg_vs_stdlib() {
for i in 0..50 {
let x = i as f32 * 0.2;
let approx = fast_exp_neg(x);
let exact = (-x).exp();
assert!(
approx >= exact - 1e-6,
"approx={approx} < exact={exact} at x={x}"
);
}
}
#[test]
fn lut_exp_at_zero() {
assert!((fast_exp_neg_lut(0.0) - 1.0).abs() < 1e-4);
}
#[test]
fn lut_exp_accuracy() {
for i in 0..80 {
let x = i as f32 * 0.1;
let approx = fast_exp_neg_lut(x);
let exact = (-x).exp();
let rel_err = if exact > 1e-10 {
(approx - exact).abs() / exact
} else {
(approx - exact).abs()
};
assert!(
rel_err < 0.01,
"x={x} approx={approx} exact={exact} rel_err={rel_err}"
);
}
}
#[test]
fn lut_exp_beyond_domain() {
let val = fast_exp_neg_lut(100.0);
assert!(val < 0.001, "val={val}");
assert!(val >= 0.0);
}
#[test]
fn lut_exp_monotonic() {
let mut prev = fast_exp_neg_lut(0.0);
for i in 1..160 {
let x = i as f32 * 0.05;
let val = fast_exp_neg_lut(x);
assert!(val <= prev + 1e-7, "not monotonic at x={x}");
prev = val;
}
}
#[test]
fn tier_selection_clear_hot() {
let cfg = default_config();
let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 0);
let target = choose_tier(&cfg, 100, &meta);
assert_eq!(target, Some(Tier::Tier1));
}
#[test]
fn tier_selection_clear_cold() {
let cfg = default_config();
let meta = make_meta(0.0, 0, 0, Tier::Tier1, 0);
let target = choose_tier(&cfg, 10_000, &meta);
assert_eq!(target, Some(Tier::Tier3));
}
#[test]
fn tier_selection_hysteresis_prevents_upgrade() {
let cfg = TierConfig {
hysteresis: 0.10,
..default_config()
};
let meta = make_meta(0.4, u64::MAX, 50, Tier::Tier2, 0);
let score = compute_score(&cfg, 50, &meta);
assert!(score > cfg.t1, "score={score}");
assert!(score < cfg.t1 + cfg.hysteresis, "score={score}");
let target = choose_tier(&cfg, 50, &meta);
assert_eq!(
target, None,
"score={score} should be within hysteresis band"
);
}
#[test]
fn tier_selection_hysteresis_prevents_downgrade() {
let cfg = TierConfig {
hysteresis: 0.10,
..default_config()
};
let meta = make_meta(0.5, 0x0000_0000_FFFF_FFFF, 90, Tier::Tier1, 0);
let score = compute_score(&cfg, 100, &meta);
assert!(
score < cfg.t1 && score > cfg.t1 - cfg.hysteresis,
"score={score}, expected in ({}, {})",
cfg.t1 - cfg.hysteresis,
cfg.t1
);
let target = choose_tier(&cfg, 100, &meta);
assert_eq!(
target, None,
"hysteresis should prevent downgrade, score={score}"
);
}
#[test]
fn touch_increments_count() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
assert_eq!(meta.access_count, 0);
touch(&cfg, 1, &mut meta);
assert_eq!(meta.access_count, 1);
touch(&cfg, 2, &mut meta);
assert_eq!(meta.access_count, 2);
}
#[test]
fn touch_updates_ema() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
assert_eq!(meta.ema_rate, 0.0);
touch(&cfg, 1, &mut meta);
assert!((meta.ema_rate - 0.3).abs() < 1e-6);
touch(&cfg, 2, &mut meta);
assert!((meta.ema_rate - 0.51).abs() < 1e-6);
}
#[test]
fn touch_updates_window() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
meta.access_window = 0;
touch(&cfg, 1, &mut meta);
assert_eq!(meta.access_window, 1);
touch(&cfg, 3, &mut meta);
assert_eq!(meta.access_window, 0b101);
}
#[test]
fn touch_same_tick() {
let cfg = default_config();
let mut meta = BlockMeta::new(5);
meta.access_window = 0b1010;
touch(&cfg, 5, &mut meta);
assert_eq!(meta.access_window, 0b1011);
}
#[test]
fn touch_large_gap_clears_window() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
meta.access_window = u64::MAX;
touch(&cfg, 100, &mut meta);
assert_eq!(meta.access_window, 1);
}
#[test]
fn min_residency_blocks_migration() {
let cfg = TierConfig {
min_residency: 10,
..default_config()
};
let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 95);
let target = choose_tier(&cfg, 100, &meta);
assert_eq!(target, None);
}
#[test]
fn min_residency_allows_after_enough_ticks() {
let cfg = TierConfig {
min_residency: 10,
..default_config()
};
let meta = make_meta(1.0, u64::MAX, 100, Tier::Tier3, 90);
let target = choose_tier(&cfg, 100, &meta);
assert_eq!(target, Some(Tier::Tier1));
}
#[test]
fn candidates_upgrades_before_downgrades() {
let cfg = default_config();
let hot_meta = make_meta(1.0, u64::MAX, 50, Tier::Tier3, 0);
let cold_meta = make_meta(0.0, 0, 0, Tier::Tier1, 0);
let blocks = vec![(BlockKey(1), &cold_meta), (BlockKey(2), &hot_meta)];
let candidates = select_candidates(&cfg, 50, &blocks);
assert!(candidates.len() >= 2, "expected at least 2 candidates");
assert_eq!(candidates[0].key, BlockKey(2));
assert_eq!(candidates[0].target_tier, Tier::Tier1);
assert_eq!(candidates[1].key, BlockKey(1));
assert_eq!(candidates[1].target_tier, Tier::Tier3);
}
#[test]
fn candidates_upgrades_sorted_by_highest_score() {
let cfg = default_config();
let meta_a = make_meta(0.9, u64::MAX, 50, Tier::Tier3, 0);
let meta_b = make_meta(1.0, u64::MAX, 50, Tier::Tier3, 0);
let blocks = vec![(BlockKey(1), &meta_a), (BlockKey(2), &meta_b)];
let candidates = select_candidates(&cfg, 50, &blocks);
assert!(candidates.len() >= 2);
assert_eq!(candidates[0].key, BlockKey(2));
assert_eq!(candidates[1].key, BlockKey(1));
}
#[test]
fn candidates_empty_when_all_stable() {
let cfg = default_config();
let meta = make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0);
let blocks = vec![(BlockKey(1), &meta)];
let candidates = select_candidates(&cfg, 50, &blocks);
let _ = candidates;
}
#[test]
fn bits_tier0() {
assert_eq!(bits_for_tier(&default_config(), Tier::Tier0, 0), 0);
}
#[test]
fn bits_tier1() {
assert_eq!(bits_for_tier(&default_config(), Tier::Tier1, 0), 8);
}
#[test]
fn bits_tier2_normal() {
assert_eq!(bits_for_tier(&default_config(), Tier::Tier2, 0), 7);
}
#[test]
fn bits_tier3() {
assert_eq!(bits_for_tier(&default_config(), Tier::Tier3, 0), 3);
}
#[test]
fn bits_tier2_aggressive() {
let cfg = TierConfig {
warm_aggressive_threshold: Some(1024),
..default_config()
};
assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 512), 7);
assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 1024), 7); assert_eq!(bits_for_tier(&cfg, Tier::Tier2, 1025), 5);
}
#[test]
fn edge_zero_access_count() {
let cfg = default_config();
let meta = BlockMeta::new(0);
let score = compute_score(&cfg, 0, &meta);
assert!((score - cfg.w_rec).abs() < 1e-4, "score={score}");
}
#[test]
fn edge_max_timestamp() {
let cfg = default_config();
let meta = make_meta(0.5, 0xAAAA_AAAA_AAAA_AAAA, u64::MAX - 1, Tier::Tier2, 0);
let score = compute_score(&cfg, u64::MAX, &meta);
assert!(score.is_finite(), "score={score}");
}
#[test]
fn edge_touch_at_u64_max() {
let cfg = default_config();
let mut meta = BlockMeta::new(u64::MAX - 1);
touch(&cfg, u64::MAX, &mut meta);
assert_eq!(meta.last_access, u64::MAX);
assert_eq!(meta.access_count, 1);
}
#[test]
fn edge_access_count_saturates() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
meta.access_count = u64::MAX;
touch(&cfg, 1, &mut meta);
assert_eq!(meta.access_count, u64::MAX);
}
#[test]
fn tick_decay_reduces_ema() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
meta.ema_rate = 1.0;
meta.access_window = 0b1111;
tick_decay(&cfg, &mut meta);
assert!((meta.ema_rate - 0.7).abs() < 1e-6, "ema={}", meta.ema_rate);
assert_eq!(meta.access_window, 0b1111_0);
}
#[test]
fn tick_decay_converges_to_zero() {
let cfg = default_config();
let mut meta = BlockMeta::new(0);
meta.ema_rate = 1.0;
for _ in 0..200 {
tick_decay(&cfg, &mut meta);
}
assert!(meta.ema_rate < 1e-10, "ema={}", meta.ema_rate);
}
#[test]
fn tier_config_default_weights_sum_to_one() {
let cfg = default_config();
let sum = cfg.w_ema + cfg.w_pop + cfg.w_rec;
assert!((sum - 1.0).abs() < 1e-6, "sum={sum}");
}
#[test]
fn block_meta_new_defaults() {
let meta = BlockMeta::new(42);
assert_eq!(meta.ema_rate, 0.0);
assert_eq!(meta.access_window, 0);
assert_eq!(meta.last_access, 42);
assert_eq!(meta.access_count, 0);
assert_eq!(meta.current_tier, Tier::Tier1);
assert_eq!(meta.tier_since, 42);
}
#[test]
fn tier_ordering() {
assert!(Tier::Tier0 < Tier::Tier1);
assert!(Tier::Tier1 < Tier::Tier2);
assert!(Tier::Tier2 < Tier::Tier3);
}
#[test]
fn batch_scores_match_individual() {
let cfg = default_config();
let metas: Vec<BlockMeta> = vec![
make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
make_meta(0.0, 0, 0, Tier::Tier3, 0),
make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0),
];
let batch = compute_scores_batch(&cfg, 100, &metas);
for (i, meta) in metas.iter().enumerate() {
let single = compute_score(&cfg, 100, meta);
assert!((batch[i] - single).abs() < 1e-6, "index {i}");
}
}
#[test]
fn batch_tiers_match_individual() {
let cfg = default_config();
let metas: Vec<BlockMeta> = vec![
make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
make_meta(0.0, 0, 0, Tier::Tier3, 0),
];
let batch = choose_tiers_batch(&cfg, 100, &metas);
for (i, meta) in metas.iter().enumerate() {
let single = choose_tier(&cfg, 100, meta);
assert_eq!(batch[i], single, "index {i}");
}
}
#[test]
fn score_and_partition_distributes_correctly() {
let cfg = default_config();
let metas: Vec<BlockMeta> = vec![
make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0), make_meta(0.5, 0x0000_0000_FFFF_FFFF, 90, Tier::Tier2, 0), make_meta(0.0, 0, 0, Tier::Tier3, 0), ];
let part = score_and_partition(&cfg, 100, &metas);
assert!(!part.hot.is_empty(), "should have hot blocks");
assert_eq!(part.scores.len(), 3);
}
#[test]
fn top_k_coldest_returns_lowest() {
let cfg = default_config();
let metas: Vec<BlockMeta> = vec![
make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0),
make_meta(0.0, 0, 0, Tier::Tier3, 0),
make_meta(0.5, 0x0000_0000_FFFF_FFFF, 50, Tier::Tier2, 0),
];
let coldest = top_k_coldest(&cfg, 100, &metas, 2);
assert_eq!(coldest.len(), 2);
assert_eq!(coldest[0].0, 1);
assert!(coldest[0].1 <= coldest[1].1);
}
#[test]
fn top_k_coldest_k_exceeds_len() {
let cfg = default_config();
let metas: Vec<BlockMeta> = vec![make_meta(1.0, u64::MAX, 100, Tier::Tier1, 0)];
let coldest = top_k_coldest(&cfg, 100, &metas, 10);
assert_eq!(coldest.len(), 1);
}
#[test]
fn batch_empty_input() {
let cfg = default_config();
let empty: Vec<BlockMeta> = vec![];
assert!(compute_scores_batch(&cfg, 100, &empty).is_empty());
assert!(choose_tiers_batch(&cfg, 100, &empty).is_empty());
let part = score_and_partition(&cfg, 100, &empty);
assert!(
part.hot.is_empty()
&& part.warm.is_empty()
&& part.cold.is_empty()
&& part.evict.is_empty()
);
assert!(top_k_coldest(&cfg, 100, &empty, 5).is_empty());
}
}