use super::hotspot::DynamicHotspotTable;
use crate::dag::arena::DagArena;
use crate::dag::node::{ChildList, DagNode, DagNodeId};
#[derive(Debug)]
pub struct CompactRemap {
blocks: Vec<u64>,
block_prefix: Vec<u32>,
n_kept: u32,
}
impl CompactRemap {
#[must_use]
pub fn build(n_slots: usize, kept: impl Iterator<Item = usize>) -> Self {
let n_words = n_slots.div_ceil(64);
let mut blocks = vec![0u64; n_words];
let mut n_kept = 0u32;
for idx in kept {
let word = idx / 64;
let bit = idx % 64;
if word < n_words {
blocks[word] |= 1u64 << bit;
n_kept += 1;
}
}
let mut block_prefix = Vec::with_capacity(n_words);
let mut running = 0u32;
for &w in &blocks {
block_prefix.push(running);
running = running.saturating_add(w.count_ones());
}
Self {
blocks,
block_prefix,
n_kept,
}
}
#[must_use]
pub fn translate(&self, old_id: DagNodeId) -> Option<DagNodeId> {
if old_id.is_none() {
return Some(DagNodeId::NONE);
}
let idx = old_id.index();
let word = idx / 64;
let bit = idx % 64;
let block = *self.blocks.get(word)?;
if block & (1u64 << bit) == 0 {
return None; }
let prefix = self.block_prefix[word];
let mask = (1u64 << bit).wrapping_shl(1).wrapping_sub(1) | (1u64 << bit);
let rank = prefix + (block & mask).count_ones();
Some(DagNodeId::new(rank - 1))
}
#[must_use]
pub const fn n_kept(&self) -> u32 {
self.n_kept
}
}
#[derive(Debug)]
pub struct EvictionResult {
pub arena: DagArena,
pub remap: CompactRemap,
}
impl EvictionResult {
#[must_use]
pub fn translate(&self, old: DagNodeId) -> Option<DagNodeId> {
self.remap.translate(old)
}
}
pub trait EvictionPolicy {
fn is_hot(
&self,
hotspots: &super::hotspot::DynamicHotspotTable,
id: crate::dag::node::DagNodeId,
) -> bool;
}
pub struct FrequencyPolicy {
pub threshold: u64,
}
impl FrequencyPolicy {
#[must_use]
pub const fn new(threshold: u64) -> Self {
Self { threshold }
}
}
impl EvictionPolicy for FrequencyPolicy {
fn is_hot(
&self,
hotspots: &super::hotspot::DynamicHotspotTable,
id: crate::dag::node::DagNodeId,
) -> bool {
hotspots.is_hot(id, self.threshold)
}
}
pub struct RecencyWeightedPolicy {
pub min_freq: u64,
pub min_ratio: f64,
}
impl RecencyWeightedPolicy {
#[must_use]
pub const fn new(min_freq: u64, min_ratio: f64) -> Self {
Self {
min_freq,
min_ratio,
}
}
}
impl EvictionPolicy for RecencyWeightedPolicy {
#[allow(clippy::cast_precision_loss)]
fn is_hot(
&self,
hotspots: &super::hotspot::DynamicHotspotTable,
id: crate::dag::node::DagNodeId,
) -> bool {
let freq = hotspots.get_frequency(id);
if freq < self.min_freq {
return false;
}
let total = hotspots.total_accesses();
if total == 0 {
return false;
}
(freq as f64 / total as f64) >= self.min_ratio
}
}
pub struct CompositePolicy<A: EvictionPolicy, B: EvictionPolicy> {
pub primary: A,
pub secondary: B,
pub require_both: bool,
}
impl<A: EvictionPolicy, B: EvictionPolicy> CompositePolicy<A, B> {
#[must_use]
pub const fn and(a: A, b: B) -> Self {
Self {
primary: a,
secondary: b,
require_both: true,
}
}
#[must_use]
pub const fn or(a: A, b: B) -> Self {
Self {
primary: a,
secondary: b,
require_both: false,
}
}
}
impl<A: EvictionPolicy, B: EvictionPolicy> EvictionPolicy for CompositePolicy<A, B> {
fn is_hot(
&self,
hotspots: &super::hotspot::DynamicHotspotTable,
id: crate::dag::node::DagNodeId,
) -> bool {
if self.require_both {
self.primary.is_hot(hotspots, id) && self.secondary.is_hot(hotspots, id)
} else {
self.primary.is_hot(hotspots, id) || self.secondary.is_hot(hotspots, id)
}
}
}
#[must_use]
pub fn evict_cold_nodes(
arena: &DagArena,
hotspots: &DynamicHotspotTable,
keep_threshold: u64,
) -> EvictionResult {
evict_nodes_with_policy(arena, hotspots, &FrequencyPolicy::new(keep_threshold))
}
#[must_use]
pub fn evict_nodes_with_policy<P: EvictionPolicy>(
arena: &DagArena,
hotspots: &DynamicHotspotTable,
policy: &P,
) -> EvictionResult {
evict_nodes_budgeted_with_policy(arena, hotspots, policy, usize::MAX)
}
#[must_use]
pub fn evict_cold_nodes_budgeted(
arena: &DagArena,
hotspots: &DynamicHotspotTable,
keep_threshold: u64,
budget: usize,
) -> EvictionResult {
evict_nodes_budgeted_with_policy(
arena,
hotspots,
&FrequencyPolicy::new(keep_threshold),
budget,
)
}
#[must_use]
pub fn evict_nodes_budgeted_with_policy<P: EvictionPolicy>(
arena: &DagArena,
hotspots: &DynamicHotspotTable,
policy: &P,
budget: usize,
) -> EvictionResult {
let total = arena.len();
let mut protected: Vec<bool> = vec![false; total];
let mut work: Vec<u32> = Vec::with_capacity(64);
#[allow(clippy::cast_possible_truncation)]
for i in 0..total as u32 {
let id = DagNodeId::new(i);
if policy.is_hot(hotspots, id) {
work.push(i);
}
}
while let Some(idx) = work.pop() {
let i = idx as usize;
if i >= total || protected[i] {
continue;
}
protected[i] = true;
if let Some(node) = arena.get(DagNodeId::new(idx)) {
for child in node.children.iter() {
let ci = child.index();
if ci < total && !protected[ci] {
work.push(child.value());
}
}
}
}
let mut compacted = DagArena::new();
let mut flat_remap: Vec<DagNodeId> = vec![DagNodeId::NONE; total];
let mut kept_indices: Vec<usize> = Vec::new();
let mut count = 0usize;
for (i, is_protected) in protected.iter().enumerate() {
if !*is_protected {
continue;
}
if count >= budget {
break;
}
count += 1;
#[allow(clippy::cast_possible_truncation)]
let old_id = DagNodeId::new(i as u32);
let Some(original) = arena.get(old_id) else {
continue;
};
let new_children: Vec<DagNodeId> = original
.children
.iter()
.filter_map(|c| {
let mapped = flat_remap
.get(c.index())
.copied()
.unwrap_or(DagNodeId::NONE);
if mapped.is_none() { None } else { Some(mapped) }
})
.collect();
let child_list = ChildList::from_slice(&new_children);
let new_node = DagNode {
kind: original.kind,
meta: original.meta.clone(),
children: child_list,
};
let new_id = compacted.alloc(new_node);
flat_remap[old_id.index()] = new_id;
kept_indices.push(i);
}
let compacted_len = compacted.len();
let mut to_clear: Vec<DagNodeId> = Vec::new();
for i in 0..compacted_len {
#[allow(clippy::cast_possible_truncation)]
let id = DagNodeId::new(i as u32);
if let Some(node) = compacted.get(id)
&& node.meta.flags.is_canonical()
{
let children_valid = node
.children
.iter()
.all(|c| !c.is_none() && c.index() < compacted_len);
if !children_valid {
to_clear.push(id);
}
}
}
for id in to_clear {
if let Some(node) = compacted.get_mut(id) {
node.meta.flags = node.meta.flags.without_canonical();
}
}
let remap = CompactRemap::build(total, kept_indices.into_iter());
EvictionResult {
arena: compacted,
remap,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::builder::DagBuilder;
#[test]
fn eviction_drops_cold_branches_and_preserves_hot_closure() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let hot = DynamicHotspotTable::new();
for _ in 0..10 {
hot.record_access(sum);
}
let result = evict_cold_nodes(b.arena(), &hot, 5);
assert_eq!(result.arena.len(), 3);
let new_sum = result.translate(sum).expect("sum survives");
let new_node = result.arena.get(new_sum).expect("new sum");
for c in new_node.children.iter() {
assert!(
result.arena.get(c).is_some(),
"child {c:?} dangles after eviction"
);
}
}
#[test]
fn cold_only_arena_compacts_to_empty() {
let mut b = DagBuilder::new();
let _ = b.variable("x");
let _ = b.variable("y");
let hot = DynamicHotspotTable::new();
let result = evict_cold_nodes(b.arena(), &hot, 1);
assert_eq!(result.arena.len(), 0);
assert_eq!(result.remap.n_kept(), 0);
}
#[test]
fn unrelated_subgraph_is_evicted() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let av = b.variable("a");
let bv = b.variable("b");
let _prod = b.mul(av, bv);
let hot = DynamicHotspotTable::new();
for _ in 0..10 {
hot.record_access(sum);
}
let result = evict_cold_nodes(b.arena(), &hot, 5);
assert_eq!(result.arena.len(), 3);
assert!(result.translate(sum).is_some());
assert!(result.translate(av).is_none());
}
#[test]
fn random_dag_has_no_dangling_after_eviction() {
let mut b = DagBuilder::new();
let mut ids: Vec<DagNodeId> = (0..6).map(|i| b.constant(f64::from(i))).collect();
for i in 6..200 {
let lhs = ids[(i * 17) % ids.len()];
let rhs = ids[(i * 23) % ids.len()];
let op = if i % 3 == 0 {
b.add(lhs, rhs)
} else if i % 3 == 1 {
b.mul(lhs, rhs)
} else {
b.sub(lhs, rhs)
};
ids.push(op);
}
let hot = DynamicHotspotTable::new();
for (i, id) in ids.iter().enumerate() {
if i % 3 == 0 {
for _ in 0..5 {
hot.record_access(*id);
}
}
}
let result = evict_cold_nodes(b.arena(), &hot, 3);
for i in 0..result.arena.len() {
#[allow(clippy::cast_possible_truncation)]
let id = DagNodeId::new(i as u32);
let node = result.arena.get(id).expect("kept node");
for c in node.children.iter() {
assert!(
result.arena.get(c).is_some(),
"node #{i} has dangling child {c:?}"
);
}
}
}
#[test]
fn recency_weighted_policy_protects_dominant_nodes() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let hot = DynamicHotspotTable::new();
for _ in 0..90 {
hot.record_access(sum);
}
for _ in 0..10 {
hot.record_access(x);
}
let policy = RecencyWeightedPolicy::new(5, 0.5);
let result = evict_nodes_with_policy(b.arena(), &hot, &policy);
assert_eq!(result.arena.len(), 3);
assert!(result.translate(sum).is_some());
}
#[test]
fn composite_and_policy_requires_both_conditions() {
let mut b = DagBuilder::new();
let x = b.variable("x");
let y = b.variable("y");
let sum = b.add(x, y);
let hot = DynamicHotspotTable::new();
for _ in 0..10 {
hot.record_access(sum);
}
let policy =
CompositePolicy::and(FrequencyPolicy::new(5), RecencyWeightedPolicy::new(1, 0.05));
let result = evict_nodes_with_policy(b.arena(), &hot, &policy);
assert!(result.translate(sum).is_some());
assert_eq!(result.arena.len(), 3);
}
#[test]
fn budgeted_eviction_respects_limit() {
let mut b = DagBuilder::new();
for i in 0..6 {
let _ = b.constant(f64::from(i));
}
let hot = DynamicHotspotTable::new();
for i in 0..6_u32 {
for _ in 0..10 {
hot.record_access(DagNodeId::new(i));
}
}
let result = evict_cold_nodes_budgeted(b.arena(), &hot, 5, 3);
assert_eq!(result.arena.len(), 3);
}
}