You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
synctv/synctv-core/benches/database_benchmarks.rs

501 lines
16 KiB
Rust

//! 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,
);