Skip to content

Commit d9d3f52

Browse files
committed
Add benchmark support
1 parent af07d0e commit d9d3f52

52 files changed

Lines changed: 4131 additions & 78 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

client/js-sys/benches/interpo.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use js_bindgen_test::{Criterion, bench};
2+
use js_sys::js_sys;
3+
4+
js_bindgen::embed_js!(module = "interpo", name = "bench", "(value) => value");
5+
6+
#[js_sys]
7+
extern "js-sys" {
8+
#[js_sys(js_embed = "bench")]
9+
fn interpo(value: u128) -> u128;
10+
}
11+
12+
#[bench]
13+
fn bench_interpo_u128(c: &mut Criterion) {
14+
c.bench_function("bench_interpo_u128", |b| {
15+
b.iter(|| {
16+
assert_eq!(interpo(4242), 4242);
17+
})
18+
});
19+
}

client/js-sys/tests/array.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use js_sys::{JsArray, JsValue};
77
fn js_value() {
88
let rust_array = [JsValue::UNDEFINED; 42];
99
let js_array = JsArray::from(&rust_array);
10-
assert_eq!(rust_array.len(), js_array.length().try_into().unwrap());
10+
assert_eq!(rust_array.len(), usize::try_from(js_array.length()).unwrap());
1111

1212
let returned_array: [JsValue; 42] = js_array.to_array().unwrap();
1313
assert!(rust_array == returned_array);
@@ -17,7 +17,7 @@ fn js_value() {
1717
fn u32() {
1818
let rust_array: [u32; 42] = array::from_fn(|i| i.try_into().unwrap());
1919
let js_array = JsArray::from(&rust_array);
20-
assert_eq!(rust_array.len(), js_array.length().try_into().unwrap());
20+
assert_eq!(rust_array.len(), usize::try_from(js_array.length()).unwrap());
2121

2222
let returned_array: [u32; 42] = js_array.to_array().unwrap();
2323
assert_eq!(rust_array, returned_array);

client/test/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@ rust-version = "1.87"
88
js-bindgen-test-macro = { workspace = true }
99
js-sys = { workspace = true, features = ["macro"] }
1010

11+
async-trait = "0.1.89"
12+
cast = "0.3"
13+
libm = "0.2.11"
14+
nu-ansi-term = { version = "0.50", default-features = false }
15+
num-traits = { version = "0.2", default-features = false, features = ["libm"] }
16+
once_cell = "1.21.4"
17+
oorandom = "11.1.5"
18+
serde = { version = "1.0", default-features = false, features = ["derive"] }
19+
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
20+
1121
[lints]
1222
workspace = true
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use alloc::vec::Vec;
2+
3+
use super::benchmark::BenchmarkConfig;
4+
use super::estimate::{
5+
ConfidenceInterval, Distributions, Estimate, Estimates, PointEstimates, build_estimates,
6+
};
7+
use super::measurement::Measurement;
8+
use super::report::{BenchmarkId, Report};
9+
use super::routine::Routine;
10+
use super::stats::bivariate::Data;
11+
use super::stats::bivariate::regression::Slope;
12+
use super::stats::univariate::Sample;
13+
use super::stats::{Distribution, Tails};
14+
use super::{Criterion, SavedSample, baseline, compare};
15+
16+
// Common analysis procedure
17+
pub(crate) async fn common<M: Measurement>(
18+
id: &BenchmarkId,
19+
routine: &mut dyn Routine<M>,
20+
config: &BenchmarkConfig,
21+
criterion: &Criterion<M>,
22+
) {
23+
criterion.report.benchmark_start(id);
24+
25+
let (sampling_mode, iters, times);
26+
let sample = routine
27+
.sample(&criterion.measurement, id, config, criterion)
28+
.await;
29+
sampling_mode = sample.0;
30+
iters = sample.1;
31+
times = sample.2;
32+
33+
criterion.report.analysis(id);
34+
35+
if times.contains(&0.0) {
36+
return;
37+
}
38+
39+
let avg_times = iters
40+
.iter()
41+
.zip(times.iter())
42+
.map(|(&iters, &elapsed)| elapsed / iters)
43+
.collect::<Vec<f64>>();
44+
let avg_times = Sample::new(&avg_times);
45+
let labeled_sample = super::stats::univariate::outliers::tukey::classify(avg_times);
46+
47+
let data = Data::new(&iters, &times);
48+
let (mut distributions, mut estimates) = estimates(avg_times, config);
49+
if sampling_mode.is_linear() {
50+
let (distribution, slope) = regression(&data, config);
51+
52+
estimates.slope = Some(slope);
53+
distributions.slope = Some(distribution);
54+
}
55+
56+
let comparison = compare::common(id, avg_times, config).map(
57+
|(t_value, t_distribution, relative_estimates, ..)| {
58+
let p_value = t_distribution.p_value(t_value, Tails::Two);
59+
super::report::ComparisonData {
60+
p_value,
61+
relative_estimates,
62+
significance_threshold: config.significance_level,
63+
noise_threshold: config.noise_threshold,
64+
}
65+
},
66+
);
67+
68+
let measurement_data = super::report::MeasurementData {
69+
avg_times: labeled_sample,
70+
absolute_estimates: estimates.clone(),
71+
comparison,
72+
};
73+
74+
criterion
75+
.report
76+
.measurement_complete(id, &measurement_data, criterion.measurement.formatter());
77+
78+
baseline::write(
79+
id.desc(),
80+
baseline::BenchmarkBaseline {
81+
file: criterion.location.as_ref().map(|l| l.file.clone()),
82+
module_path: criterion.location.as_ref().map(|l| l.module_path.clone()),
83+
iters: data.x().as_ref().to_vec(),
84+
times: data.y().as_ref().to_vec(),
85+
sample: SavedSample {
86+
sampling_mode,
87+
iters: data.x().as_ref().to_vec(),
88+
times: data.y().as_ref().to_vec(),
89+
},
90+
estimates,
91+
},
92+
);
93+
}
94+
95+
// Performs a simple linear regression on the sample
96+
fn regression(
97+
data: &Data<'_, f64, f64>,
98+
config: &BenchmarkConfig,
99+
) -> (Distribution<f64>, Estimate) {
100+
let cl = config.confidence_level;
101+
102+
let distribution = data.bootstrap(config.nresamples, |d| (Slope::fit(&d).0,)).0;
103+
104+
let point = Slope::fit(data);
105+
let (lb, ub) = distribution.confidence_interval(config.confidence_level);
106+
let se = distribution.std_dev(None);
107+
108+
(
109+
distribution,
110+
Estimate {
111+
confidence_interval: ConfidenceInterval {
112+
confidence_level: cl,
113+
lower_bound: lb,
114+
upper_bound: ub,
115+
},
116+
point_estimate: point.0,
117+
standard_error: se,
118+
},
119+
)
120+
}
121+
122+
// Estimates the statistics of the population from the sample
123+
fn estimates(avg_times: &Sample<f64>, config: &BenchmarkConfig) -> (Distributions, Estimates) {
124+
fn stats(sample: &Sample<f64>) -> (f64, f64, f64, f64) {
125+
let mean = sample.mean();
126+
let std_dev = sample.std_dev(Some(mean));
127+
let median = sample.percentiles().median();
128+
let mad = sample.median_abs_dev(Some(median));
129+
130+
(mean, std_dev, median, mad)
131+
}
132+
133+
let cl = config.confidence_level;
134+
let nresamples = config.nresamples;
135+
136+
let (mean, std_dev, median, mad) = stats(avg_times);
137+
let points = PointEstimates {
138+
mean,
139+
median,
140+
std_dev,
141+
median_abs_dev: mad,
142+
};
143+
144+
let (dist_mean, dist_stddev, dist_median, dist_mad) = avg_times.bootstrap(nresamples, stats);
145+
146+
let distributions = Distributions {
147+
mean: dist_mean,
148+
slope: None,
149+
median: dist_median,
150+
median_abs_dev: dist_mad,
151+
std_dev: dist_stddev,
152+
};
153+
154+
let estimates = build_estimates(&distributions, &points, cl);
155+
156+
(distributions, estimates)
157+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//! Record previous benchmark data
2+
3+
use crate::console_log;
4+
use alloc::collections::BTreeMap;
5+
use alloc::string::String;
6+
use alloc::vec::Vec;
7+
use core::cell::RefCell;
8+
use js_sys::{JsString, js_sys};
9+
10+
use serde::{Deserialize, Serialize};
11+
12+
use super::SavedSample;
13+
use super::estimate::Estimates;
14+
use crate::utils::LazyCell;
15+
16+
#[cfg_attr(target_feature = "atomics", thread_local)]
17+
static BASELINE: LazyCell<RefCell<BTreeMap<String, BenchmarkBaseline>>> =
18+
LazyCell::new(|| RefCell::new(BTreeMap::new()));
19+
20+
#[derive(Debug, Serialize, Deserialize, Clone)]
21+
pub(crate) struct BenchmarkBaseline {
22+
pub(crate) file: Option<String>,
23+
pub(crate) module_path: Option<String>,
24+
pub(crate) iters: Vec<f64>,
25+
pub(crate) times: Vec<f64>,
26+
pub(crate) sample: SavedSample,
27+
pub(crate) estimates: Estimates,
28+
}
29+
30+
/// Write the corresponding benchmark ID and corresponding data into the table.
31+
pub(crate) fn write(id: &str, baseline: BenchmarkBaseline) {
32+
BASELINE.borrow_mut().insert(id.into(), baseline);
33+
}
34+
35+
/// Read the data corresponding to the benchmark ID from the table.
36+
pub(crate) fn read(id: &str) -> Option<BenchmarkBaseline> {
37+
BASELINE.borrow().get(id).cloned()
38+
}
39+
40+
#[js_sys]
41+
extern "js-sys" {
42+
#[js_sys(js_import)]
43+
fn import_bench_baseline() -> JsString;
44+
45+
#[js_sys(js_import)]
46+
fn dump_bench_baseline(baseline: &JsString);
47+
}
48+
49+
/// Used to read previous benchmark data before the benchmark, for later
50+
/// comparison.
51+
pub(crate) fn import_baseline() {
52+
match serde_json::from_str(&String::from(&import_bench_baseline())) {
53+
Ok(prev) => {
54+
*BASELINE.borrow_mut() = prev;
55+
}
56+
Err(e) => {
57+
console_log!("Failed to import previous benchmark {e:?}");
58+
}
59+
}
60+
}
61+
62+
/// Used to read benchmark data, and then the runner stores it on the local
63+
/// disk.
64+
pub(crate) fn dump_baseline() {
65+
let baseline = BASELINE.borrow();
66+
if !baseline.is_empty() {
67+
let baseline = JsString::from(
68+
serde_json::to_string(&*baseline)
69+
.unwrap_or_default()
70+
.as_str(),
71+
);
72+
dump_bench_baseline(&baseline);
73+
}
74+
}

0 commit comments

Comments
 (0)