From 6a5310dc09cd7f827670ed010704e7951c05167d Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 20 Oct 2026 20:25:51 +0800 Subject: [PATCH] refactor(benchmarks): measure fixed workloads in milliseconds --- Cargo.lock | 69 +----- Cargo.toml | 1 - README.md | 4 + benchmarks/Cargo.toml | 3 +- benchmarks/README.md | 67 ++++++ benchmarks/benches/ecosystem/broadcast/mod.rs | 6 + .../ecosystem/broadcast/mpmc/bounded.rs | 111 ++++++--- .../benches/ecosystem/broadcast/mpmc/mod.rs | 7 + .../broadcast/mpmc/no_backpressure.rs | 92 +++++-- .../ecosystem/broadcast/mpmc/support.rs | 2 +- benchmarks/benches/ecosystem/main.rs | 11 +- benchmarks/benches/ecosystem/mpmc/bounded.rs | 77 ++++-- benchmarks/benches/ecosystem/mpmc/mod.rs | 7 + .../benches/ecosystem/mpmc/unbounded.rs | 77 ++++-- benchmarks/benches/ecosystem/mpsc/bounded.rs | 177 ++++++++++---- benchmarks/benches/ecosystem/mpsc/mod.rs | 12 +- .../benches/ecosystem/mpsc/reservation.rs | 80 +++++-- benchmarks/benches/ecosystem/mpsc/support.rs | 5 +- .../benches/ecosystem/mpsc/unbounded.rs | 193 ++++++++++----- benchmarks/benches/ecosystem/spmc/bounded.rs | 53 ++-- benchmarks/benches/ecosystem/spmc/mod.rs | 7 + .../benches/ecosystem/spmc/unbounded.rs | 53 ++-- benchmarks/benches/ecosystem/waitgroup.rs | 85 ++++++- benchmarks/benches/ecosystem/watch/mod.rs | 6 + benchmarks/benches/ecosystem/watch/paths.rs | 83 ++++++- benchmarks/benches/primitives/barrier/mod.rs | 6 + benchmarks/benches/primitives/barrier/wait.rs | 27 ++- .../benches/primitives/blocking/block_on.rs | 24 +- benchmarks/benches/primitives/blocking/mod.rs | 7 + .../primitives/blocking/wait_timeout.rs | 15 +- .../benches/primitives/broadcast/mod.rs | 6 + .../primitives/broadcast/mpmc/bounded.rs | 144 +++++++---- .../benches/primitives/broadcast/mpmc/mod.rs | 7 + .../primitives/broadcast/mpmc/unbounded.rs | 83 ++++++- .../benches/primitives/completion/mod.rs | 62 ++++- benchmarks/benches/primitives/condvar/mod.rs | 6 + benchmarks/benches/primitives/condvar/wait.rs | 31 ++- benchmarks/benches/primitives/event/mod.rs | 6 + benchmarks/benches/primitives/event/wait.rs | 147 ++++++++---- benchmarks/benches/primitives/latch/mod.rs | 6 + benchmarks/benches/primitives/latch/wait.rs | 33 ++- benchmarks/benches/primitives/main.rs | 27 ++- benchmarks/benches/primitives/mpmc/bounded.rs | 31 ++- benchmarks/benches/primitives/mpmc/mod.rs | 8 +- .../benches/primitives/mpmc/unbounded.rs | 38 ++- benchmarks/benches/primitives/mpsc/bounded.rs | 33 ++- benchmarks/benches/primitives/mpsc/mod.rs | 7 + .../benches/primitives/mpsc/unbounded.rs | 22 +- benchmarks/benches/primitives/mutex/lock.rs | 27 ++- benchmarks/benches/primitives/mutex/mod.rs | 6 + .../benches/primitives/once/get_or_init.rs | 27 ++- benchmarks/benches/primitives/once/mod.rs | 7 + benchmarks/benches/primitives/once/wait.rs | 34 ++- .../benches/primitives/once_map/initialize.rs | 163 +++++++------ .../benches/primitives/once_map/lookup.rs | 119 ++++++--- .../primitives/once_map/maintenance.rs | 50 ++-- benchmarks/benches/primitives/once_map/mod.rs | 8 + .../benches/primitives/once_map/support.rs | 3 - benchmarks/benches/primitives/oneshot/mod.rs | 6 + benchmarks/benches/primitives/oneshot/send.rs | 22 +- benchmarks/benches/primitives/phaser/mod.rs | 71 +++++- benchmarks/benches/primitives/pool/bounded.rs | 39 ++- benchmarks/benches/primitives/pool/mod.rs | 7 + .../benches/primitives/pool/unbounded.rs | 16 +- benchmarks/benches/primitives/rwlock/mod.rs | 7 + benchmarks/benches/primitives/rwlock/read.rs | 23 +- benchmarks/benches/primitives/rwlock/write.rs | 20 +- .../benches/primitives/semaphore/acquire.rs | 71 +++++- .../benches/primitives/semaphore/mod.rs | 7 + .../benches/primitives/semaphore/release.rs | 41 ++-- benchmarks/benches/primitives/shutdown/mod.rs | 6 + .../primitives/shutdown/request_shutdown.rs | 18 +- .../primitives/singleflight/coalescing.rs | 111 +++++---- .../primitives/singleflight/leaders.rs | 90 ++++--- .../benches/primitives/singleflight/mod.rs | 7 + .../primitives/singleflight/support.rs | 3 - .../benches/primitives/waitgroup/mod.rs | 6 + .../benches/primitives/waitgroup/wait.rs | 20 +- benchmarks/src/channels/mpmc.rs | 2 +- benchmarks/src/channels/spmc.rs | 2 +- benchmarks/src/harness.rs | 226 ++++++++++++++++++ benchmarks/src/lib.rs | 1 + benchmarks/src/support.rs | 6 - xtask/src/main.rs | 2 +- 84 files changed, 2550 insertions(+), 788 deletions(-) create mode 100644 benchmarks/README.md create mode 100644 benchmarks/src/harness.rs diff --git a/Cargo.lock b/Cargo.lock index 07175ce9..39f8b9f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "async-broadcast", "async-channel", "asyncband", - "divan", + "clap", "flume", "tokio", "waitgroup", @@ -189,7 +189,6 @@ dependencies = [ "anstyle", "clap_lex", "strsim", - "terminal_size", ] [[package]] @@ -225,12 +224,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "condtype" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" - [[package]] name = "cookie" version = "0.18.1" @@ -283,31 +276,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "divan" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a405457ec78b8fe08b0e32b4a3570ab5dff6dd16eb9e76a5ee0a9d9cbd898933" -dependencies = [ - "cfg-if", - "clap", - "condtype", - "divan-macros", - "libc", - "regex-lite", -] - -[[package]] -name = "divan-macros" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9556bc800956545d6420a640173e5ba7dfa82f38d3ea5a167eb555bc69ac3323" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "document-features" version = "0.2.12" @@ -569,12 +537,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "litemap" version = "0.8.2" @@ -720,12 +682,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - [[package]] name = "ring" version = "0.17.14" @@ -740,19 +696,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.23.43" @@ -939,16 +882,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "terminal_size" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" -dependencies = [ - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "tests-integration" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 8689fca6..229085ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,6 @@ async-broadcast = { version = "0.7.2" } async-channel = { version = "2.5.0" } cargo_metadata = { version = "0.23.1" } clap = { version = "4.6.5" } -divan = { version = "0.1.21" } flume = { version = "0.12.0", default-features = false } semver = { version = "1.0.28" } serde = { version = "1.0.229" } diff --git a/README.md b/README.md index 17ee4426..660c1f1c 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,10 @@ Public paths stay direct—such as `asyncband::mutex`, `asyncband::pool`, and `a Runnable examples live in the [`examples`](examples) workspace crate. They demonstrate how to choose and compose Asyncband primitives in complete programs. +## Benchmarks + +Use `cargo x bench` to run fixed workloads measured in milliseconds. See the [benchmark guide](benchmarks/README.md) for filtering, repeated comparisons, workload sizes, and timing boundaries. + ## API map | Area | API | Feature | Use | diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml index adbc3ad0..fffebeee 100644 --- a/benchmarks/Cargo.toml +++ b/benchmarks/Cargo.toml @@ -49,7 +49,7 @@ asyncband = { workspace = true, features = [ "waitgroup", "watch", ] } -divan = { workspace = true } +clap = { workspace = true, features = ["derive"] } flume = { workspace = true, features = ["async"] } tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } @@ -60,7 +60,6 @@ waitgroup = { workspace = true } [lib] bench = false doctest = false -test = false [[bench]] harness = false diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 00000000..2bfb49ad --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,67 @@ + + +# Benchmarks + +These benchmarks follow the approach in [Benchmark In Milliseconds](https://matklad.github.io/2026/10/05/benchmark-milliseconds.html): run a fixed amount of useful work, aim for roughly 300 ms, and repeat it to develop intuition about a change. Work sizes are checked into the source. The runner never adjusts them in response to measured performance. + +## Running + +Use `cargo x bench` to build and run the release-profile benchmark executables. Select a target and filter cases for a short feedback loop: + +```shell +# Discover the commands and available workloads. +cargo x bench --help +cargo x bench --bench primitives -- --help +cargo x bench -- --list + +# Repeat one primitive workload and inspect every elapsed time. +cargo x bench --bench primitives -- mutex::lock::uncontended_reuse --repeat 10 + +# Compare all implementations of the same channel workload. +cargo x bench --bench ecosystem -- mpsc::bounded::try_send_receive --repeat 10 + +# Compile only, or exercise every case once per worker without timing. +cargo x bench --no-run +cargo x bench -- --test +``` + +Filters are case-sensitive substrings; multiple filters must all match. Use `--bench primitives` or `--bench ecosystem` with filters, since each executable rejects an empty selection. `--list` shows the resolved round counts without constructing fixtures or starting workers. + +Every measured row prints the integer elapsed milliseconds, total rounds, case name, and repetition number. The default is one measurement per case; `--repeat 10` prints ten independent runs, including the first. There is no hidden warm-up run, averaging, outlier removal, or conversion to a per-operation time. + +## Work and timing boundaries + +`primitives` exercises operation cycles with controlled polling and reference-counted wakers. It covers ready paths, registration, handoff, cancellation, fan-in and fan-out; selected cases also exercise shared state from multiple OS threads. Hand-polled cases include waker bookkeeping but do not model executor scheduling. + +`ecosystem` compares equivalent workloads across Asyncband and other libraries. It includes hand-polled paths, message batches on threads or Tokio tasks, bounded backpressure, retained backlogs, payload sizes, and notification fan-out. Existing semantic differences remain explicit: for example, Tokio broadcast does not participate in lossless bounded backpressure cases. + +A round is one invocation of the case's operation closure. It may transfer one message, complete a group of waiters, or run a batch of messages. Channel batches use the message counts in their support modules. The `threads=N` variants divide the total rounds among N workers and start them at a barrier; they share the same fixture and do the same total work at every thread count. Other concurrency settings are part of the case name and describe the workers inside each round. + +The clock surrounds the entire workload function, from fixture construction to cleanup. Outputs are consumed and dropped each round. Reusable fixtures survive across rounds, while cases that need fresh state construct and dispose of it inside the loop. Channel workloads that prime reusable workers or storage do so once inside this measured interval. Setup, teardown, allocation, key generation, coordination, and correctness checks are not separately subtracted. This makes the result a workload cost; it does not isolate the nanosecond cost of the operation in the function name. + +Each repetition recreates the workload's fixture. Repeated rounds must return it to a usable state: drain messages, release permits, finish workers, and keep retained memory bounded. The smoke mode keeps each case's topology and inner message batch intact while reducing its outer loop to one round per worker. + +## Sizing and comparing + +Choose round counts that put representative cases in the hundreds of milliseconds on a development machine. Treat 300 ms as a sizing guide, not a deadline or an assertion: implementations, contention, hardware, and schedulers differ. A result below 10 ms needs more work before small differences are useful; a result above a second usually needs fewer rounds for convenient iteration. + +Use `--rounds N` to try a different fixed count on a filtered workload, or `--scale N` to multiply the checked-in counts. These options do not change message capacities, payloads, or topology. Copy a useful count into the case's registration when changing the workload definition. Keep it identical for all implementations of the same case and parameter set; never calibrate each implementation or revision independently to the same elapsed time. + +Compare the same workload definitions, parameters, round counts, toolchain, and machine. Run without competing builds or benchmarks, repeat a narrow selection, and inspect the spread before attributing a difference to the code. Changing a workload or its timing boundary establishes a new baseline; these results are not directly comparable with the former Divan per-operation measurements. The full matrix is useful for coverage, while a filtered set is the normal optimization workflow. diff --git a/benchmarks/benches/ecosystem/broadcast/mod.rs b/benchmarks/benches/ecosystem/broadcast/mod.rs index b078f4ac..3828af5c 100644 --- a/benchmarks/benches/ecosystem/broadcast/mod.rs +++ b/benchmarks/benches/ecosystem/broadcast/mod.rs @@ -16,3 +16,9 @@ // under the License. mod mpmc; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + mpmc::register(runner); +} diff --git a/benchmarks/benches/ecosystem/broadcast/mpmc/bounded.rs b/benchmarks/benches/ecosystem/broadcast/mpmc/bounded.rs index 262b4567..c07f017a 100644 --- a/benchmarks/benches/ecosystem/broadcast/mpmc/bounded.rs +++ b/benchmarks/benches/ecosystem/broadcast/mpmc/bounded.rs @@ -17,18 +17,18 @@ // Tokio broadcast overwrites at capacity, so it cannot participate in lossless backpressure cases. +use std::hint::black_box; + use benchmarks::channels::runtime; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; use tokio::runtime::Builder; use crate::broadcast::mpmc::adapters::AsyncBroadcast; use crate::broadcast::mpmc::adapters::Asyncband; use crate::broadcast::mpmc::adapters::BoundedBroadcastMpmc; use crate::broadcast::mpmc::support::BACKPRESSURE_SHAPE; -use crate::broadcast::mpmc::support::BATCH_MESSAGES; use crate::broadcast::mpmc::support::BOUNDED_SHAPES; use crate::broadcast::mpmc::support::BoundedConcurrent; use crate::broadcast::mpmc::support::BoundedShape; @@ -36,7 +36,6 @@ use crate::broadcast::mpmc::support::BoundedTasks; use crate::broadcast::mpmc::support::ROUND_TRIP_CAPACITY; // Send-then-receive pairing keeps at most one message retained, so these never reach capacity. -#[divan::bench(types = [Asyncband, AsyncBroadcast], sample_size = 512)] fn try_send_receive(bencher: Bencher) { let (sender, mut receivers) = C::channel(ROUND_TRIP_CAPACITY, 1); let mut receiver = receivers.pop().unwrap(); @@ -47,7 +46,6 @@ fn try_send_receive(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, AsyncBroadcast], sample_size = 512)] fn ready_send_receive(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(ROUND_TRIP_CAPACITY, 1); @@ -59,42 +57,91 @@ fn ready_send_receive(bencher: Bencher) { }); } -// Keep one fixture per sample so workers from other fixtures are not alive during timing. -#[divan::bench( - types = [Asyncband, AsyncBroadcast], - args = BOUNDED_SHAPES, - sample_count = 10, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] +// Finish and join each fixture before starting the next round. fn thread_batch(bencher: Bencher, shape: BoundedShape) { - bencher - .with_inputs(|| BoundedConcurrent::new::(shape)) - .bench_local_refs(BoundedConcurrent::run); + bencher.bench_local(|| { + let mut input = BoundedConcurrent::new::(shape); + BoundedConcurrent::run(&mut input) + }); } -#[divan::bench( - types = [Asyncband, AsyncBroadcast], - args = BOUNDED_SHAPES, - sample_count = 10, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn task_batch(bencher: Bencher, shape: BoundedShape) { let runtime = Builder::new_multi_thread() .worker_threads(4) .build() .unwrap(); - bencher - .with_inputs(|| BoundedTasks::new::(&runtime, shape)) - .bench_local_refs(|tasks| tasks.run(&runtime)); + bencher.bench_local(|| { + let mut tasks = BoundedTasks::new::(&runtime, shape); + tasks.run(&runtime) + }); } -#[divan::bench(types = [Asyncband, AsyncBroadcast], sample_count = 10, sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES))] fn capacity_one_task_handoff(bencher: Bencher) { let runtime = runtime(4); - bencher - .with_inputs(|| BoundedTasks::new::(&runtime, BACKPRESSURE_SHAPE)) - .bench_local_refs(|tasks| tasks.run(&runtime)); + bencher.bench_local(|| { + let mut tasks = BoundedTasks::new::(&runtime, BACKPRESSURE_SHAPE); + tasks.run(&runtime) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "AsyncBroadcast"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: BoundedBroadcastMpmc, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::try_send_receive") + ), + 9_000_000, + 1, + try_send_receive::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::ready_send_receive") + ), + 10_000_000, + 1, + ready_send_receive::, + ); + + for &arg in BOUNDED_SHAPES { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::thread_batch") + ), + 128, + 1, + |bencher| thread_batch::(bencher, arg), + ); + } + for &arg in BOUNDED_SHAPES { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::task_batch") + ), + 320, + 1, + |bencher| task_batch::(bencher, arg), + ); + } + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::capacity_one_task_handoff") + ), + 80, + 1, + capacity_one_task_handoff::, + ); } diff --git a/benchmarks/benches/ecosystem/broadcast/mpmc/mod.rs b/benchmarks/benches/ecosystem/broadcast/mpmc/mod.rs index 4d503fa8..8c8cf1a7 100644 --- a/benchmarks/benches/ecosystem/broadcast/mpmc/mod.rs +++ b/benchmarks/benches/ecosystem/broadcast/mpmc/mod.rs @@ -21,3 +21,10 @@ mod adapters; mod bounded; mod no_backpressure; mod support; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + no_backpressure::register(runner); +} diff --git a/benchmarks/benches/ecosystem/broadcast/mpmc/no_backpressure.rs b/benchmarks/benches/ecosystem/broadcast/mpmc/no_backpressure.rs index 1073fa4d..4d4caa14 100644 --- a/benchmarks/benches/ecosystem/broadcast/mpmc/no_backpressure.rs +++ b/benchmarks/benches/ecosystem/broadcast/mpmc/no_backpressure.rs @@ -17,23 +17,22 @@ // Bounded peers have room for the entire batch, excluding their differing overflow policies. +use std::hint::black_box; + +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; use crate::broadcast::mpmc::adapters::AsyncBroadcast; use crate::broadcast::mpmc::adapters::Asyncband; use crate::broadcast::mpmc::adapters::BroadcastMpmc; use crate::broadcast::mpmc::adapters::Tokio; -use crate::broadcast::mpmc::support::BATCH_MESSAGES; use crate::broadcast::mpmc::support::ConcurrentSend; use crate::broadcast::mpmc::support::Fanout; use crate::broadcast::mpmc::support::PRODUCER_COUNTS; use crate::broadcast::mpmc::support::RECEIVER_COUNTS; use crate::broadcast::mpmc::support::ROUND_TRIP_CAPACITY; -#[divan::bench(types = [Asyncband, Tokio, AsyncBroadcast])] fn try_send_receive(bencher: Bencher) { let (sender, mut receivers) = C::channel(ROUND_TRIP_CAPACITY, 1); let mut receiver = receivers.pop().unwrap(); @@ -44,7 +43,6 @@ fn try_send_receive(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio, AsyncBroadcast])] fn ready_send_receive(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(ROUND_TRIP_CAPACITY, 1); @@ -56,28 +54,70 @@ fn ready_send_receive(bencher: Bencher) { }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncBroadcast], - args = PRODUCER_COUNTS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn publish_batch_then_drain(bencher: Bencher, producer_count: usize) { - bencher - .with_inputs(|| ConcurrentSend::::new(producer_count)) - .bench_local_refs(ConcurrentSend::run); + bencher.bench_local(|| { + let mut input = ConcurrentSend::::new(producer_count); + ConcurrentSend::run(&mut input) + }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncBroadcast], - args = RECEIVER_COUNTS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn publish_then_parallel_drain(bencher: Bencher, receiver_count: usize) { - bencher - .with_inputs(|| Fanout::::new(receiver_count)) - .bench_local_refs(Fanout::run); + bencher.bench_local(|| { + let mut input = Fanout::::new(receiver_count); + Fanout::run(&mut input) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "Tokio"); + register_implementation::(runner, "AsyncBroadcast"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: BroadcastMpmc, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::try_send_receive") + ), + 12_000_000, + 1, + try_send_receive::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::ready_send_receive") + ), + 10_000_000, + 1, + ready_send_receive::, + ); + + for &arg in PRODUCER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::publish_batch_then_drain") + ), + 1_024, + 1, + |bencher| publish_batch_then_drain::(bencher, arg), + ); + } + for &arg in RECEIVER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::publish_then_parallel_drain") + ), + 640, + 1, + |bencher| publish_then_parallel_drain::(bencher, arg), + ); + } } diff --git a/benchmarks/benches/ecosystem/broadcast/mpmc/support.rs b/benchmarks/benches/ecosystem/broadcast/mpmc/support.rs index 17f1133e..1035c382 100644 --- a/benchmarks/benches/ecosystem/broadcast/mpmc/support.rs +++ b/benchmarks/benches/ecosystem/broadcast/mpmc/support.rs @@ -16,13 +16,13 @@ // under the License. use std::fmt; +use std::hint::black_box; use std::marker::PhantomData; use std::sync::Arc; use std::sync::Barrier; use std::thread; use std::thread::JoinHandle; -use divan::black_box; use tokio::runtime::Runtime; use tokio::task::JoinSet; diff --git a/benchmarks/benches/ecosystem/main.rs b/benchmarks/benches/ecosystem/main.rs index 6bc26dae..561eedce 100644 --- a/benchmarks/benches/ecosystem/main.rs +++ b/benchmarks/benches/ecosystem/main.rs @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +use benchmarks::harness::Runner; + mod broadcast; mod mpmc; mod mpsc; @@ -23,5 +25,12 @@ mod waitgroup; mod watch; fn main() { - divan::main(); + let mut runner = Runner::from_args(); + broadcast::register(&mut runner); + mpmc::register(&mut runner); + mpsc::register(&mut runner); + spmc::register(&mut runner); + waitgroup::register(&mut runner); + watch::register(&mut runner); + runner.finish(); } diff --git a/benchmarks/benches/ecosystem/mpmc/bounded.rs b/benchmarks/benches/ecosystem/mpmc/bounded.rs index 6cc84fe2..155f52fd 100644 --- a/benchmarks/benches/ecosystem/mpmc/bounded.rs +++ b/benchmarks/benches/ecosystem/mpmc/bounded.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use benchmarks::channels::BATCH_MESSAGES; use benchmarks::channels::BOUNDED_CAPACITY; use benchmarks::channels::adapters::AsyncChannel; use benchmarks::channels::adapters::Bounded; @@ -27,33 +26,63 @@ use benchmarks::channels::mpmc::TaskBatch; use benchmarks::channels::mpmc::ThreadBatch; use benchmarks::channels::mpmc::Topology; use benchmarks::channels::runtime; -use divan::Bencher; -use divan::counter::ItemsCount; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench( - types = [Mpmc, AsyncChannel, Flume], - args = TOPOLOGIES, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn thread_batch(bencher: Bencher, topology: Topology) { - bencher - .with_inputs(|| ThreadBatch::new_bounded::(BOUNDED_CAPACITY, topology)) - .bench_local_refs(|batch| batch.run()); + bencher.bench_local(|| { + let mut batch = ThreadBatch::new_bounded::(BOUNDED_CAPACITY, topology); + batch.run() + }); } -#[divan::bench( - types = [Mpmc, AsyncChannel, Flume], - consts = [0, 4], - args = TOPOLOGIES, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn task_batch(bencher: Bencher, topology: Topology) { let runtime = runtime(WORKERS); - bencher - .with_inputs(|| TaskBatch::new::>(&runtime, topology)) - .bench_local_refs(|batch| runtime.block_on(batch.run())); + bencher.bench_local(|| { + let mut batch = TaskBatch::new::>(&runtime, topology); + runtime.block_on(batch.run()) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Mpmc"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: BoundedMpmc, +{ + for &arg in TOPOLOGIES { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::thread_batch") + ), + 32, + 1, + |bencher| thread_batch::(bencher, arg), + ); + } + for &arg in TOPOLOGIES { + runner.case( + format!( + "{}/{implementation}/0/{arg}", + concat!(module_path!(), "::task_batch") + ), + 320, + 1, + |bencher| task_batch::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/4/{arg}", + concat!(module_path!(), "::task_batch") + ), + 320, + 1, + |bencher| task_batch::(bencher, arg), + ); + } } diff --git a/benchmarks/benches/ecosystem/mpmc/mod.rs b/benchmarks/benches/ecosystem/mpmc/mod.rs index e0ac8347..1a7fea9f 100644 --- a/benchmarks/benches/ecosystem/mpmc/mod.rs +++ b/benchmarks/benches/ecosystem/mpmc/mod.rs @@ -17,3 +17,10 @@ mod bounded; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/ecosystem/mpmc/unbounded.rs b/benchmarks/benches/ecosystem/mpmc/unbounded.rs index 253ad97c..1c422a5f 100644 --- a/benchmarks/benches/ecosystem/mpmc/unbounded.rs +++ b/benchmarks/benches/ecosystem/mpmc/unbounded.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use benchmarks::channels::BATCH_MESSAGES; use benchmarks::channels::adapters::AsyncChannel; use benchmarks::channels::adapters::Flume; use benchmarks::channels::adapters::Mpmc; @@ -26,33 +25,63 @@ use benchmarks::channels::mpmc::TaskBatch; use benchmarks::channels::mpmc::ThreadBatch; use benchmarks::channels::mpmc::Topology; use benchmarks::channels::runtime; -use divan::Bencher; -use divan::counter::ItemsCount; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench( - types = [Mpmc, AsyncChannel, Flume], - args = TOPOLOGIES, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn thread_batch(bencher: Bencher, topology: Topology) { - bencher - .with_inputs(|| ThreadBatch::new_unbounded::(topology)) - .bench_local_refs(|batch| batch.run()); + bencher.bench_local(|| { + let mut batch = ThreadBatch::new_unbounded::(topology); + batch.run() + }); } -#[divan::bench( - types = [Mpmc, AsyncChannel, Flume], - consts = [0, 4], - args = TOPOLOGIES, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn task_batch(bencher: Bencher, topology: Topology) { let runtime = runtime(WORKERS); - bencher - .with_inputs(|| TaskBatch::new::>(&runtime, topology)) - .bench_local_refs(|batch| runtime.block_on(batch.run())); + bencher.bench_local(|| { + let mut batch = TaskBatch::new::>(&runtime, topology); + runtime.block_on(batch.run()) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Mpmc"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: UnboundedMpmc, +{ + for &arg in TOPOLOGIES { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::thread_batch") + ), + 256, + 1, + |bencher| thread_batch::(bencher, arg), + ); + } + for &arg in TOPOLOGIES { + runner.case( + format!( + "{}/{implementation}/0/{arg}", + concat!(module_path!(), "::task_batch") + ), + 512, + 1, + |bencher| task_batch::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/4/{arg}", + concat!(module_path!(), "::task_batch") + ), + 512, + 1, + |bencher| task_batch::(bencher, arg), + ); + } } diff --git a/benchmarks/benches/ecosystem/mpsc/bounded.rs b/benchmarks/benches/ecosystem/mpsc/bounded.rs index 8d324f3f..af5de2fc 100644 --- a/benchmarks/benches/ecosystem/mpsc/bounded.rs +++ b/benchmarks/benches/ecosystem/mpsc/bounded.rs @@ -15,17 +15,17 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; use crate::mpsc::adapters::AsyncChannel; use crate::mpsc::adapters::Asyncband; use crate::mpsc::adapters::BoundedMpsc; use crate::mpsc::adapters::Flume; use crate::mpsc::adapters::Tokio; -use crate::mpsc::support::BATCH_MESSAGES; use crate::mpsc::support::BOUNDED_CAPACITY; use crate::mpsc::support::Bounded; use crate::mpsc::support::PARALLEL_TASK_SHAPES; @@ -35,8 +35,6 @@ use crate::mpsc::support::RepeatedTasks; use crate::mpsc::support::TASK_SHAPES; use crate::mpsc::support::TaskShape; -// Fixed sample sizes keep cold-start costs from skewing Divan's iteration count. -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume], sample_size = 512)] fn try_send_receive(bencher: Bencher) { let (sender, mut receiver) = C::channel(BOUNDED_CAPACITY); @@ -46,7 +44,6 @@ fn try_send_receive(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume], sample_size = 256)] fn ready_send_receive(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receiver) = C::channel(BOUNDED_CAPACITY); @@ -57,27 +54,12 @@ fn ready_send_receive(bencher: Bencher) { }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = PRODUCER_COUNTS, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn reused_threads(bencher: Bencher, producer_count: usize) { let mut batch = RepeatedBatch::>::new(producer_count); batch.run(); bencher.bench_local(|| batch.run()); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [64, 1024], - args = TASK_SHAPES, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn reused_tasks( bencher: Bencher, TaskShape { producers, workers }: TaskShape, @@ -87,14 +69,6 @@ fn reused_tasks( bencher.bench_local(|| batch.run()); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [64, 1024], - args = PARALLEL_TASK_SHAPES, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn reused_tasks_inline_1k, const CAPACITY: usize>( bencher: Bencher, TaskShape { producers, workers }: TaskShape, @@ -104,43 +78,152 @@ fn reused_tasks_inline_1k, const CAPACITY: usize>( bencher.bench_local(|| batch.run()); } -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume], sample_count = 20, sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES))] fn capacity_one_task_handoff(bencher: Bencher) { let mut batch = RepeatedTasks::>::new(4, 4); batch.run(); bencher.bench_local(|| batch.run()); } -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume], sample_size = 2048)] fn clone_drop_sender(bencher: Bencher) { let (sender, _receiver) = C::channel(BOUNDED_CAPACITY); bencher.bench_local(|| drop(black_box(sender.clone()))); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [1, 8], - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn external_receiver(bencher: Bencher, producers: usize) { let mut batch = RepeatedTasks::>::external_receiver(producers, 4); batch.run(); bencher.bench_local(|| batch.run()); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [1, 8], - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn external_receiver_inline>(bencher: Bencher, producers: usize) { let mut batch = RepeatedTasks::>::external_receiver(producers, 4); batch.run(); bencher.bench_local(|| batch.run()); } + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "Tokio"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: BoundedMpsc + BoundedMpsc<[u8; 1024]>, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::try_send_receive") + ), + 30_000_000, + 1, + try_send_receive::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::ready_send_receive") + ), + 20_000_000, + 1, + ready_send_receive::, + ); + + for &arg in PRODUCER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::reused_threads") + ), + 128, + 1, + |bencher| reused_threads::(bencher, arg), + ); + } + for &arg in TASK_SHAPES { + runner.case( + format!( + "{}/{implementation}/64/{arg}", + concat!(module_path!(), "::reused_tasks") + ), + 512, + 1, + |bencher| reused_tasks::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/1024/{arg}", + concat!(module_path!(), "::reused_tasks") + ), + 512, + 1, + |bencher| reused_tasks::(bencher, arg), + ); + } + for &arg in PARALLEL_TASK_SHAPES { + runner.case( + format!( + "{}/{implementation}/64/{arg}", + concat!(module_path!(), "::reused_tasks_inline_1k") + ), + 64, + 1, + |bencher| reused_tasks_inline_1k::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/1024/{arg}", + concat!(module_path!(), "::reused_tasks_inline_1k") + ), + 64, + 1, + |bencher| reused_tasks_inline_1k::(bencher, arg), + ); + } + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::capacity_one_task_handoff") + ), + 64, + 1, + capacity_one_task_handoff::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::clone_drop_sender") + ), + 96_000_000, + 1, + clone_drop_sender::, + ); + + for &arg in &[1, 8] { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::external_receiver") + ), + 128, + 1, + |bencher| external_receiver::(bencher, arg), + ); + } + for &arg in &[1, 8] { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::external_receiver_inline") + ), + 64, + 1, + |bencher| external_receiver_inline::(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/ecosystem/mpsc/mod.rs b/benchmarks/benches/ecosystem/mpsc/mod.rs index 446d899b..aa89c070 100644 --- a/benchmarks/benches/ecosystem/mpsc/mod.rs +++ b/benchmarks/benches/ecosystem/mpsc/mod.rs @@ -15,11 +15,19 @@ // specific language governing permissions and limitations // under the License. -//! Reused workers and channels are warmed up before timing; batch coordination and checksums are -//! timed. +//! Reuses workers and channels across rounds. Startup, priming, coordination, checksums, and +//! shutdown are all included in the workload time. mod adapters; mod bounded; mod reservation; mod support; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + reservation::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/ecosystem/mpsc/reservation.rs b/benchmarks/benches/ecosystem/mpsc/reservation.rs index fbbd8637..c32cc86e 100644 --- a/benchmarks/benches/ecosystem/mpsc/reservation.rs +++ b/benchmarks/benches/ecosystem/mpsc/reservation.rs @@ -16,19 +16,18 @@ // under the License. use std::future::Future; +use std::hint::black_box; use std::marker::PhantomData; use asyncband::blocking::FutureExt; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; use crate::mpsc::adapters::Asyncband; use crate::mpsc::adapters::BoundedMpsc; use crate::mpsc::adapters::Tokio; -use crate::mpsc::support::BATCH_MESSAGES; use crate::mpsc::support::ConcurrentMpsc; use crate::mpsc::support::RepeatedTasks; use crate::mpsc::support::TASK_SHAPES; @@ -67,8 +66,6 @@ impl Reservable for Tokio { } } -// `sample_size` is pinned for the reason documented in `bounded.rs`. -#[divan::bench(types = [Asyncband, Tokio], sample_size = 512)] fn reserve_publish_receive(bencher: Bencher) { let (sender, mut receiver) = C::channel(64); let mut context = bench_context(); @@ -102,14 +99,6 @@ impl ConcurrentMpsc for Reserved( bencher: Bencher, TaskShape { producers, workers }: TaskShape, @@ -119,16 +108,73 @@ fn reused_tasks( bencher.bench_local(|| batch.run()); } -#[divan::bench(types = [Asyncband, Tokio], sample_count = 20, sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES))] fn capacity_one_task_handoff(bencher: Bencher) { let mut batch = RepeatedTasks::>::new(4, 4); batch.run(); bencher.bench_local(|| batch.run()); } -#[divan::bench(types = [Asyncband, Tokio], sample_size = 1024)] fn cancel_reserved_capacity(bencher: Bencher) { let (sender, _receiver) = C::channel(64); bencher.bench_local(|| drop(black_box(C::try_reserve(&sender)))); } + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "Tokio"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: Reservable, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::reserve_publish_receive") + ), + 15_000_000, + 1, + reserve_publish_receive::, + ); + + for &arg in TASK_SHAPES { + runner.case( + format!( + "{}/{implementation}/64/{arg}", + concat!(module_path!(), "::reused_tasks") + ), + 512, + 1, + |bencher| reused_tasks::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/1024/{arg}", + concat!(module_path!(), "::reused_tasks") + ), + 512, + 1, + |bencher| reused_tasks::(bencher, arg), + ); + } + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::capacity_one_task_handoff") + ), + 128, + 1, + capacity_one_task_handoff::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::cancel_reserved_capacity") + ), + 30_000_000, + 1, + cancel_reserved_capacity::, + ); +} diff --git a/benchmarks/benches/ecosystem/mpsc/support.rs b/benchmarks/benches/ecosystem/mpsc/support.rs index 14dde891..29a78a34 100644 --- a/benchmarks/benches/ecosystem/mpsc/support.rs +++ b/benchmarks/benches/ecosystem/mpsc/support.rs @@ -17,6 +17,7 @@ use std::fmt; use std::future::Future; +use std::hint::black_box; use std::marker::PhantomData; use std::sync::Arc; use std::sync::Barrier; @@ -27,7 +28,6 @@ use std::thread::JoinHandle; pub use benchmarks::channels::BATCH_MESSAGES; pub use benchmarks::channels::BOUNDED_CAPACITY; -use divan::black_box; use tokio::runtime::Builder; use tokio::runtime::Runtime; use tokio::sync::Notify; @@ -184,7 +184,8 @@ impl ConcurrentMpsc for Unbounded { } } -// Reuse worker threads and channel storage so steady-state samples exclude thread creation. +// Reuse worker threads and channel storage across rounds; startup and shutdown happen once per +// workload. pub struct RepeatedBatch> { receiver: C::Receiver, start: Arc, diff --git a/benchmarks/benches/ecosystem/mpsc/unbounded.rs b/benchmarks/benches/ecosystem/mpsc/unbounded.rs index 0e1b6784..5af3df18 100644 --- a/benchmarks/benches/ecosystem/mpsc/unbounded.rs +++ b/benchmarks/benches/ecosystem/mpsc/unbounded.rs @@ -17,15 +17,15 @@ use std::future::Future; use std::future::poll_fn; +use std::hint::black_box; use std::pin::pin; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; use tokio::runtime::Builder; use tokio::sync::Notify; @@ -42,7 +42,6 @@ use crate::mpsc::support::TASK_SHAPES; use crate::mpsc::support::TaskShape; use crate::mpsc::support::Unbounded; -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume])] fn ready_send_receive(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receiver) = C::channel(); @@ -53,7 +52,6 @@ fn ready_send_receive(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume])] fn try_send_receive(bencher: Bencher) { let (sender, mut receiver) = C::channel(); @@ -63,23 +61,10 @@ fn try_send_receive(bencher: Bencher) { }); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [32, 1024, 65_536], - sample_count = 20, - sample_size = 1, -)] fn burst_drain(bencher: Bencher, messages: usize) { repeated_bursts::(bencher, messages, 0, || usize::MAX); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - consts = [64, 1024], - args = [1024], - sample_count = 20, - sample_size = 1, -)] fn burst_drain_inline, const SIZE: usize>( bencher: Bencher, messages: usize, @@ -87,23 +72,11 @@ fn burst_drain_inline, const SIZE: usize>( repeated_bursts::(bencher, messages, 0, || [1; SIZE]); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [1024], - sample_count = 20, - sample_size = 1, -)] fn burst_drain_boxed>>(bencher: Bencher, messages: usize) { // Include payload allocation and destruction to compare the complete boxed-message lifecycle. repeated_bursts::(bencher, messages, 0, || Box::new([1; 1024])); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [1024, 65_536], - sample_count = 20, - sample_size = 1, -)] fn burst_with_backlog(bencher: Bencher, messages: usize) { repeated_bursts::(bencher, messages, messages / 2, || usize::MAX); } @@ -114,7 +87,7 @@ fn repeated_bursts, T, F: Fn() -> T>( backlog: usize, make_value: F, ) { - // Keep one channel alive across samples so allocation reuse and reclamation are measured. + // Keep one channel alive across rounds so allocation reuse and reclamation are measured. let (sender, mut receiver) = C::channel(); for _ in 0..backlog { C::send(&sender, make_value()); @@ -130,29 +103,15 @@ fn repeated_bursts, T, F: Fn() -> T>( // Measure recurring bursts after the initial allocation, including a deliberately retained // backlog where requested. Do not require an extra `try_recv` call to trigger reclamation. run(); - bencher.counter(ItemsCount::new(messages)).bench_local(run); + bencher.bench_local(run); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = PRODUCER_COUNTS, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn reused_threads(bencher: Bencher, producer_count: usize) { let mut batch = RepeatedBatch::>::new(producer_count); batch.run(); bencher.bench_local(|| batch.run()); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = TASK_SHAPES, - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn reused_tasks(bencher: Bencher, TaskShape { producers, workers }: TaskShape) { let mut batch = RepeatedTasks::>::new(producers, workers); batch.run(); @@ -168,19 +127,11 @@ fn check_inline_message(value: [u8; 1024], expected: &mut [u64]) -> usize { usize::from(value[1023]) } -#[divan::bench(types = [Asyncband, Tokio, AsyncChannel, Flume])] fn clone_drop_sender(bencher: Bencher) { let (sender, _receiver) = C::channel(); bencher.bench_local(|| drop(black_box(sender.clone()))); } -#[divan::bench( - types = [Asyncband, Tokio, AsyncChannel, Flume], - args = [(1, 64), (8, 64), (1, 1024), (8, 1024)], - sample_count = 50, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn scheduled_bursts_inline>( bencher: Bencher, (producers, burst_messages): (usize, usize), @@ -275,3 +226,137 @@ fn scheduled_bursts_inline>( } }); } + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "Tokio"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: UnboundedMpsc + + UnboundedMpsc<[u8; 64]> + + UnboundedMpsc<[u8; 1024]> + + UnboundedMpsc>, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::ready_send_receive") + ), + 30_000_000, + 1, + ready_send_receive::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::try_send_receive") + ), + 30_000_000, + 1, + try_send_receive::, + ); + + for &arg in &[32, 1024, 65_536] { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::burst_drain") + ), + (32_000_000 / arg).max(1), + 1, + |bencher| burst_drain::(bencher, arg), + ); + } + { + let &arg = &1024; + runner.case( + format!( + "{}/{implementation}/64/{arg}", + concat!(module_path!(), "::burst_drain_inline") + ), + (8_000_000 / arg).max(1), + 1, + |bencher| burst_drain_inline::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/1024/{arg}", + concat!(module_path!(), "::burst_drain_inline") + ), + (8_000_000 / arg).max(1), + 1, + |bencher| burst_drain_inline::(bencher, arg), + ); + } + { + let &arg = &1024; + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::burst_drain_boxed") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| burst_drain_boxed::(bencher, arg), + ); + } + for &arg in &[1024, 65_536] { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::burst_with_backlog") + ), + (32_000_000 / arg).max(1), + 1, + |bencher| burst_with_backlog::(bencher, arg), + ); + } + for &arg in PRODUCER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::reused_threads") + ), + 320, + 1, + |bencher| reused_threads::(bencher, arg), + ); + } + for &arg in TASK_SHAPES { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::reused_tasks") + ), + 512, + 1, + |bencher| reused_tasks::(bencher, arg), + ); + } + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::clone_drop_sender") + ), + 96_000_000, + 1, + clone_drop_sender::, + ); + + for &arg in &[(1, 64), (8, 64), (1, 1024), (8, 1024)] { + runner.case( + format!( + "{}/{implementation}/{arg:?}", + concat!(module_path!(), "::scheduled_bursts_inline") + ), + 64, + 1, + |bencher| scheduled_bursts_inline::(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/ecosystem/spmc/bounded.rs b/benchmarks/benches/ecosystem/spmc/bounded.rs index b2d8735c..bb00e646 100644 --- a/benchmarks/benches/ecosystem/spmc/bounded.rs +++ b/benchmarks/benches/ecosystem/spmc/bounded.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use benchmarks::channels::BATCH_MESSAGES; use benchmarks::channels::adapters::AsyncChannel; use benchmarks::channels::adapters::Bounded; use benchmarks::channels::adapters::Channel; @@ -25,23 +24,49 @@ use benchmarks::channels::adapters::Spmc; use benchmarks::channels::runtime; use benchmarks::channels::spmc::CONSUMERS; use benchmarks::channels::spmc::TaskBatch; -use divan::Bencher; -use divan::counter::ItemsCount; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench( - types = [Spmc, Mpmc, AsyncChannel, Flume], - consts = [0, 4], - args = CONSUMERS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn task_batch(bencher: Bencher, consumers: usize) where Bounded: Channel, { let runtime = runtime(WORKERS); - bencher - .with_inputs(|| TaskBatch::new::>(&runtime, consumers)) - .bench_local_refs(|batch| runtime.block_on(batch.run())); + bencher.bench_local(|| { + let mut batch = TaskBatch::new::>(&runtime, consumers); + runtime.block_on(batch.run()) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Spmc"); + register_implementation::(runner, "Mpmc"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + Bounded: Channel, +{ + for &arg in CONSUMERS { + runner.case( + format!( + "{}/{implementation}/0/{arg}", + concat!(module_path!(), "::task_batch") + ), + 320, + 1, + |bencher| task_batch::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/4/{arg}", + concat!(module_path!(), "::task_batch") + ), + 320, + 1, + |bencher| task_batch::(bencher, arg), + ); + } } diff --git a/benchmarks/benches/ecosystem/spmc/mod.rs b/benchmarks/benches/ecosystem/spmc/mod.rs index 4d1e0a0e..c11ffcca 100644 --- a/benchmarks/benches/ecosystem/spmc/mod.rs +++ b/benchmarks/benches/ecosystem/spmc/mod.rs @@ -19,3 +19,10 @@ mod bounded; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/ecosystem/spmc/unbounded.rs b/benchmarks/benches/ecosystem/spmc/unbounded.rs index 4370c7b9..f0887a06 100644 --- a/benchmarks/benches/ecosystem/spmc/unbounded.rs +++ b/benchmarks/benches/ecosystem/spmc/unbounded.rs @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -use benchmarks::channels::BATCH_MESSAGES; use benchmarks::channels::adapters::AsyncChannel; use benchmarks::channels::adapters::Channel; use benchmarks::channels::adapters::Flume; @@ -25,23 +24,49 @@ use benchmarks::channels::adapters::Unbounded; use benchmarks::channels::runtime; use benchmarks::channels::spmc::CONSUMERS; use benchmarks::channels::spmc::TaskBatch; -use divan::Bencher; -use divan::counter::ItemsCount; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench( - types = [Spmc, Mpmc, AsyncChannel, Flume], - consts = [0, 4], - args = CONSUMERS, - sample_count = 20, - sample_size = 1, - counter = ItemsCount::new(BATCH_MESSAGES), -)] fn task_batch(bencher: Bencher, consumers: usize) where Unbounded: Channel, { let runtime = runtime(WORKERS); - bencher - .with_inputs(|| TaskBatch::new::>(&runtime, consumers)) - .bench_local_refs(|batch| runtime.block_on(batch.run())); + bencher.bench_local(|| { + let mut batch = TaskBatch::new::>(&runtime, consumers); + runtime.block_on(batch.run()) + }); +} + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Spmc"); + register_implementation::(runner, "Mpmc"); + register_implementation::(runner, "AsyncChannel"); + register_implementation::(runner, "Flume"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + Unbounded: Channel, +{ + for &arg in CONSUMERS { + runner.case( + format!( + "{}/{implementation}/0/{arg}", + concat!(module_path!(), "::task_batch") + ), + 512, + 1, + |bencher| task_batch::(bencher, arg), + ); + runner.case( + format!( + "{}/{implementation}/4/{arg}", + concat!(module_path!(), "::task_batch") + ), + 512, + 1, + |bencher| task_batch::(bencher, arg), + ); + } } diff --git a/benchmarks/benches/ecosystem/waitgroup.rs b/benchmarks/benches/ecosystem/waitgroup.rs index f9886452..b5607826 100644 --- a/benchmarks/benches/ecosystem/waitgroup.rs +++ b/benchmarks/benches/ecosystem/waitgroup.rs @@ -17,13 +17,14 @@ use std::future::Future; use std::future::IntoFuture; +use std::hint::black_box; use std::pin::pin; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const WORKER_COUNTS: &[usize] = &[1, 4, 16]; @@ -76,7 +77,6 @@ impl WaitGroup for WaitgroupRs { } } -#[divan::bench(types = [Asyncband, WaitgroupRs])] fn complete_waiter(bencher: Bencher) { let mut context = bench_context(); @@ -91,7 +91,6 @@ fn complete_waiter(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs], args = WORKER_COUNTS)] fn complete_worker_batch(bencher: Bencher, worker_count: usize) { let mut context = bench_context(); @@ -108,7 +107,6 @@ fn complete_worker_batch(bencher: Bencher, worker_count: usize) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs])] fn ready_empty(bencher: Bencher) { let mut context = bench_context(); @@ -118,7 +116,6 @@ fn ready_empty(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs])] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -133,7 +130,6 @@ fn cancel_pending(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs], args = WORKER_COUNTS)] fn worker_batch(bencher: Bencher, worker_count: usize) { bencher.bench_local(|| { let group = C::new(); @@ -145,7 +141,6 @@ fn worker_batch(bencher: Bencher, worker_count: usize) { }); } -#[divan::bench(types = [Asyncband, WaitgroupRs], args = WORKER_COUNTS)] fn nested_worker_batch(bencher: Bencher, worker_count: usize) { bencher.bench_local(|| { let group = C::new(); @@ -158,3 +153,77 @@ fn nested_worker_batch(bencher: Bencher, worker_count: usize) { black_box(group); }); } + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "WaitgroupRs"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: WaitGroup, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::complete_waiter") + ), + 6_000_000, + 1, + complete_waiter::, + ); + + for &arg in WORKER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::complete_worker_batch") + ), + (10_000_000 / arg).max(1), + 1, + |bencher| complete_worker_batch::(bencher, arg), + ); + } + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::ready_empty") + ), + 10_000_000, + 1, + ready_empty::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::cancel_pending") + ), + 6_000_000, + 1, + cancel_pending::, + ); + + for &arg in WORKER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::worker_batch") + ), + (10_000_000 / arg).max(1), + 1, + |bencher| worker_batch::(bencher, arg), + ); + } + for &arg in WORKER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::nested_worker_batch") + ), + (10_000_000 / arg).max(1), + 1, + |bencher| nested_worker_batch::(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/ecosystem/watch/mod.rs b/benchmarks/benches/ecosystem/watch/mod.rs index c9afe70d..6e8f64d2 100644 --- a/benchmarks/benches/ecosystem/watch/mod.rs +++ b/benchmarks/benches/ecosystem/watch/mod.rs @@ -17,3 +17,9 @@ mod adapters; mod paths; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + paths::register(runner); +} diff --git a/benchmarks/benches/ecosystem/watch/paths.rs b/benchmarks/benches/ecosystem/watch/paths.rs index aa66807a..a796555b 100644 --- a/benchmarks/benches/ecosystem/watch/paths.rs +++ b/benchmarks/benches/ecosystem/watch/paths.rs @@ -18,14 +18,15 @@ // Both adapters return an owned `usize`. The Tokio adapter's `recv` combines `changed` and // `borrow_and_update`. +use std::hint::black_box; use std::pin::pin; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; use crate::watch::adapters::Asyncband; use crate::watch::adapters::Tokio; @@ -33,7 +34,6 @@ use crate::watch::adapters::Watch; const RECEIVER_COUNTS: &[usize] = &[2, 4, 8, 32]; -#[divan::bench(types = [Asyncband, Tokio])] fn get_current(bencher: Bencher) { let (sender, mut receivers) = C::channel(1); let receiver = receivers.pop().unwrap(); @@ -41,7 +41,6 @@ fn get_current(bencher: Bencher) { black_box(sender); } -#[divan::bench(types = [Asyncband, Tokio])] fn publish_then_snapshot(bencher: Bencher) { let (sender, mut receivers) = C::channel(1); let receiver = receivers.pop().unwrap(); @@ -51,7 +50,6 @@ fn publish_then_snapshot(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio])] fn publish_then_receive(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(1); @@ -62,7 +60,6 @@ fn publish_then_receive(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio])] fn publish_then_observe_version(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(1); @@ -73,7 +70,6 @@ fn publish_then_observe_version(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio])] fn register_publish_observe(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(1); @@ -86,7 +82,6 @@ fn register_publish_observe(bencher: Bencher) { }); } -#[divan::bench(types = [Asyncband, Tokio], args = RECEIVER_COUNTS)] fn register_publish_observe_fanout(bencher: Bencher, receiver_count: usize) { let mut context = bench_context(); let (sender, mut receivers) = C::channel(receiver_count); @@ -106,3 +101,75 @@ fn register_publish_observe_fanout(bencher: Bencher, receiver_count: u } }); } + +pub fn register(runner: &mut Runner) { + register_implementation::(runner, "Asyncband"); + register_implementation::(runner, "Tokio"); +} + +fn register_implementation(runner: &mut Runner, implementation: &str) +where + C: Watch, +{ + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::get_current") + ), + 80_000_000, + 1, + get_current::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::publish_then_snapshot") + ), + 15_000_000, + 1, + publish_then_snapshot::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::publish_then_receive") + ), + 9_000_000, + 1, + publish_then_receive::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::publish_then_observe_version") + ), + 12_000_000, + 1, + publish_then_observe_version::, + ); + + runner.case( + format!( + "{}/{implementation}", + concat!(module_path!(), "::register_publish_observe") + ), + 6_000_000, + 1, + register_publish_observe::, + ); + + for &arg in RECEIVER_COUNTS { + runner.case( + format!( + "{}/{implementation}/{arg}", + concat!(module_path!(), "::register_publish_observe_fanout") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| register_publish_observe_fanout::(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/barrier/mod.rs b/benchmarks/benches/primitives/barrier/mod.rs index 35540691..a9957bb4 100644 --- a/benchmarks/benches/primitives/barrier/mod.rs +++ b/benchmarks/benches/primitives/barrier/mod.rs @@ -16,3 +16,9 @@ // under the License. mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/barrier/wait.rs b/benchmarks/benches/primitives/barrier/wait.rs index 3589bfce..09bd9749 100644 --- a/benchmarks/benches/primitives/barrier/wait.rs +++ b/benchmarks/benches/primitives/barrier/wait.rs @@ -15,19 +15,19 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::barrier::Barrier; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const PARTICIPANT_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = PARTICIPANT_COUNTS)] fn reused_generation_fanout(bencher: Bencher, participant_count: usize) { let mut context = bench_context(); let barrier = Barrier::new((participant_count + 1) as u32); @@ -49,7 +49,6 @@ fn reused_generation_fanout(bencher: Bencher, participant_count: usize) { }); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -62,3 +61,23 @@ fn cancel_pending(bencher: Bencher) { black_box(barrier) }); } + +pub fn register(runner: &mut Runner) { + for &arg in PARTICIPANT_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::reused_generation_fanout") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| reused_generation_fanout(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending"), + 6_000_000, + 1, + cancel_pending, + ); +} diff --git a/benchmarks/benches/primitives/blocking/block_on.rs b/benchmarks/benches/primitives/blocking/block_on.rs index 8faa57af..0077543c 100644 --- a/benchmarks/benches/primitives/blocking/block_on.rs +++ b/benchmarks/benches/primitives/blocking/block_on.rs @@ -16,21 +16,20 @@ // under the License. use std::future::poll_fn; +use std::hint::black_box; use std::sync::mpsc; use std::task::Poll; use std::task::Waker; use std::thread; use asyncband::blocking::FutureExt; -use divan::Bencher; -use divan::black_box; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench] fn ready(bencher: Bencher) { bencher.bench_local(|| async { black_box(42usize) }.block_on()); } -#[divan::bench] fn self_wake(bencher: Bencher) { bencher.bench_local(|| { let mut polled = false; @@ -47,7 +46,6 @@ fn self_wake(bencher: Bencher) { }); } -#[divan::bench] fn cross_thread_wake(bencher: Bencher) { let (wakers_tx, wakers_rx) = mpsc::channel::(); let worker = thread::spawn(move || { @@ -73,3 +71,19 @@ fn cross_thread_wake(bencher: Bencher) { drop(wakers_tx); worker.join().unwrap(); } + +pub fn register(runner: &mut Runner) { + runner.case(concat!(module_path!(), "::ready"), 100_000_000, 1, ready); + runner.case( + concat!(module_path!(), "::self_wake"), + 60_000_000, + 1, + self_wake, + ); + runner.case( + concat!(module_path!(), "::cross_thread_wake"), + 60_000, + 1, + cross_thread_wake, + ); +} diff --git a/benchmarks/benches/primitives/blocking/mod.rs b/benchmarks/benches/primitives/blocking/mod.rs index 0213fb0a..ebcee423 100644 --- a/benchmarks/benches/primitives/blocking/mod.rs +++ b/benchmarks/benches/primitives/blocking/mod.rs @@ -19,3 +19,10 @@ mod block_on; mod wait_timeout; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + block_on::register(runner); + wait_timeout::register(runner); +} diff --git a/benchmarks/benches/primitives/blocking/wait_timeout.rs b/benchmarks/benches/primitives/blocking/wait_timeout.rs index c0e62725..aee3a664 100644 --- a/benchmarks/benches/primitives/blocking/wait_timeout.rs +++ b/benchmarks/benches/primitives/blocking/wait_timeout.rs @@ -15,13 +15,13 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::time::Duration; use asyncband::blocking::FutureExt; -use divan::Bencher; -use divan::black_box; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench] fn ready_with_timeout(bencher: Bencher) { bencher.bench_local(|| { async { black_box(42usize) } @@ -29,3 +29,12 @@ fn ready_with_timeout(bencher: Bencher) { .unwrap() }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::ready_with_timeout"), + 20_000_000, + 1, + ready_with_timeout, + ); +} diff --git a/benchmarks/benches/primitives/broadcast/mod.rs b/benchmarks/benches/primitives/broadcast/mod.rs index b078f4ac..3828af5c 100644 --- a/benchmarks/benches/primitives/broadcast/mod.rs +++ b/benchmarks/benches/primitives/broadcast/mod.rs @@ -16,3 +16,9 @@ // under the License. mod mpmc; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + mpmc::register(runner); +} diff --git a/benchmarks/benches/primitives/broadcast/mpmc/bounded.rs b/benchmarks/benches/primitives/broadcast/mpmc/bounded.rs index dd03bdf5..8652c884 100644 --- a/benchmarks/benches/primitives/broadcast/mpmc/bounded.rs +++ b/benchmarks/benches/primitives/broadcast/mpmc/bounded.rs @@ -15,23 +15,22 @@ // specific language governing permissions and limitations // under the License. -// Restore backlog and permits each iteration so later samples cannot stall on exhausted capacity. +// Restore backlog and permits each iteration so later rounds cannot stall on exhausted capacity. +use std::hint::black_box; use std::pin::pin; use asyncband::broadcast::mpmc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use benchmarks::support::defer_input_drop; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const RECEIVER_COUNTS: &[usize] = &[1, 4, 16]; const BLOCKED_SENDER_COUNTS: &[usize] = &[1, 4, 16]; const CAPACITY: usize = 64; -#[divan::bench] fn try_send_and_try_recv(bencher: Bencher) { let (tx, mut rx) = mpmc::bounded(CAPACITY); @@ -41,7 +40,6 @@ fn try_send_and_try_recv(bencher: Bencher) { }); } -#[divan::bench(args = RECEIVER_COUNTS)] fn try_send_and_drain_fanout(bencher: Bencher, receiver_count: usize) { let (tx, rx) = mpmc::bounded(CAPACITY); let mut receivers = Vec::with_capacity(receiver_count); @@ -60,43 +58,37 @@ fn try_send_and_drain_fanout(bencher: Bencher, receiver_count: usize) { }); } -#[divan::bench(args = BLOCKED_SENDER_COUNTS)] fn full_channel_sender_handoff_cycle(bencher: Bencher, sender_count: usize) { let mut context = bench_context(); + let (tx, mut rx) = mpmc::bounded(1); + tx.try_send(0).unwrap(); // Register every producer on a full channel, complete one `send` future after reclaiming a // slot, cancel the remaining futures, and restore the original full backlog. Registration // and cancellation are timed as part of this cycle. - bencher - .with_inputs(|| { - let (tx, rx) = mpmc::bounded(1); - tx.try_send(0).unwrap(); - (tx, rx) - }) - .bench_local_refs(|(tx, rx)| { - let mut sends = (0..sender_count) - .map(|value| Box::pin(tx.send(value))) - .collect::>(); - for send in &mut sends { - poll_pending(send.as_mut(), &mut context); - } + bencher.bench_local(|| { + let mut sends = (0..sender_count) + .map(|value| Box::pin(tx.send(value))) + .collect::>(); + for send in &mut sends { + poll_pending(send.as_mut(), &mut context); + } - // Poll pending `send` futures until one publishes into the freed slot. - black_box(rx.try_recv().unwrap()); - for send in &mut sends { - if send.as_mut().poll(&mut context).is_ready() { - break; - } + // Poll pending `send` futures until one publishes into the freed slot. + black_box(rx.try_recv().unwrap()); + for send in &mut sends { + if send.as_mut().poll(&mut context).is_ready() { + break; } + } - // Drain the republished message so the next iteration starts from the same state. - black_box(rx.try_recv().unwrap()); - drop(sends); - tx.try_send(0).unwrap(); - }); + // Drain the republished message so the next iteration starts from the same state. + black_box(rx.try_recv().unwrap()); + drop(sends); + tx.try_send(0).unwrap(); + }); } -#[divan::bench] fn deliver_to_waiting_receiver(bencher: Bencher) { let mut context = bench_context(); let (tx, mut rx) = mpmc::bounded(CAPACITY); @@ -109,7 +101,6 @@ fn deliver_to_waiting_receiver(bencher: Bencher) { }); } -#[divan::bench] fn send_without_receivers(bencher: Bencher) { // With no subscribers, this measures the discard path. let (tx, rx) = mpmc::bounded(CAPACITY); @@ -118,7 +109,6 @@ fn send_without_receivers(bencher: Bencher) { bencher.bench_local(|| tx.try_send(black_box(1))); } -#[divan::bench] fn try_send_when_full(bencher: Bencher) { let (tx, _rx) = mpmc::bounded(1); tx.try_send(0).unwrap(); @@ -126,7 +116,6 @@ fn try_send_when_full(bencher: Bencher) { bencher.bench_local(|| black_box(tx.try_send(black_box(1))).is_err()); } -#[divan::bench] fn cancel_blocked_send(bencher: Bencher) { let mut context = bench_context(); let (tx, _rx) = mpmc::bounded(1); @@ -138,10 +127,9 @@ fn cancel_blocked_send(bencher: Bencher) { }); } -#[divan::bench(args = [1, 2, 32, 256], sample_size = 64)] fn drop_lagging_receiver_wakes_senders(bencher: Bencher, backlog: usize) { - bencher - .with_inputs(|| { + bencher.bench_local(|| { + let (slow, fast, sends) = { let (sender, mut fast) = mpmc::bounded(backlog); let slow = sender.subscribe(); for value in 0..backlog { @@ -159,12 +147,76 @@ fn drop_lagging_receiver_wakes_senders(bencher: Bencher, backlog: usize) { poll_pending(send.as_mut(), &mut context); } (slow, fast, sends) - }) - .bench_local_values(|(slow, fast, sends)| { - // The fast subscription stays alive so this measures reclaim, not last-receiver - // exit. Preparing the backlog, parking senders, and disposing of - // futures are outside timing. - drop(slow); - defer_input_drop((fast, sends), ()) - }); + }; + // The fast subscription stays alive so this measures reclaim, not last-receiver + // exit. Each round also prepares the backlog, parks senders, and disposes of futures. + drop(slow); + drop((fast, sends)); + }); +} + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::try_send_and_try_recv"), + 9_000_000, + 1, + try_send_and_try_recv, + ); + for &arg in RECEIVER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::try_send_and_drain_fanout") + ), + (20_000_000 / arg).max(1), + 1, + |bencher| try_send_and_drain_fanout(bencher, arg), + ); + } + for &arg in BLOCKED_SENDER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::full_channel_sender_handoff_cycle") + ), + (2_000_000 / arg).max(1), + 1, + |bencher| full_channel_sender_handoff_cycle(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::deliver_to_waiting_receiver"), + 6_000_000, + 1, + deliver_to_waiting_receiver, + ); + runner.case( + concat!(module_path!(), "::send_without_receivers"), + 48_000_000, + 1, + send_without_receivers, + ); + runner.case( + concat!(module_path!(), "::try_send_when_full"), + 60_000_000, + 1, + try_send_when_full, + ); + runner.case( + concat!(module_path!(), "::cancel_blocked_send"), + 6_000_000, + 1, + cancel_blocked_send, + ); + for &arg in &[1, 2, 32, 256] { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::drop_lagging_receiver_wakes_senders") + ), + (2_000_000 / arg).max(1), + 1, + |bencher| drop_lagging_receiver_wakes_senders(bencher, arg), + ); + } } diff --git a/benchmarks/benches/primitives/broadcast/mpmc/mod.rs b/benchmarks/benches/primitives/broadcast/mpmc/mod.rs index e0ac8347..1a7fea9f 100644 --- a/benchmarks/benches/primitives/broadcast/mpmc/mod.rs +++ b/benchmarks/benches/primitives/broadcast/mpmc/mod.rs @@ -17,3 +17,10 @@ mod bounded; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/primitives/broadcast/mpmc/unbounded.rs b/benchmarks/benches/primitives/broadcast/mpmc/unbounded.rs index 7728c17b..aad16c50 100644 --- a/benchmarks/benches/primitives/broadcast/mpmc/unbounded.rs +++ b/benchmarks/benches/primitives/broadcast/mpmc/unbounded.rs @@ -15,17 +15,18 @@ // specific language governing permissions and limitations // under the License. -// Drain each iteration to keep retained memory bounded across samples. +// Drain each iteration to keep retained memory bounded across rounds. use std::fmt; +use std::hint::black_box; use std::pin::pin; use asyncband::broadcast::mpmc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const RECEIVER_COUNTS: &[usize] = &[1, 4, 16]; @@ -57,7 +58,6 @@ const RECLAIM_FANOUTS: &[Fanout] = &[ ]; // Two receivers read the shared payload; the second reclaims the slot. -#[divan::bench] fn send_and_try_recv_shared(bencher: Bencher) { let (sender, mut first) = mpmc::unbounded(); let mut second = sender.subscribe(); @@ -74,7 +74,6 @@ fn payload() -> String { "x".repeat(64) } -#[divan::bench] fn send_and_try_recv_owned(bencher: Bencher) { let (sender, mut receiver) = mpmc::unbounded(); bencher.bench_local(|| { @@ -83,7 +82,6 @@ fn send_and_try_recv_owned(bencher: Bencher) { }); } -#[divan::bench] fn send_and_try_recv_owned_shared(bencher: Bencher) { let (sender, mut first) = mpmc::unbounded(); let mut second = sender.subscribe(); @@ -94,7 +92,6 @@ fn send_and_try_recv_owned_shared(bencher: Bencher) { }); } -#[divan::bench] fn deliver_to_waiting_receiver(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receiver) = mpmc::unbounded(); @@ -109,7 +106,6 @@ fn deliver_to_waiting_receiver(bencher: Bencher) { }); } -#[divan::bench(args = RECEIVER_COUNTS)] fn register_and_deliver_fanout(bencher: Bencher, receiver_count: usize) { let mut context = bench_context(); @@ -135,14 +131,12 @@ fn register_and_deliver_fanout(bencher: Bencher, receiver_count: usize) { }); } -#[divan::bench] fn send_without_receivers(bencher: Bencher) { let (sender, receiver) = mpmc::unbounded::(); drop(receiver); bencher.bench_local(|| sender.send(black_box(1))); } -#[divan::bench] fn try_recv_empty(bencher: Bencher) { let (sender, mut receiver) = mpmc::unbounded::(); bencher.bench_local(|| black_box(receiver.try_recv())); @@ -150,7 +144,6 @@ fn try_recv_empty(bencher: Bencher) { } // Compare reclamation with identical historical peaks but different live subscriber counts. -#[divan::bench(args = RECLAIM_FANOUTS)] fn drain_with_receivers(bencher: Bencher, fanout: Fanout) { let (sender, receiver) = mpmc::unbounded(); drop(receiver); @@ -169,7 +162,6 @@ fn drain_with_receivers(bencher: Bencher, fanout: Fanout) { }); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -182,3 +174,70 @@ fn cancel_pending(bencher: Bencher) { black_box((sender, receiver)) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::send_and_try_recv_shared"), + 6_000_000, + 1, + send_and_try_recv_shared, + ); + runner.case( + concat!(module_path!(), "::send_and_try_recv_owned"), + 3_000_000, + 1, + send_and_try_recv_owned, + ); + runner.case( + concat!(module_path!(), "::send_and_try_recv_owned_shared"), + 3_000_000, + 1, + send_and_try_recv_owned_shared, + ); + runner.case( + concat!(module_path!(), "::deliver_to_waiting_receiver"), + 6_000_000, + 1, + deliver_to_waiting_receiver, + ); + for &arg in RECEIVER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::register_and_deliver_fanout") + ), + (3_000_000 / arg).max(1), + 1, + |bencher| register_and_deliver_fanout(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::send_without_receivers"), + 12_000_000, + 1, + send_without_receivers, + ); + runner.case( + concat!(module_path!(), "::try_recv_empty"), + 60_000_000, + 1, + try_recv_empty, + ); + for &arg in RECLAIM_FANOUTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::drain_with_receivers") + ), + (10_000_000 / arg.live).max(1), + 1, + |bencher| drain_with_receivers(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending"), + 3_000_000, + 1, + cancel_pending, + ); +} diff --git a/benchmarks/benches/primitives/completion/mod.rs b/benchmarks/benches/primitives/completion/mod.rs index 5da85a8a..e52f0b1d 100644 --- a/benchmarks/benches/primitives/completion/mod.rs +++ b/benchmarks/benches/primitives/completion/mod.rs @@ -15,19 +15,19 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::completion; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const OBSERVER_COUNTS: &[usize] = &[2, 4, 16]; -#[divan::bench] fn ready_wait(bencher: Bencher) { let mut context = bench_context(); let (completer, completion) = completion::new(); @@ -36,7 +36,6 @@ fn ready_wait(bencher: Bencher) { bencher.bench_local(|| black_box(*poll_ready(completion.wait(), &mut context).unwrap())); } -#[divan::bench] fn complete_then_wait(bencher: Bencher) { let mut context = bench_context(); @@ -47,7 +46,6 @@ fn complete_then_wait(bencher: Bencher) { }); } -#[divan::bench] fn notify_pending(bencher: Bencher) { let mut context = bench_context(); @@ -61,7 +59,6 @@ fn notify_pending(bencher: Bencher) { }); } -#[divan::bench(args = OBSERVER_COUNTS)] fn notify_pending_fanout(bencher: Bencher, observer_count: usize) { let mut context = bench_context(); @@ -81,7 +78,6 @@ fn notify_pending_fanout(bencher: Bencher, observer_count: usize) { }); } -#[divan::bench] fn abandoned_wait(bencher: Bencher) { let mut context = bench_context(); let (completer, completion) = completion::new::(); @@ -90,7 +86,6 @@ fn abandoned_wait(bencher: Bencher) { bencher.bench_local(|| black_box(poll_ready(completion.wait(), &mut context).unwrap_err())); } -#[divan::bench] fn repoll_pending(bencher: Bencher) { let mut context = bench_context(); let (_completer, completion) = completion::new::(); @@ -100,7 +95,6 @@ fn repoll_pending(bencher: Bencher) { bencher.bench_local(|| poll_pending(wait.as_mut(), &mut context)); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); let (_completer, completion) = completion::new::(); @@ -111,3 +105,53 @@ fn cancel_pending(bencher: Bencher) { }); black_box(completion); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::ready_wait"), + 100_000_000, + 1, + ready_wait, + ); + runner.case( + concat!(module_path!(), "::complete_then_wait"), + 6_000_000, + 1, + complete_then_wait, + ); + runner.case( + concat!(module_path!(), "::notify_pending"), + 3_000_000, + 1, + notify_pending, + ); + for &arg in OBSERVER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::notify_pending_fanout") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| notify_pending_fanout(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::abandoned_wait"), + 96_000_000, + 1, + abandoned_wait, + ); + runner.case( + concat!(module_path!(), "::repoll_pending"), + 50_000_000, + 1, + repoll_pending, + ); + runner.case( + concat!(module_path!(), "::cancel_pending"), + 24_000_000, + 1, + cancel_pending, + ); +} diff --git a/benchmarks/benches/primitives/condvar/mod.rs b/benchmarks/benches/primitives/condvar/mod.rs index 35540691..a9957bb4 100644 --- a/benchmarks/benches/primitives/condvar/mod.rs +++ b/benchmarks/benches/primitives/condvar/mod.rs @@ -16,3 +16,9 @@ // under the License. mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/condvar/wait.rs b/benchmarks/benches/primitives/condvar/wait.rs index 4434d99b..9bcd33a5 100644 --- a/benchmarks/benches/primitives/condvar/wait.rs +++ b/benchmarks/benches/primitives/condvar/wait.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::condvar::Condvar; use asyncband::mutex::Mutex; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const WAITER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench] fn notify_waiter(bencher: Bencher) { let mut context = bench_context(); @@ -46,7 +46,6 @@ fn notify_waiter(bencher: Bencher) { }); } -#[divan::bench(args = WAITER_COUNTS)] fn notify_waiter_batch(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); @@ -70,7 +69,6 @@ fn notify_waiter_batch(bencher: Bencher, waiter_count: usize) { }); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -87,3 +85,26 @@ fn cancel_pending(bencher: Bencher) { black_box(mutex.try_lock().is_some()) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::notify_waiter"), + 3_000_000, + 1, + notify_waiter, + ); + for &arg in WAITER_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::notify_waiter_batch")), + (3_000_000 / arg).max(1), + 1, + |bencher| notify_waiter_batch(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending"), + 3_000_000, + 1, + cancel_pending, + ); +} diff --git a/benchmarks/benches/primitives/event/mod.rs b/benchmarks/benches/primitives/event/mod.rs index 35540691..a9957bb4 100644 --- a/benchmarks/benches/primitives/event/mod.rs +++ b/benchmarks/benches/primitives/event/mod.rs @@ -16,3 +16,9 @@ // under the License. mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/event/wait.rs b/benchmarks/benches/primitives/event/wait.rs index 95b79420..eab3748b 100644 --- a/benchmarks/benches/primitives/event/wait.rs +++ b/benchmarks/benches/primitives/event/wait.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use std::sync::Arc; use std::task::Context; @@ -22,20 +23,16 @@ use std::task::Wake; use std::task::Waker; use asyncband::event::ManualResetEvent; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use benchmarks::support::defer_input_drop; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; const WAITER_COUNTS: &[usize] = &[1, 4, 16]; const THREAD_COUNTS: &[usize] = &[1, 2, 4, 8]; -const CONTENDED_SAMPLE_SIZE: u32 = 256; // All benchmark threads read the same event without concurrent state transitions. -#[divan::bench(args = [false, true], threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn is_set(bencher: Bencher, is_set: bool) { let event = ManualResetEvent::with_state(is_set); @@ -43,7 +40,6 @@ fn is_set(bencher: Bencher, is_set: bool) { } // Each iteration creates and polls a fresh wait against the same already-set event. -#[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn wait_already_set(bencher: Bencher) { let event = ManualResetEvent::with_state(true); @@ -55,7 +51,6 @@ fn wait_already_set(bencher: Bencher) { }); } -#[divan::bench] fn set_reset_cycle(bencher: Bencher) { let event = ManualResetEvent::new(); @@ -66,7 +61,6 @@ fn set_reset_cycle(bencher: Bencher) { }); } -#[divan::bench] fn reused_waiter_handoff(bencher: Bencher) { let mut context = bench_context(); let event = ManualResetEvent::new(); @@ -83,31 +77,27 @@ fn reused_waiter_handoff(bencher: Bencher) { } // Measures the full fan-out lifecycle, including allocation, registration, set, and completion. -#[divan::bench(args = WAITER_COUNTS)] fn waiter_fan_out(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); - bencher - .counter(ItemsCount::new(waiter_count)) - .bench_local(|| { - let event = ManualResetEvent::new(); - let mut waiters = (0..waiter_count) - .map(|_| Box::pin(event.wait())) - .collect::>(); - for waiter in &mut waiters { - poll_pending(waiter.as_mut(), &mut context); - } + bencher.bench_local(|| { + let event = ManualResetEvent::new(); + let mut waiters = (0..waiter_count) + .map(|_| Box::pin(event.wait())) + .collect::>(); + for waiter in &mut waiters { + poll_pending(waiter.as_mut(), &mut context); + } - event.set(); - for mut waiter in waiters { - poll_pinned_ready(waiter.as_mut(), &mut context); - } - black_box(event) - }); + event.set(); + for mut waiter in waiters { + poll_pinned_ready(waiter.as_mut(), &mut context); + } + black_box(event) + }); } // A fresh event per iteration includes allocating its waiter storage. -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -122,7 +112,6 @@ fn cancel_pending(bencher: Bencher) { } // Reusing the event retains waiter storage capacity across registration/cancellation cycles. -#[divan::bench] fn cancel_pending_reused(bencher: Bencher) { let mut context = bench_context(); let event = ManualResetEvent::new(); @@ -133,8 +122,7 @@ fn cancel_pending_reused(bencher: Bencher) { }); } -// Initial registration is outside the timed loop; every repoll uses the same waker. -#[divan::bench] +// Register once before the repeated polls; every repoll uses the same waker. fn repoll_pending(bencher: Bencher) { let mut context = bench_context(); let event = ManualResetEvent::new(); @@ -144,7 +132,6 @@ fn repoll_pending(bencher: Bencher) { bencher.bench_local(|| poll_pending(wait.as_mut(), &mut context)); } -#[divan::bench] fn replace_pending_waker(bencher: Bencher) { struct Task; #[allow(clippy::manual_noop_waker)] @@ -166,12 +153,10 @@ fn replace_pending_waker(bencher: Bencher) { }); } -// Isolates set/detach/wake from registration and cleanup. The smaller sample size limits setup -// costs for large cohorts; use --sample-count 200 without a time limit for repeated measurements. -#[divan::bench(args = [1, 16, 256], sample_size = 64)] +// Register a fresh cohort, detach it with set, then cancel the completed registrations. fn set_registered(bencher: Bencher, waiter_count: usize) { - bencher - .with_inputs(|| { + bencher.bench_local(|| { + let (event, waiters) = { let event = Arc::new(ManualResetEvent::new()); let mut context = bench_context(); let mut waiters = (0..waiter_count) @@ -181,10 +166,88 @@ fn set_registered(bencher: Bencher, waiter_count: usize) { poll_pending(waiter.as_mut(), &mut context); } (event, waiters) - }) - .bench_local_values(|(event, waiters)| { - // Time cohort detachment and wake callbacks, excluding registration and cleanup. - event.set(); - defer_input_drop((event, waiters), ()) - }); + }; + // Include registration, cohort detachment, wake callbacks, and cleanup in each round. + event.set(); + drop((event, waiters)); + }); +} + +pub fn register(runner: &mut Runner) { + for &arg in &[false, true] { + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/{arg}/threads={threads}", + concat!(module_path!(), "::is_set") + ), + 960_000_000, + threads, + |bencher| is_set(bencher, arg), + ); + } + } + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/threads={threads}", + concat!(module_path!(), "::wait_already_set") + ), + 240_000_000, + threads, + wait_already_set, + ); + } + runner.case( + concat!(module_path!(), "::set_reset_cycle"), + 24_000_000, + 1, + set_reset_cycle, + ); + runner.case( + concat!(module_path!(), "::reused_waiter_handoff"), + 12_000_000, + 1, + reused_waiter_handoff, + ); + for &arg in WAITER_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::waiter_fan_out")), + (5_000_000 / arg).max(1), + 1, + |bencher| waiter_fan_out(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending"), + 6_000_000, + 1, + cancel_pending, + ); + runner.case( + concat!(module_path!(), "::cancel_pending_reused"), + 24_000_000, + 1, + cancel_pending_reused, + ); + runner.case( + concat!(module_path!(), "::repoll_pending"), + 50_000_000, + 1, + repoll_pending, + ); + runner.case( + concat!(module_path!(), "::replace_pending_waker"), + 48_000_000, + 1, + replace_pending_waker, + ); + for &arg in &[1, 16, 256] { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::set_registered")), + (5_000_000 / arg).max(1), + 1, + |bencher| set_registered(bencher, arg), + ); + } } diff --git a/benchmarks/benches/primitives/latch/mod.rs b/benchmarks/benches/primitives/latch/mod.rs index 35540691..a9957bb4 100644 --- a/benchmarks/benches/primitives/latch/mod.rs +++ b/benchmarks/benches/primitives/latch/mod.rs @@ -16,3 +16,9 @@ // under the License. mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/latch/wait.rs b/benchmarks/benches/primitives/latch/wait.rs index 6e5c573f..cf491bb7 100644 --- a/benchmarks/benches/primitives/latch/wait.rs +++ b/benchmarks/benches/primitives/latch/wait.rs @@ -15,18 +15,18 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::latch::Latch; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const WORKER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = WORKER_COUNTS)] fn worker_fan_in(bencher: Bencher, worker_count: usize) { let mut context = bench_context(); @@ -43,7 +43,6 @@ fn worker_fan_in(bencher: Bencher, worker_count: usize) { }); } -#[divan::bench(args = WORKER_COUNTS)] fn waiter_fan_out(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); @@ -64,7 +63,6 @@ fn waiter_fan_out(bencher: Bencher, waiter_count: usize) { }); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); @@ -77,3 +75,28 @@ fn cancel_pending(bencher: Bencher) { black_box(latch) }); } + +pub fn register(runner: &mut Runner) { + for &arg in WORKER_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::worker_fan_in")), + (16_000_000 / arg).max(1), + 1, + |bencher| worker_fan_in(bencher, arg), + ); + } + for &arg in WORKER_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::waiter_fan_out")), + (5_000_000 / arg).max(1), + 1, + |bencher| waiter_fan_out(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending"), + 6_000_000, + 1, + cancel_pending, + ); +} diff --git a/benchmarks/benches/primitives/main.rs b/benchmarks/benches/primitives/main.rs index 6d9f4c32..9ebba1ba 100644 --- a/benchmarks/benches/primitives/main.rs +++ b/benchmarks/benches/primitives/main.rs @@ -15,7 +15,9 @@ // specific language governing permissions and limitations // under the License. -//! Hand-polled operation costs. The waker exercises reference counting without task scheduling. +//! Fixed operation workloads. The waker exercises reference counting without task scheduling. + +use benchmarks::harness::Runner; mod barrier; mod blocking; @@ -39,5 +41,26 @@ mod singleflight; mod waitgroup; fn main() { - divan::main(); + let mut runner = Runner::from_args(); + barrier::register(&mut runner); + blocking::register(&mut runner); + broadcast::register(&mut runner); + completion::register(&mut runner); + condvar::register(&mut runner); + event::register(&mut runner); + latch::register(&mut runner); + mpmc::register(&mut runner); + mpsc::register(&mut runner); + mutex::register(&mut runner); + once::register(&mut runner); + once_map::register(&mut runner); + oneshot::register(&mut runner); + phaser::register(&mut runner); + pool::register(&mut runner); + rwlock::register(&mut runner); + semaphore::register(&mut runner); + shutdown::register(&mut runner); + singleflight::register(&mut runner); + waitgroup::register(&mut runner); + runner.finish(); } diff --git a/benchmarks/benches/primitives/mpmc/bounded.rs b/benchmarks/benches/primitives/mpmc/bounded.rs index 5c3e52ac..62fd2516 100644 --- a/benchmarks/benches/primitives/mpmc/bounded.rs +++ b/benchmarks/benches/primitives/mpmc/bounded.rs @@ -15,19 +15,17 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::mpmc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; -use crate::mpmc::FAST_SAMPLE_SIZE; - -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn try_send_then_try_recv(bencher: Bencher) { let (sender, receiver) = mpmc::bounded(1); bencher.bench_local(|| { @@ -36,7 +34,6 @@ fn try_send_then_try_recv(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn send_then_recv(bencher: Bencher) { let mut context = bench_context(); let (sender, receiver) = mpmc::bounded(1); @@ -46,7 +43,6 @@ fn send_then_recv(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn wake_blocked_sender(bencher: Bencher) { let mut context = bench_context(); let (sender, receiver) = mpmc::bounded(1); @@ -58,3 +54,24 @@ fn wake_blocked_sender(bencher: Bencher) { poll_pinned_ready(send.as_mut(), &mut context).unwrap(); }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::try_send_then_try_recv"), + 30_000_000, + 1, + try_send_then_try_recv, + ); + runner.case( + concat!(module_path!(), "::send_then_recv"), + 24_000_000, + 1, + send_then_recv, + ); + runner.case( + concat!(module_path!(), "::wake_blocked_sender"), + 9_000_000, + 1, + wake_blocked_sender, + ); +} diff --git a/benchmarks/benches/primitives/mpmc/mod.rs b/benchmarks/benches/primitives/mpmc/mod.rs index a00276ab..1a7fea9f 100644 --- a/benchmarks/benches/primitives/mpmc/mod.rs +++ b/benchmarks/benches/primitives/mpmc/mod.rs @@ -18,5 +18,9 @@ mod bounded; mod unbounded; -// Fixed sample sizes keep one-time warm-up work from changing Divan's iteration granularity. -const FAST_SAMPLE_SIZE: u32 = 256; +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/primitives/mpmc/unbounded.rs b/benchmarks/benches/primitives/mpmc/unbounded.rs index 5e71cc56..cf08e258 100644 --- a/benchmarks/benches/primitives/mpmc/unbounded.rs +++ b/benchmarks/benches/primitives/mpmc/unbounded.rs @@ -15,19 +15,17 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::mpmc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; -use crate::mpmc::FAST_SAMPLE_SIZE; - -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn send_then_try_recv(bencher: Bencher) { let (sender, receiver) = mpmc::unbounded(); bencher.bench_local(|| { @@ -36,7 +34,6 @@ fn send_then_try_recv(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn send_then_recv(bencher: Bencher) { let mut context = bench_context(); let (sender, receiver) = mpmc::unbounded(); @@ -46,7 +43,6 @@ fn send_then_recv(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn wake_pending_receiver(bencher: Bencher) { let mut context = bench_context(); let (sender, receiver) = mpmc::unbounded(); @@ -58,7 +54,6 @@ fn wake_pending_receiver(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn repoll_pending_receiver(bencher: Bencher) { let mut context = bench_context(); let (_sender, receiver) = mpmc::unbounded::(); @@ -66,3 +61,30 @@ fn repoll_pending_receiver(bencher: Bencher) { poll_pending(recv.as_mut(), &mut context); bencher.bench_local(|| poll_pending(recv.as_mut(), &mut context)); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::send_then_try_recv"), + 30_000_000, + 1, + send_then_try_recv, + ); + runner.case( + concat!(module_path!(), "::send_then_recv"), + 24_000_000, + 1, + send_then_recv, + ); + runner.case( + concat!(module_path!(), "::wake_pending_receiver"), + 9_000_000, + 1, + wake_pending_receiver, + ); + runner.case( + concat!(module_path!(), "::repoll_pending_receiver"), + 40_000_000, + 1, + repoll_pending_receiver, + ); +} diff --git a/benchmarks/benches/primitives/mpsc/bounded.rs b/benchmarks/benches/primitives/mpsc/bounded.rs index 1b9f47c0..ddbab11a 100644 --- a/benchmarks/benches/primitives/mpsc/bounded.rs +++ b/benchmarks/benches/primitives/mpsc/bounded.rs @@ -15,16 +15,17 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + use asyncband::mpsc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const SENDER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = SENDER_COUNTS)] fn drain_backpressured_senders(bencher: Bencher, sender_count: usize) { let mut context = bench_context(); let (sender, mut receiver) = mpsc::bounded(1); @@ -51,7 +52,6 @@ fn drain_backpressured_senders(bencher: Bencher, sender_count: usize) { }); } -#[divan::bench(args = SENDER_COUNTS)] fn cancel_backpressured_senders(bencher: Bencher, sender_count: usize) { let mut context = bench_context(); let (sender, mut receiver) = mpsc::bounded(1); @@ -74,3 +74,28 @@ fn cancel_backpressured_senders(bencher: Bencher, sender_count: usize) { black_box(receiver.try_recv().unwrap()) }); } + +pub fn register(runner: &mut Runner) { + for &arg in SENDER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::drain_backpressured_senders") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| drain_backpressured_senders(bencher, arg), + ); + } + for &arg in SENDER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::cancel_backpressured_senders") + ), + (8_000_000 / arg).max(1), + 1, + |bencher| cancel_backpressured_senders(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/mpsc/mod.rs b/benchmarks/benches/primitives/mpsc/mod.rs index e0ac8347..1a7fea9f 100644 --- a/benchmarks/benches/primitives/mpsc/mod.rs +++ b/benchmarks/benches/primitives/mpsc/mod.rs @@ -17,3 +17,10 @@ mod bounded; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/primitives/mpsc/unbounded.rs b/benchmarks/benches/primitives/mpsc/unbounded.rs index 65d03a29..db96b44a 100644 --- a/benchmarks/benches/primitives/mpsc/unbounded.rs +++ b/benchmarks/benches/primitives/mpsc/unbounded.rs @@ -15,16 +15,16 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::mpsc; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; -#[divan::bench] fn wake_pending_receiver(bencher: Bencher) { let mut context = bench_context(); let (sender, mut receiver) = mpsc::unbounded(); @@ -37,7 +37,6 @@ fn wake_pending_receiver(bencher: Bencher) { }); } -#[divan::bench] fn reregister_pending_receiver(bencher: Bencher) { let mut context = bench_context(); let (_sender, mut receiver) = mpsc::unbounded::(); @@ -46,3 +45,18 @@ fn reregister_pending_receiver(bencher: Bencher) { bencher.bench_local(|| poll_pending(recv.as_mut(), &mut context)); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::wake_pending_receiver"), + 15_000_000, + 1, + wake_pending_receiver, + ); + runner.case( + concat!(module_path!(), "::reregister_pending_receiver"), + 40_000_000, + 1, + reregister_pending_receiver, + ); +} diff --git a/benchmarks/benches/primitives/mutex/lock.rs b/benchmarks/benches/primitives/mutex/lock.rs index 2c173c20..385eb25e 100644 --- a/benchmarks/benches/primitives/mutex/lock.rs +++ b/benchmarks/benches/primitives/mutex/lock.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::Pin; use std::sync::Arc; use asyncband::mutex::Mutex; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const QUEUE_DEPTHS: &[usize] = &[1, 4, 16]; -#[divan::bench] fn uncontended_reuse(bencher: Bencher) { let mutex = Mutex::new(0usize); let mut context = bench_context(); @@ -40,7 +40,6 @@ fn uncontended_reuse(bencher: Bencher) { }); } -#[divan::bench(args = QUEUE_DEPTHS)] fn queued_handoff_cycle(bencher: Bencher, queue_depth: usize) { let mut context = bench_context(); let mutex = Arc::new(Mutex::new(0usize)); @@ -64,3 +63,23 @@ fn queued_handoff_cycle(bencher: Bencher, queue_depth: usize) { black_box(value) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::uncontended_reuse"), + 24_000_000, + 1, + uncontended_reuse, + ); + for &arg in QUEUE_DEPTHS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::queued_handoff_cycle") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| queued_handoff_cycle(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/mutex/mod.rs b/benchmarks/benches/primitives/mutex/mod.rs index 4701924b..2df0b7a7 100644 --- a/benchmarks/benches/primitives/mutex/mod.rs +++ b/benchmarks/benches/primitives/mutex/mod.rs @@ -16,3 +16,9 @@ // under the License. mod lock; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + lock::register(runner); +} diff --git a/benchmarks/benches/primitives/once/get_or_init.rs b/benchmarks/benches/primitives/once/get_or_init.rs index 2f95ecf1..51714ee8 100644 --- a/benchmarks/benches/primitives/once/get_or_init.rs +++ b/benchmarks/benches/primitives/once/get_or_init.rs @@ -16,19 +16,19 @@ // under the License. use std::cell::Cell; +use std::hint::black_box; use asyncband::once::OnceCell; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; use benchmarks::support::wait_until_open; -use divan::Bencher; -use divan::black_box; const WAITER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench] fn initialize_cell(bencher: Bencher) { let mut context = bench_context(); @@ -42,7 +42,6 @@ fn initialize_cell(bencher: Bencher) { }); } -#[divan::bench(args = WAITER_COUNTS)] fn initialize_cell_waiter_batch(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); @@ -72,3 +71,23 @@ fn initialize_cell_waiter_batch(bencher: Bencher, waiter_count: usize) { } }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::initialize_cell"), + 6_000_000, + 1, + initialize_cell, + ); + for &arg in WAITER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::initialize_cell_waiter_batch") + ), + (4_000_000 / arg).max(1), + 1, + |bencher| initialize_cell_waiter_batch(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/once/mod.rs b/benchmarks/benches/primitives/once/mod.rs index afcd89c6..522a543b 100644 --- a/benchmarks/benches/primitives/once/mod.rs +++ b/benchmarks/benches/primitives/once/mod.rs @@ -17,3 +17,10 @@ mod get_or_init; mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + get_or_init::register(runner); + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/once/wait.rs b/benchmarks/benches/primitives/once/wait.rs index 22ebd0df..0930670d 100644 --- a/benchmarks/benches/primitives/once/wait.rs +++ b/benchmarks/benches/primitives/once/wait.rs @@ -15,19 +15,19 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::once::Once; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const WAITER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench] fn complete_waiter(bencher: Bencher) { let mut context = bench_context(); @@ -42,7 +42,6 @@ fn complete_waiter(bencher: Bencher) { }); } -#[divan::bench(args = WAITER_COUNTS)] fn complete_waiter_batch(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); @@ -63,7 +62,6 @@ fn complete_waiter_batch(bencher: Bencher, waiter_count: usize) { }); } -#[divan::bench] fn cancel_pending_wait(bencher: Bencher) { let mut context = bench_context(); @@ -76,3 +74,29 @@ fn cancel_pending_wait(bencher: Bencher) { black_box(once) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::complete_waiter"), + 3_000_000, + 1, + complete_waiter, + ); + for &arg in WAITER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::complete_waiter_batch") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| complete_waiter_batch(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending_wait"), + 6_000_000, + 1, + cancel_pending_wait, + ); +} diff --git a/benchmarks/benches/primitives/once_map/initialize.rs b/benchmarks/benches/primitives/once_map/initialize.rs index fe378fbb..fd5f39fd 100644 --- a/benchmarks/benches/primitives/once_map/initialize.rs +++ b/benchmarks/benches/primitives/once_map/initialize.rs @@ -16,107 +16,91 @@ // under the License. use std::cell::Cell; +use std::hint::black_box; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use benchmarks::support::defer_input_drop; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; use benchmarks::support::wait_until_open; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; -use crate::once_map::support::BATCH_SAMPLE_SIZE; use crate::once_map::support::BATCH_SIZES; use crate::once_map::support::BenchMap; -use crate::once_map::support::FAST_SAMPLE_SIZE; use crate::once_map::support::READY_ENTRY_COUNTS; use crate::once_map::support::ready_map; const ABSENT_KEY: usize = usize::MAX; -// Measures the complete absent-key path: lookup, pending entry creation, publication, and return. -// Input construction and map destruction stay outside the timed section. -#[divan::bench(args = READY_ENTRY_COUNTS, sample_size = BATCH_SAMPLE_SIZE)] -fn initialize_success(bencher: Bencher, ready_entries: usize) { +// Reuse the ready working set and remove the new entry after each successful initialization. +// Rebuilding the whole map on every round would hide the cost of the absent-key path. +fn initialize_remove_cycle(bencher: Bencher, ready_entries: usize) { + let map = ready_map(ready_entries); let mut context = bench_context(); - bencher - .with_inputs(|| ready_map(ready_entries)) - .bench_local_values(|map| { - let result = black_box(poll_ready( - map.compute(black_box(ABSENT_KEY), || async { black_box(1) }), - &mut context, - )); - defer_input_drop(map, result) - }); + bencher.bench_local(|| { + black_box(poll_ready( + map.compute(black_box(ABSENT_KEY), || async { black_box(1) }), + &mut context, + )); + black_box(map.remove(&ABSENT_KEY)); + }); } // Deterministically polls every caller while the leader is suspended. This measures actual // same-key coalescing rather than relying on OS threads to overlap by chance. -#[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] fn initialize_same_key(bencher: Bencher, caller_count: usize) { let mut context = bench_context(); - bencher - .with_inputs(BenchMap::default) - .counter(ItemsCount::new(caller_count)) - .bench_local_values(|map| { - let gate = Cell::new(false); - let mut calls = (0..caller_count) - .map(|_| { - Box::pin(map.compute(0, || async { - wait_until_open(&gate).await; - black_box(1usize) - })) - }) - .collect::>(); - for call in &mut calls { - poll_pending(call.as_mut(), &mut context); - } - - gate.set(true); - for mut call in calls { - black_box(poll_pinned_ready(call.as_mut(), &mut context)); - } + bencher.bench_local(|| { + let map = BenchMap::default(); + let gate = Cell::new(false); + let mut calls = (0..caller_count) + .map(|_| { + Box::pin(map.compute(0, || async { + wait_until_open(&gate).await; + black_box(1usize) + })) + }) + .collect::>(); + for call in &mut calls { + poll_pending(call.as_mut(), &mut context); + } - defer_input_drop(map, ()) - }); + gate.set(true); + for mut call in calls { + black_box(poll_pinned_ready(call.as_mut(), &mut context)); + } + }); } // Uses the same scheduling scaffold as initialize_same_key, but every caller initializes a // different key. Their difference isolates coalescing from independent cold-key bookkeeping. -#[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] fn initialize_distinct_keys(bencher: Bencher, caller_count: usize) { let mut context = bench_context(); - bencher - .with_inputs(BenchMap::default) - .counter(ItemsCount::new(caller_count)) - .bench_local_values(|map| { - let gate = Cell::new(false); - let mut calls = (0..caller_count) - .map(|key| { - let gate = &gate; - Box::pin(map.compute(key, move || async move { - wait_until_open(gate).await; - black_box(key) - })) - }) - .collect::>(); - for call in &mut calls { - poll_pending(call.as_mut(), &mut context); - } - - gate.set(true); - for mut call in calls { - black_box(poll_pinned_ready(call.as_mut(), &mut context)); - } + bencher.bench_local(|| { + let map = BenchMap::default(); + let gate = Cell::new(false); + let mut calls = (0..caller_count) + .map(|key| { + let gate = &gate; + Box::pin(map.compute(key, move || async move { + wait_until_open(gate).await; + black_box(key) + })) + }) + .collect::>(); + for call in &mut calls { + poll_pending(call.as_mut(), &mut context); + } - defer_input_drop(map, ()) - }); + gate.set(true); + for mut call in calls { + black_box(poll_pinned_ready(call.as_mut(), &mut context)); + } + }); } // Failed initialization leaves the key absent, allowing retries on the same map. -#[divan::bench(args = READY_ENTRY_COUNTS, sample_size = FAST_SAMPLE_SIZE)] fn initialize_error(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); let mut context = bench_context(); @@ -127,3 +111,44 @@ fn initialize_error(bencher: Bencher, ready_entries: usize) { )) }); } + +pub fn register(runner: &mut Runner) { + for &arg in READY_ENTRY_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::initialize_remove_cycle") + ), + 3_000_000, + 1, + |bencher| initialize_remove_cycle(bencher, arg), + ); + } + for &arg in BATCH_SIZES { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::initialize_same_key")), + (3_000_000 / arg).max(1), + 1, + |bencher| initialize_same_key(bencher, arg), + ); + } + for &arg in BATCH_SIZES { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::initialize_distinct_keys") + ), + (2_000_000 / arg).max(1), + 1, + |bencher| initialize_distinct_keys(bencher, arg), + ); + } + for &arg in READY_ENTRY_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::initialize_error")), + 3_000_000, + 1, + |bencher| initialize_error(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/once_map/lookup.rs b/benchmarks/benches/primitives/once_map/lookup.rs index bfef1cca..22b9e8b0 100644 --- a/benchmarks/benches/primitives/once_map/lookup.rs +++ b/benchmarks/benches/primitives/once_map/lookup.rs @@ -15,12 +15,13 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; -use crate::once_map::support::CONTENDED_SAMPLE_SIZE; use crate::once_map::support::HOT_ENTRY_COUNTS; use crate::once_map::support::READY_ENTRY_COUNTS; use crate::once_map::support::THREAD_COUNTS; @@ -28,39 +29,29 @@ use crate::once_map::support::distributed_absent_key; use crate::once_map::support::distributed_ready_key; use crate::once_map::support::ready_map; -#[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn get_hit_same_key(bencher: Bencher) { let map = ready_map(1); bencher.bench(|| black_box(map.get(black_box(&0)))); } -#[divan::bench( - threads = THREAD_COUNTS, - args = HOT_ENTRY_COUNTS, - sample_size = CONTENDED_SAMPLE_SIZE -)] fn get_hit_distributed(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); - bencher - .with_inputs(|| distributed_ready_key(ready_entries)) - .bench_values(|key| black_box(map.get(black_box(&key)))); + bencher.bench(|| { + let key = distributed_ready_key(ready_entries); + black_box(map.get(black_box(&key))) + }); } // Negative get is still a read-only steady-state path: it traverses the ready index but never // creates pending state. Varying absent hashes avoids measuring one unusually cache-hot miss. -#[divan::bench( - threads = THREAD_COUNTS, - args = READY_ENTRY_COUNTS, - sample_size = CONTENDED_SAMPLE_SIZE -)] fn get_miss_distributed(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); - bencher - .with_inputs(|| distributed_absent_key(ready_entries)) - .bench_values(|key| black_box(map.get(black_box(&key)))); + bencher.bench(|| { + let key = distributed_absent_key(ready_entries); + black_box(map.get(black_box(&key))) + }); } -#[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn compute_hit_same_key(bencher: Bencher) { let map = ready_map(1); bencher.bench(|| { @@ -72,20 +63,78 @@ fn compute_hit_same_key(bencher: Bencher) { }); } -#[divan::bench( - threads = THREAD_COUNTS, - args = HOT_ENTRY_COUNTS, - sample_size = CONTENDED_SAMPLE_SIZE -)] fn compute_hit_distributed(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); - bencher - .with_inputs(|| distributed_ready_key(ready_entries)) - .bench_values(|key| { - let mut context = bench_context(); - black_box(poll_ready( - map.compute(black_box(key), || async { unreachable!() }), - &mut context, - )) - }); + bencher.bench(|| { + let key = distributed_ready_key(ready_entries); + let mut context = bench_context(); + black_box(poll_ready( + map.compute(black_box(key), || async { unreachable!() }), + &mut context, + )) + }); +} + +pub fn register(runner: &mut Runner) { + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/threads={threads}", + concat!(module_path!(), "::get_hit_same_key") + ), + 16_000_000, + threads, + get_hit_same_key, + ); + } + for &arg in HOT_ENTRY_COUNTS { + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/{arg}/threads={threads}", + concat!(module_path!(), "::get_hit_distributed") + ), + 8_000_000, + threads, + |bencher| get_hit_distributed(bencher, arg), + ); + } + } + for &arg in READY_ENTRY_COUNTS { + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/{arg}/threads={threads}", + concat!(module_path!(), "::get_miss_distributed") + ), + 16_000_000, + threads, + |bencher| get_miss_distributed(bencher, arg), + ); + } + } + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/threads={threads}", + concat!(module_path!(), "::compute_hit_same_key") + ), + 10_000_000, + threads, + compute_hit_same_key, + ); + } + for &arg in HOT_ENTRY_COUNTS { + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/{arg}/threads={threads}", + concat!(module_path!(), "::compute_hit_distributed") + ), + 4_000_000, + threads, + |bencher| compute_hit_distributed(bencher, arg), + ); + } + } } diff --git a/benchmarks/benches/primitives/once_map/maintenance.rs b/benchmarks/benches/primitives/once_map/maintenance.rs index 39c8e8e1..01fcc047 100644 --- a/benchmarks/benches/primitives/once_map/maintenance.rs +++ b/benchmarks/benches/primitives/once_map/maintenance.rs @@ -15,29 +15,51 @@ // specific language governing permissions and limitations // under the License. -use benchmarks::support::defer_input_drop; -use divan::Bencher; -use divan::black_box; +use std::hint::black_box; + +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; +use benchmarks::support::bench_context; +use benchmarks::support::poll_ready; -use crate::once_map::support::BATCH_SAMPLE_SIZE; -use crate::once_map::support::FAST_SAMPLE_SIZE; use crate::once_map::support::NONEMPTY_ENTRY_COUNTS; use crate::once_map::support::READY_ENTRY_COUNTS; use crate::once_map::support::ready_map; -#[divan::bench(args = NONEMPTY_ENTRY_COUNTS, sample_size = BATCH_SAMPLE_SIZE)] -fn remove_hit(bencher: Bencher, ready_entries: usize) { +// Restore the removed middle entry so every round sees the same populated working set. +fn remove_reinitialize_cycle(bencher: Bencher, ready_entries: usize) { let key = ready_entries / 2; - bencher - .with_inputs(|| ready_map(ready_entries)) - .bench_local_values(|map| { - let removed = black_box(map.remove(black_box(&key))); - defer_input_drop(map, removed) - }); + let map = ready_map(ready_entries); + let mut context = bench_context(); + bencher.bench_local(|| { + black_box(map.remove(black_box(&key))); + black_box(poll_ready(map.compute(key, || async { key }), &mut context)); + }); } -#[divan::bench(args = READY_ENTRY_COUNTS, sample_size = FAST_SAMPLE_SIZE)] fn remove_miss(bencher: Bencher, ready_entries: usize) { let map = ready_map(ready_entries); bencher.bench_local(|| black_box(map.remove(black_box(&usize::MAX)))); } + +pub fn register(runner: &mut Runner) { + for &arg in NONEMPTY_ENTRY_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::remove_reinitialize_cycle") + ), + 3_000_000, + 1, + |bencher| remove_reinitialize_cycle(bencher, arg), + ); + } + for &arg in READY_ENTRY_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::remove_miss")), + 48_000_000, + 1, + |bencher| remove_miss(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/once_map/mod.rs b/benchmarks/benches/primitives/once_map/mod.rs index 865205c9..8d1e418b 100644 --- a/benchmarks/benches/primitives/once_map/mod.rs +++ b/benchmarks/benches/primitives/once_map/mod.rs @@ -22,3 +22,11 @@ mod initialize; mod lookup; mod maintenance; mod support; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + initialize::register(runner); + lookup::register(runner); + maintenance::register(runner); +} diff --git a/benchmarks/benches/primitives/once_map/support.rs b/benchmarks/benches/primitives/once_map/support.rs index 4415f3c1..eac523ce 100644 --- a/benchmarks/benches/primitives/once_map/support.rs +++ b/benchmarks/benches/primitives/once_map/support.rs @@ -24,9 +24,6 @@ use benchmarks::support::poll_ready; use benchmarks::support::thread_slot_ticket; pub const BATCH_SIZES: &[usize] = &[2, 4, 16]; -pub const BATCH_SAMPLE_SIZE: u32 = 64; -pub const CONTENDED_SAMPLE_SIZE: u32 = 256; -pub const FAST_SAMPLE_SIZE: u32 = 256; pub const HOT_ENTRY_COUNTS: &[usize] = &[64, 1024]; pub const NONEMPTY_ENTRY_COUNTS: &[usize] = &[1, 64, 1024]; pub const READY_ENTRY_COUNTS: &[usize] = &[0, 64, 1024]; diff --git a/benchmarks/benches/primitives/oneshot/mod.rs b/benchmarks/benches/primitives/oneshot/mod.rs index 9601ee03..e9a61482 100644 --- a/benchmarks/benches/primitives/oneshot/mod.rs +++ b/benchmarks/benches/primitives/oneshot/mod.rs @@ -16,3 +16,9 @@ // under the License. mod send; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + send::register(runner); +} diff --git a/benchmarks/benches/primitives/oneshot/send.rs b/benchmarks/benches/primitives/oneshot/send.rs index 54cb0c58..38141821 100644 --- a/benchmarks/benches/primitives/oneshot/send.rs +++ b/benchmarks/benches/primitives/oneshot/send.rs @@ -16,16 +16,16 @@ // under the License. use std::future::IntoFuture; +use std::hint::black_box; use std::pin::pin; use asyncband::oneshot::channel; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; -#[divan::bench] fn send_before_poll(bencher: Bencher) { let mut context = bench_context(); @@ -39,7 +39,6 @@ fn send_before_poll(bencher: Bencher) { }); } -#[divan::bench] fn poll_before_send(bencher: Bencher) { let mut context = bench_context(); @@ -53,3 +52,18 @@ fn poll_before_send(bencher: Bencher) { black_box(poll_pinned_ready(receiver.as_mut(), &mut context).unwrap()) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::send_before_poll"), + 15_000_000, + 1, + send_before_poll, + ); + runner.case( + concat!(module_path!(), "::poll_before_send"), + 15_000_000, + 1, + poll_before_send, + ); +} diff --git a/benchmarks/benches/primitives/phaser/mod.rs b/benchmarks/benches/primitives/phaser/mod.rs index 43984508..3483eaf7 100644 --- a/benchmarks/benches/primitives/phaser/mod.rs +++ b/benchmarks/benches/primitives/phaser/mod.rs @@ -15,19 +15,19 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use asyncband::phaser::Phaser; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const PARTICIPANT_COUNTS: &[usize] = &[2, 4, 16]; -#[divan::bench] fn ready_wait(bencher: Bencher) { let mut context = bench_context(); let phaser = Phaser::new(); @@ -37,7 +37,6 @@ fn ready_wait(bencher: Bencher) { bencher.bench_local(|| black_box(poll_ready(phaser.wait(observed), &mut context).unwrap())); } -#[divan::bench(args = PARTICIPANT_COUNTS)] fn arrive_then_wait(bencher: Bencher, parties: usize) { let mut context = bench_context(); let phaser = Phaser::new(); @@ -53,7 +52,6 @@ fn arrive_then_wait(bencher: Bencher, parties: usize) { }); } -#[divan::bench(args = PARTICIPANT_COUNTS)] fn reused_phase_handoff(bencher: Bencher, parties: usize) { let mut context = bench_context(); let phaser = Phaser::new(); @@ -72,7 +70,6 @@ fn reused_phase_handoff(bencher: Bencher, parties: usize) { }); } -#[divan::bench] fn repoll_pending(bencher: Bencher) { let mut context = bench_context(); let phaser = Phaser::new(); @@ -82,7 +79,6 @@ fn repoll_pending(bencher: Bencher) { bencher.bench_local(|| poll_pending(wait.as_mut(), &mut context)); } -#[divan::bench] fn cancel_pending(bencher: Bencher) { let mut context = bench_context(); let phaser = Phaser::new(); @@ -95,7 +91,6 @@ fn cancel_pending(bencher: Bencher) { black_box(phaser); } -#[divan::bench(args = PARTICIPANT_COUNTS)] fn register_batch(bencher: Bencher, parties: usize) { let phaser = Phaser::new(); @@ -105,7 +100,6 @@ fn register_batch(bencher: Bencher, parties: usize) { }); } -#[divan::bench(args = PARTICIPANT_COUNTS)] fn register_individually(bencher: Bencher, parties: usize) { let phaser = Phaser::new(); @@ -116,3 +110,62 @@ fn register_individually(bencher: Bencher, parties: usize) { drop(black_box(participants)); }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::ready_wait"), + 50_000_000, + 1, + ready_wait, + ); + for &arg in PARTICIPANT_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::arrive_then_wait")), + (20_000_000 / arg).max(1), + 1, + |bencher| arrive_then_wait(bencher, arg), + ); + } + for &arg in PARTICIPANT_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::reused_phase_handoff") + ), + (8_000_000 / arg).max(1), + 1, + |bencher| reused_phase_handoff(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::repoll_pending"), + 50_000_000, + 1, + repoll_pending, + ); + runner.case( + concat!(module_path!(), "::cancel_pending"), + 24_000_000, + 1, + cancel_pending, + ); + for &arg in PARTICIPANT_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::register_batch")), + (20_000_000 / arg).max(1), + 1, + |bencher| register_batch(bencher, arg), + ); + } + for &arg in PARTICIPANT_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::register_individually") + ), + (16_000_000 / arg).max(1), + 1, + |bencher| register_individually(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/pool/bounded.rs b/benchmarks/benches/primitives/pool/bounded.rs index 79089906..59e23ec4 100644 --- a/benchmarks/benches/primitives/pool/bounded.rs +++ b/benchmarks/benches/primitives/pool/bounded.rs @@ -16,17 +16,18 @@ // under the License. use std::convert::Infallible; +use std::hint::black_box; use std::pin::pin; use asyncband::pool::ManageObject; use asyncband::pool::ObjectStatus; use asyncband::pool::bounded; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; struct Manager; @@ -47,7 +48,6 @@ impl ManageObject for Manager { } } -#[divan::bench] fn bounded_warm_get_and_return(bencher: Bencher) { let pool = bounded::Pool::new(bounded::PoolConfig::new(1), Manager); let mut context = bench_context(); @@ -60,7 +60,6 @@ fn bounded_warm_get_and_return(bencher: Bencher) { }); } -#[divan::bench] fn bounded_warm_get_and_return_with_spare_capacity(bencher: Bencher) { let pool = bounded::Pool::new(bounded::PoolConfig::new(8), Manager); let mut context = bench_context(); @@ -73,7 +72,6 @@ fn bounded_warm_get_and_return_with_spare_capacity(bencher: Bencher) { }); } -#[divan::bench] fn bounded_contended_handoff(bencher: Bencher) { let pool = bounded::Pool::new(bounded::PoolConfig::new(1), Manager); let mut context = bench_context(); @@ -91,7 +89,6 @@ fn bounded_contended_handoff(bencher: Bencher) { }); } -#[divan::bench] fn cancel_bounded_waiter(bencher: Bencher) { let pool = bounded::Pool::new(bounded::PoolConfig::new(1), Manager); let mut context = bench_context(); @@ -106,3 +103,33 @@ fn cancel_bounded_waiter(bencher: Bencher) { drop(held); }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::bounded_warm_get_and_return"), + 6_000_000, + 1, + bounded_warm_get_and_return, + ); + runner.case( + concat!( + module_path!(), + "::bounded_warm_get_and_return_with_spare_capacity" + ), + 6_000_000, + 1, + bounded_warm_get_and_return_with_spare_capacity, + ); + runner.case( + concat!(module_path!(), "::bounded_contended_handoff"), + 3_000_000, + 1, + bounded_contended_handoff, + ); + runner.case( + concat!(module_path!(), "::cancel_bounded_waiter"), + 3_000_000, + 1, + cancel_bounded_waiter, + ); +} diff --git a/benchmarks/benches/primitives/pool/mod.rs b/benchmarks/benches/primitives/pool/mod.rs index 14071221..f793c714 100644 --- a/benchmarks/benches/primitives/pool/mod.rs +++ b/benchmarks/benches/primitives/pool/mod.rs @@ -19,3 +19,10 @@ mod bounded; mod unbounded; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + bounded::register(runner); + unbounded::register(runner); +} diff --git a/benchmarks/benches/primitives/pool/unbounded.rs b/benchmarks/benches/primitives/pool/unbounded.rs index dd891ccb..7a45378e 100644 --- a/benchmarks/benches/primitives/pool/unbounded.rs +++ b/benchmarks/benches/primitives/pool/unbounded.rs @@ -15,11 +15,12 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + use asyncband::pool::unbounded; -use divan::Bencher; -use divan::black_box; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; -#[divan::bench] fn unbounded_warm_try_get_and_return(bencher: Bencher) { let pool = unbounded::Pool::::never_manage(unbounded::PoolConfig::default()); pool.extend_one(0); @@ -30,3 +31,12 @@ fn unbounded_warm_try_get_and_return(bencher: Bencher) { drop(object); }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::unbounded_warm_try_get_and_return"), + 9_000_000, + 1, + unbounded_warm_try_get_and_return, + ); +} diff --git a/benchmarks/benches/primitives/rwlock/mod.rs b/benchmarks/benches/primitives/rwlock/mod.rs index ef6eca51..c3845927 100644 --- a/benchmarks/benches/primitives/rwlock/mod.rs +++ b/benchmarks/benches/primitives/rwlock/mod.rs @@ -17,3 +17,10 @@ mod read; mod write; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + read::register(runner); + write::register(runner); +} diff --git a/benchmarks/benches/primitives/rwlock/read.rs b/benchmarks/benches/primitives/rwlock/read.rs index b498c0e5..d8032c57 100644 --- a/benchmarks/benches/primitives/rwlock/read.rs +++ b/benchmarks/benches/primitives/rwlock/read.rs @@ -15,13 +15,14 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + use asyncband::rwlock::RwLock; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; -#[divan::bench] fn eight_reads_then_one_write(bencher: Bencher) { const READS_PER_WRITE: usize = 8; @@ -39,7 +40,6 @@ fn eight_reads_then_one_write(bencher: Bencher) { }); } -#[divan::bench] fn read_reuse(bencher: Bencher) { let lock = RwLock::new(0usize); let mut context = bench_context(); @@ -49,3 +49,18 @@ fn read_reuse(bencher: Bencher) { black_box(*guard) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::eight_reads_then_one_write"), + 3_000_000, + 1, + eight_reads_then_one_write, + ); + runner.case( + concat!(module_path!(), "::read_reuse"), + 30_000_000, + 1, + read_reuse, + ); +} diff --git a/benchmarks/benches/primitives/rwlock/write.rs b/benchmarks/benches/primitives/rwlock/write.rs index 92e8e4f9..618b2c2c 100644 --- a/benchmarks/benches/primitives/rwlock/write.rs +++ b/benchmarks/benches/primitives/rwlock/write.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use std::sync::Arc; use asyncband::rwlock::RwLock; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const READER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = READER_COUNTS)] fn readers_then_queued_writer(bencher: Bencher, reader_count: usize) { let mut context = bench_context(); @@ -46,3 +46,17 @@ fn readers_then_queued_writer(bencher: Bencher, reader_count: usize) { black_box(*guard) }); } + +pub fn register(runner: &mut Runner) { + for &arg in READER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::readers_then_queued_writer") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| readers_then_queued_writer(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/semaphore/acquire.rs b/benchmarks/benches/primitives/semaphore/acquire.rs index a61392a8..bde2e202 100644 --- a/benchmarks/benches/primitives/semaphore/acquire.rs +++ b/benchmarks/benches/primitives/semaphore/acquire.rs @@ -15,20 +15,20 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::pin::pin; use std::sync::Arc; use asyncband::semaphore::Semaphore; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const QUEUE_DEPTHS: &[usize] = &[1, 4, 16]; -#[divan::bench] fn handoff_permit(bencher: Bencher) { let mut context = bench_context(); @@ -45,7 +45,6 @@ fn handoff_permit(bencher: Bencher) { }); } -#[divan::bench] fn try_acquire_release(bencher: Bencher) { let semaphore = Semaphore::new(1); @@ -54,7 +53,6 @@ fn try_acquire_release(bencher: Bencher) { }); } -#[divan::bench] fn try_acquire_release_with_spare_permits(bencher: Bencher) { let semaphore = Semaphore::new(8); @@ -63,7 +61,6 @@ fn try_acquire_release_with_spare_permits(bencher: Bencher) { }); } -#[divan::bench] fn owned_try_acquire_release(bencher: Bencher) { let semaphore = Arc::new(Semaphore::new(8)); @@ -77,7 +74,6 @@ fn owned_try_acquire_release(bencher: Bencher) { }); } -#[divan::bench(args = QUEUE_DEPTHS)] fn queued_owned_burst(bencher: Bencher, queue_depth: usize) { const PERMITS: usize = 8; @@ -102,7 +98,6 @@ fn queued_owned_burst(bencher: Bencher, queue_depth: usize) { }); } -#[divan::bench] fn cancel_pending_acquire(bencher: Bencher) { let mut context = bench_context(); @@ -116,7 +111,6 @@ fn cancel_pending_acquire(bencher: Bencher) { }); } -#[divan::bench] fn owned_try_acquire_rejected(bencher: Bencher) { let semaphore = Arc::new(Semaphore::new(8)); let held = semaphore @@ -129,7 +123,6 @@ fn owned_try_acquire_rejected(bencher: Bencher) { drop(held); } -#[divan::bench(args = QUEUE_DEPTHS)] fn cancel_pending_owned_batch(bencher: Bencher, queue_depth: usize) { let semaphore = Arc::new(Semaphore::new(0)); let mut context = bench_context(); @@ -145,3 +138,61 @@ fn cancel_pending_owned_batch(bencher: Bencher, queue_depth: usize) { black_box(semaphore.available_permits()) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::handoff_permit"), + 3_000_000, + 1, + handoff_permit, + ); + runner.case( + concat!(module_path!(), "::try_acquire_release"), + 30_000_000, + 1, + try_acquire_release, + ); + runner.case( + concat!(module_path!(), "::try_acquire_release_with_spare_permits"), + 60_000_000, + 1, + try_acquire_release_with_spare_permits, + ); + runner.case( + concat!(module_path!(), "::owned_try_acquire_release"), + 48_000_000, + 1, + owned_try_acquire_release, + ); + for &arg in QUEUE_DEPTHS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::queued_owned_burst")), + (4_000_000 / arg).max(1), + 1, + |bencher| queued_owned_burst(bencher, arg), + ); + } + runner.case( + concat!(module_path!(), "::cancel_pending_acquire"), + 3_000_000, + 1, + cancel_pending_acquire, + ); + runner.case( + concat!(module_path!(), "::owned_try_acquire_rejected"), + 96_000_000, + 1, + owned_try_acquire_rejected, + ); + for &arg in QUEUE_DEPTHS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::cancel_pending_owned_batch") + ), + (8_000_000 / arg).max(1), + 1, + |bencher| cancel_pending_owned_batch(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/semaphore/mod.rs b/benchmarks/benches/primitives/semaphore/mod.rs index 7a9c960a..9c841ae2 100644 --- a/benchmarks/benches/primitives/semaphore/mod.rs +++ b/benchmarks/benches/primitives/semaphore/mod.rs @@ -17,3 +17,10 @@ mod acquire; mod release; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + acquire::register(runner); + release::register(runner); +} diff --git a/benchmarks/benches/primitives/semaphore/release.rs b/benchmarks/benches/primitives/semaphore/release.rs index db939fa5..b9374966 100644 --- a/benchmarks/benches/primitives/semaphore/release.rs +++ b/benchmarks/benches/primitives/semaphore/release.rs @@ -15,16 +15,15 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::sync::Arc; use asyncband::semaphore::Semaphore; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; -use benchmarks::support::defer_input_drop; use benchmarks::support::poll_pending; -use divan::Bencher; -use divan::black_box; -#[divan::bench] fn fulfill_debt_repeatedly(bencher: Bencher) { const CYCLES: usize = 64; @@ -38,10 +37,9 @@ fn fulfill_debt_repeatedly(bencher: Bencher) { }); } -#[divan::bench(args = [1, 2, 32, 256], sample_size = 64)] fn release_to_waiters(bencher: Bencher, waiter_count: usize) { - bencher - .with_inputs(|| { + bencher.bench_local(|| { + let (semaphore, waiters) = { let semaphore = Arc::new(Semaphore::new(0)); let mut context = bench_context(); let mut waiters = (0..waiter_count) @@ -51,11 +49,26 @@ fn release_to_waiters(bencher: Bencher, waiter_count: usize) { poll_pending(waiter.as_mut(), &mut context); } (semaphore, waiters) - }) - .bench_local_values(|(semaphore, waiters)| { - // Only release and wake callbacks are timed; registration and future cleanup are - // not. - semaphore.release(black_box(waiter_count)); - defer_input_drop((semaphore, waiters), ()) - }); + }; + // Include registration, release, wake callbacks, and cancellation in each round. + semaphore.release(black_box(waiter_count)); + drop((semaphore, waiters)); + }); +} + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::fulfill_debt_repeatedly"), + 300_000, + 1, + fulfill_debt_repeatedly, + ); + for &arg in &[1, 2, 32, 256] { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::release_to_waiters")), + (4_000_000 / arg).max(1), + 1, + |bencher| release_to_waiters(bencher, arg), + ); + } } diff --git a/benchmarks/benches/primitives/shutdown/mod.rs b/benchmarks/benches/primitives/shutdown/mod.rs index d6954503..abc5da37 100644 --- a/benchmarks/benches/primitives/shutdown/mod.rs +++ b/benchmarks/benches/primitives/shutdown/mod.rs @@ -16,3 +16,9 @@ // under the License. mod request_shutdown; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + request_shutdown::register(runner); +} diff --git a/benchmarks/benches/primitives/shutdown/request_shutdown.rs b/benchmarks/benches/primitives/shutdown/request_shutdown.rs index 30d53c6f..e52f29b2 100644 --- a/benchmarks/benches/primitives/shutdown/request_shutdown.rs +++ b/benchmarks/benches/primitives/shutdown/request_shutdown.rs @@ -15,17 +15,18 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; + use asyncband::shutdown; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; -use divan::Bencher; -use divan::black_box; const RECEIVER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = RECEIVER_COUNTS)] fn signal_and_join(bencher: Bencher, receiver_count: usize) { let mut context = bench_context(); @@ -53,3 +54,14 @@ fn signal_and_join(bencher: Bencher, receiver_count: usize) { black_box(()) }); } + +pub fn register(runner: &mut Runner) { + for &arg in RECEIVER_COUNTS { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::signal_and_join")), + (3_000_000 / arg).max(1), + 1, + |bencher| signal_and_join(bencher, arg), + ); + } +} diff --git a/benchmarks/benches/primitives/singleflight/coalescing.rs b/benchmarks/benches/primitives/singleflight/coalescing.rs index 4b555de3..b658ff9c 100644 --- a/benchmarks/benches/primitives/singleflight/coalescing.rs +++ b/benchmarks/benches/primitives/singleflight/coalescing.rs @@ -16,78 +16,93 @@ // under the License. use std::cell::Cell; +use std::hint::black_box; use std::pin::pin; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::wait_until_open; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; -use crate::singleflight::support::BATCH_SAMPLE_SIZE; use crate::singleflight::support::BATCH_SIZES; use crate::singleflight::support::BenchGroup; // Measures the complete successful fan-in: one leader is suspended, every duplicate joins it, and // all callers receive the cloned result after the leader completes. -#[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] fn complete_coalesced(bencher: Bencher, caller_count: usize) { let group = BenchGroup::default(); let mut context = bench_context(); - bencher - .counter(ItemsCount::new(caller_count)) - .bench_local(|| { - let gate = Cell::new(false); - let mut leader = pin!(group.work(0, || async { - wait_until_open(&gate).await; - black_box(1usize) - })); - poll_pending(leader.as_mut(), &mut context); + bencher.bench_local(|| { + let gate = Cell::new(false); + let mut leader = pin!(group.work(0, || async { + wait_until_open(&gate).await; + black_box(1usize) + })); + poll_pending(leader.as_mut(), &mut context); - let mut duplicates = (1..caller_count) - .map(|_| Box::pin(group.work(0, || async { unreachable!() }))) - .collect::>(); - for duplicate in &mut duplicates { - poll_pending(duplicate.as_mut(), &mut context); - } + let mut duplicates = (1..caller_count) + .map(|_| Box::pin(group.work(0, || async { unreachable!() }))) + .collect::>(); + for duplicate in &mut duplicates { + poll_pending(duplicate.as_mut(), &mut context); + } - gate.set(true); - black_box(poll_pinned_ready(leader.as_mut(), &mut context)); - for mut duplicate in duplicates { - black_box(poll_pinned_ready(duplicate.as_mut(), &mut context)); - } - }); + gate.set(true); + black_box(poll_pinned_ready(leader.as_mut(), &mut context)); + for mut duplicate in duplicates { + black_box(poll_pinned_ready(duplicate.as_mut(), &mut context)); + } + }); } // The first leader fails; each waiter then retries with an immediately ready initializer. // Those retries complete sequentially, so they do not coalesce with each other. -#[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] fn sequential_retries_after_leader_error(bencher: Bencher, caller_count: usize) { let group = BenchGroup::default(); let mut context = bench_context(); - bencher - .counter(ItemsCount::new(caller_count)) - .bench_local(|| { - let gate = Cell::new(false); - let mut leader = pin!(group.try_work(0, || async { - wait_until_open(&gate).await; - Err::(()) - })); - poll_pending(leader.as_mut(), &mut context); + bencher.bench_local(|| { + let gate = Cell::new(false); + let mut leader = pin!(group.try_work(0, || async { + wait_until_open(&gate).await; + Err::(()) + })); + poll_pending(leader.as_mut(), &mut context); - let mut retries = (1..caller_count) - .map(|_| Box::pin(group.try_work(0, || async { Ok::(1) }))) - .collect::>(); - for retry in &mut retries { - poll_pending(retry.as_mut(), &mut context); - } + let mut retries = (1..caller_count) + .map(|_| Box::pin(group.try_work(0, || async { Ok::(1) }))) + .collect::>(); + for retry in &mut retries { + poll_pending(retry.as_mut(), &mut context); + } - gate.set(true); - let _ = black_box(poll_pinned_ready(leader.as_mut(), &mut context)); - for mut retry in retries { - let _ = black_box(poll_pinned_ready(retry.as_mut(), &mut context)); - } - }); + gate.set(true); + let _ = black_box(poll_pinned_ready(leader.as_mut(), &mut context)); + for mut retry in retries { + let _ = black_box(poll_pinned_ready(retry.as_mut(), &mut context)); + } + }); +} + +pub fn register(runner: &mut Runner) { + for &arg in BATCH_SIZES { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::complete_coalesced")), + (3_000_000 / arg).max(1), + 1, + |bencher| complete_coalesced(bencher, arg), + ); + } + for &arg in BATCH_SIZES { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::sequential_retries_after_leader_error") + ), + (3_000_000 / arg).max(1), + 1, + |bencher| sequential_retries_after_leader_error(bencher, arg), + ); + } } diff --git a/benchmarks/benches/primitives/singleflight/leaders.rs b/benchmarks/benches/primitives/singleflight/leaders.rs index ca1675ad..fe4a2e3f 100644 --- a/benchmarks/benches/primitives/singleflight/leaders.rs +++ b/benchmarks/benches/primitives/singleflight/leaders.rs @@ -16,26 +16,22 @@ // under the License. use std::cell::Cell; +use std::hint::black_box; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; use benchmarks::support::poll_ready; use benchmarks::support::wait_until_open; -use divan::Bencher; -use divan::black_box; -use divan::counter::ItemsCount; -use crate::singleflight::support::BATCH_SAMPLE_SIZE; use crate::singleflight::support::BATCH_SIZES; use crate::singleflight::support::BenchGroup; -use crate::singleflight::support::CONTENDED_SAMPLE_SIZE; -use crate::singleflight::support::FAST_SAMPLE_SIZE; use crate::singleflight::support::THREAD_COUNTS; use crate::singleflight::support::unique_thread_key; // Reuse the group so table capacity survives completed calls. -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn leader_success(bencher: Bencher) { let group = BenchGroup::default(); let mut context = bench_context(); @@ -48,39 +44,36 @@ fn leader_success(bencher: Bencher) { } // Suspend all leaders so their distinct-key entries coexist. -#[divan::bench(args = BATCH_SIZES, sample_size = BATCH_SAMPLE_SIZE)] fn distinct_keys(bencher: Bencher, caller_count: usize) { let group = BenchGroup::default(); let mut context = bench_context(); - bencher - .counter(ItemsCount::new(caller_count)) - .bench_local(|| { - let gate = Cell::new(false); - let mut calls = (0..caller_count) - .map(|key| { - let gate = &gate; - Box::pin(group.work(key, move || async move { - wait_until_open(gate).await; - black_box(key) - })) - }) - .collect::>(); - for call in &mut calls { - poll_pending(call.as_mut(), &mut context); - } + bencher.bench_local(|| { + let gate = Cell::new(false); + let mut calls = (0..caller_count) + .map(|key| { + let gate = &gate; + Box::pin(group.work(key, move || async move { + wait_until_open(gate).await; + black_box(key) + })) + }) + .collect::>(); + for call in &mut calls { + poll_pending(call.as_mut(), &mut context); + } - gate.set(true); - for mut call in calls { - black_box(poll_pinned_ready(call.as_mut(), &mut context)); - } - }); + gate.set(true); + for mut call in calls { + black_box(poll_pinned_ready(call.as_mut(), &mut context)); + } + }); } // Unique keys isolate shared-table contention from same-key coalescing. -#[divan::bench(threads = THREAD_COUNTS, sample_size = CONTENDED_SAMPLE_SIZE)] fn distributed_leaders(bencher: Bencher) { let group = BenchGroup::default(); - bencher.with_inputs(unique_thread_key).bench_values(|key| { + bencher.bench(|| { + let key = unique_thread_key(); let mut context = bench_context(); black_box(poll_ready( group.work(black_box(key), || async move { black_box(key) }), @@ -89,7 +82,6 @@ fn distributed_leaders(bencher: Bencher) { }); } -#[divan::bench(sample_size = FAST_SAMPLE_SIZE)] fn leader_error(bencher: Bencher) { let group = BenchGroup::default(); let mut context = bench_context(); @@ -100,3 +92,37 @@ fn leader_error(bencher: Bencher) { )) }); } + +pub fn register(runner: &mut Runner) { + runner.case( + concat!(module_path!(), "::leader_success"), + 3_000_000, + 1, + leader_success, + ); + for &arg in BATCH_SIZES { + runner.case( + format!("{}/{arg}", concat!(module_path!(), "::distinct_keys")), + (2_000_000 / arg).max(1), + 1, + |bencher| distinct_keys(bencher, arg), + ); + } + for &threads in THREAD_COUNTS { + runner.case( + format!( + "{}/threads={threads}", + concat!(module_path!(), "::distributed_leaders") + ), + 1_000_000, + threads, + distributed_leaders, + ); + } + runner.case( + concat!(module_path!(), "::leader_error"), + 3_000_000, + 1, + leader_error, + ); +} diff --git a/benchmarks/benches/primitives/singleflight/mod.rs b/benchmarks/benches/primitives/singleflight/mod.rs index 36c9a594..1ec32049 100644 --- a/benchmarks/benches/primitives/singleflight/mod.rs +++ b/benchmarks/benches/primitives/singleflight/mod.rs @@ -18,3 +18,10 @@ mod coalescing; mod leaders; mod support; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + coalescing::register(runner); + leaders::register(runner); +} diff --git a/benchmarks/benches/primitives/singleflight/support.rs b/benchmarks/benches/primitives/singleflight/support.rs index 69ad84d6..57bff5ed 100644 --- a/benchmarks/benches/primitives/singleflight/support.rs +++ b/benchmarks/benches/primitives/singleflight/support.rs @@ -22,9 +22,6 @@ use asyncband::singleflight::Group; use benchmarks::support::thread_slot_ticket; pub const BATCH_SIZES: &[usize] = &[2, 4, 16]; -pub const BATCH_SAMPLE_SIZE: u32 = 64; -pub const CONTENDED_SAMPLE_SIZE: u32 = 64; -pub const FAST_SAMPLE_SIZE: u32 = 256; pub const THREAD_COUNTS: &[usize] = &[1, 2, 4, 8]; pub type BenchGroup = Group>; diff --git a/benchmarks/benches/primitives/waitgroup/mod.rs b/benchmarks/benches/primitives/waitgroup/mod.rs index 35540691..a9957bb4 100644 --- a/benchmarks/benches/primitives/waitgroup/mod.rs +++ b/benchmarks/benches/primitives/waitgroup/mod.rs @@ -16,3 +16,9 @@ // under the License. mod wait; + +use benchmarks::harness::Runner; + +pub fn register(runner: &mut Runner) { + wait::register(runner); +} diff --git a/benchmarks/benches/primitives/waitgroup/wait.rs b/benchmarks/benches/primitives/waitgroup/wait.rs index 0a13cd0b..95d5c7f4 100644 --- a/benchmarks/benches/primitives/waitgroup/wait.rs +++ b/benchmarks/benches/primitives/waitgroup/wait.rs @@ -16,17 +16,17 @@ // under the License. use std::future::IntoFuture; +use std::hint::black_box; use asyncband::waitgroup::WaitGroup; +use benchmarks::harness::Bencher; +use benchmarks::harness::Runner; use benchmarks::support::bench_context; use benchmarks::support::poll_pending; use benchmarks::support::poll_pinned_ready; -use divan::Bencher; -use divan::black_box; const WAITER_COUNTS: &[usize] = &[1, 4, 16]; -#[divan::bench(args = WAITER_COUNTS)] fn complete_waiter_batch(bencher: Bencher, waiter_count: usize) { let mut context = bench_context(); @@ -48,3 +48,17 @@ fn complete_waiter_batch(bencher: Bencher, waiter_count: usize) { black_box(()) }); } + +pub fn register(runner: &mut Runner) { + for &arg in WAITER_COUNTS { + runner.case( + format!( + "{}/{arg}", + concat!(module_path!(), "::complete_waiter_batch") + ), + (5_000_000 / arg).max(1), + 1, + |bencher| complete_waiter_batch(bencher, arg), + ); + } +} diff --git a/benchmarks/src/channels/mpmc.rs b/benchmarks/src/channels/mpmc.rs index 8348058d..ec32759a 100644 --- a/benchmarks/src/channels/mpmc.rs +++ b/benchmarks/src/channels/mpmc.rs @@ -16,12 +16,12 @@ // under the License. use std::fmt; +use std::hint::black_box; use std::sync::Arc; use std::sync::Barrier; use std::thread; use std::thread::JoinHandle; -use divan::black_box; use tokio::runtime::Runtime; use tokio::task::JoinSet; diff --git a/benchmarks/src/channels/spmc.rs b/benchmarks/src/channels/spmc.rs index efdc7d29..24027052 100644 --- a/benchmarks/src/channels/spmc.rs +++ b/benchmarks/src/channels/spmc.rs @@ -15,9 +15,9 @@ // specific language governing permissions and limitations // under the License. +use std::hint::black_box; use std::sync::Arc; -use divan::black_box; use tokio::runtime::Runtime; use tokio::sync::Barrier; use tokio::task::JoinSet; diff --git a/benchmarks/src/harness.rs b/benchmarks/src/harness.rs new file mode 100644 index 00000000..1860b7e1 --- /dev/null +++ b/benchmarks/src/harness.rs @@ -0,0 +1,226 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::hint::black_box; +use std::num::NonZeroUsize; +use std::sync::Barrier; +use std::thread; +use std::time::Instant; + +use clap::Parser; + +#[derive(Parser)] +#[command(about = "Time fixed workloads in milliseconds.")] +struct Options { + /// Case name substrings; a case must contain all of them. + filters: Vec, + + /// List matching cases and their work sizes without running them. + #[arg(long, conflicts_with = "test")] + list: bool, + + /// Exercise each matching case once per worker, without measuring performance. + #[arg(long)] + test: bool, + + /// Run each workload this many times, printing every elapsed time. + #[arg(long, default_value = "1")] + repeat: NonZeroUsize, + + /// Multiply the checked-in work sizes by this factor. + #[arg(long, default_value = "1", conflicts_with = "rounds")] + scale: NonZeroUsize, + + /// Override the total rounds, for sizing a filtered workload. + #[arg(long)] + rounds: Option, + + // Cargo passes this flag to harness-free benchmark executables. + #[arg(long = "bench", hide = true)] + _bench: bool, +} + +pub struct Runner { + options: Options, + matched: usize, +} + +impl Runner { + pub fn from_args() -> Self { + Self { + options: Options::parse(), + matched: 0, + } + } + + pub fn case( + &mut self, + name: impl AsRef, + rounds: usize, + threads: usize, + workload: impl Fn(Bencher), + ) { + let name = name.as_ref(); + if !self + .options + .filters + .iter() + .all(|filter| name.contains(filter)) + { + return; + } + self.matched += 1; + assert!(rounds > 0 && threads > 0); + let rounds = if self.options.test { + threads + } else if let Some(rounds) = self.options.rounds { + rounds.get() + } else { + rounds + .checked_mul(self.options.scale.get()) + .expect("scaled workload is too large") + }; + assert!(rounds >= threads, "use at least one round per worker"); + if self.options.list { + println!("{rounds:>10} rounds {name}"); + return; + } + if self.options.test { + workload(Bencher { rounds, threads }); + println!("ok {name}"); + return; + } + for repetition in 1..=self.options.repeat.get() { + // Time the whole workload, including fixture setup, worker startup and cleanup. + // No per-operation clocks, overhead subtraction, or adaptive iteration counts. + let start = Instant::now(); + workload(Bencher { rounds, threads }); + let elapsed = start.elapsed(); + println!( + "{:>6} ms {rounds:>10} rounds {name} [{repetition}/{}]", + elapsed.as_millis(), + self.options.repeat, + ); + } + } + + pub fn finish(self) { + if self.matched == 0 { + eprintln!( + "No matching cases. Use --list and select a target with cargo x bench --bench." + ); + std::process::exit(1); + } + } +} + +/// Executes a fixed total number of rounds; the runner times the enclosing workload. +pub struct Bencher { + rounds: usize, + threads: usize, +} + +impl Bencher { + pub fn bench_local(self, mut operation: impl FnMut() -> O) { + assert_eq!(self.threads, 1); + for _ in 0..self.rounds { + black_box(operation()); + } + } + + pub fn bench(self, operation: impl Fn() -> O + Sync) { + if self.threads == 1 { + self.bench_local(operation); + return; + } + let start = Barrier::new(self.threads); + thread::scope(|scope| { + for worker in 0..self.threads { + let operation = &operation; + let start = &start; + // Keep total work constant across thread counts, including uneven divisions. + let rounds = + self.rounds / self.threads + usize::from(worker < self.rounds % self.threads); + scope.spawn(move || { + start.wait(); + for _ in 0..rounds { + black_box(operation()); + } + }); + } + }); + } +} + +#[cfg(test)] +mod tests { + use std::cell::Cell; + use std::collections::HashMap; + use std::sync::Mutex; + + use super::*; + + #[test] + fn repeated_workloads_recreate_state_and_finish_cleanup() { + let options = Options::try_parse_from(["bench", "--repeat", "3", "--rounds", "7"]).unwrap(); + let live = Cell::new(0); + let completed = Cell::new(0); + struct Output<'a>(&'a Cell); + impl Drop for Output<'_> { + fn drop(&mut self) { + self.0.set(self.0.get() - 1); + } + } + let mut runner = Runner { + options, + matched: 0, + }; + runner.case("lifecycle", 100, 1, |bencher| { + assert_eq!(live.get(), 0); + let mut rounds = 0; + bencher.bench_local(|| { + assert_eq!(live.get(), 0); + live.set(1); + rounds += 1; + Output(&live) + }); + assert_eq!(rounds, 7); + assert_eq!(live.get(), 0); + completed.set(completed.get() + 1); + }); + assert_eq!(completed.get(), 3); + } + + #[test] + fn parallel_rounds_share_state_and_preserve_total_work() { + let workers = Mutex::new(HashMap::new()); + Bencher { + rounds: 19, + threads: 4, + } + .bench(|| { + *workers + .lock() + .unwrap() + .entry(thread::current().id()) + .or_insert(0) += 1; + }); + let mut counts: Vec<_> = workers.into_inner().unwrap().into_values().collect(); + counts.sort_unstable(); + assert_eq!(counts, [4, 5, 5, 5]); + } +} diff --git a/benchmarks/src/lib.rs b/benchmarks/src/lib.rs index e0cbd359..1631654a 100644 --- a/benchmarks/src/lib.rs +++ b/benchmarks/src/lib.rs @@ -16,4 +16,5 @@ // under the License. pub mod channels; +pub mod harness; pub mod support; diff --git a/benchmarks/src/support.rs b/benchmarks/src/support.rs index 8fd29a45..c99497b6 100644 --- a/benchmarks/src/support.rs +++ b/benchmarks/src/support.rs @@ -82,12 +82,6 @@ pub fn thread_slot_ticket() -> (usize, usize) { (slot, ticket) } -// Move the input into the benchmark output so Divan drops it outside the timed section. -#[inline] -pub fn defer_input_drop(input: I, output: O) -> (I, O) { - (input, output) -} - pub async fn wait_until_open(gate: &Cell) { poll_fn(|_| { if gate.get() { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 08f0f058..841b7090 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -78,7 +78,7 @@ struct CommandBench { #[arg( last = true, conflicts_with = "no_run", - help = "Arguments passed to Divan." + help = "Arguments passed to the fixed-workload benchmark runner (see -- --help)." )] args: Vec, }