From 5df04d75126d71d0790028c4a215acb571f25616 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sat, 29 Aug 2026 22:14:01 +0700 Subject: [PATCH] feat: criterion benchmarks proving primitive overhead is negligible --- .hermes/plans/mytheclipse-round19-spec.md | 34 ++++ Cargo.lock | 161 ++++++++++++++++++- crates/mytheclipse/Cargo.toml | 7 + crates/mytheclipse/benches/primitives.rs | 180 ++++++++++++++++++++++ 4 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 .hermes/plans/mytheclipse-round19-spec.md create mode 100644 crates/mytheclipse/benches/primitives.rs diff --git a/.hermes/plans/mytheclipse-round19-spec.md b/.hermes/plans/mytheclipse-round19-spec.md new file mode 100644 index 0000000..3cd7222 --- /dev/null +++ b/.hermes/plans/mytheclipse-round19-spec.md @@ -0,0 +1,34 @@ +# Implementation Spec: Round 19 — Criterion Benchmarks + +## Goal +Buktikan klaim "secepat mungkin" (tujuan awal project) dengan benchmark +nyata. Ukur overhead primitives race-safe vs baseline naif, supaya user +tahu trade-off dan bisa memilih fitur dengan data. + +## New files + +### crates/mytheclipse/benches/primitives.rs +Criterion bench untuk primitives core (feature `full`): +- `parallel_map`: throughput 1000 item, concurrency 8 vs sequential loop + (pakai `black_box`) +- `retry_ext`: overhead `.retry()` success-first vs 2 retries +- `rate_limiter`: `RateLimiter::try_acquire` throughput (atomic CAS) +- `semaphore_pool`: acquire/release cycle throughput — bukti no-leak + low + overhead +- `aggregate_error`: `from_results` 1000 results all-ok vs 50% err +- `shutdown_guard`: new + drop cost + +## Dependency +- dev-deps: `criterion = "0.5"` + `[[bench]]` harness = false +- `harness = false` di Cargo.toml bench section (criterion punya main sendiri) + +## Verification +1. `cargo bench -p mytheclipse --bench primitives --all-features` — runs, + reports times +2. `cargo build --workspace --all-features` — exit 0 +3. `cargo clippy --workspace --all-features` — 0 new warnings +4. Spec + commit + push + +## Notes +- Criterion 0.5 mendukung MSRV 1.60 — aman untuk rust-version 1.75. +- Bench tidak jalan di CI (hanya manual) — tidak mempengaruhi pipeline. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 7804ea8..0b7ccdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,6 +100,12 @@ dependencies = [ "url", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -1086,6 +1092,12 @@ dependencies = [ "either", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -1130,6 +1142,33 @@ dependencies = [ "rand_core 0.10.1", ] +[[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 = "cipher" version = "0.4.4" @@ -1317,6 +1356,44 @@ 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 0.10.5", + "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 0.10.5", +] + [[package]] name = "crossbeam-channel" version = "0.5.16" @@ -1351,6 +1428,12 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -2116,6 +2199,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.15.5" @@ -2561,12 +2655,32 @@ version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi 0.5.2", + "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 = "itertools" version = "0.13.0" @@ -2830,6 +2944,7 @@ name = "mytheclipse" version = "1.18.0" dependencies = [ "async-trait", + "criterion", "num_cpus", "rand 0.8.8", "rayon", @@ -3136,6 +3251,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 = "opaque-debug" version = "0.3.1" @@ -3494,6 +3615,34 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[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 = "polling" version = "2.8.0" @@ -3826,7 +3975,7 @@ dependencies = [ "bytes", "combine", "futures-util", - "itertools", + "itertools 0.13.0", "itoa", "num-bigint", "percent-encoding", @@ -4732,6 +4881,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 = "tinyvec" version = "1.12.0" diff --git a/crates/mytheclipse/Cargo.toml b/crates/mytheclipse/Cargo.toml index 2fc3e9d..cd164ad 100644 --- a/crates/mytheclipse/Cargo.toml +++ b/crates/mytheclipse/Cargo.toml @@ -25,6 +25,7 @@ thiserror = "2" [dev-dependencies] tokio = { version = "1.53", features = ["full"] } tracing-subscriber = "0.3" +criterion = { version = "0.5", features = ["async_tokio"] } [features] default = [] @@ -42,6 +43,12 @@ name = "main" path = "examples/main.rs" required-features = ["full"] +[[bench]] +name = "primitives" +path = "benches/primitives.rs" +harness = false +required-features = ["full"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file diff --git a/crates/mytheclipse/benches/primitives.rs b/crates/mytheclipse/benches/primitives.rs new file mode 100644 index 0000000..93a9e8d --- /dev/null +++ b/crates/mytheclipse/benches/primitives.rs @@ -0,0 +1,180 @@ +//! Criterion benchmarks for the high-level primitives. +//! +//! Run: `cargo bench -p mytheclipse --bench primitives --all-features` +//! +//! These measure the *overhead* of the race-safe abstractions — the cost you +//! pay for auto thread/core allocation, bounded fan-out, and exactly-once +//! cleanup. Numbers (per-op) should be tiny; anything in the µs+ range for a +//! single primitive would signal a regression. + +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use tokio::runtime::Runtime; + +use mytheclipse::aggregate_error::AggregateError; +use mytheclipse::parallel_map::{parallel_for_each, parallel_map}; +use mytheclipse::pool::{Pool, SemaphorePool}; +use mytheclipse::ratelimit::RateLimiter; +use mytheclipse::retry_ext::RetryExt; +use mytheclipse::retry::RetryConfig; +use mytheclipse::shutdown_guard::ShutdownGuard; + +fn rt() -> Runtime { + Runtime::new().expect("tokio runtime") +} + +fn bench_parallel_map(c: &mut Criterion) { + let rt = rt(); + c.bench_function("parallel_map/1000x8", |b| { + b.to_async(&rt).iter(|| async { + let items: Vec = (0..1000).collect(); + let out = parallel_map(items, 8, |x| async move { Ok::<_, std::io::Error>(x * 2) }) + .await + .unwrap(); + black_box(out); + }); + }); + + // Sequential baseline for comparison. + c.bench_function("sequential/map_1000", |b| { + b.iter(|| { + let items: Vec = (0..1000).collect(); + let out: Vec = items.into_iter().map(|x| x * 2).collect(); + black_box(out); + }); + }); +} + +fn bench_parallel_for_each(c: &mut Criterion) { + let rt = rt(); + c.bench_function("parallel_for_each/1000x8", |b| { + b.to_async(&rt).iter(|| async { + parallel_for_each(0u32..1000, 8, |_| async move { Ok::<_, std::io::Error>(()) }) + .await + .unwrap(); + black_box(()); + }); + }); +} + +fn bench_retry_ext(c: &mut Criterion) { + let rt = rt(); + c.bench_function("retry_ext/success_first", |b| { + b.to_async(&rt).iter(|| async { + let op = || async { Ok::(42) }; + let out = async { Ok::(42) } + .retry( + RetryConfig { + max_attempts: 3, + base_delay: Duration::from_millis(1), + ..RetryConfig::default() + }, + |_: &std::io::Error| true, + op, + ) + .await + .unwrap(); + black_box(out); + }); + }); + + c.bench_function("retry_ext/retries_twice", |b| { + b.to_async(&rt).iter(|| async { + let attempts = Arc::new(AtomicU32::new(0)); + let a = Arc::clone(&attempts); + let op = move || { + let a = Arc::clone(&a); + async move { + if a.fetch_add(1, Ordering::SeqCst) < 2 { + Err::(std::io::Error::other("x")) + } else { + Ok(42) + } + } + }; + let out = async { Err::(std::io::Error::other("first")) } + .retry( + RetryConfig { + max_attempts: 3, + base_delay: Duration::from_millis(1), + ..RetryConfig::default() + }, + |_: &std::io::Error| true, + op, + ) + .await + .unwrap(); + black_box(out); + }); + }); +} + +fn bench_rate_limiter(c: &mut Criterion) { + c.bench_function("rate_limiter/try_acquire", |b| { + let rl = RateLimiter::new(1_000_000.0, 10_000_000); + b.iter(|| { + let _ = black_box(rl.try_acquire()); + }); + }); +} + +fn bench_semaphore_pool(c: &mut Criterion) { + let rt = rt(); + c.bench_function("semaphore_pool/acquire_release", |b| { + b.to_async(&rt).iter(|| async { + let pool = SemaphorePool::new(vec![1u32, 2, 3, 4, 5]); + let item = pool.acquire().await.unwrap(); + black_box(item.resource); + }); + }); +} + +fn bench_aggregate_error(c: &mut Criterion) { + c.bench_function("aggregate_error/all_ok_1000", |b| { + b.iter(|| { + let results: Vec> = (0..1000).map(Ok).collect(); + let out = AggregateError::from_results(black_box(results)).unwrap(); + black_box(out); + }); + }); + + c.bench_function("aggregate_error/500err_1000", |b| { + b.iter(|| { + let results: Vec> = (0..1000) + .map(|i| { + if i % 2 == 0 { + Err(std::io::Error::other("x")) + } else { + Ok(i) + } + }) + .collect(); + let out = AggregateError::from_results(black_box(results)); + black_box(out); + }); + }); +} + +fn bench_shutdown_guard(c: &mut Criterion) { + c.bench_function("shutdown_guard/new_drop", |b| { + b.iter(|| { + let g = ShutdownGuard::new(|| {}); + black_box(&g); + }); + }); +} + +criterion_group!( + benches, + bench_parallel_map, + bench_parallel_for_each, + bench_retry_ext, + bench_rate_limiter, + bench_semaphore_pool, + bench_aggregate_error, + bench_shutdown_guard, +); +criterion_main!(benches); \ No newline at end of file