//! Database performance benchmarks with real `PostgreSQL` //! //! Run with: cargo bench --bench `database_benchmarks` //! //! These benchmarks use the shared `synctv-core-testing` PostgreSQL fixture and //! require Docker to be running. //! //! Performance targets: //! - Single row query: < 1ms //! - Paginated list query (100 items): < 10ms //! - Batch insert (100 items): < 100ms //! - Index lookup: < 1ms #![allow(clippy::unwrap_used)] use std::env; use std::hint::black_box; use std::time::{Duration, Instant}; use chrono::Utc; use criterion::{criterion_group, BenchmarkId, Criterion, Throughput}; use sqlx::PgPool; use synctv_core::{ models::{ id::PlaylistId, media::Media, playlist::Playlist, MediaId, PageParams, Room, RoomId, RoomListQuery, RoomMember, RoomRole, RoomStatus, SourceProvider, User, UserId, UserRole, UserStatus, }, repository::{ MediaRepository, PlaylistRepository, RoomMemberRepository, RoomRepository, UserRepository, }, }; use synctv_core_testing::TestContainer; struct BenchmarkDatabase { container: TestContainer, pool: PgPool, } impl BenchmarkDatabase { fn pool(&self) -> &PgPool { &self.pool } fn shutdown(self, rt: &tokio::runtime::Runtime) { rt.block_on(async move { self.pool.close().await; self.container.cleanup().await; }); } } fn print_benchmark_list() { for room_count in [100, 500, 1000] { println!("list_rooms/paginated/{room_count}: benchmark"); } for media_count in [100, 500, 1000] { println!("list_media/paginated/{media_count}: benchmark"); } for batch_size in [10, 50, 100] { println!("batch_insert/media/{batch_size}: benchmark"); } println!("index_effectiveness/get_by_room_indexed: benchmark"); println!("single_row_operations/create_room: benchmark"); println!("single_row_operations/get_room_by_id: benchmark"); for page in [1u32, 5, 25, 45] { println!("pagination_offset/page/{page}: benchmark"); } } fn is_nextest_list_mode() -> bool { env::args().any(|arg| arg == "--list") } fn main() { if is_nextest_list_mode() { print_benchmark_list(); return; } benches(); Criterion::default().configure_from_args().final_summary(); } async fn setup_test_db() -> BenchmarkDatabase { let (container, pool) = synctv_core_testing::create_test_pool_with_options_and_label( "synctv_bench", "database-benchmarks", 16, Duration::from_secs(30), ) .await; BenchmarkDatabase { container, pool } } /// Create test user fn make_user(username: &str) -> User { let now = Utc::now(); User { id: UserId::new(), username: username.to_string(), role: UserRole::User, avatar_file_reference_id: None, status: UserStatus::Active, signup_method: synctv_core::models::SignupMethod::Email, created_at: now, updated_at: now, version: 0, deleted_at: None, is_banned: false, banned_at: None, banned_by: None, banned_reason: None, } } /// Create test room fn make_room(name: &str, owner_id: &UserId) -> Room { let now = Utc::now(); Room { id: RoomId::new(), name: name.to_string(), description: String::new(), cover_file_reference_id: None, category: None, labels: Vec::new(), created_by: *owner_id, status: RoomStatus::Active, is_banned: false, is_public: true, closed_at: None, created_at: now, updated_at: now, deleted_at: None, version: 0, last_activity_at: now, } } /// Create test playlist fn make_playlist(room_id: &RoomId, parent_id: Option<&PlaylistId>, name: &str) -> Playlist { let now = Utc::now(); Playlist { id: PlaylistId::new(), room_id: *room_id, creator_id: None, browse_access_mode: synctv_core::models::PlaylistBrowseAccessMode::Default, name: name.to_string(), description: String::new(), cover_file_reference_id: None, parent_id: parent_id.copied(), position: 0.0, source_provider: None, source_config: None, provider_instance_name: None, created_at: now, updated_at: now, version: 0, } } /// Benchmark: Room list queries with different data sizes fn bench_list_rooms_with_data(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool.clone()); // Create test user let owner = rt .block_on(user_repo.create(&make_user("bench_owner"))) .unwrap(); let mut group = c.benchmark_group("list_rooms"); group.measurement_time(Duration::from_secs(10)); group.sample_size(50); // Test different data sizes for &room_count in &[100, 500, 1000] { // Seed data for i in 0..room_count { let room = make_room(&format!("bench_room_{room_count}_{i}"), &owner.id); rt.block_on(room_repo.create(&room)).unwrap(); } let bench_id = BenchmarkId::new("paginated", room_count); group.throughput(Throughput::Elements(20)); // page size group.bench_with_input(bench_id, &room_count, |b, _| { b.to_async(&rt).iter(|| async { let query = RoomListQuery { pagination: PageParams::new(Some(1), Some(20)), ..Default::default() }; let rooms = room_repo.list(&query).await.unwrap(); black_box(rooms); }); }); // Cleanup for next iteration rt.block_on(sqlx::query!("DELETE FROM rooms").execute(&pool)) .unwrap(); } group.finish(); db.shutdown(&rt); } /// Benchmark: Media list queries with different playlist sizes fn bench_list_media_with_data(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool.clone()); let playlist_repo = PlaylistRepository::new(pool.clone()); let media_repo = MediaRepository::new(pool.clone()); // Create test data let owner = rt .block_on(user_repo.create(&make_user("media_bench_owner"))) .unwrap(); let room = make_room("media_bench_room", &owner.id); let room = rt.block_on(room_repo.create(&room)).unwrap(); let playlist = make_playlist(&room.id, None, "bench_playlist"); let playlist = rt.block_on(playlist_repo.create(&playlist)).unwrap(); let mut group = c.benchmark_group("list_media"); group.measurement_time(Duration::from_secs(10)); group.sample_size(50); // Test different data sizes for &media_count in &[100, 500, 1000] { // Seed media for i in 0..media_count { let media = Media { id: MediaId::new(), playlist_id: Some(playlist.id), room_id: room.id, creator_id: None, name: format!("media_{media_count}_{i}"), description: String::new(), position: f64::from(i), source_provider: SourceProvider::DirectUrl, source_config: synctv_core_testing::direct_url_media_source_config(format!( "https://example.com/{i}.mp4" )), provider_instance_name: None, cover_file_reference_id: None, thumbnail_file_reference_id: None, added_at: Utc::now(), updated_at: Utc::now(), version: 0, }; rt.block_on(media_repo.create(&media)).unwrap(); } let bench_id = BenchmarkId::new("paginated", media_count); group.throughput(Throughput::Elements(20)); // page size group.bench_with_input(bench_id, &media_count, |b, _| { b.to_async(&rt).iter(|| async { let media = media_repo .get_by_playlist_limit_offset(&playlist.id, 20, 0) .await .unwrap(); black_box(media); }); }); // Cleanup rt.block_on(sqlx::query!("DELETE FROM media").execute(&pool)) .unwrap(); } group.finish(); db.shutdown(&rt); } /// Benchmark: Batch insert operations fn bench_batch_insert_operations(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool.clone()); let playlist_repo = PlaylistRepository::new(pool.clone()); let media_repo = MediaRepository::new(pool); // Create test data let owner = rt .block_on(user_repo.create(&make_user("batch_insert_owner"))) .unwrap(); let room = make_room("batch_insert_room", &owner.id); let room = rt.block_on(room_repo.create(&room)).unwrap(); let mut group = c.benchmark_group("batch_insert"); group.measurement_time(Duration::from_secs(15)); group.sample_size(20); // Test different batch sizes for &batch_size in &[10, 50, 100] { let playlist = make_playlist(&room.id, None, &format!("batch_playlist_{batch_size}")); let _playlist = rt.block_on(playlist_repo.create(&playlist)).unwrap(); let bench_id = BenchmarkId::new("media", batch_size); group.throughput(Throughput::Elements( u64::try_from(batch_size).unwrap_or_default(), )); group.bench_with_input(bench_id, &batch_size, |b, &batch_size| { b.iter_custom(|iters| { let room_id = room.id; rt.block_on(async { let start = Instant::now(); for _ in 0..iters { // Create a new playlist for each iteration let new_playlist = make_playlist( &room_id, None, &format!("batch_{}", uuid::Uuid::new_v4()), ); let new_playlist = playlist_repo.create(&new_playlist).await.unwrap(); for i in 0..batch_size { let media = Media { id: MediaId::new(), playlist_id: Some(new_playlist.id), room_id, creator_id: None, name: format!("batch_media_{i}"), description: String::new(), position: f64::from(i), source_provider: SourceProvider::DirectUrl, source_config: synctv_core_testing::direct_url_media_source_config( "https://example.com/video.mp4", ), provider_instance_name: None, cover_file_reference_id: None, thumbnail_file_reference_id: None, added_at: Utc::now(), updated_at: Utc::now(), version: 0, }; media_repo.create(&media).await.unwrap(); } } start.elapsed() }) }); }); } group.finish(); db.shutdown(&rt); } /// Benchmark: Index effectiveness comparison fn bench_index_effectiveness(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool.clone()); let member_repo = RoomMemberRepository::new(pool); // Create test data let owner = rt .block_on(user_repo.create(&make_user("index_owner"))) .unwrap(); let room = make_room("index_room", &owner.id); let room = rt.block_on(room_repo.create(&room)).unwrap(); // Create members for i in 0..500 { let user = rt .block_on(user_repo.create(&make_user(&format!("member_{i}")))) .unwrap(); let member = RoomMember::new(room.id, user.id, RoomRole::Member); rt.block_on(member_repo.add(&member)).unwrap(); } let mut group = c.benchmark_group("index_effectiveness"); group.sample_size(100); // Test indexed lookup (room_id is indexed) group.bench_function("get_by_room_indexed", |b| { b.to_async(&rt).iter(|| async { let members = member_repo.list_by_room(&room.id).await.unwrap(); black_box(members); }); }); group.finish(); db.shutdown(&rt); } /// Benchmark: Single row CRUD operations fn bench_single_row_operations(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool); let owner = rt .block_on(user_repo.create(&make_user("crud_owner"))) .unwrap(); let mut group = c.benchmark_group("single_row_operations"); group.sample_size(100); // Benchmark: Single room creation group.bench_function("create_room", |b| { b.to_async(&rt).iter(|| async { let room = make_room(&format!("crud_room_{}", uuid::Uuid::new_v4()), &owner.id); let created = room_repo.create(&room).await.unwrap(); black_box(created); }); }); // Pre-create a room for get tests let room = make_room("crud_test_room", &owner.id); let created_room = rt.block_on(room_repo.create(&room)).unwrap(); // Benchmark: Single room get group.bench_function("get_room_by_id", |b| { b.to_async(&rt).iter(|| async { let room = room_repo.get_by_id(&created_room.id).await.unwrap(); black_box(room); }); }); group.finish(); db.shutdown(&rt); } /// Benchmark: Pagination performance at different offsets fn bench_pagination_offset_performance(c: &mut Criterion) { let rt = tokio::runtime::Runtime::new().unwrap(); let db = rt.block_on(setup_test_db()); let pool = db.pool().clone(); let user_repo = UserRepository::new(pool.clone()); let room_repo = RoomRepository::new(pool); let owner = rt .block_on(user_repo.create(&make_user("pagination_owner"))) .unwrap(); // Create 1000 rooms for i in 0..1000 { let room = make_room(&format!("page_room_{i:04}"), &owner.id); rt.block_on(room_repo.create(&room)).unwrap(); } let mut group = c.benchmark_group("pagination_offset"); group.sample_size(50); // Test different page numbers (simulating different offsets) for &page in &[1u32, 5, 25, 45] { let bench_id = BenchmarkId::new("page", page); group.bench_with_input(bench_id, &page, |b, &page| { b.to_async(&rt).iter(|| async { let query = RoomListQuery { pagination: PageParams::new(Some(page), Some(20)), ..Default::default() }; let rooms = room_repo.list(&query).await.unwrap(); black_box(rooms); }); }); } group.finish(); db.shutdown(&rt); } criterion_group!( benches, bench_list_rooms_with_data, bench_list_media_with_data, bench_batch_insert_operations, bench_index_effectiveness, bench_single_row_operations, bench_pagination_offset_performance, );