Skip to content

Commit d172a34

Browse files
committed
~40% improvement by switching away from owned APIs (though users might be sad?)
1 parent 1c5f5f5 commit d172a34

11 files changed

Lines changed: 547 additions & 1300 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ resolver = "2"
2727
] }
2828
nom = { version = "8.0.0" }
2929
rstest = { version = "0.26" }
30+
smallvec = { version = "1.14", features = ["union", "const_generics", "const_new"]}
3031
serde = { version = "1.0", features = ["derive"] }
3132
serde_json = { version = "1.0" }
3233
thiserror = { version = "2.0.17" }

crates/berry-bench-bin/src/main.rs

Lines changed: 148 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use berry::parse::parse_lockfile;
2-
use berry_test::load_fixture;
2+
use berry_test::{load_fixture, load_fixture_from_path};
33
use clap::Parser;
44
use memory_stats::memory_stats;
55
use std::collections::HashMap;
66
use std::fs;
7-
use std::path::Path;
7+
use std::hint::black_box;
8+
use std::path::{Path, PathBuf};
89
use std::time::Instant;
910

1011
#[derive(Parser)]
@@ -15,6 +16,10 @@ struct Args {
1516
#[arg(short, long)]
1617
fixture: Option<String>,
1718

19+
/// Path to a lockfile to benchmark
20+
#[arg(long = "fixture-path", value_name = "PATH", conflicts_with_all = ["fixture", "all"])]
21+
fixture_path: Option<PathBuf>,
22+
1823
/// Benchmark all fixtures
1924
#[arg(short, long)]
2025
all: bool,
@@ -52,6 +57,55 @@ struct Args {
5257
fail_on_regression: bool,
5358
}
5459

60+
#[derive(Clone)]
61+
enum FixtureSource {
62+
FixtureName(String),
63+
ArbitraryPath(PathBuf),
64+
}
65+
66+
#[derive(Clone)]
67+
struct FixtureTarget {
68+
label: String,
69+
source: FixtureSource,
70+
}
71+
72+
impl FixtureTarget {
73+
fn from_fixture_name(name: impl Into<String>) -> Self {
74+
let name = name.into();
75+
Self {
76+
label: name.clone(),
77+
source: FixtureSource::FixtureName(name),
78+
}
79+
}
80+
81+
fn from_path(path: impl Into<PathBuf>) -> Self {
82+
let path = path.into();
83+
let label = path
84+
.file_name()
85+
.and_then(|name| name.to_str())
86+
.map(std::string::ToString::to_string)
87+
.unwrap_or_else(|| path.display().to_string());
88+
Self {
89+
label,
90+
source: FixtureSource::ArbitraryPath(path),
91+
}
92+
}
93+
94+
fn source_path(&self) -> Option<&Path> {
95+
match &self.source {
96+
FixtureSource::ArbitraryPath(path) => Some(path.as_path()),
97+
FixtureSource::FixtureName(_) => None,
98+
}
99+
}
100+
101+
fn load_contents(&self) -> String {
102+
match &self.source {
103+
FixtureSource::FixtureName(name) => load_fixture(name),
104+
FixtureSource::ArbitraryPath(path) => load_fixture_from_path(path),
105+
}
106+
}
107+
}
108+
55109
#[derive(serde::Serialize, serde::Deserialize, Clone)]
56110
struct BenchmarkResult {
57111
fixture: String,
@@ -60,6 +114,12 @@ struct BenchmarkResult {
60114
min_time_ms: f64,
61115
max_time_ms: f64,
62116
std_dev_ms: f64,
117+
#[serde(default)]
118+
std_error_ms: f64,
119+
#[serde(default)]
120+
ci_95_lower_ms: f64,
121+
#[serde(default)]
122+
ci_95_upper_ms: f64,
63123
runs: usize,
64124
heap_usage_bytes: Option<usize>,
65125
virtual_usage_bytes: Option<usize>,
@@ -68,34 +128,75 @@ struct BenchmarkResult {
68128
mb_per_s: f64,
69129
}
70130

131+
#[derive(Default)]
132+
struct StatsSummary {
133+
mean: f64,
134+
min: f64,
135+
max: f64,
136+
std_dev: f64,
137+
std_error: f64,
138+
ci_low: f64,
139+
ci_high: f64,
140+
}
141+
71142
#[allow(clippy::cast_precision_loss)]
72-
fn calculate_stats(times: &[f64]) -> (f64, f64, f64, f64) {
143+
fn calculate_stats(times: &[f64]) -> StatsSummary {
144+
if times.is_empty() {
145+
return StatsSummary::default();
146+
}
147+
73148
let mean = times.iter().sum::<f64>() / times.len() as f64;
74-
let variance = times.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / times.len() as f64;
75-
let std_dev = variance.sqrt();
76149
let min = times.iter().fold(f64::INFINITY, |a, &b| a.min(b));
77150
let max = times.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
78-
79-
(mean, min, max, std_dev)
151+
let variance = if times.len() > 1 {
152+
times.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (times.len() - 1) as f64
153+
} else {
154+
0.0
155+
};
156+
let std_dev = variance.sqrt();
157+
let std_error = if times.len() > 1 {
158+
std_dev / (times.len() as f64).sqrt()
159+
} else {
160+
0.0
161+
};
162+
let ci_margin = 1.96 * std_error;
163+
let ci_low = (mean - ci_margin).max(0.0);
164+
let ci_high = mean + ci_margin;
165+
166+
StatsSummary {
167+
mean,
168+
min,
169+
max,
170+
std_dev,
171+
std_error,
172+
ci_low,
173+
ci_high,
174+
}
80175
}
81176

82177
fn benchmark_fixture(
83-
fixture_name: &str,
178+
target: &FixtureTarget,
84179
warmup: usize,
85180
runs: usize,
86181
verbose: bool,
87182
) -> BenchmarkResult {
88-
let fixture = load_fixture(fixture_name);
183+
let fixture = target.load_contents();
89184
let file_size = fixture.len();
185+
let fixture_str = fixture.as_str();
90186

91-
println!("Benchmarking {fixture_name} ({file_size} bytes)...");
187+
println!("Benchmarking {} ({file_size} bytes)...", target.label);
188+
if verbose {
189+
if let Some(path) = target.source_path() {
190+
println!(" Source path: {}", path.display());
191+
}
192+
}
92193

93194
// Warmup runs
94195
for i in 0..warmup {
95196
let start = Instant::now();
96-
let result = parse_lockfile(&fixture);
197+
let result = parse_lockfile(black_box(fixture_str));
97198
let duration = start.elapsed();
98-
assert!(result.is_ok(), "Should parse {fixture_name} successfully");
199+
assert!(result.is_ok(), "Should parse {} successfully", target.label);
99200

100201
if verbose {
101202
println!(
@@ -109,15 +210,15 @@ fn benchmark_fixture(
109210

110211
// Measure heap usage with a single run
111212
let before = memory_stats().unwrap();
112-
let result = parse_lockfile(&fixture);
213+
let result = parse_lockfile(black_box(fixture_str));
113214
let after = memory_stats().unwrap();
114215

115216
let heap_usage = isize::try_from(after.physical_mem).expect("physical mem too large")
116217
- isize::try_from(before.physical_mem).expect("physical mem too large");
117218
let virtual_usage = isize::try_from(after.virtual_mem).expect("virtual mem too large")
118219
- isize::try_from(before.virtual_mem).expect("virtual mem too large");
119220

120-
assert!(result.is_ok(), "Should parse {fixture_name} successfully");
221+
assert!(result.is_ok(), "Should parse {} successfully", target.label);
121222

122223
if verbose {
123224
println!(" Heap usage: {heap_usage} bytes (physical), {virtual_usage} bytes (virtual)");
@@ -128,7 +229,7 @@ fn benchmark_fixture(
128229

129230
for i in 0..runs {
130231
let start = Instant::now();
131-
let result = parse_lockfile(&fixture);
232+
let result = parse_lockfile(black_box(fixture_str));
132233
let duration = start.elapsed();
133234
let time_ms = duration.as_secs_f64() * 1000.0;
134235
times.push(time_ms);
@@ -137,28 +238,31 @@ fn benchmark_fixture(
137238
println!(" Run {}: {:.3}ms", i + 1, time_ms);
138239
}
139240

140-
assert!(result.is_ok(), "Should parse {fixture_name} successfully");
241+
assert!(result.is_ok(), "Should parse {} successfully", target.label);
141242
}
142243

143-
let (mean, min, max, std_dev) = calculate_stats(&times);
244+
let stats = calculate_stats(&times);
144245

145246
// Derived metrics
146247
let kib = file_size as f64 / 1024.0;
147-
let time_per_kib_ms = if kib > 0.0 { mean / kib } else { 0.0 };
248+
let time_per_kib_ms = if kib > 0.0 { stats.mean / kib } else { 0.0 };
148249
let mb = file_size as f64 / 1_000_000.0;
149-
let mb_per_s = if mean > 0.0 {
150-
mb / (mean / 1000.0)
250+
let mb_per_s = if stats.mean > 0.0 {
251+
mb / (stats.mean / 1000.0)
151252
} else {
152253
f64::INFINITY
153254
};
154255

155256
BenchmarkResult {
156-
fixture: fixture_name.to_string(),
257+
fixture: target.label.clone(),
157258
file_size,
158-
mean_time_ms: mean,
159-
min_time_ms: min,
160-
max_time_ms: max,
161-
std_dev_ms: std_dev,
259+
mean_time_ms: stats.mean,
260+
min_time_ms: stats.min,
261+
max_time_ms: stats.max,
262+
std_dev_ms: stats.std_dev,
263+
std_error_ms: stats.std_error,
264+
ci_95_lower_ms: stats.ci_low,
265+
ci_95_upper_ms: stats.ci_high,
162266
runs,
163267
heap_usage_bytes: Some(heap_usage.unsigned_abs()),
164268
virtual_usage_bytes: Some(virtual_usage.unsigned_abs()),
@@ -259,17 +363,19 @@ fn print_results(results: &[BenchmarkResult], format: &str) {
259363
} else {
260364
println!("\nBenchmark Results:");
261365
println!(
262-
"{:<28} {:>12} {:>12} {:>12} {:>12} {:>12} {:>12}",
263-
"Fixture", "Bytes", "Mean (ms)", "Min (ms)", "Max (ms)", "ms/KiB", "MB/s"
366+
"{:<28} {:>12} {:>20} {:>12} {:>12} {:>12} {:>12}",
367+
"Fixture", "Bytes", "Mean +/- CI95 (ms)", "Min (ms)", "Max (ms)", "ms/KiB", "MB/s"
264368
);
265-
println!("{:-<104}", "");
369+
println!("{:-<120}", "");
266370

267371
for result in results {
372+
let ci_margin = (result.ci_95_upper_ms - result.ci_95_lower_ms).abs() / 2.0;
373+
let mean_column = format!("{:.3} +/- {:.3}", result.mean_time_ms, ci_margin);
268374
println!(
269-
"{:<28} {:>12} {:>12.3} {:>12.3} {:>12.3} {:>12.3} {:>12.2}",
375+
"{:<28} {:>12} {:>20} {:>12.3} {:>12.3} {:>12.3} {:>12.2}",
270376
result.fixture,
271377
result.file_size,
272-
result.mean_time_ms,
378+
mean_column,
273379
result.min_time_ms,
274380
result.max_time_ms,
275381
result.time_per_kib_ms,
@@ -282,23 +388,28 @@ fn print_results(results: &[BenchmarkResult], format: &str) {
282388
fn main() {
283389
let args = Args::parse();
284390

285-
let fixtures = if let Some(fixture) = args.fixture {
286-
vec![fixture]
391+
let fixtures: Vec<FixtureTarget> = if let Some(path) = args.fixture_path {
392+
vec![FixtureTarget::from_path(path)]
393+
} else if let Some(fixture) = args.fixture {
394+
vec![FixtureTarget::from_fixture_name(fixture)]
287395
} else if args.all {
288396
discover_all_fixture_names()
397+
.into_iter()
398+
.map(FixtureTarget::from_fixture_name)
399+
.collect()
289400
} else {
290401
// Default to a few key fixtures
291402
vec![
292-
"minimal-berry.lock".to_string(),
293-
"workspaces.yarn.lock".to_string(),
294-
"auxiliary-packages.yarn.lock".to_string(),
403+
FixtureTarget::from_fixture_name("minimal-berry.lock"),
404+
FixtureTarget::from_fixture_name("workspaces.yarn.lock"),
405+
FixtureTarget::from_fixture_name("auxiliary-packages.yarn.lock"),
295406
]
296407
};
297408

298409
let mut results = Vec::new();
299410

300-
for fixture in fixtures {
301-
let result = benchmark_fixture(&fixture, args.warmup, args.runs, args.verbose);
411+
for fixture in &fixtures {
412+
let result = benchmark_fixture(fixture, args.warmup, args.runs, args.verbose);
302413
results.push(result);
303414
}
304415

0 commit comments

Comments
 (0)