Skip to content

Commit d505305

Browse files
bench_tools: add absolute limits to benchmark runs
1 parent b246fe1 commit d505305

File tree

5 files changed

+113
-18
lines changed

5 files changed

+113
-18
lines changed

crates/bench_tools/src/comparison.rs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fs;
23
use std::path::PathBuf;
34

@@ -8,7 +9,9 @@ use crate::types::estimates::Estimates;
89
pub struct BenchmarkComparison {
910
pub name: String,
1011
pub change_percentage: f64,
11-
pub exceeds_limit: bool,
12+
pub exceeds_regression_limit: bool,
13+
pub absolute_time_ns: f64,
14+
pub exceeds_absolute_limit: bool,
1215
}
1316

1417
type RegressionError = (String, Vec<BenchmarkComparison>);
@@ -37,6 +40,29 @@ fn load_change_estimates(bench_name: &str) -> Estimates {
3740
})
3841
}
3942

43+
/// Loads absolute timing estimates from criterion's new directory for a given benchmark.
44+
/// Panics if the estimates file doesn't exist.
45+
fn load_absolute_estimates(bench_name: &str) -> Estimates {
46+
let estimates_path =
47+
PathBuf::from("target/criterion").join(bench_name).join("new/estimates.json");
48+
49+
if !estimates_path.exists() {
50+
panic!(
51+
"Estimates file not found for benchmark '{}': {}\nThis likely means the benchmark \
52+
hasn't been run yet. Run the benchmark before using comparison features.",
53+
bench_name,
54+
estimates_path.display()
55+
);
56+
}
57+
58+
let data = fs::read_to_string(&estimates_path)
59+
.unwrap_or_else(|e| panic!("Failed to read {}: {}", estimates_path.display(), e));
60+
61+
serde_json::from_str(&data).unwrap_or_else(|e| {
62+
panic!("Failed to deserialize {}: {}\nContent: {}", estimates_path.display(), e, data)
63+
})
64+
}
65+
4066
/// Converts change estimates to percentage.
4167
/// The mean.point_estimate in change/estimates.json represents fractional change
4268
/// (e.g., 0.0706 = 7.06% change).
@@ -46,33 +72,48 @@ pub(crate) fn get_regression_percentage(change_estimates: &Estimates) -> f64 {
4672

4773
/// Checks all benchmarks for regressions against a specified limit.
4874
/// Returns a vector of comparison results for all benchmarks.
49-
/// If any benchmark exceeds the regression limit, returns an error with detailed results.
50-
/// Panics if change file is not found for any benchmark.
75+
/// If any benchmark exceeds the regression limit or absolute time threshold, returns an error with
76+
/// detailed results. Panics if change file is not found for any benchmark.
5177
pub fn check_regressions(
5278
bench_names: &[&str],
5379
regression_limit: f64,
80+
absolute_time_ns_limits: &HashMap<String, f64>,
5481
) -> BenchmarkComparisonsResult {
5582
let mut results = Vec::new();
5683
let mut exceeded_count = 0;
5784

5885
for bench_name in bench_names {
5986
let change_estimates = load_change_estimates(bench_name);
6087
let change_percentage = get_regression_percentage(&change_estimates);
61-
let exceeds_limit = change_percentage > regression_limit;
88+
let exceeds_regression_limit = change_percentage > regression_limit;
89+
90+
// Load absolute timing estimates.
91+
let absolute_estimates = load_absolute_estimates(bench_name);
92+
let absolute_time_ns = absolute_estimates.mean.point_estimate;
93+
94+
// Check if this benchmark has a specific absolute time limit.
95+
let exceeds_absolute_limit =
96+
if let Some(&threshold) = absolute_time_ns_limits.get(*bench_name) {
97+
absolute_time_ns > threshold
98+
} else {
99+
false
100+
};
62101

63-
if exceeds_limit {
102+
if exceeds_regression_limit || exceeds_absolute_limit {
64103
exceeded_count += 1;
65104
}
66105

67106
results.push(BenchmarkComparison {
68107
name: bench_name.to_string(),
69108
change_percentage,
70-
exceeds_limit,
109+
exceeds_regression_limit,
110+
absolute_time_ns,
111+
exceeds_absolute_limit,
71112
});
72113
}
73114

74115
if exceeded_count > 0 {
75-
let error_msg = format!("{} benchmark(s) exceeded regression threshold!", exceeded_count);
116+
let error_msg = format!("{} benchmark(s) exceeded threshold(s)!", exceeded_count);
76117
Err((error_msg, results))
77118
} else {
78119
Ok(results)

crates/bench_tools/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ pub mod runner;
88
#[cfg(test)]
99
pub mod test_utils;
1010
pub mod types;
11-
pub(crate) mod utils;
11+
pub mod utils;
1212
#[cfg(test)]
1313
mod utils_test;

crates/bench_tools/src/main.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use bench_tools::types::benchmark_config::{
66
find_benchmarks_by_package,
77
BENCHMARKS,
88
};
9+
use bench_tools::utils::parse_absolute_time_limits;
910
use clap::{Parser, Subcommand};
1011

1112
#[derive(Parser)]
@@ -45,6 +46,10 @@ enum Commands {
4546
/// Maximum acceptable regression percentage (e.g., 5.0 for 5%).
4647
#[arg(long)]
4748
regression_limit: f64,
49+
/// Set absolute time limit for a specific benchmark (can be used multiple times).
50+
/// Format: --set-absolute-time-ns-limit <bench_name> <limit_ns>
51+
#[arg(long, value_names = ["BENCH_NAME", "LIMIT_NS"], num_args = 2, action = clap::ArgAction::Append)]
52+
set_absolute_time_ns_limit: Vec<String>,
4853
},
4954
/// List benchmarks for a package.
5055
List {
@@ -75,18 +80,27 @@ fn main() {
7580

7681
bench_tools::runner::run_benchmarks(&benchmarks, input_dir.as_deref(), &out);
7782
}
78-
Commands::RunAndCompare { package, out, input_dir, regression_limit } => {
83+
Commands::RunAndCompare {
84+
package,
85+
out,
86+
input_dir,
87+
regression_limit,
88+
set_absolute_time_ns_limit,
89+
} => {
7990
let benchmarks = find_benchmarks_by_package(&package);
8091

8192
if benchmarks.is_empty() {
8293
panic!("No benchmarks found for package: {}", package);
8394
}
8495

96+
let absolute_time_ns_limits = parse_absolute_time_limits(set_absolute_time_ns_limit);
97+
8598
bench_tools::runner::run_and_compare_benchmarks(
8699
&benchmarks,
87100
input_dir.as_deref(),
88101
&out,
89102
regression_limit,
103+
absolute_time_ns_limits,
90104
);
91105
}
92106
Commands::List { package } => match package {

crates/bench_tools/src/runner.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fs;
23
use std::path::PathBuf;
34

@@ -113,6 +114,7 @@ pub fn run_and_compare_benchmarks(
113114
input_dir: Option<&str>,
114115
output_dir: &str,
115116
regression_limit: f64,
117+
absolute_time_ns_limits: HashMap<String, f64>,
116118
) {
117119
// Run benchmarks first.
118120
run_benchmarks(benchmarks, input_dir, output_dir);
@@ -123,8 +125,15 @@ pub fn run_and_compare_benchmarks(
123125
bench_names.extend(bench.criterion_benchmark_names.unwrap_or(&[bench.name]));
124126
}
125127

126-
println!("\n📊 Checking for performance regressions (limit: {}%):", regression_limit);
127-
let regression_result = crate::comparison::check_regressions(&bench_names, regression_limit);
128+
print!("\n📊 Checking for performance regressions (limit: {}%", regression_limit);
129+
if !absolute_time_ns_limits.is_empty() {
130+
print!(", {} benchmark(s) with absolute time limits", absolute_time_ns_limits.len());
131+
}
132+
let regression_result = crate::comparison::check_regressions(
133+
&bench_names,
134+
regression_limit,
135+
&absolute_time_ns_limits,
136+
);
128137

129138
match regression_result {
130139
Ok(_) => {
@@ -134,15 +143,26 @@ pub fn run_and_compare_benchmarks(
134143
// Some benchmarks exceeded the limit - print detailed results.
135144
println!("\nBenchmark Results:");
136145
for result in results {
137-
if result.exceeds_limit {
138-
println!(
139-
" ❌ {}: {:+.2}% (EXCEEDS {:.1}% limit)",
140-
result.name, result.change_percentage, regression_limit
141-
);
146+
if result.exceeds_regression_limit || result.exceeds_absolute_limit {
147+
if result.exceeds_regression_limit {
148+
println!(
149+
"❌ {}: {:+.2}% (EXCEEDS {:.1}% limit)",
150+
result.name, result.change_percentage, regression_limit
151+
);
152+
}
153+
if result.exceeds_absolute_limit {
154+
if let Some(&limit) = absolute_time_ns_limits.get(&result.name) {
155+
println!(
156+
" ❌ {}: {:.2}ns (EXCEEDS {:.0}ns limit)",
157+
result.name, result.absolute_time_ns, limit
158+
);
159+
}
160+
}
161+
println!();
142162
} else {
143163
println!(
144-
" ✓ {}: {:+.2}% (within {:.1}% limit)",
145-
result.name, result.change_percentage, regression_limit
164+
" ✓ {}: {:+.2}% | {:.2}ns",
165+
result.name, result.change_percentage, result.absolute_time_ns
146166
);
147167
}
148168
}

crates/bench_tools/src/utils.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::fs;
23
use std::path::Path;
34

@@ -61,3 +62,22 @@ pub(crate) fn copy_dir_contents(src: &Path, dst: &Path) {
6162
}
6263
}
6364
}
65+
66+
/// Parses a flat Vec<String> of benchmark names and limits into a HashMap.
67+
/// The input vector should contain pairs: [bench_name1, limit1, bench_name2, limit2, ...].
68+
///
69+
/// # Panics
70+
/// Panics if any limit value cannot be parsed as f64.
71+
pub fn parse_absolute_time_limits(args: Vec<String>) -> HashMap<String, f64> {
72+
let mut limits = HashMap::new();
73+
for chunk in args.chunks(2) {
74+
if chunk.len() == 2 {
75+
let bench_name = chunk[0].clone();
76+
let limit = chunk[1].parse::<f64>().unwrap_or_else(|_| {
77+
panic!("Invalid limit value for benchmark '{}': '{}'", bench_name, chunk[1])
78+
});
79+
limits.insert(bench_name, limit);
80+
}
81+
}
82+
limits
83+
}

0 commit comments

Comments
 (0)