#![allow( clippy::unwrap_used, clippy::cast_precision_loss, clippy::cast_possible_truncation )] //! Criterion benchmarks for social graph filter and co-engagement operations. //! //! Validates the spec performance target: `social_graph` filter at depth=2 //! with 20 users / 10 followed creators completes in < 50ms. //! //! Also measures: //! - `CoEngagementIndex::record_positive` at capacity (eviction path) //! - `CoEngagementIndex::score` on a populated index use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; use tidaldb::entities::{CoEngagementIndex, CreatorItemsBitmap, PreferenceVectors, UserStateIndex}; use tidaldb::query::executor::social_filter::{resolve_social_subgraph_users, social_graph_bitmap}; use tidaldb::schema::EntityId; // ── Social graph bitmap benchmark ───────────────────────────────────────────── /// Build a social graph with `num_creators` followed creators, each having /// `followers_per_creator` followers, and `items_per_creator` items. fn build_social_state( num_creators: usize, followers_per_creator: usize, items_per_creator: usize, ) -> (UserStateIndex, CreatorItemsBitmap) { let user_state = UserStateIndex::new(); let creator_items = CreatorItemsBitmap::new(); let user_id = 1u64; for c in 0..num_creators { let creator_id = (100 + c) as u64; // User 1 follows this creator. user_state.add_follow(user_id, creator_id); user_state.add_creator_follower(creator_id, user_id); // Other followers for this creator. for f in 0..followers_per_creator { let follower_id = (1000 + c * followers_per_creator + f) as u64; user_state.add_creator_follower(creator_id, follower_id); // Each co-follower has seen some items (for depth-2 expansion). for i in 0..5u32 { user_state.mark_seen(follower_id, (follower_id as u32) * 100 + i); } } // Items from this creator. for i in 0..items_per_creator { let item_id = ((c * items_per_creator + i) as u32) + 1; creator_items.add_item(creator_id, item_id); } } (user_state, creator_items) } fn bench_social_graph_bitmap(c: &mut Criterion) { let mut group = c.benchmark_group("social_graph_bitmap"); // Spec scenario: 10 followed creators, 20 co-followers per creator, 50 items per creator. let (user_state, creator_items) = build_social_state(10, 20, 50); group.bench_function("depth1_10creators_50items", |b| { b.iter(|| social_graph_bitmap(black_box(1), black_box(1), &user_state, &creator_items)); }); group.bench_function("depth2_10creators_20followers_50items", |b| { b.iter(|| social_graph_bitmap(black_box(1), black_box(2), &user_state, &creator_items)); }); // Larger scenario: stress test fan-out cap. let (user_state_large, creator_items_large) = build_social_state(10, 100, 100); group.bench_function("depth2_10creators_100followers_100items", |b| { b.iter(|| { social_graph_bitmap( black_box(1), black_box(2), &user_state_large, &creator_items_large, ) }); }); group.finish(); } fn bench_resolve_social_subgraph(c: &mut Criterion) { let mut group = c.benchmark_group("resolve_social_subgraph_users"); let (user_state, _) = build_social_state(10, 20, 50); group.bench_function("depth1_10creators_20followers", |b| { b.iter(|| resolve_social_subgraph_users(black_box(1), black_box(1), &user_state)); }); group.bench_function("depth2_10creators_20followers", |b| { b.iter(|| resolve_social_subgraph_users(black_box(1), black_box(2), &user_state)); }); group.finish(); } // ── CoEngagementIndex benchmarks ───────────────────────────────────────────── fn bench_co_engagement(c: &mut Criterion) { let mut group = c.benchmark_group("co_engagement"); // Benchmark score lookup on a populated index. let index = CoEngagementIndex::new(); for user in 1..=50u64 { for item in 1..=20u64 { index.record_positive(user, EntityId::new(item)); } } group.bench_function("score_populated", |b| { b.iter(|| index.score(black_box(EntityId::new(10)), black_box(EntityId::new(5)))); }); // Benchmark record_positive at capacity (eviction fires). group.bench_function("record_positive_at_capacity_1k", |b| { let cap_index = CoEngagementIndex::with_capacity(1_000); // Pre-fill to capacity. for user in 1..=40u64 { for item in 1..=25u64 { cap_index.record_positive(user, EntityId::new(item)); } } // Now benchmark a single record_positive that triggers eviction. b.iter(|| { cap_index.record_positive(black_box(999), black_box(EntityId::new(42))); }); }); // Benchmark record_positive at default 50K capacity. group.bench_function("record_positive_at_capacity_50k", |b| { let cap_index = CoEngagementIndex::with_capacity(50_000); // Pre-fill close to capacity. for user in 1..=200u64 { for item in 1..=50u64 { cap_index.record_positive(user, EntityId::new(item)); } } b.iter(|| { cap_index.record_positive(black_box(9999), black_box(EntityId::new(77))); }); }); group.finish(); } // ── Social graph at 1M items ────────────────────────────────────────────────── /// Build a social graph at production scale: 100 followed creators, /// 500 followers/creator, 10K items/creator = 1M items total. fn build_1m_social_state() -> (UserStateIndex, CreatorItemsBitmap) { let user_state = UserStateIndex::new(); let creator_items = CreatorItemsBitmap::new(); let user_id = 1u64; for c in 0..100usize { let creator_id = (100 + c) as u64; user_state.add_follow(user_id, creator_id); user_state.add_creator_follower(creator_id, user_id); // 500 other followers per creator. for f in 0..500usize { let follower_id = (10_000 + c * 500 + f) as u64; user_state.add_creator_follower(creator_id, follower_id); } // 10K items per creator = 1M total. for i in 0..10_000usize { let item_id = ((c * 10_000 + i) as u32) + 1; creator_items.add_item(creator_id, item_id); } } (user_state, creator_items) } fn bench_social_graph_1m_items(c: &mut Criterion) { let (user_state, creator_items) = build_1m_social_state(); let mut group = c.benchmark_group("social_graph_1m"); group.bench_function("depth1_100creators_1m_items", |b| { b.iter(|| social_graph_bitmap(black_box(1), black_box(1), &user_state, &creator_items)); }); group.bench_function("depth2_100creators_500followers_1m_items", |b| { b.iter(|| social_graph_bitmap(black_box(1), black_box(2), &user_state, &creator_items)); }); group.finish(); } // ── Cross-session preference merge ─────────────────────────────────────────── /// Benchmark `PreferenceVectors::update_with_custom_rate()` for 128D EMA update. /// /// Targets: single merge < 1ms, 10-session batch < 10ms. fn bench_preference_merge(c: &mut Criterion) { const DIM: usize = 128; let prefs = PreferenceVectors::new(DIM); // Pre-populate 100K users. for user_id in 0u64..100_000 { let mut emb: Vec = (0..DIM) .map(|i| ((user_id + i as u64) % 7) as f32) .collect(); let norm: f32 = emb.iter().map(|x| x * x).sum::().sqrt(); if norm > f32::EPSILON { for x in &mut emb { *x /= norm; } } prefs.update_with_custom_rate(user_id, &emb, 0.1); } let interaction: Vec = { let mut v: Vec = (0..DIM).map(|i| (i % 5) as f32).collect(); let norm: f32 = v.iter().map(|x| x * x).sum::().sqrt(); if norm > f32::EPSILON { for x in &mut v { *x /= norm; } } v }; let mut group = c.benchmark_group("preference_merge"); group.bench_function("single_128d_ema_100k_users", |b| { let mut user_id = 0u64; b.iter(|| { prefs.update_with_custom_rate( black_box(user_id % 100_000), black_box(&interaction), 0.1, ); user_id += 1; }); }); group.bench_function("10_session_batch_128d_100k_users", |b| { let mut user_id = 0u64; b.iter(|| { for _ in 0..10 { prefs.update_with_custom_rate( black_box(user_id % 100_000), black_box(&interaction), 0.1, ); user_id += 1; } }); }); group.finish(); } // ── CoEngagementIndex eviction at scale ────────────────────────────────────── /// Benchmark eviction latency at various capacities. /// /// Eviction is O(N log N) — verify acceptable latency at 10K, 50K, 100K edges. fn bench_co_engagement_eviction_at_scale(c: &mut Criterion) { let mut group = c.benchmark_group("co_engagement_eviction"); for &capacity in &[10_000usize, 50_000, 100_000] { let index = CoEngagementIndex::with_capacity(capacity); // Pre-fill to capacity. let users = (capacity / 50).max(1); for user_id in 1..=users as u64 { for item_id in 1..=50u64 { index.record_positive(user_id, EntityId::new(item_id)); } } group.bench_with_input( BenchmarkId::from_parameter(capacity), &capacity, |b, _cap| { // Each iteration adds an edge for user 9999 (new user, forces eviction). b.iter(|| { index.record_positive(black_box(99_999), black_box(EntityId::new(42))); }); }, ); } group.finish(); } criterion_group!( benches, bench_social_graph_bitmap, bench_resolve_social_subgraph, bench_co_engagement, bench_social_graph_1m_items, bench_preference_merge, bench_co_engagement_eviction_at_scale, ); criterion_main!(benches);