diff --git a/Cargo.lock b/Cargo.lock index c313735..87ea78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,6 +258,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.18" @@ -444,6 +450,12 @@ dependencies = [ "bytes", ] +[[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.9" @@ -526,6 +538,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "charybdis_bench" +version = "0.1.0" +dependencies = [ + "charybdis 0.7.11", + "chrono", + "criterion", + "futures", + "scylla", + "tokio", +] + [[package]] name = "charybdis_macros" version = "0.7.11" @@ -602,6 +626,33 @@ dependencies = [ "windows-targets", ] +[[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.26" @@ -699,6 +750,73 @@ dependencies = [ "cfg-if", ] +[[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", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -996,6 +1114,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1018,6 +1146,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "histogram" version = "0.6.9" @@ -1231,12 +1365,32 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -1436,6 +1590,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + [[package]] name = "openssl" version = "0.10.68" @@ -1527,6 +1687,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[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 = "powerfmt" version = "0.2.0" @@ -1599,6 +1787,26 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "reddit-sample" version = "0.1.0" @@ -1716,7 +1924,7 @@ dependencies = [ "futures", "hashbrown 0.14.5", "histogram", - "itertools", + "itertools 0.13.0", "lazy_static", "lz4_flex", "openssl", @@ -2016,6 +2224,16 @@ dependencies = [ "zerovec", ] +[[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.43.0" @@ -2258,6 +2476,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 1ed9224..06a843d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "charybdis-macros", "charybdis-parser", "examples/actix-web/reddit-api", + "charybdis_bench", ] [workspace.lints.clippy] diff --git a/charybdis_bench/Cargo.toml b/charybdis_bench/Cargo.toml new file mode 100644 index 0000000..6671111 --- /dev/null +++ b/charybdis_bench/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "charybdis_bench" +version = "0.1.0" +edition = "2021" + +[dev-dependencies] +criterion = "0.5.1" +charybdis = { path = "../charybdis" } +chrono = "0.4.39" +scylla = "0.15.1" +tokio = { version = "1.42.0", features = ["rt-multi-thread"] } + + +[[bench]] +name = "orm_vs_native" +harness = false +[dependencies] +futures = "0.3.31" \ No newline at end of file diff --git a/charybdis_bench/benches/orm_vs_native.rs b/charybdis_bench/benches/orm_vs_native.rs new file mode 100644 index 0000000..aacba58 --- /dev/null +++ b/charybdis_bench/benches/orm_vs_native.rs @@ -0,0 +1,388 @@ +use charybdis::batch::ModelBatch; +use charybdis::macros::{charybdis_model, charybdis_udt_model}; +use charybdis::operations::{Delete, Insert, Update}; +use charybdis::types::{Boolean, Text, Timestamp, Uuid}; +use chrono::Utc; +use criterion::{criterion_group, criterion_main, Criterion}; +use scylla::batch::{Batch, BatchStatement, BatchType}; +use scylla::transport::errors::QueryError; +use scylla::{CachingSession, SessionBuilder}; +use tokio::runtime::Runtime; + +const NODES: [&str; 3] = ["127.0.0.1:9042", "127.0.0.1:9043", "127.0.0.1:9044"]; +const KEYSPACE: &str = "benchmarks"; +const CACHE_SIZE: usize = 1000; + +#[charybdis_model( + table_name = bench_users, + partition_keys = [id], + clustering_keys = [] +)] +#[derive(Clone)] +pub struct BenchUser { + pub id: Uuid, + pub username: Text, + pub email: Text, + pub created_at: Timestamp, +} + +#[derive(Default, Clone)] +#[charybdis_udt_model(type_name = profile)] +pub struct Profile { + pub first_name: Text, + pub last_name: Text, + pub username: Text, + pub email: Text, +} + +impl Profile { + pub fn sample() -> Self { + Self { + first_name: "Homer".to_string(), + last_name: "Simpson".to_string(), + username: "homer".to_string(), + email: "homer@simpson.com".to_string(), + } + } +} + +#[charybdis_model( + table_name = posts, + partition_keys = [community_id], + clustering_keys = [created_at, id], + global_secondary_indexes = [], + table_options = " + CLUSTERING ORDER BY (created_at DESC) + " +)] +#[derive(Default, Clone)] +pub struct Post { + pub community_id: Uuid, + pub created_at: Timestamp, + pub id: Uuid, + pub title: Text, + pub description: Text, + pub updated_at: Timestamp, + pub creator_id: Uuid, + pub creator: Profile, + pub is_archived: Boolean, +} + +impl Post { + pub fn sample() -> Post { + Post { + community_id: Uuid::new_v4(), + created_at: Utc::now(), + id: Uuid::new_v4(), + title: "Test".to_string(), + description: "Test".to_string(), + updated_at: Default::default(), + creator_id: Uuid::new_v4(), + creator: Profile::sample(), + is_archived: false, + } + } + + pub async fn populate_sample_posts_per_partition(community_id: Uuid, db_session: &CachingSession) { + let mut posts = vec![]; + for i in 0..10000 { + let mut post = Post::sample(); + post.community_id = community_id; + post.title = format!("Post {}", i); + post.description = format!("Post {}", i); + post.creator_id = Uuid::new_v4(); + posts.push(post); + } + + Post::batch() + .chunked_insert(&db_session, &posts, 10000) + .await + .expect("Failed to insert posts"); + } +} + +async fn create_keyspace() { + let create_keyspace_query = format!( + r#" + CREATE KEYSPACE IF NOT EXISTS {} + WITH REPLICATION = {{ + 'class': 'SimpleStrategy', + 'replication_factor': 1 + }} + "#, + KEYSPACE + ); + + let session: CachingSession = CachingSession::from( + SessionBuilder::new() + .known_nodes(NODES) + .build() + .await + .expect("Unable to connect to scylla hosts"), + CACHE_SIZE, + ); + + session.execute_unpaged(create_keyspace_query, ()).await.unwrap(); +} + +async fn setup_database() -> CachingSession { + create_keyspace().await; + + let session: CachingSession = CachingSession::from( + SessionBuilder::new() + .use_keyspace(KEYSPACE, false) + .known_nodes(NODES) + .build() + .await + .expect("Unable to connect to scylla hosts"), + CACHE_SIZE, + ); + let create_user_table = " + CREATE TABLE IF NOT EXISTS bench_users ( + id UUID PRIMARY KEY, + username TEXT, + email TEXT, + created_at TIMESTAMP + ) + "; + + let create_post_table = " + CREATE TABLE IF NOT EXISTS posts ( + community_id UUID, + created_at TIMESTAMP, + id UUID, + title TEXT, + description TEXT, + updated_at TIMESTAMP, + creator_id UUID, + creator Profile, + is_archived BOOLEAN, + PRIMARY KEY (community_id, created_at, id) + ) + "; + + let create_profile_udt = " + CREATE TYPE IF NOT EXISTS profile ( + first_name TEXT, + last_name TEXT, + username TEXT, + email TEXT + ) + "; + session.execute_unpaged(create_profile_udt, ()).await.unwrap(); + session.execute_unpaged(create_user_table, ()).await.unwrap(); + session.execute_unpaged(create_post_table, ()).await.unwrap(); + + session +} + +async fn drop_keyspace() { + let drop_keyspace_query = format!("DROP KEYSPACE IF EXISTS {}", KEYSPACE); + + let session: CachingSession = CachingSession::from( + SessionBuilder::new() + .known_nodes(NODES) + .build() + .await + .expect("Unable to connect to scylla hosts"), + CACHE_SIZE, + ); + + session.execute_unpaged(drop_keyspace_query, ()).await.unwrap(); +} + +fn bench_orm_vs_native(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + let session = rt.block_on(async { setup_database().await }); + + let test_user = BenchUser { + id: Uuid::new_v4(), + username: "charybdis".to_string(), + email: "charybdis@orm.com".to_string(), + created_at: Utc::now(), + }; + + let category_id: Uuid = Uuid::new_v4(); + rt.block_on(async { Post::populate_sample_posts_per_partition(category_id, &session).await }); + + // Benchmark Insert + c.bench_function("ORM Insert", |b| { + b.iter(|| { + rt.block_on(async { + test_user.clone().insert().execute(&session).await.unwrap(); + }); + }); + }); + + c.bench_function("Native Insert", |b| { + b.iter(|| { + rt.block_on(async { + session + .execute_unpaged( + "INSERT INTO bench_users (id, username, email, created_at) VALUES (?, ?, ?, ?)", + (test_user.id, "charybdis", "charybdis@orm.com", test_user.created_at), + ) + .await + .unwrap(); + }); + }); + }); + + c.bench_function("ORM Batch Insert", |b| { + b.iter(|| { + let orm_users = (0..1000) + .map(|_| BenchUser { + id: Uuid::new_v4(), + username: "charybdis".to_string(), + email: "charybdis@email.com".to_string(), + created_at: Utc::now(), + }) + .collect::>(); + + rt.block_on(async { + BenchUser::batch() + .chunked_insert(&session, &orm_users, 1000) + .await + .unwrap(); + }); + }); + }); + + c.bench_function("Native Batch Insert", |b| { + b.iter(|| { + rt.block_on(async { + let native_insert_statements = (0..1000) + .map(|_| { + BatchStatement::Query(scylla::query::Query::new( + "INSERT INTO bench_users (id, username, email, created_at) VALUES (?, ?, ?, ?)", + )) + }) + .collect::>(); + + let native_values = (0..1000) + .map(|_| (Uuid::new_v4(), "charybdis", "test@mail.com", Utc::now())) + .collect::>(); + + let batch = Batch::new_with_statements(BatchType::Logged, native_insert_statements); + session.batch(&batch, native_values).await.unwrap(); + }); + }); + }); + + // Benchmark Find + c.bench_function("ORM Find", |b| { + b.iter(|| { + rt.block_on(async { + BenchUser::find_by_id(test_user.id).execute(&session).await.unwrap(); + }); + }); + }); + + c.bench_function("Native Find", |b| { + b.iter(|| { + rt.block_on(async { + let res = session + .execute_unpaged( + "SELECT id, username, email, created_at FROM bench_users WHERE id = ?", + (test_user.id,), + ) + .await + .unwrap(); + + let res = res.into_rows_result().unwrap(); + + res.first_row::().unwrap(); + }); + }); + }); + + c.bench_function("ORM Stream - Find Posts Per Partition", |b| { + b.iter(|| { + rt.block_on(async { + Post::find_by_community_id(category_id) + .execute(&session) + .await + .unwrap() + .try_collect() + .await + .unwrap(); + }); + }); + }); + + c.bench_function("Native Stream - Find Posts Per Partition", |b| { + b.iter(|| { + rt.block_on(async { + use futures::TryStreamExt; + + let res = session + .execute_iter("SELECT * FROM posts WHERE community_id = ?", (category_id,)) + .await + .unwrap() + .rows_stream::() + .unwrap(); + + let results: Result, QueryError> = res.try_collect().await; + results.unwrap(); + }); + }); + }); + + // Benchmark Update + let updated_user = BenchUser { + username: "updated_charybdis".to_string(), + ..test_user.clone() + }; + + c.bench_function("ORM Update", |b| { + b.iter(|| { + rt.block_on(async { + updated_user.clone().update().execute(&session).await.unwrap(); + }); + }); + }); + + c.bench_function("Native Update", |b| { + b.iter(|| { + rt.block_on(async { + session + .execute_unpaged( + "UPDATE bench_users SET username = ? WHERE id = ?", + ("updated_charybdis", updated_user.id), + ) + .await + .unwrap(); + }); + }); + }); + + // Benchmark Delete + c.bench_function("ORM Delete", |b| { + b.iter(|| { + rt.block_on(async { + test_user.clone().delete().execute(&session).await.unwrap(); + }); + }); + }); + + c.bench_function("Native Delete", |b| { + b.iter(|| { + rt.block_on(async { + session + .execute_unpaged("DELETE FROM bench_users WHERE id = ?", (test_user.id,)) + .await + .unwrap(); + }); + }); + }); + + rt.block_on(async { drop_keyspace().await }); +} + +criterion_group! { + name = benches; + config = Criterion::default().sample_size(100); + targets = bench_orm_vs_native +} + +criterion_main!(benches);