#![allow(unsafe_code)]
use std::collections::HashMap;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::dag::node::DagNodeId;
pub const NUM_SHARDS: usize = 32;
#[derive(Debug, Default)]
#[repr(align(128))]
struct Shard {
frequencies: RwLock<HashMap<DagNodeId, AtomicU64>>,
total_accesses: AtomicU64,
}
#[derive(Debug)]
pub struct DynamicHotspotTable {
shards: Box<[Shard; NUM_SHARDS]>,
}
impl Default for DynamicHotspotTable {
fn default() -> Self {
Self::new()
}
}
impl DynamicHotspotTable {
#[must_use]
pub fn new() -> Self {
let mut shards: Vec<Shard> = Vec::with_capacity(NUM_SHARDS);
for _ in 0..NUM_SHARDS {
shards.push(Shard::default());
}
let array: Box<[Shard; NUM_SHARDS]> = shards
.into_boxed_slice()
.try_into()
.unwrap_or_else(|_| unreachable!("Vec length is exactly NUM_SHARDS by construction"));
Self { shards: array }
}
const fn shard_idx(id: DagNodeId) -> usize {
id.0 as usize % NUM_SHARDS
}
fn shard(&self, id: DagNodeId) -> &Shard {
&self.shards[Self::shard_idx(id)]
}
pub fn record_access(&self, id: DagNodeId) {
let shard = self.shard(id);
{
let guard = shard
.frequencies
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(counter) = guard.get(&id) {
counter.fetch_add(1, Ordering::Relaxed);
shard.total_accesses.fetch_add(1, Ordering::Release);
return;
}
}
{
let mut guard = shard
.frequencies
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard
.entry(id)
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
shard.total_accesses.fetch_add(1, Ordering::Release);
}
#[must_use]
pub fn get_frequency(&self, id: DagNodeId) -> u64 {
let shard = self.shard(id);
let guard = shard
.frequencies
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard.get(&id).map_or(0, |c| c.load(Ordering::Relaxed))
}
#[must_use]
pub fn is_hot(&self, id: DagNodeId, threshold: u64) -> bool {
self.get_frequency(id) >= threshold
}
#[must_use]
pub fn total_accesses(&self) -> u64 {
self.shards
.iter()
.map(|s| s.total_accesses.load(Ordering::Acquire))
.sum()
}
#[must_use]
pub fn snapshot(&self) -> Vec<(DagNodeId, u64)> {
let mut out = Vec::new();
for shard in self.shards.iter() {
let guard = shard
.frequencies
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
out.extend(guard.iter().map(|(k, v)| (*k, v.load(Ordering::Relaxed))));
}
out
}
pub fn clear(&self) {
for shard in self.shards.iter() {
shard
.frequencies
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
shard.total_accesses.store(0, Ordering::Release);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shards_are_cache_line_aligned() {
assert_eq!(core::mem::align_of::<Shard>(), 128);
}
#[test]
fn records_and_reads_consistently() {
let table = DynamicHotspotTable::new();
let id = DagNodeId::new(7);
for _ in 0..10 {
table.record_access(id);
}
assert_eq!(table.get_frequency(id), 10);
assert!(table.is_hot(id, 10));
assert!(!table.is_hot(id, 11));
assert_eq!(table.total_accesses(), 10);
}
#[test]
fn distinct_ids_land_on_distinct_shards_modulo_n() {
let table = DynamicHotspotTable::new();
for i in 0..NUM_SHARDS {
#[allow(clippy::cast_possible_truncation)]
let id = DagNodeId::new(i as u32);
table.record_access(id);
}
let snap = table.snapshot();
assert_eq!(snap.len(), NUM_SHARDS);
}
#[test]
fn parallel_record_accesses_count_correctly() {
use std::sync::Arc;
let table = Arc::new(DynamicHotspotTable::new());
let id = DagNodeId::new(42);
let threads: Vec<_> = (0..8)
.map(|_| {
let table = Arc::clone(&table);
std::thread::spawn(move || {
for _ in 0..1000 {
table.record_access(id);
}
})
})
.collect();
for t in threads {
t.join().expect("thread joined");
}
assert_eq!(table.get_frequency(id), 8 * 1000);
assert_eq!(table.total_accesses(), 8 * 1000);
}
#[test]
fn clear_resets_everything() {
let table = DynamicHotspotTable::new();
for i in 0..50_u32 {
table.record_access(DagNodeId::new(i));
}
table.clear();
assert_eq!(table.total_accesses(), 0);
assert_eq!(table.snapshot().len(), 0);
}
}