#![warn(clippy::doc_markdown, missing_docs)]
#![warn(bare_trait_objects)]
#![allow(
clippy::just_underscores_and_digits, // Used in the stats code
clippy::transmute_ptr_to_ptr, // Used in the stats code
)]
mod analysis;
mod baseline;
mod bencher;
mod benchmark;
mod compare;
mod estimate;
mod format;
mod measurement;
mod report;
mod routine;
mod stats;
use core::future::Future;
use core::pin::Pin;
use core::ptr;
use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use core::time::Duration;
use libm::{ceil, sqrt};
use serde::{Deserialize, Serialize};
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use benchmark::BenchmarkConfig;
use measurement::WallTime;
use report::WasmReport;
pub use bencher::Bencher;
pub use measurement::Measurement;
pub struct Criterion<M: Measurement = WallTime> {
config: BenchmarkConfig,
report: WasmReport,
measurement: M,
location: Option<Location>,
}
pub(crate) struct Location {
file: String,
module_path: String,
}
impl Default for Criterion {
fn default() -> Criterion {
Criterion {
config: BenchmarkConfig {
confidence_level: 0.95,
measurement_time: Duration::from_secs(5),
noise_threshold: 0.01,
nresamples: 100_000,
sample_size: 100,
significance_level: 0.05,
warm_up_time: Duration::from_secs(3),
sampling_mode: SamplingMode::Auto,
},
report: WasmReport,
measurement: WallTime,
location: None,
}
}
}
impl<M: Measurement> Criterion<M> {
pub fn with_measurement<M2: Measurement>(self, m: M2) -> Criterion<M2> {
Criterion {
config: self.config,
report: self.report,
measurement: m,
location: self.location,
}
}
#[must_use]
pub fn with_location(self, file: &str, module_path: &str) -> Criterion<M> {
Criterion {
location: Some(Location {
file: file.into(),
module_path: module_path.into(),
}),
..self
}
}
#[must_use]
pub fn sample_size(mut self, n: usize) -> Criterion<M> {
assert!(n >= 10);
self.config.sample_size = n;
self
}
#[must_use]
pub fn warm_up_time(mut self, dur: Duration) -> Criterion<M> {
assert!(dur.as_nanos() > 0);
self.config.warm_up_time = dur;
self
}
#[must_use]
pub fn measurement_time(mut self, dur: Duration) -> Criterion<M> {
assert!(dur.as_nanos() > 0);
self.config.measurement_time = dur;
self
}
#[must_use]
pub fn nresamples(mut self, n: usize) -> Criterion<M> {
assert!(n > 0);
if n <= 1000 {
console_error!("\nWarning: It is not recommended to reduce nresamples below 1000.");
}
self.config.nresamples = n;
self
}
#[must_use]
pub fn noise_threshold(mut self, threshold: f64) -> Criterion<M> {
assert!(threshold >= 0.0);
self.config.noise_threshold = threshold;
self
}
#[must_use]
pub fn confidence_level(mut self, cl: f64) -> Criterion<M> {
assert!(cl > 0.0 && cl < 1.0);
if cl < 0.5 {
console_error!(
"\nWarning: It is not recommended to reduce confidence level below 0.5."
);
}
self.config.confidence_level = cl;
self
}
#[must_use]
pub fn significance_level(mut self, sl: f64) -> Criterion<M> {
assert!(sl > 0.0 && sl < 1.0);
self.config.significance_level = sl;
self
}
}
impl<M> Criterion<M>
where
M: Measurement + 'static,
{
pub fn bench_function<F>(&mut self, desc: &str, f: F) -> &mut Criterion<M>
where
F: FnMut(&mut Bencher<'_, M>),
{
const NOOP: RawWaker = {
const VTABLE: RawWakerVTable = RawWakerVTable::new(
|_| NOOP,
|_| {},
|_| {},
|_| {},
);
RawWaker::new(ptr::null(), &VTABLE)
};
fn block_on(f: impl Future<Output = ()>) {
let waker = unsafe { Waker::from_raw(NOOP) };
let mut ctx = Context::from_waker(&waker);
match core::pin::pin!(f).poll(&mut ctx) {
Poll::Ready(_) => (),
Poll::Pending => unreachable!(),
}
}
let id = report::BenchmarkId::new(desc.into());
block_on(analysis::common(
&id,
&mut routine::Function::new(f),
&self.config,
self,
));
self
}
pub async fn bench_async_function<F>(&mut self, desc: &str, f: F) -> &mut Criterion<M>
where
for<'b> F: FnMut(&'b mut Bencher<'_, M>) -> Pin<Box<dyn Future<Output = ()> + 'b>>,
{
let id = report::BenchmarkId::new(desc.into());
analysis::common(&id, &mut routine::AsyncFunction::new(f), &self.config, self).await;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Throughput {
Bytes(u64),
BytesDecimal(u64),
Elements(u64),
Bits(u64),
}
#[derive(Debug, Default, Clone, Copy)]
pub enum SamplingMode {
#[default]
Auto,
Linear,
Flat,
}
impl SamplingMode {
pub(crate) fn choose_sampling_mode(
&self,
warmup_mean_execution_time: f64,
sample_count: u64,
target_time: f64,
) -> ActualSamplingMode {
match self {
SamplingMode::Linear => ActualSamplingMode::Linear,
SamplingMode::Flat => ActualSamplingMode::Flat,
SamplingMode::Auto => {
let total_runs = sample_count * (sample_count + 1) / 2;
let d = ceil(target_time / warmup_mean_execution_time / total_runs as f64) as u64;
let expected_ns = total_runs as f64 * d as f64 * warmup_mean_execution_time;
if expected_ns > (2.0 * target_time) {
ActualSamplingMode::Flat
} else {
ActualSamplingMode::Linear
}
}
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub(crate) enum ActualSamplingMode {
Linear,
Flat,
}
impl ActualSamplingMode {
pub(crate) fn iteration_counts(
&self,
warmup_mean_execution_time: f64,
sample_count: u64,
target_time: &Duration,
) -> Vec<u64> {
match self {
ActualSamplingMode::Linear => {
let n = sample_count;
let met = warmup_mean_execution_time;
let m_ns = target_time.as_nanos();
let total_runs = n * (n + 1) / 2;
let d = (ceil(m_ns as f64 / met / total_runs as f64) as u64).max(1);
let expected_ns = total_runs as f64 * d as f64 * met;
if d == 1 {
let recommended_sample_size =
ActualSamplingMode::recommend_linear_sample_size(m_ns as f64, met);
let actual_time = Duration::from_nanos(expected_ns as u64);
console_error!("\nWarning: Unable to complete {} samples in {:.1?}. You may wish to increase target time to {:.1?}",
n, target_time, actual_time);
if recommended_sample_size != n {
console_error!(
", enable flat sampling, or reduce sample count to {}.",
recommended_sample_size
);
} else {
console_error!(" or enable flat sampling.");
}
}
(1..(n + 1)).map(|a| a * d).collect::<Vec<u64>>()
}
ActualSamplingMode::Flat => {
let n = sample_count;
let met = warmup_mean_execution_time;
let m_ns = target_time.as_nanos() as f64;
let time_per_sample = m_ns / (n as f64);
let iterations_per_sample = (ceil(time_per_sample / met) as u64).max(1);
let expected_ns = met * (iterations_per_sample * n) as f64;
if iterations_per_sample == 1 {
let recommended_sample_size =
ActualSamplingMode::recommend_flat_sample_size(m_ns, met);
let actual_time = Duration::from_nanos(expected_ns as u64);
console_error!("\nWarning: Unable to complete {} samples in {:.1?}. You may wish to increase target time to {:.1?}",
n, target_time, actual_time);
if recommended_sample_size != n {
console_error!(", or reduce sample count to {}.", recommended_sample_size);
} else {
console_error!(".");
}
}
vec![iterations_per_sample; n as usize]
}
}
}
fn is_linear(&self) -> bool {
matches!(self, ActualSamplingMode::Linear)
}
fn recommend_linear_sample_size(target_time: f64, met: f64) -> u64 {
let c = target_time / met;
let sample_size = (-1.0 + sqrt(4.0 * c)) / 2.0;
let sample_size = sample_size as u64;
let sample_size = (sample_size / 10) * 10;
if sample_size < 10 {
10
} else {
sample_size
}
}
fn recommend_flat_sample_size(target_time: f64, met: f64) -> u64 {
let sample_size = (target_time / met) as u64;
let sample_size = (sample_size / 10) * 10;
if sample_size < 10 {
10
} else {
sample_size
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct SavedSample {
pub(crate) sampling_mode: ActualSamplingMode,
pub(crate) iters: Vec<f64>,
pub(crate) times: Vec<f64>,
}