#![forbid(unsafe_code)]
use lru::LruCache;
use rustc_hash::FxHasher;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
pub const DEFAULT_CACHE_CAPACITY: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub size: usize,
pub capacity: usize,
}
impl CacheStats {
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
#[derive(Debug)]
pub struct WidthCache {
cache: LruCache<u64, usize>,
hits: u64,
misses: u64,
}
impl WidthCache {
#[must_use]
pub fn new(capacity: usize) -> Self {
let capacity = NonZeroUsize::new(capacity.max(1)).expect("capacity must be > 0");
Self {
cache: LruCache::new(capacity),
hits: 0,
misses: 0,
}
}
#[must_use]
pub fn with_default_capacity() -> Self {
Self::new(DEFAULT_CACHE_CAPACITY)
}
#[inline]
pub fn get_or_compute(&mut self, text: &str) -> usize {
self.get_or_compute_with(text, crate::display_width)
}
pub fn get_or_compute_with<F>(&mut self, text: &str, compute: F) -> usize
where
F: FnOnce(&str) -> usize,
{
let hash = hash_text(text);
if let Some(&width) = self.cache.get(&hash) {
self.hits += 1;
return width;
}
self.misses += 1;
let width = compute(text);
self.cache.put(hash, width);
width
}
#[must_use]
pub fn contains(&self, text: &str) -> bool {
let hash = hash_text(text);
self.cache.contains(&hash)
}
#[must_use]
pub fn get(&mut self, text: &str) -> Option<usize> {
let hash = hash_text(text);
self.cache.get(&hash).copied()
}
#[must_use]
pub fn peek(&self, text: &str) -> Option<usize> {
let hash = hash_text(text);
self.cache.peek(&hash).copied()
}
pub fn preload(&mut self, text: &str) {
let hash = hash_text(text);
if !self.cache.contains(&hash) {
let width = crate::display_width(text);
self.cache.put(hash, width);
}
}
pub fn preload_many<'a>(&mut self, texts: impl IntoIterator<Item = &'a str>) {
for text in texts {
self.preload(text);
}
}
pub fn clear(&mut self) {
self.cache.clear();
}
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
#[inline]
#[must_use]
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits,
misses: self.misses,
size: self.cache.len(),
capacity: self.cache.cap().get(),
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.cache.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
#[inline]
#[must_use]
pub fn capacity(&self) -> usize {
self.cache.cap().get()
}
pub fn resize(&mut self, new_capacity: usize) {
let new_capacity = NonZeroUsize::new(new_capacity.max(1)).expect("capacity must be > 0");
self.cache.resize(new_capacity);
}
}
impl Default for WidthCache {
fn default() -> Self {
Self::with_default_capacity()
}
}
#[inline]
fn hash_text(text: &str) -> u64 {
let mut hasher = FxHasher::default();
text.hash(&mut hasher);
hasher.finish()
}
#[cfg(feature = "thread_local_cache")]
thread_local! {
static THREAD_CACHE: std::cell::RefCell<WidthCache> =
std::cell::RefCell::new(WidthCache::with_default_capacity());
}
#[cfg(feature = "thread_local_cache")]
pub fn cached_width(text: &str) -> usize {
THREAD_CACHE.with(|cache| cache.borrow_mut().get_or_compute(text))
}
#[cfg(feature = "thread_local_cache")]
pub fn clear_thread_cache() {
THREAD_CACHE.with(|cache| cache.borrow_mut().clear());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_cache_is_empty() {
let cache = WidthCache::new(100);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
assert_eq!(cache.capacity(), 100);
}
#[test]
fn default_capacity() {
let cache = WidthCache::with_default_capacity();
assert_eq!(cache.capacity(), DEFAULT_CACHE_CAPACITY);
}
#[test]
fn get_or_compute_caches_value() {
let mut cache = WidthCache::new(100);
let width1 = cache.get_or_compute("hello");
assert_eq!(width1, 5);
assert_eq!(cache.len(), 1);
let width2 = cache.get_or_compute("hello");
assert_eq!(width2, 5);
assert_eq!(cache.len(), 1);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn get_or_compute_different_strings() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello");
cache.get_or_compute("world");
cache.get_or_compute("foo");
assert_eq!(cache.len(), 3);
let stats = cache.stats();
assert_eq!(stats.misses, 3);
assert_eq!(stats.hits, 0);
}
#[test]
fn get_or_compute_cjk() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("你好");
assert_eq!(width, 4); }
#[test]
fn contains() {
let mut cache = WidthCache::new(100);
assert!(!cache.contains("hello"));
cache.get_or_compute("hello");
assert!(cache.contains("hello"));
}
#[test]
fn get_returns_none_for_missing() {
let mut cache = WidthCache::new(100);
assert!(cache.get("missing").is_none());
}
#[test]
fn get_returns_cached_value() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello");
let width = cache.get("hello");
assert_eq!(width, Some(5));
}
#[test]
fn peek_does_not_update_lru() {
let mut cache = WidthCache::new(2);
cache.get_or_compute("a");
cache.get_or_compute("b");
let _ = cache.peek("a");
cache.get_or_compute("c");
assert!(!cache.contains("a"));
assert!(cache.contains("b"));
assert!(cache.contains("c"));
}
#[test]
fn lru_eviction() {
let mut cache = WidthCache::new(2);
cache.get_or_compute("a");
cache.get_or_compute("b");
cache.get_or_compute("c");
assert!(!cache.contains("a"));
assert!(cache.contains("b"));
assert!(cache.contains("c"));
}
#[test]
fn lru_refresh_on_access() {
let mut cache = WidthCache::new(2);
cache.get_or_compute("a");
cache.get_or_compute("b");
cache.get_or_compute("a"); cache.get_or_compute("c");
assert!(cache.contains("a"));
assert!(!cache.contains("b"));
assert!(cache.contains("c"));
}
#[test]
fn preload() {
let mut cache = WidthCache::new(100);
cache.preload("hello");
assert!(cache.contains("hello"));
assert_eq!(cache.peek("hello"), Some(5));
let stats = cache.stats();
assert_eq!(stats.misses, 0); assert_eq!(stats.hits, 0);
}
#[test]
fn preload_many() {
let mut cache = WidthCache::new(100);
cache.preload_many(["hello", "world", "foo"]);
assert_eq!(cache.len(), 3);
}
#[test]
fn clear() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello");
cache.get_or_compute("world");
cache.clear();
assert!(cache.is_empty());
assert!(!cache.contains("hello"));
}
#[test]
fn reset_stats() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello");
cache.get_or_compute("hello");
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
cache.reset_stats();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn hit_rate() {
let stats = CacheStats {
hits: 75,
misses: 25,
size: 100,
capacity: 1000,
};
assert!((stats.hit_rate() - 0.75).abs() < 0.001);
}
#[test]
fn hit_rate_no_requests() {
let stats = CacheStats::default();
assert_eq!(stats.hit_rate(), 0.0);
}
#[test]
fn resize_smaller() {
let mut cache = WidthCache::new(100);
for i in 0..50 {
cache.get_or_compute(&format!("text{i}"));
}
assert_eq!(cache.len(), 50);
cache.resize(10);
assert!(cache.len() <= 10);
assert_eq!(cache.capacity(), 10);
}
#[test]
fn resize_larger() {
let mut cache = WidthCache::new(10);
cache.resize(100);
assert_eq!(cache.capacity(), 100);
}
#[test]
fn custom_compute_function() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute_with("hello", |_| 42);
assert_eq!(width, 42);
assert_eq!(cache.peek("hello"), Some(42));
}
#[test]
fn empty_string() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("");
assert_eq!(width, 0);
}
#[test]
fn hash_collision_handling() {
let mut cache = WidthCache::new(1000);
for i in 0..500 {
cache.get_or_compute(&format!("string{i}"));
}
assert_eq!(cache.len(), 500);
}
#[test]
fn unicode_strings() {
let mut cache = WidthCache::new(100);
assert_eq!(cache.get_or_compute("café"), 4);
assert_eq!(cache.get_or_compute("日本語"), 6);
assert_eq!(cache.get_or_compute("🎉"), 2);
assert_eq!(cache.len(), 3);
}
#[test]
fn combining_characters() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("e\u{0301}");
assert_eq!(width, 1);
}
#[test]
fn default_cache() {
let cache = WidthCache::default();
assert!(cache.is_empty());
assert_eq!(cache.capacity(), DEFAULT_CACHE_CAPACITY);
}
#[test]
fn cache_stats_debug() {
let stats = CacheStats {
hits: 10,
misses: 5,
size: 15,
capacity: 100,
};
let debug = format!("{:?}", stats);
assert!(debug.contains("CacheStats"));
assert!(debug.contains("10")); }
#[test]
fn cache_stats_default() {
let stats = CacheStats::default();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.size, 0);
assert_eq!(stats.capacity, 0);
}
#[test]
fn cache_stats_equality() {
let stats1 = CacheStats {
hits: 10,
misses: 5,
size: 15,
capacity: 100,
};
let stats2 = stats1; assert_eq!(stats1, stats2);
}
#[test]
fn clear_after_preload() {
let mut cache = WidthCache::new(100);
cache.preload_many(["hello", "world", "test"]);
assert_eq!(cache.len(), 3);
cache.clear();
assert!(cache.is_empty());
assert!(!cache.contains("hello"));
}
#[test]
fn preload_existing_is_noop() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello"); let len_before = cache.len();
cache.preload("hello"); assert_eq!(cache.len(), len_before);
}
#[test]
fn minimum_capacity_is_one() {
let cache = WidthCache::new(0);
assert_eq!(cache.capacity(), 1);
}
#[test]
fn width_cache_debug() {
let cache = WidthCache::new(10);
let debug = format!("{:?}", cache);
assert!(debug.contains("WidthCache"));
}
#[test]
fn emoji_zwj_sequence() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("👨👩👧");
assert!(width >= 1);
}
#[test]
fn emoji_with_skin_tone() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("👍🏻");
assert!(width >= 1);
}
#[test]
fn flag_emoji() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("🇺🇸");
assert!(width >= 1);
}
#[test]
fn mixed_width_strings() {
let mut cache = WidthCache::new(100);
let width = cache.get_or_compute("Hello你好World");
assert_eq!(width, 14); }
#[test]
fn stats_size_reflects_cache_len() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("a");
cache.get_or_compute("b");
cache.get_or_compute("c");
let stats = cache.stats();
assert_eq!(stats.size, cache.len());
assert_eq!(stats.size, 3);
}
#[test]
fn stats_capacity_matches() {
let cache = WidthCache::new(42);
let stats = cache.stats();
assert_eq!(stats.capacity, 42);
}
#[test]
fn resize_to_zero_becomes_one() {
let mut cache = WidthCache::new(100);
cache.resize(0);
assert_eq!(cache.capacity(), 1);
}
#[test]
fn get_updates_lru_order() {
let mut cache = WidthCache::new(2);
cache.get_or_compute("a");
cache.get_or_compute("b");
let _ = cache.get("a");
cache.get_or_compute("c");
assert!(cache.contains("a"));
assert!(!cache.contains("b"));
assert!(cache.contains("c"));
}
#[test]
fn contains_does_not_modify_stats() {
let mut cache = WidthCache::new(100);
cache.get_or_compute("hello");
let stats_before = cache.stats();
let _ = cache.contains("hello");
let _ = cache.contains("missing");
let stats_after = cache.stats();
assert_eq!(stats_before.hits, stats_after.hits);
assert_eq!(stats_before.misses, stats_after.misses);
}
#[test]
fn peek_returns_none_for_missing() {
let cache = WidthCache::new(100);
assert!(cache.peek("missing").is_none());
}
#[test]
fn custom_compute_called_once() {
let mut cache = WidthCache::new(100);
let mut call_count = 0;
cache.get_or_compute_with("test", |_| {
call_count += 1;
10
});
cache.get_or_compute_with("test", |_| {
call_count += 1;
20 });
assert_eq!(call_count, 1);
assert_eq!(cache.peek("test"), Some(10));
}
#[test]
fn whitespace_strings() {
let mut cache = WidthCache::new(100);
assert_eq!(cache.get_or_compute(" "), 3); assert_eq!(cache.get_or_compute("\t"), 1); assert_eq!(cache.get_or_compute("\n"), 1); }
}
#[derive(Debug, Clone)]
pub struct CountMinSketch {
counters: Vec<Vec<u8>>,
depth: usize,
width: usize,
total_increments: u64,
reset_interval: u64,
}
const CMS_MAX_COUNT: u8 = 15;
const CMS_DEFAULT_WIDTH: usize = 1024;
const CMS_DEFAULT_DEPTH: usize = 4;
const CMS_DEFAULT_RESET_INTERVAL: u64 = 8192;
impl CountMinSketch {
pub fn new(width: usize, depth: usize, reset_interval: u64) -> Self {
let width = width.max(1);
let depth = depth.max(1);
Self {
counters: vec![vec![0u8; width]; depth],
depth,
width,
total_increments: 0,
reset_interval: reset_interval.max(1),
}
}
pub fn with_defaults() -> Self {
Self::new(
CMS_DEFAULT_WIDTH,
CMS_DEFAULT_DEPTH,
CMS_DEFAULT_RESET_INTERVAL,
)
}
pub fn increment(&mut self, hash: u64) {
for row in 0..self.depth {
let idx = self.index(hash, row);
self.counters[row][idx] = self.counters[row][idx].saturating_add(1).min(CMS_MAX_COUNT);
}
self.total_increments += 1;
if self.total_increments >= self.reset_interval {
self.halve();
}
}
pub fn estimate(&self, hash: u64) -> u8 {
let mut min = u8::MAX;
for row in 0..self.depth {
let idx = self.index(hash, row);
min = min.min(self.counters[row][idx]);
}
min
}
pub fn total_increments(&self) -> u64 {
self.total_increments
}
fn halve(&mut self) {
for row in &mut self.counters {
for c in row.iter_mut() {
*c /= 2;
}
}
self.total_increments = 0;
}
pub fn clear(&mut self) {
for row in &mut self.counters {
row.fill(0);
}
self.total_increments = 0;
}
#[inline]
fn index(&self, hash: u64, row: usize) -> usize {
let mixed = hash
.wrapping_mul(0x517c_c1b7_2722_0a95)
.wrapping_add(row as u64);
let mixed = mixed ^ (mixed >> 32);
(mixed as usize) % self.width
}
}
#[derive(Debug, Clone)]
pub struct Doorkeeper {
bits: Vec<u64>,
num_bits: usize,
}
const DOORKEEPER_DEFAULT_BITS: usize = 2048;
impl Doorkeeper {
pub fn new(num_bits: usize) -> Self {
let num_bits = num_bits.max(64);
let num_words = num_bits.div_ceil(64);
Self {
bits: vec![0u64; num_words],
num_bits,
}
}
pub fn with_defaults() -> Self {
Self::new(DOORKEEPER_DEFAULT_BITS)
}
pub fn check_and_set(&mut self, hash: u64) -> bool {
let idx = (hash as usize) % self.num_bits;
let word = idx / 64;
let bit = idx % 64;
let was_set = (self.bits[word] >> bit) & 1 == 1;
self.bits[word] |= 1 << bit;
was_set
}
pub fn contains(&self, hash: u64) -> bool {
let idx = (hash as usize) % self.num_bits;
let word = idx / 64;
let bit = idx % 64;
(self.bits[word] >> bit) & 1 == 1
}
pub fn clear(&mut self) {
self.bits.fill(0);
}
}
#[inline]
pub fn fingerprint_hash(text: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325; for &b in text.as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(0x0100_0000_01b3); }
h
}
#[inline]
pub fn tinylfu_admit(candidate_freq: u8, victim_freq: u8) -> bool {
candidate_freq > victim_freq
}
#[derive(Debug, Clone)]
struct TinyLfuEntry {
width: usize,
fingerprint: u64,
}
#[derive(Debug)]
pub struct TinyLfuWidthCache {
window: LruCache<u64, TinyLfuEntry>,
main: LruCache<u64, TinyLfuEntry>,
sketch: CountMinSketch,
doorkeeper: Doorkeeper,
total_capacity: usize,
hits: u64,
misses: u64,
}
impl TinyLfuWidthCache {
pub fn new(total_capacity: usize) -> Self {
let total_capacity = total_capacity.max(2);
let window_cap = (total_capacity / 100).max(1);
let main_cap = total_capacity - window_cap;
Self {
window: LruCache::new(NonZeroUsize::new(window_cap).unwrap()),
main: LruCache::new(NonZeroUsize::new(main_cap.max(1)).unwrap()),
sketch: CountMinSketch::with_defaults(),
doorkeeper: Doorkeeper::with_defaults(),
total_capacity,
hits: 0,
misses: 0,
}
}
pub fn get_or_compute(&mut self, text: &str) -> usize {
self.get_or_compute_with(text, crate::display_width)
}
pub fn get_or_compute_with<F>(&mut self, text: &str, compute: F) -> usize
where
F: FnOnce(&str) -> usize,
{
let hash = hash_text(text);
let fp = fingerprint_hash(text);
let seen = self.doorkeeper.check_and_set(hash);
if seen {
self.sketch.increment(hash);
}
if let Some(entry) = self.main.get(&hash) {
if entry.fingerprint == fp {
self.hits += 1;
return entry.width;
}
self.main.pop(&hash);
}
if let Some(entry) = self.window.get(&hash) {
if entry.fingerprint == fp {
self.hits += 1;
return entry.width;
}
self.window.pop(&hash);
}
self.misses += 1;
let width = compute(text);
let new_entry = TinyLfuEntry {
width,
fingerprint: fp,
};
if self.window.len() >= self.window.cap().get() {
if let Some((evicted_hash, evicted_entry)) = self.window.pop_lru() {
self.try_admit_to_main(evicted_hash, evicted_entry);
}
}
self.window.put(hash, new_entry);
width
}
fn try_admit_to_main(&mut self, candidate_hash: u64, candidate_entry: TinyLfuEntry) {
let candidate_freq = self.sketch.estimate(candidate_hash);
if self.main.len() < self.main.cap().get() {
self.main.put(candidate_hash, candidate_entry);
return;
}
if let Some((&victim_hash, _)) = self.main.peek_lru() {
let victim_freq = self.sketch.estimate(victim_hash);
if tinylfu_admit(candidate_freq, victim_freq) {
self.main.pop_lru();
self.main.put(candidate_hash, candidate_entry);
}
}
}
pub fn contains(&self, text: &str) -> bool {
let hash = hash_text(text);
let fp = fingerprint_hash(text);
if let Some(e) = self.main.peek(&hash)
&& e.fingerprint == fp
{
return true;
}
if let Some(e) = self.window.peek(&hash)
&& e.fingerprint == fp
{
return true;
}
false
}
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits,
misses: self.misses,
size: self.window.len() + self.main.len(),
capacity: self.total_capacity,
}
}
pub fn clear(&mut self) {
self.window.clear();
self.main.clear();
self.sketch.clear();
self.doorkeeper.clear();
}
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
pub fn len(&self) -> usize {
self.window.len() + self.main.len()
}
pub fn is_empty(&self) -> bool {
self.window.is_empty() && self.main.is_empty()
}
pub fn capacity(&self) -> usize {
self.total_capacity
}
pub fn main_len(&self) -> usize {
self.main.len()
}
pub fn window_len(&self) -> usize {
self.window.len()
}
}
#[derive(Debug)]
pub struct S3FifoWidthCache {
cache: ftui_core::s3_fifo::S3Fifo<u64, S3FifoEntry>,
hits: u64,
misses: u64,
total_capacity: usize,
}
#[derive(Debug, Clone, Copy)]
struct S3FifoEntry {
width: usize,
fingerprint: u64,
}
impl S3FifoWidthCache {
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(2);
Self {
cache: ftui_core::s3_fifo::S3Fifo::new(capacity),
hits: 0,
misses: 0,
total_capacity: capacity,
}
}
#[must_use]
pub fn with_default_capacity() -> Self {
Self::new(DEFAULT_CACHE_CAPACITY)
}
pub fn get_or_compute(&mut self, text: &str) -> usize {
self.get_or_compute_with(text, crate::display_width)
}
pub fn get_or_compute_with<F>(&mut self, text: &str, compute: F) -> usize
where
F: FnOnce(&str) -> usize,
{
let hash = hash_text(text);
let fp = fingerprint_hash(text);
if let Some(entry) = self.cache.get(&hash) {
if entry.fingerprint == fp {
self.hits += 1;
return entry.width;
}
self.cache.remove(&hash);
}
self.misses += 1;
let width = compute(text);
self.cache.insert(
hash,
S3FifoEntry {
width,
fingerprint: fp,
},
);
width
}
pub fn contains(&self, text: &str) -> bool {
let hash = hash_text(text);
self.cache.contains_key(&hash)
}
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits,
misses: self.misses,
size: self.cache.len(),
capacity: self.total_capacity,
}
}
pub fn clear(&mut self) {
self.cache.clear();
self.hits = 0;
self.misses = 0;
}
pub fn reset_stats(&mut self) {
self.hits = 0;
self.misses = 0;
}
pub fn len(&self) -> usize {
self.cache.len()
}
pub fn is_empty(&self) -> bool {
self.cache.is_empty()
}
pub fn capacity(&self) -> usize {
self.total_capacity
}
}
impl Default for S3FifoWidthCache {
fn default() -> Self {
Self::with_default_capacity()
}
}
#[cfg(test)]
mod s3_fifo_width_tests {
use super::*;
#[test]
fn s3fifo_new_cache_is_empty() {
let cache = S3FifoWidthCache::new(100);
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn s3fifo_get_or_compute_caches_value() {
let mut cache = S3FifoWidthCache::new(100);
let w1 = cache.get_or_compute("hello");
assert_eq!(w1, 5);
assert_eq!(cache.len(), 1);
let w2 = cache.get_or_compute("hello");
assert_eq!(w2, 5);
assert_eq!(cache.len(), 1);
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn s3fifo_different_strings() {
let mut cache = S3FifoWidthCache::new(100);
cache.get_or_compute("hello");
cache.get_or_compute("world");
cache.get_or_compute("foo");
assert_eq!(cache.len(), 3);
}
#[test]
fn s3fifo_cjk_width() {
let mut cache = S3FifoWidthCache::new(100);
let w = cache.get_or_compute("你好");
assert_eq!(w, 4);
}
#[test]
fn s3fifo_contains() {
let mut cache = S3FifoWidthCache::new(100);
assert!(!cache.contains("hello"));
cache.get_or_compute("hello");
assert!(cache.contains("hello"));
}
#[test]
fn s3fifo_clear_resets() {
let mut cache = S3FifoWidthCache::new(100);
cache.get_or_compute("hello");
cache.get_or_compute("world");
cache.clear();
assert!(cache.is_empty());
assert!(!cache.contains("hello"));
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn s3fifo_produces_same_widths_as_lru() {
let mut lru = WidthCache::new(100);
let mut s3 = S3FifoWidthCache::new(100);
let texts = [
"hello",
"你好世界",
"abc",
"🎉🎉",
"",
" ",
"a\tb",
"mixed中english文",
];
for text in &texts {
let lru_w = lru.get_or_compute(text);
let s3_w = s3.get_or_compute(text);
assert_eq!(lru_w, s3_w, "width mismatch for {:?}", text);
}
}
#[test]
fn s3fifo_scan_resistance_preserves_hot_set() {
let mut cache = S3FifoWidthCache::new(50);
let hot: Vec<String> = (0..20).map(|i| format!("hot_{i}")).collect();
for text in &hot {
cache.get_or_compute(text);
cache.get_or_compute(text); }
for i in 0..200 {
cache.get_or_compute(&format!("scan_{i}"));
}
let mut survivors = 0;
for text in &hot {
if cache.contains(text) {
survivors += 1;
}
}
assert!(
survivors > 5,
"scan resistance: only {survivors}/20 hot items survived"
);
}
#[test]
fn s3fifo_default_capacity() {
let cache = S3FifoWidthCache::with_default_capacity();
assert_eq!(cache.capacity(), DEFAULT_CACHE_CAPACITY);
}
#[test]
fn s3fifo_reset_stats() {
let mut cache = S3FifoWidthCache::new(100);
cache.get_or_compute("a");
cache.get_or_compute("a");
cache.reset_stats();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn cached_width_matches_direct(s in "[a-zA-Z0-9 ]{1,50}") {
let mut cache = WidthCache::new(100);
let cached = cache.get_or_compute(&s);
let direct = crate::display_width(&s);
prop_assert_eq!(cached, direct);
}
#[test]
fn second_access_is_hit(s in "[a-zA-Z0-9]{1,20}") {
let mut cache = WidthCache::new(100);
cache.get_or_compute(&s);
let stats_before = cache.stats();
cache.get_or_compute(&s);
let stats_after = cache.stats();
prop_assert_eq!(stats_after.hits, stats_before.hits + 1);
prop_assert_eq!(stats_after.misses, stats_before.misses);
}
#[test]
fn lru_never_exceeds_capacity(
strings in prop::collection::vec("[a-z]{1,5}", 10..100),
capacity in 5usize..20
) {
let mut cache = WidthCache::new(capacity);
for s in &strings {
cache.get_or_compute(s);
prop_assert!(cache.len() <= capacity);
}
}
#[test]
fn preload_then_access_is_hit(s in "[a-zA-Z]{1,20}") {
let mut cache = WidthCache::new(100);
cache.preload(&s);
let stats_before = cache.stats();
cache.get_or_compute(&s);
let stats_after = cache.stats();
prop_assert_eq!(stats_after.hits, stats_before.hits + 1);
}
}
}
#[cfg(test)]
mod tinylfu_tests {
use super::*;
#[test]
fn unit_cms_single_key_count() {
let mut cms = CountMinSketch::with_defaults();
let h = hash_text("hello");
for _ in 0..5 {
cms.increment(h);
}
assert_eq!(cms.estimate(h), 5);
}
#[test]
fn unit_cms_unseen_key_is_zero() {
let cms = CountMinSketch::with_defaults();
assert_eq!(cms.estimate(hash_text("never_seen")), 0);
}
#[test]
fn unit_cms_saturates_at_max() {
let mut cms = CountMinSketch::with_defaults();
let h = hash_text("hot");
for _ in 0..100 {
cms.increment(h);
}
assert_eq!(cms.estimate(h), CMS_MAX_COUNT);
}
#[test]
fn unit_cms_bounds() {
let mut cms = CountMinSketch::new(1024, 4, u64::MAX); let n: u64 = 1000;
for i in 0..n {
cms.increment(i.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1));
}
let target = 0xDEAD_BEEF_u64;
for _ in 0..5 {
cms.increment(target);
}
let est = cms.estimate(target);
let epsilon = std::f64::consts::E / 1024.0;
let upper_bound = 5.0 + epsilon * (n + 5) as f64;
assert!(
(est as f64) <= upper_bound,
"estimate {} exceeds bound {:.1} (epsilon={:.5}, N={})",
est,
upper_bound,
epsilon,
n + 5,
);
assert!(est >= 5, "estimate {} should be >= true count 5", est);
}
#[test]
fn unit_cms_bounds_mass_test() {
let mut cms = CountMinSketch::new(1024, 4, u64::MAX);
let n = 2000u64;
let mut true_counts = vec![0u8; n as usize];
for i in 0..n {
let count = (i % 10 + 1) as u8;
true_counts[i as usize] = count;
for _ in 0..count {
cms.increment(i);
}
}
let total = cms.total_increments();
let epsilon = std::f64::consts::E / 1024.0;
let mut violations = 0u32;
for i in 0..n {
let est = cms.estimate(i);
let true_c = true_counts[i as usize];
let upper = true_c as f64 + epsilon * total as f64;
if est as f64 > upper + 0.5 {
violations += 1;
}
assert!(
est >= true_c,
"key {}: estimate {} < true count {}",
i,
est,
true_c
);
}
let violation_rate = violations as f64 / n as f64;
assert!(
violation_rate <= 0.10,
"violation rate {:.3} exceeds delta threshold",
violation_rate,
);
}
#[test]
fn unit_cms_halving_ages_counts() {
let mut cms = CountMinSketch::new(64, 2, 100);
let h = hash_text("test");
for _ in 0..10 {
cms.increment(h);
}
assert_eq!(cms.estimate(h), 10);
for _ in 10..100 {
cms.increment(hash_text("noise"));
}
let est = cms.estimate(h);
assert!(est <= 5, "After halving, estimate {} should be <= 5", est);
}
#[test]
fn unit_cms_monotone() {
let mut cms = CountMinSketch::with_defaults();
let h = hash_text("key");
let mut prev_est = 0u8;
for _ in 0..CMS_MAX_COUNT {
cms.increment(h);
let est = cms.estimate(h);
assert!(est >= prev_est, "estimate should be monotone");
prev_est = est;
}
}
#[test]
fn unit_doorkeeper_first_access_returns_false() {
let mut dk = Doorkeeper::with_defaults();
assert!(!dk.check_and_set(hash_text("new")));
}
#[test]
fn unit_doorkeeper_second_access_returns_true() {
let mut dk = Doorkeeper::with_defaults();
let h = hash_text("key");
dk.check_and_set(h);
assert!(dk.check_and_set(h));
}
#[test]
fn unit_doorkeeper_contains() {
let mut dk = Doorkeeper::with_defaults();
let h = hash_text("key");
assert!(!dk.contains(h));
dk.check_and_set(h);
assert!(dk.contains(h));
}
#[test]
fn unit_doorkeeper_clear_resets() {
let mut dk = Doorkeeper::with_defaults();
let h = hash_text("key");
dk.check_and_set(h);
dk.clear();
assert!(!dk.contains(h));
assert!(!dk.check_and_set(h));
}
#[test]
fn unit_doorkeeper_false_positive_rate() {
let mut dk = Doorkeeper::new(2048);
let n = 100u64;
for i in 0..n {
dk.check_and_set(i * 0x9E37_79B9 + 1);
}
let mut false_positives = 0u32;
for i in 0..1000 {
let h = (i + 100_000) * 0x6A09_E667 + 7;
if dk.contains(h) {
false_positives += 1;
}
}
let fp_rate = false_positives as f64 / 1000.0;
assert!(
fp_rate < 0.15,
"FP rate {:.3} too high (expected < 0.15)",
fp_rate,
);
}
#[test]
fn unit_admission_rule() {
assert!(tinylfu_admit(5, 3)); assert!(!tinylfu_admit(3, 5)); assert!(!tinylfu_admit(3, 3)); }
#[test]
fn unit_admission_rule_extremes() {
assert!(tinylfu_admit(1, 0));
assert!(!tinylfu_admit(0, 0));
assert!(!tinylfu_admit(0, 1));
assert!(tinylfu_admit(CMS_MAX_COUNT, CMS_MAX_COUNT - 1));
assert!(!tinylfu_admit(CMS_MAX_COUNT, CMS_MAX_COUNT));
}
#[test]
fn unit_fingerprint_guard() {
let fp1 = fingerprint_hash("hello");
let fp2 = fingerprint_hash("world");
let fp3 = fingerprint_hash("hello");
assert_ne!(
fp1, fp2,
"Different strings should have different fingerprints"
);
assert_eq!(fp1, fp3, "Same string should have same fingerprint");
}
#[test]
fn unit_fingerprint_guard_collision_rate() {
let mut fps = std::collections::HashSet::new();
let n = 10_000;
for i in 0..n {
let s = format!("string_{}", i);
fps.insert(fingerprint_hash(&s));
}
let collisions = n - fps.len();
assert!(
collisions == 0,
"Expected 0 collisions in 10k items, got {}",
collisions,
);
}
#[test]
fn unit_fingerprint_independent_of_primary_hash() {
let text = "test_string";
let primary = hash_text(text);
let secondary = fingerprint_hash(text);
assert_ne!(
primary, secondary,
"Fingerprint and primary hash should differ"
);
}
#[test]
fn unit_doorkeeper_cms_pipeline() {
let mut dk = Doorkeeper::with_defaults();
let mut cms = CountMinSketch::with_defaults();
let h = hash_text("item");
assert!(!dk.check_and_set(h));
assert_eq!(cms.estimate(h), 0);
assert!(dk.check_and_set(h));
cms.increment(h);
assert_eq!(cms.estimate(h), 1);
assert!(dk.check_and_set(h));
cms.increment(h);
assert_eq!(cms.estimate(h), 2);
}
#[test]
fn unit_doorkeeper_filters_one_hit_wonders() {
let mut dk = Doorkeeper::with_defaults();
let mut cms = CountMinSketch::with_defaults();
for i in 0u64..100 {
let h = i * 0x9E37_79B9 + 1;
let seen = dk.check_and_set(h);
if seen {
cms.increment(h);
}
}
assert_eq!(cms.total_increments(), 0);
let h = 1; assert!(dk.check_and_set(h));
cms.increment(h);
assert_eq!(cms.total_increments(), 1);
}
}
#[cfg(test)]
mod tinylfu_impl_tests {
use super::*;
#[test]
fn basic_get_or_compute() {
let mut cache = TinyLfuWidthCache::new(100);
let w = cache.get_or_compute("hello");
assert_eq!(w, 5);
assert_eq!(cache.len(), 1);
let w2 = cache.get_or_compute("hello");
assert_eq!(w2, 5);
let stats = cache.stats();
assert_eq!(stats.misses, 1);
assert_eq!(stats.hits, 1);
}
#[test]
fn window_to_main_promotion() {
let mut cache = TinyLfuWidthCache::new(100);
for _ in 0..10 {
cache.get_or_compute("frequent");
}
for i in 0..5 {
cache.get_or_compute(&format!("item_{}", i));
}
assert!(cache.contains("frequent"));
assert!(cache.main_len() > 0 || cache.window_len() > 0);
}
#[test]
fn unit_window_promotion() {
let mut cache = TinyLfuWidthCache::new(50);
for _ in 0..20 {
cache.get_or_compute("hot");
}
for i in 0..10 {
cache.get_or_compute(&format!("filler_{}", i));
}
assert!(cache.contains("hot"), "Frequent item should be retained");
}
#[test]
fn fingerprint_guard_detects_collision() {
let mut cache = TinyLfuWidthCache::new(100);
let w = cache.get_or_compute_with("hello", |_| 42);
assert_eq!(w, 42);
assert!(cache.contains("hello"));
}
#[test]
fn admission_rejects_infrequent() {
let mut cache = TinyLfuWidthCache::new(10);
for i in 0..9 {
let s = format!("hot_{}", i);
for _ in 0..5 {
cache.get_or_compute(&s);
}
}
for i in 0..20 {
cache.get_or_compute(&format!("cold_{}", i));
}
let hot_survivors: usize = (0..9)
.filter(|i| cache.contains(&format!("hot_{}", i)))
.count();
assert!(
hot_survivors >= 5,
"Expected most hot items to survive, got {}/9",
hot_survivors
);
}
#[test]
fn clear_empties_everything() {
let mut cache = TinyLfuWidthCache::new(100);
cache.get_or_compute("a");
cache.get_or_compute("b");
cache.clear();
assert!(cache.is_empty());
assert_eq!(cache.len(), 0);
}
#[test]
fn stats_reflect_usage() {
let mut cache = TinyLfuWidthCache::new(100);
cache.get_or_compute("a");
cache.get_or_compute("a");
cache.get_or_compute("b");
let stats = cache.stats();
assert_eq!(stats.misses, 2);
assert_eq!(stats.hits, 1);
assert_eq!(stats.size, 2);
}
#[test]
fn capacity_is_respected() {
let mut cache = TinyLfuWidthCache::new(20);
for i in 0..100 {
cache.get_or_compute(&format!("item_{}", i));
}
assert!(
cache.len() <= 20,
"Cache size {} exceeds capacity 20",
cache.len()
);
}
#[test]
fn reset_stats_works() {
let mut cache = TinyLfuWidthCache::new(100);
cache.get_or_compute("x");
cache.get_or_compute("x");
cache.reset_stats();
let stats = cache.stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn perf_cache_hit_rate() {
let mut cache = TinyLfuWidthCache::new(50);
for _ in 0..20 {
for i in 0..10 {
cache.get_or_compute(&format!("hot_{}", i));
}
}
for i in 0..100 {
cache.get_or_compute(&format!("cold_{}", i));
}
cache.reset_stats();
for i in 0..10 {
cache.get_or_compute(&format!("hot_{}", i));
}
let stats = cache.stats();
assert!(
stats.hits >= 5,
"Expected at least 5/10 hot items to hit, got {}",
stats.hits
);
}
#[test]
fn unicode_strings_work() {
let mut cache = TinyLfuWidthCache::new(100);
assert_eq!(cache.get_or_compute("日本語"), 6);
assert_eq!(cache.get_or_compute("café"), 4);
assert_eq!(cache.get_or_compute("日本語"), 6); assert_eq!(cache.stats().hits, 1);
}
#[test]
fn empty_string() {
let mut cache = TinyLfuWidthCache::new(100);
assert_eq!(cache.get_or_compute(""), 0);
}
#[test]
fn minimum_capacity() {
let cache = TinyLfuWidthCache::new(0);
assert!(cache.capacity() >= 2);
}
}
#[cfg(test)]
struct Lcg(u64);
#[cfg(test)]
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
self.0
}
fn next_usize(&mut self, max: usize) -> usize {
(self.next_u64() % (max as u64)) as usize
}
}
#[cfg(test)]
mod property_cache_equivalence {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn tinylfu_cached_equals_computed(s in "[a-zA-Z0-9 ]{1,80}") {
let mut cache = TinyLfuWidthCache::new(200);
let cached = cache.get_or_compute(&s);
let direct = crate::display_width(&s);
prop_assert_eq!(cached, direct,
"TinyLFU returned {} but display_width says {} for {:?}", cached, direct, s);
}
#[test]
fn tinylfu_second_access_same_value(s in "[a-zA-Z0-9]{1,40}") {
let mut cache = TinyLfuWidthCache::new(200);
let first = cache.get_or_compute(&s);
let second = cache.get_or_compute(&s);
prop_assert_eq!(first, second,
"First access returned {} but second returned {} for {:?}", first, second, s);
}
#[test]
fn tinylfu_never_exceeds_capacity(
strings in prop::collection::vec("[a-z]{1,5}", 10..200),
capacity in 10usize..50
) {
let mut cache = TinyLfuWidthCache::new(capacity);
for s in &strings {
cache.get_or_compute(s);
prop_assert!(cache.len() <= capacity,
"Cache size {} exceeded capacity {}", cache.len(), capacity);
}
}
#[test]
fn tinylfu_custom_fn_matches(s in "[a-z]{1,20}") {
let mut cache = TinyLfuWidthCache::new(100);
let custom_fn = |text: &str| text.len(); let cached = cache.get_or_compute_with(&s, custom_fn);
prop_assert_eq!(cached, s.len(),
"Custom fn: cached {} != expected {} for {:?}", cached, s.len(), s);
}
}
#[test]
fn deterministic_seed_equivalence() {
let mut rng = super::Lcg::new(0xDEAD_BEEF);
let mut cache1 = TinyLfuWidthCache::new(50);
let mut results1 = Vec::new();
for _ in 0..500 {
let idx = rng.next_usize(100);
let s = format!("key_{}", idx);
results1.push(cache1.get_or_compute(&s));
}
let mut rng2 = super::Lcg::new(0xDEAD_BEEF);
let mut cache2 = TinyLfuWidthCache::new(50);
let mut results2 = Vec::new();
for _ in 0..500 {
let idx = rng2.next_usize(100);
let s = format!("key_{}", idx);
results2.push(cache2.get_or_compute(&s));
}
assert_eq!(
results1, results2,
"Deterministic seed must produce identical results"
);
}
#[test]
fn both_caches_agree_on_widths() {
let mut lru = WidthCache::new(200);
let mut tlfu = TinyLfuWidthCache::new(200);
let inputs = [
"",
"hello",
"日本語テスト",
"café résumé",
"a\tb",
"🎉🎊🎈",
"mixed日本eng",
" spaces ",
"AAAAAAAAAAAAAAAAAAAAAAAAA",
"x",
];
for &s in &inputs {
let w1 = lru.get_or_compute(s);
let w2 = tlfu.get_or_compute(s);
assert_eq!(
w1, w2,
"Width mismatch for {:?}: LRU={}, TinyLFU={}",
s, w1, w2
);
}
}
}
#[cfg(test)]
mod e2e_cache_replay {
use super::*;
use std::time::Instant;
#[derive(Debug)]
struct ReplayRecord {
step: usize,
key: String,
width: usize,
hit: bool,
latency_ns: u128,
}
impl ReplayRecord {
fn to_jsonl(&self) -> String {
format!(
r#"{{"step":{},"key":"{}","width":{},"hit":{},"latency_ns":{}}}"#,
self.step, self.key, self.width, self.hit, self.latency_ns,
)
}
}
fn zipfian_workload(rng: &mut super::Lcg, n: usize, universe: usize) -> Vec<String> {
(0..n)
.map(|_| {
let raw = rng.next_usize(universe * universe);
let idx = (raw as f64).sqrt() as usize % universe;
format!("item_{}", idx)
})
.collect()
}
#[test]
fn replay_zipfian_tinylfu() {
let mut rng = super::Lcg::new(0x1234_5678);
let workload = zipfian_workload(&mut rng, 2000, 200);
let mut cache = TinyLfuWidthCache::new(50);
let mut records = Vec::with_capacity(workload.len());
for (i, key) in workload.iter().enumerate() {
let stats_before = cache.stats();
let t0 = Instant::now();
let width = cache.get_or_compute(key);
let elapsed = t0.elapsed().as_nanos();
let stats_after = cache.stats();
let hit = stats_after.hits > stats_before.hits;
records.push(ReplayRecord {
step: i,
key: key.clone(),
width,
hit,
latency_ns: elapsed,
});
}
for r in &records[..5] {
let line = r.to_jsonl();
assert!(
line.starts_with('{') && line.ends_with('}'),
"Bad JSONL: {}",
line
);
}
let total = records.len();
let hits = records.iter().filter(|r| r.hit).count();
let hit_rate = hits as f64 / total as f64;
assert!(
hit_rate > 0.10,
"Zipfian hit rate too low: {:.2}% ({}/{})",
hit_rate * 100.0,
hits,
total
);
}
#[test]
fn replay_zipfian_lru_comparison() {
let mut rng = super::Lcg::new(0x1234_5678);
let workload = zipfian_workload(&mut rng, 2000, 200);
let mut tlfu = TinyLfuWidthCache::new(50);
let mut lru = WidthCache::new(50);
for key in &workload {
tlfu.get_or_compute(key);
lru.get_or_compute(key);
}
let tlfu_stats = tlfu.stats();
let lru_stats = lru.stats();
assert_eq!(tlfu_stats.hits + tlfu_stats.misses, 2000);
assert_eq!(lru_stats.hits + lru_stats.misses, 2000);
assert!(
tlfu_stats.hit_rate() >= lru_stats.hit_rate() * 0.8,
"TinyLFU hit rate {:.2}% much worse than LRU {:.2}%",
tlfu_stats.hit_rate() * 100.0,
lru_stats.hit_rate() * 100.0,
);
}
#[test]
fn replay_deterministic_reproduction() {
let run = |seed: u64| -> Vec<bool> {
let mut rng = super::Lcg::new(seed);
let workload = zipfian_workload(&mut rng, 500, 100);
let mut cache = TinyLfuWidthCache::new(30);
let mut hits = Vec::with_capacity(500);
for key in &workload {
let before = cache.stats().hits;
cache.get_or_compute(key);
hits.push(cache.stats().hits > before);
}
hits
};
let run1 = run(0xABCD_EF01);
let run2 = run(0xABCD_EF01);
assert_eq!(run1, run2, "Deterministic replay diverged");
}
#[test]
fn replay_uniform_workload() {
let mut rng = super::Lcg::new(0x9999);
let universe = 100;
let cache_size = 25;
let n = 5000;
let mut cache = TinyLfuWidthCache::new(cache_size);
for i in 0..universe {
cache.get_or_compute(&format!("u_{}", i));
}
cache.reset_stats();
for _ in 0..n {
let idx = rng.next_usize(universe);
cache.get_or_compute(&format!("u_{}", idx));
}
let stats = cache.stats();
let hit_rate = stats.hit_rate();
assert!(
hit_rate > 0.10 && hit_rate < 0.60,
"Uniform hit rate {:.2}% outside expected range",
hit_rate * 100.0,
);
}
}
#[cfg(test)]
mod perf_cache_overhead {
use super::*;
use std::time::Instant;
fn measure_latencies<F: FnMut(usize)>(n: usize, mut op: F) -> Vec<u128> {
let mut latencies = Vec::with_capacity(n);
for i in 0..n {
let t0 = Instant::now();
op(i);
latencies.push(t0.elapsed().as_nanos());
}
latencies.sort_unstable();
latencies
}
fn p95(sorted: &[u128]) -> u128 {
let len = sorted.len();
let idx = ((len as f64 * 0.95) as usize).min(len.saturating_sub(1));
sorted[idx]
}
fn p99(sorted: &[u128]) -> u128 {
let len = sorted.len();
let idx = ((len as f64 * 0.99) as usize).min(len.saturating_sub(1));
sorted[idx]
}
fn median(sorted: &[u128]) -> u128 {
sorted[sorted.len() / 2]
}
#[allow(unexpected_cfgs)]
fn perf_budget_ns(base_ns: u128) -> u128 {
if cfg!(coverage) || cfg!(coverage_nightly) {
base_ns.saturating_mul(10)
} else {
base_ns
}
}
#[test]
fn perf_lru_hit_latency() {
let mut cache = WidthCache::new(1000);
for i in 0..100 {
cache.get_or_compute(&format!("warm_{}", i));
}
let keys: Vec<String> = (0..100).map(|i| format!("warm_{}", i)).collect();
let latencies = measure_latencies(10_000, |i| {
let _ = cache.get_or_compute(&keys[i % 100]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(5_000);
assert!(
p95_ns < budget_ns,
"LRU hit p95 = {}ns exceeds {}ns budget (median={}ns, p99={}ns)",
p95_ns,
budget_ns,
median(&latencies),
p99(&latencies),
);
}
#[test]
fn perf_tinylfu_hit_latency() {
let mut cache = TinyLfuWidthCache::new(1000);
for _ in 0..5 {
for i in 0..100 {
cache.get_or_compute(&format!("warm_{}", i));
}
}
let keys: Vec<String> = (0..100).map(|i| format!("warm_{}", i)).collect();
let latencies = measure_latencies(10_000, |i| {
let _ = cache.get_or_compute(&keys[i % 100]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(5_000);
assert!(
p95_ns < budget_ns,
"TinyLFU hit p95 = {}ns exceeds {}ns budget (median={}ns, p99={}ns)",
p95_ns,
budget_ns,
median(&latencies),
p99(&latencies),
);
}
#[test]
fn perf_tinylfu_miss_latency() {
let mut cache = TinyLfuWidthCache::new(100);
let keys: Vec<String> = (0..10_000).map(|i| format!("miss_{}", i)).collect();
let latencies = measure_latencies(10_000, |i| {
let _ = cache.get_or_compute(&keys[i]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(20_000);
assert!(
p95_ns < budget_ns,
"TinyLFU miss p95 = {}ns exceeds {}ns budget (median={}ns, p99={}ns)",
p95_ns,
budget_ns,
median(&latencies),
p99(&latencies),
);
}
#[test]
fn perf_cms_increment_latency() {
let mut cms = CountMinSketch::with_defaults();
let hashes: Vec<u64> = (0..10_000).map(|i| hash_text(&format!("k{}", i))).collect();
let latencies = measure_latencies(10_000, |i| {
cms.increment(hashes[i]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(2_000);
assert!(
p95_ns < budget_ns,
"CMS increment p95 = {}ns exceeds {}ns budget (median={}ns)",
p95_ns,
budget_ns,
median(&latencies),
);
}
#[test]
fn perf_doorkeeper_latency() {
let mut dk = Doorkeeper::with_defaults();
let hashes: Vec<u64> = (0..10_000).map(|i| hash_text(&format!("d{}", i))).collect();
let latencies = measure_latencies(10_000, |i| {
let _ = dk.check_and_set(hashes[i]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(1_000);
assert!(
p95_ns < budget_ns,
"Doorkeeper p95 = {}ns exceeds {}ns budget (median={}ns)",
p95_ns,
budget_ns,
median(&latencies),
);
}
#[test]
fn perf_fingerprint_hash_latency() {
let keys: Vec<String> = (0..10_000).map(|i| format!("fp_{}", i)).collect();
let latencies = measure_latencies(10_000, |i| {
let _ = fingerprint_hash(&keys[i]);
});
let p95_ns = p95(&latencies);
let budget_ns = perf_budget_ns(1_000);
assert!(
p95_ns < budget_ns,
"fingerprint_hash p95 = {}ns exceeds {}ns budget (median={}ns)",
p95_ns,
budget_ns,
median(&latencies),
);
}
}