//! Revocation Cache Performance Benchmark //! //! Measures the performance impact of local revocation cache: //! - Cache hit latency: TARGET <10ns //! - Cache miss latency: ~500μs (Redis network) //! - Cache hit rate: TARGET >95% //! - Memory overhead: Minimal with TTL expiration //! //! This benchmark compares: //! 1. Direct Redis calls (no cache) //! 2. Local DashMap cache with 60s TTL //! 3. Cache behavior under different access patterns use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; use dashmap::DashMap; use std::sync::Arc; use std::time::{Duration, Instant}; /// Simulated revocation result #[derive(Debug, Clone)] struct CachedRevocationResult { is_revoked: bool, cached_at: Instant, } /// Local revocation cache implementation struct LocalRevocationCache { cache: Arc>, ttl: Duration, hits: Arc, misses: Arc, } impl LocalRevocationCache { fn new(ttl: Duration) -> Self { Self { cache: Arc::new(DashMap::new()), ttl, hits: Arc::new(std::sync::atomic::AtomicU64::new(0)), misses: Arc::new(std::sync::atomic::AtomicU64::new(0)), } } fn check_revoked(&self, token_id: &str, simulate_redis_latency: bool) -> bool { // Check cache first if let Some(entry) = self.cache.get(token_id) { if entry.cached_at.elapsed() < self.ttl { // Cache hit self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed); return entry.is_revoked; } else { // Expired - remove it drop(entry); self.cache.remove(token_id); } } // Cache miss - simulate Redis lookup self.misses .fetch_add(1, std::sync::atomic::Ordering::Relaxed); if simulate_redis_latency { // Simulate 500μs Redis network latency std::thread::sleep(Duration::from_micros(500)); } // Simulate Redis result (99% not revoked in production) let is_revoked = token_id.contains("revoked"); // Update cache self.cache.insert( token_id.to_string(), CachedRevocationResult { is_revoked, cached_at: Instant::now(), }, ); is_revoked } fn stats(&self) -> (u64, u64, f64) { let hits = self.hits.load(std::sync::atomic::Ordering::Relaxed); let misses = self.misses.load(std::sync::atomic::Ordering::Relaxed); let total = hits + misses; let hit_rate = if total > 0 { (hits as f64 / total as f64) * 100.0 } else { 0.0 }; (hits, misses, hit_rate) } } /// Benchmark 1: Cache hit latency (TARGET: <10ns) fn bench_cache_hit_latency(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); // Prepopulate cache with 1000 tokens for i in 0..1000 { let token_id = format!("token_{}", i); cache.check_revoked(&token_id, false); // Prime cache without latency } c.bench_function("revocation_cache_hit", |b| { b.iter(|| { let token_id = format!("token_{}", black_box(500)); let is_revoked = cache.check_revoked(&token_id, false); black_box(is_revoked); }); }); } /// Benchmark 2: Cache miss latency (with simulated Redis) fn bench_cache_miss_latency(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); c.bench_function("revocation_cache_miss_with_redis", |b| { let mut counter = 0; b.iter(|| { let token_id = format!("new_token_{}", black_box(counter)); let is_revoked = cache.check_revoked(&token_id, true); // Simulate Redis latency black_box(is_revoked); counter += 1; }); }); } /// Benchmark 3: Hot token pattern (95% cache hit rate) fn bench_hot_token_pattern(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); // Prepopulate with hot tokens for i in 0..10 { let token_id = format!("hot_token_{}", i); cache.check_revoked(&token_id, false); } c.bench_function("hot_token_pattern_95pct_hits", |b| { let mut counter = 0; b.iter(|| { // 95% hits (tokens 0-9), 5% misses (tokens 10-19) let token_num = if counter % 20 < 19 { counter % 10 } else { 10 + (counter % 10) }; let token_id = format!("hot_token_{}", black_box(token_num)); let is_revoked = cache.check_revoked(&token_id, false); black_box(is_revoked); counter += 1; }); }); // Report statistics let (hits, misses, hit_rate) = cache.stats(); println!( "\nHot token pattern stats: {} hits, {} misses, {:.2}% hit rate", hits, misses, hit_rate ); } /// Benchmark 4: TTL expiration behavior fn bench_ttl_expiration(c: &mut Criterion) { let mut group = c.benchmark_group("ttl_expiration"); // Short TTL (1ms) - high expiration rate let cache_short = LocalRevocationCache::new(Duration::from_millis(1)); for i in 0..100 { cache_short.check_revoked(&format!("token_{}", i), false); } group.bench_function("1ms_ttl", |b| { b.iter(|| { let token_id = format!("token_{}", black_box(50)); let is_revoked = cache_short.check_revoked(&token_id, false); black_box(is_revoked); }); }); // Long TTL (60s) - low expiration rate let cache_long = LocalRevocationCache::new(Duration::from_secs(60)); for i in 0..100 { cache_long.check_revoked(&format!("token_{}", i), false); } group.bench_function("60s_ttl", |b| { b.iter(|| { let token_id = format!("token_{}", black_box(50)); let is_revoked = cache_long.check_revoked(&token_id, false); black_box(is_revoked); }); }); group.finish(); } /// Benchmark 5: Cache size impact fn bench_cache_size_impact(c: &mut Criterion) { let mut group = c.benchmark_group("cache_size_impact"); for size in &[100, 1_000, 10_000, 100_000] { let cache = LocalRevocationCache::new(Duration::from_secs(60)); // Prepopulate to target size for i in 0..*size { cache.check_revoked(&format!("token_{}", i), false); } group.bench_with_input(BenchmarkId::new("lookup", size), size, |b, &n| { b.iter(|| { let token_id = format!("token_{}", black_box(n / 2)); let is_revoked = cache.check_revoked(&token_id, false); black_box(is_revoked); }); }); } group.finish(); } /// Benchmark 6: Concurrent access pattern fn bench_concurrent_access(c: &mut Criterion) { let cache = Arc::new(LocalRevocationCache::new(Duration::from_secs(60))); // Prepopulate for i in 0..1000 { cache.check_revoked(&format!("token_{}", i), false); } c.bench_function("concurrent_cache_access", |b| { b.iter(|| { let cache_clone = cache.clone(); let token_id = format!("token_{}", black_box(500)); let is_revoked = cache_clone.check_revoked(&token_id, false); black_box(is_revoked); }); }); } /// Benchmark 7: Mixed revoked/valid token pattern fn bench_mixed_revocation(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); // Prepopulate with mix of revoked and valid tokens for i in 0..1000 { let token_id = if i % 10 == 0 { format!("revoked_token_{}", i) // 10% revoked } else { format!("valid_token_{}", i) // 90% valid }; cache.check_revoked(&token_id, false); } c.bench_function("mixed_revocation_pattern", |b| { let mut counter = 0; b.iter(|| { let token_id = if counter % 10 == 0 { format!("revoked_token_{}", black_box(counter)) } else { format!("valid_token_{}", black_box(counter)) }; let is_revoked = cache.check_revoked(&token_id, false); black_box(is_revoked); counter = (counter + 1) % 1000; }); }); } /// Benchmark 8: Cache vs no-cache comparison fn bench_cache_vs_no_cache(c: &mut Criterion) { let mut group = c.benchmark_group("cache_vs_no_cache"); // No cache - direct Redis simulation group.bench_function("no_cache_direct_redis", |b| { b.iter(|| { // Simulate 500μs Redis latency every time std::thread::sleep(Duration::from_micros(500)); let is_revoked = false; // Simulate result black_box(is_revoked); }); }); // With cache - 95% hit rate let cache = LocalRevocationCache::new(Duration::from_secs(60)); for i in 0..10 { cache.check_revoked(&format!("token_{}", i), false); } group.bench_function("with_cache_95pct_hits", |b| { let mut counter = 0; b.iter(|| { let token_num = if counter % 20 < 19 { counter % 10 // Hit } else { 10 + (counter % 10) // Miss }; let token_id = format!("token_{}", black_box(token_num)); let is_revoked = cache.check_revoked(&token_id, counter % 20 >= 19); // Only simulate latency on misses black_box(is_revoked); counter += 1; }); }); group.finish(); } /// Benchmark 9: Memory overhead measurement fn bench_memory_overhead(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); c.bench_function("cache_entry_insertion", |b| { let mut counter = 0; b.iter(|| { let token_id = format!("token_{}", black_box(counter)); cache.check_revoked(&token_id, false); counter += 1; }); }); } /// Benchmark 10: Realistic production workload fn bench_production_workload(c: &mut Criterion) { let cache = LocalRevocationCache::new(Duration::from_secs(60)); // Simulate production: 1000 active users, 95% cache hit rate, 1% revoked for i in 0..1000 { let token_id = if i % 100 == 0 { format!("revoked_token_{}", i) } else { format!("valid_token_{}", i) }; cache.check_revoked(&token_id, false); } c.bench_function("production_workload_simulation", |b| { let mut counter = 0; b.iter(|| { // 95% hits to existing tokens, 5% new tokens let token_id = if counter % 20 < 19 { // Cache hit - existing token let idx = counter % 1000; if idx % 100 == 0 { format!("revoked_token_{}", idx) } else { format!("valid_token_{}", idx) } } else { // Cache miss - new token format!("new_token_{}", counter) }; let simulate_latency = counter % 20 >= 19; let is_revoked = cache.check_revoked(&token_id, simulate_latency); black_box(is_revoked); counter += 1; }); }); // Report final statistics let (hits, misses, hit_rate) = cache.stats(); println!( "\nProduction workload stats: {} hits, {} misses, {:.2}% hit rate", hits, misses, hit_rate ); } criterion_group!( revocation_cache_benches, bench_cache_hit_latency, bench_cache_miss_latency, bench_hot_token_pattern, bench_ttl_expiration, bench_cache_size_impact, bench_concurrent_access, bench_mixed_revocation, bench_cache_vs_no_cache, bench_memory_overhead, bench_production_workload ); criterion_main!(revocation_cache_benches);