mirror of https://github.com/synctv-org/synctv
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.
501 lines
16 KiB
Rust
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,
|
|
);
|