diff --git a/Cargo.lock b/Cargo.lock index dc8faad4..a034967a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,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.21" @@ -235,6 +241,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[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.55" @@ -272,6 +284,33 @@ 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.57" @@ -352,6 +391,54 @@ 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", + "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" @@ -367,6 +454,12 @@ 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 = "derive_more" version = "1.0.0" @@ -400,6 +493,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "ember-cluster" version = "0.2.1" @@ -431,6 +530,7 @@ name = "ember-protocol" version = "0.2.1" dependencies = [ "bytes", + "criterion", "itoa", "thiserror 2.0.18", ] @@ -464,6 +564,7 @@ name = "emberkv-core" version = "0.2.1" dependencies = [ "bytes", + "criterion", "ember-persistence", "ember-protocol", "ordered-float", @@ -657,6 +758,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -687,6 +799,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.4.0" @@ -832,12 +950,32 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[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.17" @@ -1006,6 +1144,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openraft" version = "0.9.21" @@ -1112,6 +1256,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[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.13.1" @@ -1278,6 +1450,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.18" @@ -1307,6 +1499,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1458,6 +1662,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[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 = "schannel" version = "0.1.28" @@ -1724,6 +1937,16 @@ dependencies = [ "cfg-if", ] +[[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 = "tinyvec" version = "1.10.0" @@ -1966,6 +2189,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 = "want" version = "0.3.1" @@ -2061,6 +2294,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 9ea372e0..0ad2bfaf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ ordered-float = "5" metrics = "0.24" metrics-exporter-prometheus = { version = "0.16", features = ["http-listener"] } +# benchmarking +criterion = { version = "0.5", features = ["html_reports", "async_tokio"] } + # internal crates (version required for crates.io publishing) emberkv-core = { version = "0.2.1", path = "crates/ember-core" } ember-protocol = { version = "0.2.1", path = "crates/ember-protocol" } diff --git a/Makefile b/Makefile index 49d50b4f..a1cbef81 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build release test fmt fmt-check clippy check clean docker-build \ release-patch release-minor release-major github-release \ - publish publish-dry-run + publish publish-dry-run bench bench-core bench-protocol # extract the workspace version from the root Cargo.toml VERSION = $(shell sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml) @@ -31,6 +31,15 @@ clean: docker-build: docker build -t ember:latest . +bench: + cargo bench --workspace + +bench-core: + cargo bench -p emberkv-core + +bench-protocol: + cargo bench -p ember-protocol + # --- versioning & releases --- # # usage: diff --git a/crates/ember-core/Cargo.toml b/crates/ember-core/Cargo.toml index d3cf9760..387fa522 100644 --- a/crates/ember-core/Cargo.toml +++ b/crates/ember-core/Cargo.toml @@ -24,3 +24,13 @@ ordered-float = { workspace = true } [dev-dependencies] tempfile = "3" +criterion = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[[bench]] +name = "keyspace" +harness = false + +[[bench]] +name = "engine" +harness = false diff --git a/crates/ember-core/benches/engine.rs b/crates/ember-core/benches/engine.rs new file mode 100644 index 00000000..61ee0194 --- /dev/null +++ b/crates/ember-core/benches/engine.rs @@ -0,0 +1,95 @@ +//! Micro-benchmarks for the engine routing layer. +//! +//! Measures the full async path: mpsc send to shard, keyspace operation, +//! oneshot reply. Isolates channel overhead from TCP/protocol costs. +//! Run with `cargo bench -p emberkv-core -- engine`. + +use std::hint::black_box; + +use bytes::Bytes; +use criterion::{criterion_group, criterion_main, Criterion}; +use ember_core::{Engine, ShardRequest}; + +const SHARD_COUNT: usize = 4; +const KEY_COUNT: usize = 10_000; + +fn make_value(size: usize) -> Bytes { + Bytes::from(vec![b'x'; size]) +} + +fn bench_engine(c: &mut Criterion) { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to create tokio runtime"); + + // pre-populate the engine with keys across all shards + let engine = rt.block_on(async { + let engine = Engine::new(SHARD_COUNT); + let value = make_value(64); + for i in 0..KEY_COUNT { + let key = format!("key:{i}"); + engine + .route( + &key, + ShardRequest::Set { + key: key.clone(), + value: value.clone(), + expire: None, + nx: false, + xx: false, + }, + ) + .await + .expect("pre-populate failed"); + } + engine + }); + + let mut group = c.benchmark_group("engine"); + + group.bench_function("route_get", |b| { + b.to_async(&rt).iter(|| async { + black_box( + engine + .route( + "key:5000", + ShardRequest::Get { + key: "key:5000".into(), + }, + ) + .await + .expect("route_get failed"), + ) + }); + }); + + group.bench_function("route_set_64B", |b| { + let value = make_value(64); + b.to_async(&rt).iter(|| { + let v = value.clone(); + async { + black_box( + engine + .route( + "key:0", + ShardRequest::Set { + key: "key:0".into(), + value: v, + expire: None, + nx: false, + xx: false, + }, + ) + .await + .expect("route_set failed"), + ) + } + }); + }); + + group.finish(); +} + +criterion_group!(benches, bench_engine); +criterion_main!(benches); diff --git a/crates/ember-core/benches/keyspace.rs b/crates/ember-core/benches/keyspace.rs new file mode 100644 index 00000000..75e9032b --- /dev/null +++ b/crates/ember-core/benches/keyspace.rs @@ -0,0 +1,131 @@ +//! Micro-benchmarks for keyspace operations. +//! +//! Measures raw data structure performance without async channels or +//! network overhead. Run with `cargo bench -p emberkv-core -- keyspace`. + +use std::hint::black_box; +use std::time::Duration; + +use bytes::Bytes; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use ember_core::keyspace::Keyspace; +use ember_core::memory; +use ember_core::types::Value; + +const KEY_COUNT: usize = 10_000; + +/// Builds a value of the given size filled with 'x'. +fn make_value(size: usize) -> Bytes { + Bytes::from(vec![b'x'; size]) +} + +/// Pre-populates a keyspace with `KEY_COUNT` keys of the given value size. +fn populated_keyspace(value_size: usize) -> Keyspace { + let mut ks = Keyspace::new(); + let value = make_value(value_size); + for i in 0..KEY_COUNT { + ks.set(format!("key:{i}"), value.clone(), None); + } + ks +} + +fn bench_get(c: &mut Criterion) { + let mut group = c.benchmark_group("keyspace_get"); + + for size in [64, 256, 1024, 16384] { + group.bench_with_input( + BenchmarkId::from_parameter(format!("{size}B")), + &size, + |b, &size| { + let mut ks = populated_keyspace(size); + b.iter(|| { + let _ = black_box(ks.get("key:5000")); + }); + }, + ); + } + + group.finish(); +} + +fn bench_set_overwrite(c: &mut Criterion) { + let mut group = c.benchmark_group("keyspace_set_overwrite"); + + for size in [64, 256, 1024, 16384] { + group.bench_with_input( + BenchmarkId::from_parameter(format!("{size}B")), + &size, + |b, &size| { + let mut ks = Keyspace::new(); + let value = make_value(size); + ks.set("key".into(), value.clone(), None); + b.iter(|| { + black_box(ks.set("key".into(), value.clone(), None)); + }); + }, + ); + } + + group.finish(); +} + +fn bench_set_with_expiry(c: &mut Criterion) { + let mut group = c.benchmark_group("keyspace_set_with_expiry"); + let value = make_value(64); + let ttl = Some(Duration::from_secs(300)); + + group.bench_function("64B_with_ttl", |b| { + let mut ks = Keyspace::new(); + ks.set("key".into(), value.clone(), ttl); + b.iter(|| { + black_box(ks.set("key".into(), value.clone(), ttl)); + }); + }); + + group.finish(); +} + +fn bench_mixed(c: &mut Criterion) { + let value = make_value(64); + + c.bench_function("keyspace_mixed_50_50", |b| { + let mut ks = populated_keyspace(64); + let mut i = 0u64; + b.iter(|| { + let key = format!("key:{}", i % KEY_COUNT as u64); + if i % 2 == 0 { + let _ = black_box(ks.get(&key)); + } else { + black_box(ks.set(key, value.clone(), None)); + } + i += 1; + }); + }); +} + +fn bench_entry_size(c: &mut Criterion) { + let mut group = c.benchmark_group("memory_entry_size"); + + let small_val = Value::String(make_value(64)); + let large_val = Value::String(make_value(16384)); + + group.bench_function("64B_string", |b| { + b.iter(|| black_box(memory::entry_size("key:12345", &small_val))); + }); + + group.bench_function("16KB_string", |b| { + b.iter(|| black_box(memory::entry_size("key:12345", &large_val))); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_get, + bench_set_overwrite, + bench_set_with_expiry, + bench_mixed, + bench_entry_size, +); +criterion_main!(benches); diff --git a/crates/ember-protocol/Cargo.toml b/crates/ember-protocol/Cargo.toml index 9b410f69..4a293095 100644 --- a/crates/ember-protocol/Cargo.toml +++ b/crates/ember-protocol/Cargo.toml @@ -13,3 +13,10 @@ readme = "README.md" bytes = { workspace = true } itoa = "1" thiserror = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true } + +[[bench]] +name = "protocol" +harness = false diff --git a/crates/ember-protocol/benches/protocol.rs b/crates/ember-protocol/benches/protocol.rs new file mode 100644 index 00000000..358daca6 --- /dev/null +++ b/crates/ember-protocol/benches/protocol.rs @@ -0,0 +1,112 @@ +//! Micro-benchmarks for RESP3 parsing, serialization, and command dispatch. +//! +//! Run with `cargo bench -p ember-protocol -- resp3` or +//! `cargo bench -p ember-protocol -- command`. + +use std::hint::black_box; + +use bytes::{Bytes, BytesMut}; +use criterion::{criterion_group, criterion_main, Criterion}; +use ember_protocol::{parse_frame, Command, Frame}; + +/// Builds the raw RESP3 bytes for `SET ` where value is `size` bytes. +fn build_set_bytes(key: &str, value_size: usize) -> Vec { + let value = "x".repeat(value_size); + // *3\r\n$3\r\nSET\r\n$\r\n\r\n$\r\n\r\n + format!( + "*3\r\n$3\r\nSET\r\n${}\r\n{}\r\n${}\r\n{}\r\n", + key.len(), + key, + value.len(), + value + ) + .into_bytes() +} + +fn bench_parse(c: &mut Criterion) { + let mut group = c.benchmark_group("resp3_parse"); + + let ping = b"*1\r\n$4\r\nPING\r\n"; + group.bench_function("ping", |b| { + b.iter(|| black_box(parse_frame(ping).unwrap().unwrap())); + }); + + let get_cmd = b"*2\r\n$3\r\nGET\r\n$9\r\nkey:12345\r\n"; + group.bench_function("get", |b| { + b.iter(|| black_box(parse_frame(get_cmd).unwrap().unwrap())); + }); + + let set_64 = build_set_bytes("key:12345", 64); + group.bench_function("set_64B", |b| { + b.iter(|| black_box(parse_frame(&set_64).unwrap().unwrap())); + }); + + let set_1k = build_set_bytes("key:12345", 1024); + group.bench_function("set_1KB", |b| { + b.iter(|| black_box(parse_frame(&set_1k).unwrap().unwrap())); + }); + + group.finish(); +} + +fn bench_serialize(c: &mut Criterion) { + let mut group = c.benchmark_group("resp3_serialize"); + + let ok = Frame::Simple("OK".into()); + group.bench_function("simple_ok", |b| { + let mut buf = BytesMut::with_capacity(64); + b.iter(|| { + buf.clear(); + ok.serialize(&mut buf); + black_box(&buf); + }); + }); + + let bulk_64 = Frame::Bulk(Bytes::from(vec![b'x'; 64])); + group.bench_function("bulk_64B", |b| { + let mut buf = BytesMut::with_capacity(128); + b.iter(|| { + buf.clear(); + bulk_64.serialize(&mut buf); + black_box(&buf); + }); + }); + + let bulk_1k = Frame::Bulk(Bytes::from(vec![b'x'; 1024])); + group.bench_function("bulk_1KB", |b| { + let mut buf = BytesMut::with_capacity(2048); + b.iter(|| { + buf.clear(); + bulk_1k.serialize(&mut buf); + black_box(&buf); + }); + }); + + group.finish(); +} + +fn bench_command_parse(c: &mut Criterion) { + let mut group = c.benchmark_group("command_from_frame"); + + let get_frame = Frame::Array(vec![ + Frame::Bulk(Bytes::from_static(b"GET")), + Frame::Bulk(Bytes::from_static(b"key:12345")), + ]); + group.bench_function("get", |b| { + b.iter(|| black_box(Command::from_frame(get_frame.clone()).unwrap())); + }); + + let set_frame = Frame::Array(vec![ + Frame::Bulk(Bytes::from_static(b"SET")), + Frame::Bulk(Bytes::from_static(b"key:12345")), + Frame::Bulk(Bytes::from(vec![b'x'; 64])), + ]); + group.bench_function("set_64B", |b| { + b.iter(|| black_box(Command::from_frame(set_frame.clone()).unwrap())); + }); + + group.finish(); +} + +criterion_group!(benches, bench_parse, bench_serialize, bench_command_parse); +criterion_main!(benches);