From 6fa8574b7e7f67cea9f1e70daa41a881652c6984 Mon Sep 17 00:00:00 2001 From: Zack Kollar Date: Tue, 2 Sep 2025 00:35:05 -0400 Subject: [PATCH] :red_car: Add performance metrics --- CLAUDE.md | 19 ++ Cargo.lock | 310 ++++++++++++++++- crates/kiko-backend/Cargo.toml | 14 + crates/kiko-backend/benches/pubsub.rs | 203 ++++++++++++ .../kiko-backend/benches/session_service.rs | 312 ++++++++++++++++++ crates/kiko-backend/src/lib.rs | 13 + crates/kiko-backend/src/main.rs | 15 +- crates/kiko-backend/src/services/sessions.rs | 1 + crates/kiko/Cargo.toml | 14 +- crates/kiko/benches/id_generation.rs | 267 +++++++++++++++ 10 files changed, 1152 insertions(+), 16 deletions(-) create mode 100644 crates/kiko-backend/benches/pubsub.rs create mode 100644 crates/kiko-backend/benches/session_service.rs create mode 100644 crates/kiko-backend/src/lib.rs create mode 100644 crates/kiko/benches/id_generation.rs diff --git a/CLAUDE.md b/CLAUDE.md index 26cf583..80427a9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `./bin/e2e --ui` - Run E2E tests in interactive UI mode - `./bin/e2e --show-report` - Show existing test report in browser +### Performance Testing +- `cargo bench` - Run all performance benchmarks +- `cargo bench --bench pubsub_performance` - Test PubSub messaging performance +- `cargo bench --bench session_service_performance` - Test session management performance +- `cargo bench -p kiko --bench id_generation_performance` - Test ID generation performance +- View HTML reports: `open target/criterion/report/index.html` + +#### Performance Benchmarks Included +- **PubSub Performance**: Tests message throughput, concurrent operations, and memory cleanup +- **Session Service**: Tests CRUD operations, participant management, and concurrent access +- **ID Generation**: Tests uniqueness, throughput, and serialization performance + +#### Frontend Performance Testing +Since WASM benchmarking is limited, use browser dev tools for frontend performance: +- **Chrome DevTools Performance**: Record and analyze component renders +- **Browser Console timing**: Use `console.time()` and `console.timeEnd()` in components +- **WebSocket message timing**: Monitor real-time message latency in Network tab +- **Memory usage**: Check for memory leaks in Memory tab during long sessions + ### Pre-commit Hooks - Pre-commit hooks automatically run `cargo fmt` and `cargo clippy` on each commit - Install with `pre-commit install` (one-time setup) diff --git a/Cargo.lock b/Cargo.lock index 13099c4..08248fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,18 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + [[package]] name = "anymap2" version = "0.13.0" @@ -53,6 +65,28 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "async-trait" version = "0.1.88" @@ -245,6 +279,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.31" @@ -274,6 +314,58 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + [[package]] name = "color-eyre" version = "0.6.5" @@ -326,12 +418,75 @@ dependencies = [ "libc", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "futures", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "tokio", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -394,6 +549,12 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -962,6 +1123,16 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -980,6 +1151,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "0.2.12" @@ -1132,6 +1309,26 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.2", + "libc", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1155,7 +1352,9 @@ dependencies = [ "async-trait", "color-eyre", "console_error_panic_hook", + "criterion", "gloo-net 0.6.0", + "rand 0.8.5", "serde", "serde_json", "thiserror 2.0.12", @@ -1174,9 +1373,13 @@ dependencies = [ "async-trait", "axum 0.8.4", "chrono", + "criterion", "dashmap 6.1.0", + "futures", "kiko", + "rand 0.8.5", "tokio", + "tokio-test", "tower 0.5.2", "tower-http", "tower_governor", @@ -1330,7 +1533,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", ] @@ -1349,6 +1552,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "overload" version = "0.1.1" @@ -1433,6 +1642,34 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1625,6 +1862,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.12" @@ -1702,6 +1959,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1974,6 +2240,16 @@ dependencies = [ "serde", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.45.0" @@ -2014,6 +2290,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-tungstenite" version = "0.26.2" @@ -2253,6 +2542,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2367,6 +2666,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +dependencies = [ + "windows-sys", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/crates/kiko-backend/Cargo.toml b/crates/kiko-backend/Cargo.toml index f216fe6..58e1e7f 100644 --- a/crates/kiko-backend/Cargo.toml +++ b/crates/kiko-backend/Cargo.toml @@ -15,3 +15,17 @@ tower = "0.5.2" tower_governor = { version = "0.3" } tower-http = { version = "0.6.4", features = ["cors", "trace"] } arc-swap = "1.7.1" + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports", "async_tokio"] } +tokio-test = "0.4" +rand = "0.8" +futures = "0.3" + +[[bench]] +name = "pubsub" +harness = false + +[[bench]] +name = "session_service" +harness = false diff --git a/crates/kiko-backend/benches/pubsub.rs b/crates/kiko-backend/benches/pubsub.rs new file mode 100644 index 0000000..dbbda27 --- /dev/null +++ b/crates/kiko-backend/benches/pubsub.rs @@ -0,0 +1,203 @@ +use std::{sync::Arc, time::Duration}; + +use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; + +use kiko::{data::SessionMessage, id::SessionId}; +use kiko_backend::messaging::PubSub; + +fn bench_pubsub_subscribe(c: &mut Criterion) { + let pubsub = Arc::new(PubSub::new()); + + c.bench_function("pubsub_subscribe_single", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + let session_id = SessionId::new(); + black_box(pubsub.subscribe(session_id).await) + }); + }); + + let mut group = c.benchmark_group("pubsub_subscribe_concurrent"); + for concurrent_count in [10, 50, 100, 500].iter() { + group.throughput(Throughput::Elements(*concurrent_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(concurrent_count), + concurrent_count, + |b, &concurrent_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + let futures: Vec<_> = (0..concurrent_count) + .map(|_| { + let pubsub = pubsub.clone(); + async move { + let session_id = SessionId::new(); + pubsub.subscribe(session_id).await + } + }) + .collect(); + + black_box(futures::future::join_all(futures).await) + }); + }, + ); + } + group.finish(); +} + +fn bench_pubsub_publish(c: &mut Criterion) { + let pubsub = Arc::new(PubSub::new()); + + let session_id = SessionId::new(); + let message = SessionMessage::CreateSession(kiko::data::CreateSession { + name: "Benchmark Session".to_string(), + duration: Duration::from_secs(3600), + }); + + // Setup subscription first + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let _notifier = pubsub.subscribe(session_id.clone()).await; + }); + + c.bench_function("pubsub_publish_single", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + pubsub.publish(session_id.clone(), message.clone()).await; + }); + }); + + let mut group = c.benchmark_group("pubsub_publish_throughput"); + for message_count in [100, 500, 1000, 5000].iter() { + group.throughput(Throughput::Elements(*message_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(message_count), + message_count, + |b, &message_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + for _ in 0..message_count { + pubsub.publish(session_id.clone(), message.clone()).await; + } + }); + }, + ); + } + group.finish(); +} + +fn bench_pubsub_consume_events(c: &mut Criterion) { + let pubsub = Arc::new(PubSub::new()); + + let session_id = SessionId::new(); + let message = SessionMessage::CreateSession(kiko::data::CreateSession { + name: "Benchmark Session".to_string(), + duration: Duration::from_secs(3600), + }); + + // Setup subscription first + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let _notifier = pubsub.subscribe(session_id.clone()).await; + pubsub.publish(session_id.clone(), message).await; + }); + + c.bench_function("pubsub_get_event", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { black_box(pubsub.get_event(&session_id).await) }); + }); + + let mut group = c.benchmark_group("pubsub_consume_event_cycle"); + for cycle_count in [10, 50, 100, 500].iter() { + group.throughput(Throughput::Elements(*cycle_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(cycle_count), + cycle_count, + |b, &cycle_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + for i in 0..cycle_count { + let msg = SessionMessage::CreateSession(kiko::data::CreateSession { + name: format!("Benchmark Session {i}"), + duration: Duration::from_secs(3600), + }); + pubsub.publish(session_id.clone(), msg).await; + black_box(pubsub.consume_event(&session_id).await); + } + }); + }, + ); + } + group.finish(); +} + +fn bench_pubsub_concurrent_operations(c: &mut Criterion) { + let pubsub = Arc::new(PubSub::new()); + + let mut group = c.benchmark_group("pubsub_concurrent_pub_sub"); + for session_count in [10, 50, 100].iter() { + group.throughput(Throughput::Elements(*session_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(session_count), + session_count, + |b, &session_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + let futures: Vec<_> = (0..session_count) + .map(|i| { + let pubsub = pubsub.clone(); + async move { + let session_id = SessionId::new(); + let _notifier = pubsub.subscribe(session_id.clone()).await; + + let message = + SessionMessage::CreateSession(kiko::data::CreateSession { + name: format!("Concurrent Session {i}"), + duration: Duration::from_secs(3600), + }); + + pubsub.publish(session_id.clone(), message).await; + pubsub.consume_event(&session_id).await + } + }) + .collect(); + + black_box(futures::future::join_all(futures).await) + }); + }, + ); + } + group.finish(); +} + +fn bench_pubsub_memory_efficiency(c: &mut Criterion) { + let pubsub = Arc::new(PubSub::new()); + + c.bench_function("pubsub_session_cleanup", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| async { + let session_ids: Vec = (0..100).map(|_| SessionId::new()).collect(); + + for session_id in &session_ids { + let _notifier = pubsub.subscribe(session_id.clone()).await; + let message = SessionMessage::CreateSession(kiko::data::CreateSession { + name: "Cleanup Test Session".to_string(), + duration: Duration::from_secs(3600), + }); + pubsub.publish(session_id.clone(), message).await; + } + + for session_id in &session_ids { + pubsub.cleanup_session(session_id).await; + } + }); + }); +} + +criterion_group!( + pubsub_benches, + bench_pubsub_subscribe, + bench_pubsub_publish, + bench_pubsub_consume_events, + bench_pubsub_concurrent_operations, + bench_pubsub_memory_efficiency +); +criterion_main!(pubsub_benches); diff --git a/crates/kiko-backend/benches/session_service.rs b/crates/kiko-backend/benches/session_service.rs new file mode 100644 index 0000000..0fd3801 --- /dev/null +++ b/crates/kiko-backend/benches/session_service.rs @@ -0,0 +1,312 @@ +use std::time::Duration; + +use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; + +use kiko::{data::CreateSession, id::SessionId}; +use kiko_backend::services::{SessionService, SessionServiceInMemory}; + +fn bench_session_create(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + c.bench_function("session_create_single", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + async move { + let create_request = CreateSession { + name: "Benchmark Session".to_string(), + duration: Duration::from_secs(3600), + }; + black_box(service.create(create_request).await.unwrap()) + } + }); + }); + + let mut group = c.benchmark_group("session_create_throughput"); + for session_count in [100, 500, 1000, 5000].iter() { + group.throughput(Throughput::Elements(*session_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(session_count), + session_count, + |b, &session_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + async move { + for i in 0..session_count { + let create_request = CreateSession { + name: format!("Benchmark Session {i}"), + duration: Duration::from_secs(3600), + }; + black_box(service.create(create_request).await.unwrap()); + } + } + }); + }, + ); + } + group.finish(); +} + +fn bench_session_get(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let session_ids: Vec = rt.block_on(async { + let mut ids = Vec::new(); + for i in 0..1000 { + let create_request = CreateSession { + name: format!("Get Benchmark Session {i}"), + duration: Duration::from_secs(3600), + }; + let session = service.create(create_request).await.unwrap(); + ids.push(session.id); + } + ids + }); + + c.bench_function("session_get_single", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_id = session_ids[0].clone(); + async move { black_box(service.get(&session_id).await.unwrap()) } + }); + }); + + let mut group = c.benchmark_group("session_get_random_access"); + for access_count in [100, 500, 1000].iter() { + group.throughput(Throughput::Elements(*access_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(access_count), + access_count, + |b, &access_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_ids = session_ids.clone(); + async move { + use rand::seq::SliceRandom; + let mut rng = rand::thread_rng(); + for _ in 0..access_count { + let session_id = session_ids.choose(&mut rng).unwrap(); + black_box(service.get(session_id).await.unwrap()); + } + } + }); + }, + ); + } + group.finish(); +} + +fn bench_session_list(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + for i in 0..1000 { + let create_request = CreateSession { + name: format!("List Benchmark Session {i}"), + duration: Duration::from_secs(3600), + }; + let _session = service.create(create_request).await.unwrap(); + } + }); + + c.bench_function("session_list_1000_sessions", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + async move { black_box(service.list().await.unwrap()) } + }); + }); +} + +fn bench_session_participant_operations(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let session_id = rt.block_on(async { + let create_request = CreateSession { + name: "Participant Benchmark Session".to_string(), + duration: Duration::from_secs(3600), + }; + let session = service.create(create_request).await.unwrap(); + session.id + }); + + c.bench_function("session_join_participant", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_id = session_id.clone(); + async move { + let participant_name = format!("Participant_{}", rand::random::()); + black_box(service.join(&session_id, &participant_name).await.unwrap()) + } + }); + }); + + let mut group = c.benchmark_group("session_participant_join_leave_cycle"); + for participant_count in [10, 50, 100, 500].iter() { + group.throughput(Throughput::Elements(*participant_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(participant_count), + participant_count, + |b, &participant_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_id = session_id.clone(); + async move { + let mut participant_ids = Vec::new(); + + // Join participants + for i in 0..participant_count { + let participant_name = format!("Cycle_Participant_{i}"); + let participant = + service.join(&session_id, &participant_name).await.unwrap(); + participant_ids.push(participant.id().clone()); + } + + // Leave participants + for participant_id in participant_ids { + service.leave(&session_id, &participant_id).await.unwrap(); + } + } + }); + }, + ); + } + group.finish(); +} + +fn bench_session_concurrent_access(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let session_id = rt.block_on(async { + let create_request = CreateSession { + name: "Concurrent Access Session".to_string(), + duration: Duration::from_secs(3600), + }; + let session = service.create(create_request).await.unwrap(); + session.id + }); + + let mut group = c.benchmark_group("session_concurrent_operations"); + for concurrent_count in [10, 50, 100].iter() { + group.throughput(Throughput::Elements(*concurrent_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(concurrent_count), + concurrent_count, + |b, &concurrent_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_id = session_id.clone(); + async move { + let futures: Vec<_> = (0..concurrent_count) + .map(|i| { + let service = service.clone(); + let session_id = session_id.clone(); + async move { + let participant_name = + format!("Concurrent_Participant_{i}"); + let participant = service + .join(&session_id, &participant_name) + .await + .unwrap(); + let _session = service.get(&session_id).await.unwrap(); + service.leave(&session_id, participant.id()).await.unwrap(); + } + }) + .collect(); + + black_box(futures::future::join_all(futures).await) + } + }); + }, + ); + } + group.finish(); +} + +fn bench_session_update_performance(c: &mut Criterion) { + let service = SessionServiceInMemory::new(); + + let rt = tokio::runtime::Runtime::new().unwrap(); + let session_id = rt.block_on(async { + let create_request = CreateSession { + name: "Update Performance Session".to_string(), + duration: Duration::from_secs(3600), + }; + let session = service.create(create_request).await.unwrap(); + session.id + }); + + c.bench_function("session_update_single", |b| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + let session_id = session_id.clone(); + async move { + let mut session = service.get(&session_id).await.unwrap(); + session.set_topic(format!("Topic {}", rand::random::())); + black_box(service.update(&session_id, &session).await.unwrap()) + } + }); + }); + + let mut group = c.benchmark_group("session_update_with_participants"); + for participant_count in [10, 50, 100].iter() { + group.throughput(Throughput::Elements(*participant_count as u64)); + group.bench_with_input( + BenchmarkId::from_parameter(participant_count), + participant_count, + |b, &participant_count| { + b.to_async(tokio::runtime::Runtime::new().unwrap()) + .iter(|| { + let service = service.clone(); + async move { + // Create fresh session with participants + let create_request = CreateSession { + name: "Update with Participants Session".to_string(), + duration: Duration::from_secs(3600), + }; + let mut session = service.create(create_request).await.unwrap(); + let session_id = session.id.clone(); + + // Add participants + for i in 0..participant_count { + let participant_name = format!("Update_Participant_{i}"); + let _participant = + service.join(&session_id, &participant_name).await.unwrap(); + } + + // Get updated session and modify + session = service.get(&session_id).await.unwrap(); + session.set_topic("Updated Topic".to_string()); + black_box(service.update(&session_id, &session).await.unwrap()); + + // Cleanup + service.end(&session_id).await.unwrap(); + } + }); + }, + ); + } + group.finish(); +} + +criterion_group!( + session_service_benches, + bench_session_create, + bench_session_get, + bench_session_list, + bench_session_participant_operations, + bench_session_concurrent_access, + bench_session_update_performance +); +criterion_main!(session_service_benches); diff --git a/crates/kiko-backend/src/lib.rs b/crates/kiko-backend/src/lib.rs new file mode 100644 index 0000000..ef1b238 --- /dev/null +++ b/crates/kiko-backend/src/lib.rs @@ -0,0 +1,13 @@ +pub mod handlers; +pub mod messaging; +pub mod services; + +use crate::{messaging::PubSub, services::SessionServiceInMemory}; +use chrono::DateTime; + +/// Shared application state containing services and configuration. +pub struct AppState { + pub started_at: DateTime, + pub sessions: SessionServiceInMemory, + pub pub_sub: PubSub, +} diff --git a/crates/kiko-backend/src/main.rs b/crates/kiko-backend/src/main.rs index 3ef4bc2..dead21e 100644 --- a/crates/kiko-backend/src/main.rs +++ b/crates/kiko-backend/src/main.rs @@ -3,10 +3,6 @@ //! A real-time session management backend built with Axum and WebSockets. //! Provides REST APIs for session management and WebSocket connections for live updates. -pub mod handlers; -pub mod messaging; -pub mod services; - use std::{net::SocketAddr, sync::Arc}; use axum::{ @@ -14,21 +10,12 @@ use axum::{ http::{Method, header}, routing::{get, post}, }; -use chrono::DateTime; use tokio::{net::TcpListener, signal}; use tower_http::cors::CorsLayer; use kiko::errors::Report; use kiko::log; - -use crate::{messaging::PubSub, services::SessionServiceInMemory}; - -/// Shared application state containing services and configuration. -pub struct AppState { - started_at: DateTime, - sessions: SessionServiceInMemory, - pub_sub: PubSub, -} +use kiko_backend::{AppState, handlers, messaging::PubSub, services::SessionServiceInMemory}; #[tokio::main] async fn main() -> Result<(), Report> { diff --git a/crates/kiko-backend/src/services/sessions.rs b/crates/kiko-backend/src/services/sessions.rs index 5a49df9..1a3bfb4 100644 --- a/crates/kiko-backend/src/services/sessions.rs +++ b/crates/kiko-backend/src/services/sessions.rs @@ -160,6 +160,7 @@ pub trait SessionService, } diff --git a/crates/kiko/Cargo.toml b/crates/kiko/Cargo.toml index cc806d7..fd06272 100644 --- a/crates/kiko/Cargo.toml +++ b/crates/kiko/Cargo.toml @@ -23,9 +23,21 @@ color-eyre = "0.6.5" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = "0.1" tracing-web = { version = "0.1.3", optional = true } -tracing-subscriber = { version = "0.3.19", features = ["fmt", "env-filter", "time"], optional = true } +tracing-subscriber = { version = "0.3.19", features = [ + "fmt", + "env-filter", + "time", +], optional = true } time = { version = "0.3", features = ["wasm-bindgen"] } [features] default = ["dev-logging"] dev-logging = ["tracing-web", "tracing-subscriber"] + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } +rand = "0.8" + +[[bench]] +name = "id_generation" +harness = false diff --git a/crates/kiko/benches/id_generation.rs b/crates/kiko/benches/id_generation.rs new file mode 100644 index 0000000..64d4cee --- /dev/null +++ b/crates/kiko/benches/id_generation.rs @@ -0,0 +1,267 @@ +use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; +use kiko::id::{Id, ParticipantId, SessionId, StoryId, VoteId}; +use std::collections::HashSet; + +fn bench_id_generation_basic(c: &mut Criterion) { + c.bench_function("session_id_new", |b| b.iter(|| black_box(SessionId::new()))); + + c.bench_function("participant_id_new", |b| { + b.iter(|| black_box(ParticipantId::new())) + }); + + c.bench_function("story_id_new", |b| b.iter(|| black_box(StoryId::new()))); + + c.bench_function("vote_id_new", |b| b.iter(|| black_box(VoteId::new()))); + + c.bench_function("generic_id_generate", |b| { + b.iter(|| black_box(Id::<()>::generate())) + }); +} + +fn bench_id_generation_throughput(c: &mut Criterion) { + let mut group = c.benchmark_group("id_generation_throughput"); + + for count in [100, 1000, 10000, 100000].iter() { + group.throughput(Throughput::Elements(*count as u64)); + + group.bench_with_input( + BenchmarkId::new("session_id_batch", count), + count, + |b, &count| { + b.iter(|| { + let ids: Vec = (0..count).map(|_| SessionId::new()).collect(); + black_box(ids) + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("participant_id_batch", count), + count, + |b, &count| { + b.iter(|| { + let ids: Vec = + (0..count).map(|_| ParticipantId::new()).collect(); + black_box(ids) + }) + }, + ); + } + + group.finish(); +} + +fn bench_id_uniqueness_validation(c: &mut Criterion) { + let mut group = c.benchmark_group("id_uniqueness"); + + for count in [1000, 10000, 100000].iter() { + group.throughput(Throughput::Elements(*count as u64)); + + group.bench_with_input( + BenchmarkId::new("session_id_uniqueness", count), + count, + |b, &count| { + b.iter(|| { + let mut ids = HashSet::new(); + for _ in 0..count { + let id = SessionId::new(); + ids.insert(id.as_str().to_string()); + } + // All IDs should be unique + assert_eq!(ids.len(), count); + black_box(ids) + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("participant_id_uniqueness", count), + count, + |b, &count| { + b.iter(|| { + let mut ids = HashSet::new(); + for _ in 0..count { + let id = ParticipantId::new(); + ids.insert(id.as_str().to_string()); + } + // All IDs should be unique + assert_eq!(ids.len(), count); + black_box(ids) + }) + }, + ); + } + + group.finish(); +} + +fn bench_id_operations(c: &mut Criterion) { + let sample_ids: Vec = (0..1000).map(|_| SessionId::new()).collect(); + let sample_strings: Vec = sample_ids + .iter() + .map(|id| id.as_str().to_string()) + .collect(); + + c.bench_function("id_as_str", |b| { + b.iter(|| { + for id in &sample_ids { + black_box(id.as_str()); + } + }) + }); + + c.bench_function("id_to_string", |b| { + b.iter(|| { + for id in &sample_ids { + black_box(id.to_string()); + } + }) + }); + + c.bench_function("id_clone", |b| { + b.iter(|| { + for id in &sample_ids { + black_box(id.clone()); + } + }) + }); + + c.bench_function("id_from_string", |b| { + b.iter(|| { + for s in &sample_strings { + black_box(SessionId::from_string(s.clone())); + } + }) + }); + + c.bench_function("id_from_str_ref", |b| { + b.iter(|| { + for s in &sample_strings { + black_box(SessionId::from(s.as_str())); + } + }) + }); +} + +fn bench_id_custom_generation(c: &mut Criterion) { + let mut group = c.benchmark_group("id_custom_generation"); + + let numeric_alphabet = "0123456789"; + let alpha_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + let alphanumeric_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for &length in [4, 6, 8, 12, 16].iter() { + group.bench_with_input( + BenchmarkId::new("numeric_custom", length), + &length, + |b, &length| b.iter(|| black_box(SessionId::generate_custom(length, numeric_alphabet))), + ); + + group.bench_with_input( + BenchmarkId::new("alpha_custom", length), + &length, + |b, &length| b.iter(|| black_box(SessionId::generate_custom(length, alpha_alphabet))), + ); + + group.bench_with_input( + BenchmarkId::new("alphanumeric_custom", length), + &length, + |b, &length| { + b.iter(|| black_box(SessionId::generate_custom(length, alphanumeric_alphabet))) + }, + ); + } + + group.finish(); +} + +fn bench_id_serialization(c: &mut Criterion) { + let sample_ids: Vec = (0..1000).map(|_| SessionId::new()).collect(); + + c.bench_function("id_serialize_json", |b| { + b.iter(|| { + for id in &sample_ids { + let json = serde_json::to_string(id).unwrap(); + black_box(json); + } + }) + }); + + let json_strings: Vec = sample_ids + .iter() + .map(|id| serde_json::to_string(id).unwrap()) + .collect(); + + c.bench_function("id_deserialize_json", |b| { + b.iter(|| { + for json in &json_strings { + let id: SessionId = serde_json::from_str(json).unwrap(); + black_box(id); + } + }) + }); +} + +fn bench_id_hash_collections(c: &mut Criterion) { + use std::collections::{HashMap, HashSet}; + + let sample_ids: Vec = (0..10000).map(|_| SessionId::new()).collect(); + + c.bench_function("id_hashset_insert", |b| { + b.iter(|| { + let mut set = HashSet::new(); + for id in &sample_ids { + set.insert(id.clone()); + } + black_box(set) + }) + }); + + c.bench_function("id_hashmap_insert", |b| { + b.iter(|| { + let mut map = HashMap::new(); + for (i, id) in sample_ids.iter().enumerate() { + map.insert(id.clone(), i); + } + black_box(map) + }) + }); + + let mut pre_populated_set: HashSet = HashSet::new(); + for id in &sample_ids { + pre_populated_set.insert(id.clone()); + } + + c.bench_function("id_hashset_lookup", |b| { + b.iter(|| { + for id in &sample_ids { + black_box(pre_populated_set.contains(id)); + } + }) + }); + + let mut pre_populated_map: HashMap = HashMap::new(); + for (i, id) in sample_ids.iter().enumerate() { + pre_populated_map.insert(id.clone(), i); + } + + c.bench_function("id_hashmap_lookup", |b| { + b.iter(|| { + for id in &sample_ids { + black_box(pre_populated_map.get(id)); + } + }) + }); +} + +criterion_group!( + id_benches, + bench_id_generation_basic, + bench_id_generation_throughput, + bench_id_uniqueness_validation, + bench_id_operations, + bench_id_custom_generation, + bench_id_serialization, + bench_id_hash_collections +); +criterion_main!(id_benches);