Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/js-sys/benches/foo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use js_bindgen_test::{Criterion, bench};
use js_sys::js_sys;

js_bindgen::embed_js!(module = "foo", name = "bench", "(value) => value");

#[js_sys]
extern "js-sys" {
#[js_sys(js_embed = "bench")]
fn val(value: u128) -> u128;
}

#[bench]
fn bench_foo(c: &mut Criterion) {
c.bench_function("i64,i64", |b| b.iter(|| {
assert_eq!(val(1), 1);
}));
}
54 changes: 33 additions & 21 deletions client/js-sys/src/numeric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,13 @@ unsafe impl Output for u128 {
const ASM_TYPE: &str = ASM_PTR_TYPE;
const ASM_CONV: Option<OutputAsmConv> = Some(OutputAsmConv {
import: Some(const_concat!(
".functype js_sys.numeric.128 (i32, i32, i32, i32, ",
".functype js_sys.numeric.128 (i64, i64,",
ASM_PTR_TYPE,
") -> ()"
)),
direct: false,
conv: "call js_sys.numeric.128",
r#type: "i32, i32, i32, i32",
r#type: "i64,i64",
});
const JS_CONV: Option<OutputJsConv> = Some(OutputJsConv {
embed: Some(("js_sys", "numeric.128.encode")),
Expand Down Expand Up @@ -239,13 +239,13 @@ unsafe impl Output for i128 {
const ASM_TYPE: &str = ASM_PTR_TYPE;
const ASM_CONV: Option<OutputAsmConv> = Some(OutputAsmConv {
import: Some(const_concat!(
".functype js_sys.numeric.128 (i32, i32, i32, i32, ",
".functype js_sys.numeric.128 (i64, i64,",
ASM_PTR_TYPE,
") -> ()"
)),
direct: false,
conv: "call js_sys.numeric.128",
r#type: "i32, i32, i32, i32",
r#type: "i64,i64",
});
const JS_CONV: Option<OutputJsConv> = Some(OutputJsConv {
embed: Some(("js_sys", "numeric.128.encode")),
Expand All @@ -270,35 +270,47 @@ const _: () = {
js_bindgen::embed_js!(
module = "js_sys",
name = "numeric.128.encode",
"(value) => {{",
" const lo_lo = Number(value & 0xFFFFFFFFn)",
" const lo_hi = Number((value >> 32n) & 0xFFFFFFFFn)",
" const hi_lo = Number((value >> 64n) & 0xFFFFFFFFn)",
" const hi_hi = Number((value >> 96n) & 0xFFFFFFFFn)",
" return [lo_lo, lo_hi, hi_lo, hi_hi]",
"(value) => {{",
" const lo = BigInt.asIntN(64, value & 0xFFFFFFFFFFFFFFFFn)",
" const hi = BigInt.asIntN(64, value >> 64n)",
" return [lo, hi]",
"}}",
);

js_bindgen::unsafe_embed_asm!(
".globl js_sys.numeric.128",
"js_sys.numeric.128:",
" .functype js_sys.numeric.128 (i32, i32, i32, i32, {}) -> ()",
" local.get 4",
" .functype js_sys.numeric.128 (i64, i64, {}) -> ()",
" local.get 2",
" local.get 0",
" i32.store 0",
" local.get 4",
" local.get 1",
" i32.store 4",
" local.get 4",
" i64.store 0",
" local.get 2",
" i32.store 8",
" local.get 4",
" local.get 3",
" i32.store 12",
" local.get 1",
" i64.store 8",
" end_function",
interpolate ASM_PTR_TYPE,
);

// js_bindgen::unsafe_embed_asm!(
// ".globl js_sys.numeric.128",
// "js_sys.numeric.128:",
// " .functype js_sys.numeric.128 (i32, i32, i32, i32, {}) -> ()",
// " local.get 4",
// " local.get 0",
// " i32.store 0",
// " local.get 4",
// " local.get 1",
// " i32.store 4",
// " local.get 4",
// " local.get 2",
// " i32.store 8",
// " local.get 4",
// " local.get 3",
// " i32.store 12",
// " end_function",
// interpolate ASM_PTR_TYPE,
// );

#[cfg(target_arch = "wasm32")]
delegate!(u32, *const T:<T>);
#[cfg(target_arch = "wasm64")]
Expand Down
11 changes: 11 additions & 0 deletions client/test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ rust-version = "1.87"
[dependencies]
js-bindgen-test-macro = { workspace = true }
js-sys = { workspace = true, features = ["macro"] }
web-sys = { workspace = true }

async-trait = "0.1.89"
cast = "0.3"
libm = "0.2.11"
nu-ansi-term = { version = "0.50", default-features = false }
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
once_cell = "1.21.4"
oorandom = "11.1.5"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }

[lints]
workspace = true
157 changes: 157 additions & 0 deletions client/test/src/criterion/analysis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use alloc::vec::Vec;

use super::benchmark::BenchmarkConfig;
use super::estimate::{
ConfidenceInterval, Distributions, Estimate, Estimates, PointEstimates, build_estimates,
};
use super::measurement::Measurement;
use super::report::{BenchmarkId, Report};
use super::routine::Routine;
use super::stats::bivariate::Data;
use super::stats::bivariate::regression::Slope;
use super::stats::univariate::Sample;
use super::stats::{Distribution, Tails};
use super::{Criterion, SavedSample, baseline, compare};

// Common analysis procedure
pub(crate) async fn common<M: Measurement>(
id: &BenchmarkId,
routine: &mut dyn Routine<M>,
config: &BenchmarkConfig,
criterion: &Criterion<M>,
) {
criterion.report.benchmark_start(id);

let (sampling_mode, iters, times);
let sample = routine
.sample(&criterion.measurement, id, config, criterion)
.await;
sampling_mode = sample.0;
iters = sample.1;
times = sample.2;

criterion.report.analysis(id);

if times.contains(&0.0) {
return;
}

let avg_times = iters
.iter()
.zip(times.iter())
.map(|(&iters, &elapsed)| elapsed / iters)
.collect::<Vec<f64>>();
let avg_times = Sample::new(&avg_times);
let labeled_sample = super::stats::univariate::outliers::tukey::classify(avg_times);

let data = Data::new(&iters, &times);
let (mut distributions, mut estimates) = estimates(avg_times, config);
if sampling_mode.is_linear() {
let (distribution, slope) = regression(&data, config);

estimates.slope = Some(slope);
distributions.slope = Some(distribution);
}

let comparison = compare::common(id, avg_times, config).map(
|(t_value, t_distribution, relative_estimates, ..)| {
let p_value = t_distribution.p_value(t_value, Tails::Two);
super::report::ComparisonData {
p_value,
relative_estimates,
significance_threshold: config.significance_level,
noise_threshold: config.noise_threshold,
}
},
);

let measurement_data = super::report::MeasurementData {
avg_times: labeled_sample,
absolute_estimates: estimates.clone(),
comparison,
};

criterion
.report
.measurement_complete(id, &measurement_data, criterion.measurement.formatter());

baseline::write(
id.desc(),
baseline::BenchmarkBaseline {
file: criterion.location.as_ref().map(|l| l.file.clone()),
module_path: criterion.location.as_ref().map(|l| l.module_path.clone()),
iters: data.x().as_ref().to_vec(),
times: data.y().as_ref().to_vec(),
sample: SavedSample {
sampling_mode,
iters: data.x().as_ref().to_vec(),
times: data.y().as_ref().to_vec(),
},
estimates,
},
);
}

// Performs a simple linear regression on the sample
fn regression(
data: &Data<'_, f64, f64>,
config: &BenchmarkConfig,
) -> (Distribution<f64>, Estimate) {
let cl = config.confidence_level;

let distribution = data.bootstrap(config.nresamples, |d| (Slope::fit(&d).0,)).0;

let point = Slope::fit(data);
let (lb, ub) = distribution.confidence_interval(config.confidence_level);
let se = distribution.std_dev(None);

(
distribution,
Estimate {
confidence_interval: ConfidenceInterval {
confidence_level: cl,
lower_bound: lb,
upper_bound: ub,
},
point_estimate: point.0,
standard_error: se,
},
)
}

// Estimates the statistics of the population from the sample
fn estimates(avg_times: &Sample<f64>, config: &BenchmarkConfig) -> (Distributions, Estimates) {
fn stats(sample: &Sample<f64>) -> (f64, f64, f64, f64) {
let mean = sample.mean();
let std_dev = sample.std_dev(Some(mean));
let median = sample.percentiles().median();
let mad = sample.median_abs_dev(Some(median));

(mean, std_dev, median, mad)
}

let cl = config.confidence_level;
let nresamples = config.nresamples;

let (mean, std_dev, median, mad) = stats(avg_times);
let points = PointEstimates {
mean,
median,
std_dev,
median_abs_dev: mad,
};

let (dist_mean, dist_stddev, dist_median, dist_mad) = avg_times.bootstrap(nresamples, stats);

let distributions = Distributions {
mean: dist_mean,
slope: None,
median: dist_median,
median_abs_dev: dist_mad,
std_dev: dist_stddev,
};

let estimates = build_estimates(&distributions, &points, cl);

(distributions, estimates)
}
61 changes: 61 additions & 0 deletions client/test/src/criterion/baseline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Record previous benchmark data

use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::RefCell;

use serde::{Deserialize, Serialize};

use super::SavedSample;
use super::estimate::Estimates;
use crate::LazyCell;

#[cfg_attr(target_feature = "atomics", thread_local)]
static BASELINE: LazyCell<RefCell<BTreeMap<String, BenchmarkBaseline>>> =
LazyCell::new(|| RefCell::new(BTreeMap::new()));

#[derive(Debug, Serialize, Deserialize, Clone)]
pub(crate) struct BenchmarkBaseline {
pub(crate) file: Option<String>,
pub(crate) module_path: Option<String>,
pub(crate) iters: Vec<f64>,
pub(crate) times: Vec<f64>,
pub(crate) sample: SavedSample,
pub(crate) estimates: Estimates,
}

/// Write the corresponding benchmark ID and corresponding data into the table.
pub(crate) fn write(id: &str, baseline: BenchmarkBaseline) {
BASELINE.borrow_mut().insert(id.into(), baseline);
}

/// Read the data corresponding to the benchmark ID from the table.
pub(crate) fn read(id: &str) -> Option<BenchmarkBaseline> {
BASELINE.borrow().get(id).cloned()
}

// /// Used to write previous benchmark data before the benchmark, for later
// /// comparison.
// #[wasm_bindgen]
// pub fn __wbgbench_import(baseline: Vec<u8>) {
// match serde_json::from_slice(&baseline) {
// Ok(prev) => {
// *BASELINE.borrow_mut() = prev;
// }
// Err(e) => {
// console_log!("Failed to import previous benchmark {e:?}");
// }
// }
// }
//
// /// Used to read benchmark data, and then the runner stores it on the local
// /// disk.
// #[wasm_bindgen]
// pub fn __wbgbench_dump() -> Option<Vec<u8>> {
// let baseline = BASELINE.borrow();
// if baseline.is_empty() {
// return None;
// }
// serde_json::to_vec(&*baseline).ok()
// }
Loading
Loading