tidaldb/tidal/benches/social.rs
2026-02-23 22:41:16 -07:00

311 lines
11 KiB
Rust

#![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<f32> = (0..DIM)
.map(|i| ((user_id + i as u64) % 7) as f32)
.collect();
let norm: f32 = emb.iter().map(|x| x * x).sum::<f32>().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<f32> = {
let mut v: Vec<f32> = (0..DIM).map(|i| (i % 5) as f32).collect();
let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().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);