diff --git a/Cargo.lock b/Cargo.lock index 1c504f16..ac5329ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,11 +621,20 @@ dependencies = [ "codspeed-divan-compat-walltime", ] +[[package]] +name = "codspeed-divan-compat-examples" +version = "0.0.0" +dependencies = [ + "codspeed-divan-compat", + "fastrand 2.3.0", +] + [[package]] name = "codspeed-divan-compat-macros" version = "2.8.0-alpha.0" dependencies = [ "divan-macros", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -798,6 +807,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -1026,6 +1041,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" @@ -1044,6 +1065,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.13" @@ -1432,6 +1463,15 @@ dependencies = [ "termtree", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1830,6 +1870,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.40" @@ -2166,6 +2223,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 4633e963..f5b9cd50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,14 @@ [workspace] members = [ - "crates/codspeed", - "crates/bencher_compat", - "crates/criterion_compat", - "crates/cargo-codspeed", - "crates/divan_compat", - "crates/divan_compat/macros", - "crates/divan_compat/divan_fork", + "crates/codspeed", + "crates/bencher_compat", + "crates/criterion_compat", + "crates/cargo-codspeed", + + "crates/divan_compat", + "crates/divan_compat/macros", + "crates/divan_compat/divan_fork", + "crates/divan_compat/examples", ] resolver = "2" diff --git a/crates/divan_compat/Cargo.toml b/crates/divan_compat/Cargo.toml index 93f191f1..0c1b8868 100644 --- a/crates/divan_compat/Cargo.toml +++ b/crates/divan_compat/Cargo.toml @@ -11,9 +11,9 @@ repository = "https://github.com/CodSpeedHQ/codspeed-rust" homepage = "https://codspeed.io" license = "MIT OR Apache-2.0" categories = [ - "development-tools", - "development-tools::profiling", - "development-tools::testing", + "development-tools", + "development-tools::profiling", + "development-tools::testing", ] keywords = ["codspeed", "benchmark", "divan"] diff --git a/crates/divan_compat/benches/basic_example.rs b/crates/divan_compat/benches/basic_example.rs index 77822dba..f74c3dc1 100644 --- a/crates/divan_compat/benches/basic_example.rs +++ b/crates/divan_compat/benches/basic_example.rs @@ -1,5 +1,3 @@ -use codspeed_divan_compat as divan; - fn fibo(n: i32) -> i32 { let mut a = 0; let mut b = 1; @@ -13,17 +11,16 @@ fn fibo(n: i32) -> i32 { a } -#[divan::bench] +#[codspeed_divan_compat::bench] fn fibo_500() -> i32 { - divan::black_box(fibo(500)) + codspeed_divan_compat::black_box(fibo(500)) } -#[divan::bench] -fn fibo_100() -> i32 { - divan::black_box(fibo(10)) +#[codspeed_divan_compat::bench] +fn fibo_10() -> i32 { + codspeed_divan_compat::black_box(fibo(10)) } fn main() { - // Run `add` benchmark: - divan::main(); + codspeed_divan_compat::main(); } diff --git a/crates/divan_compat/divan_fork/src/divan.rs b/crates/divan_compat/divan_fork/src/divan.rs index c6007b20..291288fb 100644 --- a/crates/divan_compat/divan_fork/src/divan.rs +++ b/crates/divan_compat/divan_fork/src/divan.rs @@ -311,6 +311,9 @@ impl Divan { if should_compute_stats { let stats = bench_context.compute_stats(); { + // WARNING: Keep in sync with `codspeed-divan-compat::uri::generate` + // Not worth doing the work of actually using the same code since this fork + // is temporary let name = bench_entry.display_name().to_string(); let file = bench_entry.meta().location.file; let mut module_path = bench_entry diff --git a/crates/divan_compat/examples/Cargo.toml b/crates/divan_compat/examples/Cargo.toml new file mode 100644 index 00000000..39931093 --- /dev/null +++ b/crates/divan_compat/examples/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "codspeed-divan-compat-examples" +version = "0.0.0" +edition = "2021" +description = "Examples for Divan, a comfy benchmarking framework." +publish = false +license = "MIT OR Apache-2.0" + +[dependencies] +fastrand = "2.3.0" +divan = { package = "codspeed-divan-compat", path = ".." } + +[[bench]] +name = "math" +harness = false + +[[bench]] +name = "sort" +harness = false + +[[bench]] +name = "time" +harness = false diff --git a/crates/divan_compat/examples/README.md b/crates/divan_compat/examples/README.md new file mode 100644 index 00000000..d615c5e9 --- /dev/null +++ b/crates/divan_compat/examples/README.md @@ -0,0 +1,3 @@ +# Example benchmarks + +Mostly copied from [divan's examples](https://github.com/nvzqz/divan/tree/main/examples). diff --git a/crates/divan_compat/examples/benches/math.rs b/crates/divan_compat/examples/benches/math.rs new file mode 100644 index 00000000..6015e9d8 --- /dev/null +++ b/crates/divan_compat/examples/benches/math.rs @@ -0,0 +1,116 @@ +//! Copied from divan's exmaples +use divan::black_box; +use std::collections::{BTreeMap, HashMap}; + +fn main() { + divan::main(); +} + +#[divan::bench] +fn add() -> i32 { + black_box(2) + black_box(1) +} + +#[divan::bench] +#[ignore] +fn sub() -> i32 { + black_box(2) - black_box(1) +} + +#[divan::bench] +fn mul() -> i32 { + black_box(2) * black_box(1) +} + +#[divan::bench] +fn div() -> i32 { + black_box(2) / black_box(1) +} + +#[divan::bench] +fn rem() -> i32 { + black_box(2) % black_box(1) +} + +// 1, 1, 2, 3, 5, ... +mod fibonacci { + use super::*; + + const VALUES: &[u64] = &[0, 5, 10, 20, 30]; + + // O(n) + #[divan::bench(args = VALUES)] + fn iterative(n: u64) -> u64 { + let mut previous = 1; + let mut current = 1; + + for _ in 2..=n { + let next = previous + current; + previous = current; + current = next; + } + + current + } + + // O(2^n) + #[divan::bench(args = VALUES, max_time = 1)] + fn recursive(n: u64) -> u64 { + if n <= 1 { + 1 + } else { + recursive(n - 2) + recursive(n - 1) + } + } + + #[allow(dead_code)] + trait Map: Default { + fn get(&self, key: u64) -> Option; + fn set(&mut self, key: u64, value: u64); + } + + impl Map for HashMap { + fn get(&self, key: u64) -> Option { + self.get(&key).copied() + } + + fn set(&mut self, key: u64, value: u64) { + self.insert(key, value); + } + } + + impl Map for BTreeMap { + fn get(&self, key: u64) -> Option { + self.get(&key).copied() + } + + fn set(&mut self, key: u64, value: u64) { + self.insert(key, value); + } + } + + // Will be ignored in instrumented mode as we do not support type generics yet + // O(n) + #[cfg(not(codspeed))] + #[divan::bench( + types = [BTreeMap, HashMap], + args = VALUES, + )] + fn recursive_memoized(n: u64) -> u64 { + fn fibonacci(n: u64, cache: &mut M) -> u64 { + if let Some(result) = cache.get(n) { + return result; + } + + if n <= 1 { + return 1; + } + + let result = fibonacci(n - 2, cache) + fibonacci(n - 1, cache); + cache.set(n, result); + result + } + + fibonacci(n, &mut M::default()) + } +} diff --git a/crates/divan_compat/examples/benches/sort.rs b/crates/divan_compat/examples/benches/sort.rs new file mode 100644 index 00000000..6c832147 --- /dev/null +++ b/crates/divan_compat/examples/benches/sort.rs @@ -0,0 +1,64 @@ +//! Copied from divan's exmaples +use divan::{AllocProfiler, Bencher}; + +#[global_allocator] +static ALLOC: AllocProfiler = AllocProfiler::system(); + +fn main() { + divan::main(); +} + +/// Functions that generate deterministic values. +mod gen { + pub const LEN: usize = 100_000; + + pub fn rand_int_generator() -> impl FnMut() -> i32 { + let mut rng = fastrand::Rng::with_seed(42); + move || rng.i32(..) + } + + pub fn rand_int_vec_generator() -> impl FnMut() -> Vec { + let mut rand_int_generator = rand_int_generator(); + move || (0..LEN).map(|_| rand_int_generator()).collect() + } + + pub fn sorted_int_vec_generator() -> impl FnMut() -> Vec { + move || (0..LEN).map(|i| i as i32).collect() + } +} + +mod random { + use super::*; + + #[divan::bench] + fn sort(bencher: Bencher) { + bencher + .with_inputs(gen::rand_int_vec_generator()) + .bench_local_refs(|v| v.sort()); + } + + #[divan::bench] + fn sort_unstable(bencher: Bencher) { + bencher + .with_inputs(gen::rand_int_vec_generator()) + .bench_local_refs(|v| v.sort_unstable()); + } +} + +mod sorted { + use super::*; + + #[divan::bench] + fn sort(bencher: Bencher) { + bencher + .with_inputs(gen::sorted_int_vec_generator()) + .bench_local_refs(|v| v.sort()); + } + + #[divan::bench] + fn sort_unstable(bencher: Bencher) { + bencher + .with_inputs(gen::sorted_int_vec_generator()) + .bench_local_refs(|v| v.sort_unstable()); + } +} diff --git a/crates/divan_compat/examples/benches/time.rs b/crates/divan_compat/examples/benches/time.rs new file mode 100644 index 00000000..122c58e7 --- /dev/null +++ b/crates/divan_compat/examples/benches/time.rs @@ -0,0 +1,98 @@ +//! Copied from divan's exmaples +use std::time::{Instant, SystemTime}; + +use divan::{AllocProfiler, Bencher}; + +#[global_allocator] +static ALLOC: AllocProfiler = AllocProfiler::system(); + +fn main() { + divan::main(); +} + +mod now { + use super::*; + + #[divan::bench] + fn instant() -> Instant { + Instant::now() + } + + #[divan::bench] + fn system_time() -> SystemTime { + SystemTime::now() + } + + #[divan::bench(name = if cfg!(target_arch = "aarch64") { + "tsc (aarch64)" + } else { + "tsc (x86)" + })] + #[cfg(all( + not(miri), + any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), + ))] + pub fn tsc() -> u64 { + #[cfg(target_arch = "aarch64")] + unsafe { + let timestamp: u64; + std::arch::asm!( + "mrs {}, cntvct_el0", + out(reg) timestamp, + // Leave off `nomem` because this should be a compiler fence. + options(nostack, preserves_flags), + ); + timestamp + } + + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + unsafe { + #[cfg(target_arch = "x86")] + use std::arch::x86; + #[cfg(target_arch = "x86_64")] + use std::arch::x86_64 as x86; + + x86::_rdtsc() + } + } +} + +mod duration_since { + use super::*; + + #[divan::bench] + fn instant(bencher: Bencher) { + bencher + .with_inputs(|| [Instant::now(), Instant::now()]) + .bench_values(|[start, end]| end.duration_since(start)); + } + + #[divan::bench] + fn system_time(bencher: Bencher) { + bencher + .with_inputs(|| [SystemTime::now(), SystemTime::now()]) + .bench_values(|[start, end]| end.duration_since(start)); + } + + #[divan::bench(name = if cfg!(target_arch = "aarch64") { + "tsc (aarch64)" + } else { + "tsc (x86)" + })] + #[cfg(all( + not(miri), + any(target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64"), + ))] + fn tsc(bencher: Bencher) { + bencher + .with_inputs(|| [crate::now::tsc(), crate::now::tsc()]) + .bench_values(|[start, end]| { + // Simply subtract because an optimized timing implementation + // would want to keep the value as TSC units for as long as + // possible before dividing by the TSC frequency. + // + // Saturating arithmetic to ensures monotonicity. + end.saturating_sub(start) + }) + } +} diff --git a/crates/divan_compat/examples/build.rs b/crates/divan_compat/examples/build.rs new file mode 100644 index 00000000..688a004f --- /dev/null +++ b/crates/divan_compat/examples/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rustc-check-cfg=cfg(codspeed)"); +} diff --git a/crates/divan_compat/macros/Cargo.toml b/crates/divan_compat/macros/Cargo.toml index 5d0df662..db513609 100644 --- a/crates/divan_compat/macros/Cargo.toml +++ b/crates/divan_compat/macros/Cargo.toml @@ -11,9 +11,9 @@ repository = "https://github.com/CodSpeedHQ/codspeed-rust" homepage = "https://codspeed.io" license = "MIT OR Apache-2.0" categories = [ - "development-tools", - "development-tools::profiling", - "development-tools::testing", + "development-tools", + "development-tools::profiling", + "development-tools::testing", ] keywords = ["codspeed", "benchmark", "divan"] publish = false # TODO: change when publishing instrumentation support @@ -23,13 +23,14 @@ proc-macro = true [dependencies] divan-macros = { version = "=0.1.17" } +proc-macro-crate = "3.2.0" proc-macro2 = "1" quote = { version = "1", default-features = false } # Versions prior to *.18 fail to parse empty attribute metadata. syn = { version = "^2.0.18", default-features = false, features = [ - "full", - "clone-impls", - "parsing", - "printing", - "proc-macro", + "full", + "clone-impls", + "parsing", + "printing", + "proc-macro", ] } diff --git a/crates/divan_compat/macros/src/lib.rs b/crates/divan_compat/macros/src/lib.rs index cacc43ed..e903e4c1 100644 --- a/crates/divan_compat/macros/src/lib.rs +++ b/crates/divan_compat/macros/src/lib.rs @@ -1,7 +1,13 @@ use proc_macro::TokenStream; -use quote::quote; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::{format_ident, quote}; use syn::{ - parse::Parse, parse_macro_input, punctuated::Punctuated, ItemFn, Meta, MetaNameValue, Token, + parse::Parse, + parse_macro_input, + punctuated::Punctuated, + ItemFn, + Meta::{self, NameValue}, + MetaNameValue, Token, }; struct MyBenchArgs { @@ -25,21 +31,51 @@ pub fn bench_compat(attr: TokenStream, item: TokenStream) -> TokenStream { for arg in parsed_args.args { match &arg { - Meta::NameValue(MetaNameValue { path, .. }) if path.is_ident("crate") => { - return quote! { - compile_error!("crate argument is not supported with codspeed_divan_compat"); + NameValue(MetaNameValue { path, .. }) => { + if path.is_ident("crate") { + return quote! { + compile_error!("`crate` argument is not supported with codspeed_divan_compat"); + }. + into(); } - .into(); + + if path.is_ident("types") { + return quote! { + compile_error!("`type` argument is not yet supported with codspeed_divan_compat"); + } + .into(); + } + + if path.is_ident("min_time") + || path.is_ident("max_time") + || path.is_ident("sample_size") + || path.is_ident("sample_count") + || path.is_ident("skip_ext_time") + { + // These arguments are ignored in instrumented mode + continue; + } + + filtered_args.push(arg); } _ => filtered_args.push(arg), } } - filtered_args.push(syn::parse_quote!(crate = ::codspeed_divan_compat)); + let codspeed_divan_crate_ident = format_ident!( + "{}", + crate_name("codspeed-divan-compat") + .map(|found_crate| match found_crate { + FoundCrate::Itself => "crate".to_string(), + FoundCrate::Name(name) => name, + }) + .unwrap_or("codspeed_divan_compat".to_string()) + ); + filtered_args.push(syn::parse_quote!(crate = ::#codspeed_divan_crate_ident)); // Important: keep macro name in sync with re-exported macro name in divan-compat lib let expanded = quote! { - #[::codspeed_divan_compat::bench_original(#(#filtered_args),*)] + #[::#codspeed_divan_crate_ident::bench_original(#(#filtered_args),*)] #input }; diff --git a/crates/divan_compat/src/compat/bench.rs b/crates/divan_compat/src/compat/bench.rs deleted file mode 100644 index 21872451..00000000 --- a/crates/divan_compat/src/compat/bench.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Handpicked stubs from [divan::entry](https://github.com/nvzqz/divan/blob/main/src/entry/mod.rs) -//! Minimally reimplemented in an API compatible way to run the benches using codspeed intrumentation -use codspeed::codspeed::CodSpeed; -use std::{cell::RefCell, rc::Rc}; - -/// Benchmarking options set directly by the user in `#[divan::bench]` and -/// `#[divan::bench_group]`. -/// -/// Changes to fields must be reflected in the "Options" sections of the docs -/// for `#[divan::bench]` and `#[divan::bench_group]`. -#[derive(Default)] -pub struct BenchOptions<'a> { - pub(crate) _marker: std::marker::PhantomData<&'a ()>, -} - -pub struct Bencher<'a, 'b> { - pub(crate) codspeed: Rc>, - pub(crate) uri: String, - pub(crate) _marker: std::marker::PhantomData<&'a &'b ()>, -} - -#[allow(clippy::needless_lifetimes)] -impl<'a, 'b> Bencher<'a, 'b> { - pub(crate) fn new(uri: String) -> Self { - Self { - codspeed: Rc::new(RefCell::new(CodSpeed::new())), - uri, - _marker: std::marker::PhantomData, - } - } -} - -#[allow(clippy::needless_lifetimes)] -impl<'a, 'b> Bencher<'a, 'b> { - pub fn bench(&self, benched: B) - where - B: Fn() -> O + Sync, - { - let mut codspeed = self.codspeed.borrow_mut(); - codspeed.start_benchmark(self.uri.as_str()); - divan::black_box(benched()); - codspeed.end_benchmark(); - } -} diff --git a/crates/divan_compat/src/compat/bench/args.rs b/crates/divan_compat/src/compat/bench/args.rs new file mode 100644 index 00000000..b0567e3f --- /dev/null +++ b/crates/divan_compat/src/compat/bench/args.rs @@ -0,0 +1,228 @@ +use crate::compat_divan::compat::util::ty::TypeCast; + +use core::slice; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + mem, + sync::OnceLock, +}; + +use super::Bencher; + +/// Holds lazily-initialized runtime arguments to be passed into a benchmark. +/// +/// `#[divan::bench]` stores this as a `__DIVAN_ARGS` global for each entry, and +/// then at runtime it is initialized once by a closure that creates the usable +/// `BenchArgsRunner`. +pub struct BenchArgs { + args: OnceLock, +} + +/// Type-erased `&'static [T]` that also stores names of the arguments. +struct ErasedArgsSlice { + /// The start of `&[T]`. + args: *const (), + + /// The start of `&[&'static str]`. + names: *const &'static str, + + /// The number of arguments. + len: usize, + + /// The ID of `T` to ensure correctness. + arg_type: TypeId, +} + +// SAFETY: Raw pointers in `ErasedArgsSlice` are used in a thread-safe way, and +// the argument type is required to be `Send + Sync` when initialized from the +// iterator in `BenchArgs::runner`. +unsafe impl Send for ErasedArgsSlice {} +unsafe impl Sync for ErasedArgsSlice {} + +/// The result of making `BenchArgs` runnable from instantiating the arguments +/// list and providing a typed benchmarking implementation. +#[derive(Clone, Copy)] +pub struct BenchArgsRunner { + args: &'static ErasedArgsSlice, + bench: fn(Bencher, &ErasedArgsSlice, arg_index: usize), +} + +impl BenchArgs { + pub const fn new() -> Self { + Self { + args: OnceLock::new(), + } + } + + pub fn runner( + &'static self, + make_args: impl FnOnce() -> I, + arg_to_string: impl Fn(&I::Item) -> String, + _bench_impl: B, + ) -> BenchArgsRunner + where + I: IntoIterator, + I::Item: Any + Send + Sync, + B: FnOnce(Bencher, &I::Item) + Copy, + { + let args = self.args.get_or_init(|| { + let args_iter = make_args().into_iter(); + + // Reuse arguments for names if already a slice of strings. + // + // NOTE: We do this over `I::IntoIter` instead of `I` since it works + // for both slices and `slice::Iter`. + let args_strings: Option<&'static [&str]> = args_iter + .cast_ref::>() + .map(|iter| iter.as_slice()); + + // Collect arguments into leaked slice. + // + // Leaking the collected `args` simplifies memory management, such + // as when reusing for `names`. We're leaking anyways since this is + // accessed via a global `OnceLock`. + // + // PERF: We could optimize this to reuse arguments when users + // provide slices. However, for slices its `Item` is a reference, so + // `slice::Iter` would never match here. To make this + // optimization, we would need to be able to get the referee type. + let args: &'static [I::Item] = Box::leak(args_iter.collect()); + + // Collect printable representations of arguments. + // + // PERF: We take multiple opportunities to reuse the provided + // arguments buffer or individual strings' buffers: + // - `&[&str]` + // - `IntoIterator` + // - `IntoIterator` + // - `IntoIterator>` + // - `IntoIterator>` + let names: &'static [&str] = 'names: { + // PERF: Reuse arguments strings slice. + if let Some(args) = args_strings { + break 'names args; + } + + // PERF: Reuse our args slice allocation. + if let Some(args) = args.cast_ref::<&[&str]>() { + break 'names args; + } + + Box::leak( + args.iter() + .map(|arg| -> &str { + // PERF: Reuse strings as-is. + if let Some(arg) = arg.cast_ref::() { + return arg; + } + if let Some(arg) = arg.cast_ref::>() { + return arg; + } + if let Some(arg) = arg.cast_ref::>() { + return arg; + } + + // Default to `arg_to_string`, which will format via + // either `ToString` or `Debug`. + Box::leak(arg_to_string(arg).into_boxed_str()) + }) + .collect(), + ) + }; + + ErasedArgsSlice { + // We `black_box` arguments to prevent the compiler from + // optimizing the benchmark for the provided values. + args: crate::black_box(args.as_ptr().cast()), + names: names.as_ptr(), + len: args.len(), + arg_type: TypeId::of::(), + } + }); + + BenchArgsRunner { + args, + bench: bench::, + } + } +} + +impl Default for BenchArgs { + fn default() -> Self { + Self::new() + } +} + +impl BenchArgsRunner { + #[inline] + pub(crate) fn bench(&self, bencher: Bencher, index: usize) { + (self.bench)(bencher, self.args, index) + } + + #[inline] + pub(crate) fn arg_names(&self) -> &'static [&'static str] { + self.args.names() + } +} + +impl ErasedArgsSlice { + /// Retrieves a slice of arguments if the type is `T`. + #[inline] + fn typed_args(&self) -> Option<&[T]> { + if self.arg_type == TypeId::of::() { + // SAFETY: `BenchArgs::runner` guarantees storing `len` instances. + Some(unsafe { slice::from_raw_parts(self.args.cast(), self.len) }) + } else { + None + } + } + + /// Returns the arguments' names. + /// + /// Names are in the same order as args and thus their indices can be used + /// to reference arguments. + #[inline] + fn names(&self) -> &'static [&str] { + // SAFETY: `BenchArgs::runner` guarantees storing `len` names. + unsafe { slice::from_raw_parts(self.names, self.len) } + } +} + +/// The `BenchArgsRunner.bench` implementation. +fn bench(bencher: Bencher, erased_args: &ErasedArgsSlice, arg_index: usize) +where + T: Any, + B: FnOnce(Bencher, &T) + Copy, +{ + // We defer type checking until the benchmark is run to make safety of this + // function easier to audit. Checking here instead of in `BenchArgs::runner` + // is late but fine since this check will only fail due to a bug in Divan's + // macro code generation. + + let Some(typed_args) = erased_args.typed_args::() else { + type_mismatch::(); + + // Reduce code size by using a separate function for each `T` instead of + // each benchmark closure. + #[cold] + #[inline(never)] + fn type_mismatch() -> ! { + unreachable!("incorrect type '{}'", std::any::type_name::()) + } + }; + + // SAFETY: The closure is a ZST, so we can construct one out of thin air. + // This can be done multiple times without invoking a `Drop` destructor + // because it implements `Copy`. + let bench_impl: B = unsafe { + assert_eq!( + size_of::(), + 0, + "benchmark closure expected to be zero-sized" + ); + mem::zeroed() + }; + + bench_impl(bencher, &typed_args[arg_index]); +} diff --git a/crates/divan_compat/src/compat/bench/mod.rs b/crates/divan_compat/src/compat/bench/mod.rs new file mode 100644 index 00000000..af38c874 --- /dev/null +++ b/crates/divan_compat/src/compat/bench/mod.rs @@ -0,0 +1,113 @@ +//! Handpicked stubs from [divan::bench](https://github.com/nvzqz/divan/blob/main/src/bench/mod.rs) +//! Minimally reimplemented in an API compatible way to run the benches using codspeed intrumentation +#![allow(clippy::needless_lifetimes)] // We keep explicit lifetime to be as close to original API as possible + +mod args; +mod options; + +pub use self::{ + args::{BenchArgs, BenchArgsRunner}, + options::BenchOptions, +}; + +use codspeed::codspeed::CodSpeed; +use std::{cell::RefCell, rc::Rc}; + +/// Using this in place of `()` for `GenI` prevents `Bencher::with_inputs` from +/// working with `()` unintentionally. +#[non_exhaustive] +pub struct Unit; + +pub struct BencherConfig { + gen_input: GenI, +} + +pub struct Bencher<'a, 'b, C = BencherConfig> { + pub(crate) codspeed: Rc>, + pub(crate) uri: String, + pub(crate) config: C, + pub(crate) _marker: std::marker::PhantomData<&'a &'b ()>, +} + +#[allow(clippy::needless_lifetimes)] +impl<'a, 'b> Bencher<'a, 'b> { + pub(crate) fn new(uri: String) -> Self { + Self { + codspeed: Rc::new(RefCell::new(CodSpeed::new())), + config: BencherConfig { gen_input: Unit }, + uri, + _marker: std::marker::PhantomData, + } + } + + pub fn with_inputs(self, gen_input: G) -> Bencher<'a, 'b, BencherConfig> { + Bencher { + config: BencherConfig { gen_input }, + codspeed: self.codspeed, + uri: self.uri, + _marker: self._marker, + } + } +} + +impl<'a, 'b> Bencher<'a, 'b> { + pub fn bench(self, benched: B) + where + B: Fn() -> O + Sync, + { + self.with_inputs(|| ()).bench_values(|_| benched()) + } + + pub fn bench_local(self, benched: B) + where + B: Fn() -> O, + { + self.with_inputs(|| ()).bench_local_values(|_| benched()) + } +} + +impl<'a, 'b, I, GenI> Bencher<'a, 'b, BencherConfig> +where + GenI: FnMut() -> I, +{ + pub fn bench_values(self, benched: B) + where + B: Fn(I) -> O + Sync, + GenI: Fn() -> I + Sync, + { + self.bench_local_values(benched) + } + + pub fn bench_refs(self, benched: B) + where + B: Fn(&mut I) -> O + Sync, + GenI: Fn() -> I + Sync, + { + self.bench_local_refs(benched) + } + + pub fn bench_local_values(mut self, benched: B) + where + B: Fn(I) -> O, + { + let mut codspeed = self.codspeed.borrow_mut(); + let gen_input = &mut self.config.gen_input; + let input = gen_input(); + codspeed.start_benchmark(self.uri.as_str()); + divan::black_box(benched(input)); + codspeed.end_benchmark(); + } + + pub fn bench_local_refs(mut self, mut benched: B) + where + B: FnMut(&mut I) -> O, + { + let mut codspeed = self.codspeed.borrow_mut(); + let gen_input = &mut self.config.gen_input; + let mut input = gen_input(); + + codspeed.start_benchmark(self.uri.as_str()); + divan::black_box(benched(&mut input)); + codspeed.end_benchmark(); + } +} diff --git a/crates/divan_compat/src/compat/bench/options.rs b/crates/divan_compat/src/compat/bench/options.rs new file mode 100644 index 00000000..c55e0fd6 --- /dev/null +++ b/crates/divan_compat/src/compat/bench/options.rs @@ -0,0 +1,13 @@ +/// Benchmarking options set directly by the user in `#[divan::bench]` and +/// `#[divan::bench_group]`. +/// +/// Changes to fields must be reflected in the "Options" sections of the docs +/// for `#[divan::bench]` and `#[divan::bench_group]`. +#[derive(Default)] +pub struct BenchOptions { + /// Whether the benchmark should be ignored. + /// + /// This may be set within the attribute or with a separate + /// [`#[ignore]`](https://doc.rust-lang.org/reference/attributes/testing.html#the-ignore-attribute). + pub ignore: Option, +} diff --git a/crates/divan_compat/src/compat/entry.rs b/crates/divan_compat/src/compat/entry.rs index 4df183a4..48166a96 100644 --- a/crates/divan_compat/src/compat/entry.rs +++ b/crates/divan_compat/src/compat/entry.rs @@ -8,7 +8,10 @@ use std::{ }, }; -use super::bench::{BenchOptions, Bencher}; +use super::{ + bench::{BenchOptions, Bencher}, + BenchArgsRunner, +}; /// Benchmark entries generated by `#[divan::bench]`. /// @@ -21,8 +24,8 @@ pub static BENCH_ENTRIES: EntryList = EntryList::root(); pub enum BenchEntryRunner { /// Benchmark without arguments. Plain(fn(Bencher)), - // /// Benchmark with runtime arguments. - // Args(fn() -> BenchArgsRunner), + /// Benchmark with runtime arguments. + Args(fn() -> BenchArgsRunner), } /// Compile-time entry for a benchmark, generated by `#[divan::bench]`. @@ -50,7 +53,14 @@ pub struct EntryMeta { /// Where the entry was defined. pub location: EntryLocation, /// Configures the benchmarker via attribute options. - pub bench_options: Option>>, + pub bench_options: Option>, +} + +impl EntryMeta { + #[inline] + pub(crate) fn module_path_components<'a>(&self) -> impl Iterator { + self.module_path.split("::") + } } /// Where an entry is located. diff --git a/crates/divan_compat/src/compat/mod.rs b/crates/divan_compat/src/compat/mod.rs index 88a1102a..85a8ec39 100644 --- a/crates/divan_compat/src/compat/mod.rs +++ b/crates/divan_compat/src/compat/mod.rs @@ -1,39 +1,58 @@ -// Used by generated code. Not public API and thus not subject to SemVer. +// Used by generated code from the macro `codspeed_divan_compat_macros::bench_compat` #[doc(hidden)] -#[path = "private.rs"] -pub mod __private; +pub mod __private { + pub use super::{ + bench::{BenchArgs, BenchOptions}, + entry::{BenchEntry, BenchEntryRunner, EntryList, EntryLocation, EntryMeta, BENCH_ENTRIES}, + }; + + pub use divan::__private::{Arg, ToStringHelper}; +} mod bench; mod entry; +mod uri; +mod util; + +pub use bench::*; pub fn main() { + // Outlined steps of original divan::main and their equivalent in codspeed instrumented mode // 1. Get registered entries // TODO: Manage bench groups - - // TODO: remove when releasing divan with instrumentation mode - todo!("Instrumentation mode with divan is not yet available."); - #[allow(unreachable_code)] let bench_entries = &entry::BENCH_ENTRIES; // 2. Build an execution tree - // TODO: + // No need, we do not manage detailed tree printing like original divan, and we extract + // codspeed URI from entry metadata directly. - // 3. Filter the tree then sort it (drop sort?) - // TODO: + // 3. Filtering + // We do not support finer filtering that bench targets for now, do nothing here, bench + // filtering is managed by the `cargo-codspeed` wrappers before we reach this point. // 4. Scan the tree and execute benchmarks - // TODO: - for entry in bench_entries.iter() { + let entry_uri = uri::generate(entry.meta.display_name, &entry.meta); + + if let Some(options) = &entry.meta.bench_options.as_ref() { + if let Some(true) = options.ignore { + println!("Skipped: {}", entry_uri); + continue; + } + } match entry.bench { entry::BenchEntryRunner::Plain(bench_fn) => { - bench_fn(bench::Bencher::new(format!( - "{}:{}::{}::{}", - entry.meta.location.file, - entry.meta.location.line, - entry.meta.module_path, - entry.meta.display_name - ))); + bench_fn(bench::Bencher::new(entry_uri)); + } + entry::BenchEntryRunner::Args(bench_runner) => { + let bench_runner = bench_runner(); + + for (arg_index, arg_name) in bench_runner.arg_names().iter().enumerate() { + let entry_name_with_arg = format!("{}::{}", entry_uri, arg_name); + let bencher = bench::Bencher::new(entry_name_with_arg); + + bench_runner.bench(bencher, arg_index); + } } } } diff --git a/crates/divan_compat/src/compat/private.rs b/crates/divan_compat/src/compat/private.rs deleted file mode 100644 index fd5dfa61..00000000 --- a/crates/divan_compat/src/compat/private.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use super::entry::{ - BenchEntry, BenchEntryRunner, EntryList, EntryLocation, EntryMeta, BENCH_ENTRIES, -}; diff --git a/crates/divan_compat/src/compat/uri.rs b/crates/divan_compat/src/compat/uri.rs new file mode 100644 index 00000000..01a2c5cb --- /dev/null +++ b/crates/divan_compat/src/compat/uri.rs @@ -0,0 +1,19 @@ +use crate::__private::EntryMeta; + +pub(crate) fn generate( + bench_display_name: impl std::fmt::Display, + bench_meta: &EntryMeta, +) -> String { + let file = bench_meta.location.file; + let mut module_path = bench_meta + .module_path_components() + .skip(1) + .collect::>() + .join("::"); + if !module_path.is_empty() { + module_path.push_str("::"); + } + let uri = format!("{file}::{module_path}{bench_display_name}"); + + uri +} diff --git a/crates/divan_compat/src/compat/util/mod.rs b/crates/divan_compat/src/compat/util/mod.rs new file mode 100644 index 00000000..87caf605 --- /dev/null +++ b/crates/divan_compat/src/compat/util/mod.rs @@ -0,0 +1 @@ +pub mod ty; diff --git a/crates/divan_compat/src/compat/util/ty.rs b/crates/divan_compat/src/compat/util/ty.rs new file mode 100644 index 00000000..b19a4fa8 --- /dev/null +++ b/crates/divan_compat/src/compat/util/ty.rs @@ -0,0 +1,37 @@ +use std::{ + any::{Any, TypeId}, + marker::PhantomData, +}; + +/// Returns a [`TypeId`] for any type regardless of whether it is `'static`. +/// +/// Note that **this is not the same** as [`TypeId::of`]. +#[inline] +pub(crate) fn proxy_type_id() -> TypeId { + // Return the type ID of a generic closure. + Any::type_id(&|| PhantomData::) +} +/// Returns `true` if the given types are equal. +#[inline] +pub(crate) fn is_type_eq() -> bool { + proxy_type_id::() == proxy_type_id::() +} + +/// Convenience trait for type conversions. +pub(crate) trait TypeCast { + /// Converts a reference if `self` is an instance of `T`. + /// + /// We require `T: 'static` since we want to ensure when providing a type + /// that any lifetimes are static, such as `Cow`. + #[inline] + fn cast_ref(&self) -> Option<&T> { + if is_type_eq::() { + // SAFETY: `self` is `&T`. + Some(unsafe { &*(self as *const Self as *const T) }) + } else { + None + } + } +} + +impl TypeCast for A {} diff --git a/crates/divan_compat/src/lib.rs b/crates/divan_compat/src/lib.rs index d9b5c09c..b977abfa 100644 --- a/crates/divan_compat/src/lib.rs +++ b/crates/divan_compat/src/lib.rs @@ -8,7 +8,7 @@ mod compat_divan { #[cfg(codspeed)] #[path = "."] mod compat_divan { - pub use divan::black_box; + pub use divan::{black_box, black_box_drop, AllocProfiler}; pub use codspeed_divan_compat_macros::bench_compat as bench; // Important: Keep in sync with the name used in the compat macro