Benchmarks: replace mocks with real build_index, search, and normalize
Previous benchmarks simulated core operations with inline loops that
didn't exercise the actual code paths. Now each benchmark calls the
real implementation:
- build_index_real: full build_index() on petstore fixture
- search_real_pet: SearchEngine::search("pet") against real index
- search_real_multi_term: multi-term query "list all pets"
- search_real_case_insensitive: case-insensitive search path
- normalize_json_input: normalize_to_json on JSON input
- pipeline_normalize_new vs pipeline_normalize_old: proves the
double-parse elimination (new returns tuple vs old parse-twice)
- detect_format_json_no_hints: byte-prefix format sniffing
- detect_format_with_content_type: format detection with hint
All benchmarks now use black_box correctly (consume return values,
not just inputs) to prevent dead-code elimination.
This commit is contained in:
186
benches/perf.rs
186
benches/perf.rs
@@ -4,9 +4,13 @@
|
||||
//! First run establishes baseline. Subsequent runs report regressions.
|
||||
|
||||
use std::fs;
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use swagger_cli::core::indexer::build_index;
|
||||
use swagger_cli::core::search::{SearchEngine, SearchOptions};
|
||||
use swagger_cli::core::spec::SpecIndex;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||
@@ -22,34 +26,32 @@ fn load_petstore_json() -> serde_json::Value {
|
||||
serde_json::from_slice(&bytes).expect("failed to parse petstore.json")
|
||||
}
|
||||
|
||||
fn load_petstore_index() -> SpecIndex {
|
||||
let raw_json = load_petstore_json();
|
||||
build_index(&raw_json, "sha256:bench", 1).expect("failed to build index")
|
||||
}
|
||||
|
||||
fn bench_json_parse(c: &mut Criterion) {
|
||||
let path = fixture_path("petstore.json");
|
||||
let bytes = fs::read(&path).expect("failed to read petstore.json fixture");
|
||||
|
||||
c.bench_function("parse_petstore_json", |b| {
|
||||
b.iter(|| {
|
||||
let _: serde_json::Value = serde_json::from_slice(&bytes).expect("parse failed");
|
||||
let v: serde_json::Value =
|
||||
serde_json::from_slice(black_box(&bytes)).expect("parse failed");
|
||||
black_box(v);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_build_index(c: &mut Criterion) {
|
||||
fn bench_build_index_real(c: &mut Criterion) {
|
||||
let raw_json = load_petstore_json();
|
||||
|
||||
c.bench_function("build_index_petstore", |b| {
|
||||
c.bench_function("build_index_real", |b| {
|
||||
b.iter(|| {
|
||||
// Simulate endpoint extraction (mirrors build_index core logic)
|
||||
if let Some(paths) = raw_json.get("paths").and_then(|v| v.as_object()) {
|
||||
let mut endpoints = Vec::new();
|
||||
for (path, methods) in paths {
|
||||
if let Some(methods_map) = methods.as_object() {
|
||||
for (method, _op) in methods_map {
|
||||
endpoints.push((path.clone(), method.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(!endpoints.is_empty());
|
||||
}
|
||||
let index =
|
||||
build_index(black_box(&raw_json), "sha256:bench", 1).expect("build_index failed");
|
||||
black_box(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -62,8 +64,9 @@ fn bench_hash_computation(c: &mut Criterion) {
|
||||
b.iter(|| {
|
||||
use sha2::{Digest, Sha256};
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&bytes);
|
||||
let _result = format!("sha256:{:x}", hasher.finalize());
|
||||
hasher.update(black_box(&bytes));
|
||||
let result = format!("sha256:{:x}", hasher.finalize());
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -73,44 +76,131 @@ fn bench_json_pointer_resolution(c: &mut Criterion) {
|
||||
|
||||
c.bench_function("json_pointer_resolution", |b| {
|
||||
b.iter(|| {
|
||||
let _ = raw_json
|
||||
let a = raw_json
|
||||
.pointer("/paths/~1pets/get/summary")
|
||||
.map(|v| v.as_str());
|
||||
let _ = raw_json
|
||||
let b_val = raw_json
|
||||
.pointer("/components/schemas/Pet")
|
||||
.map(|v| v.is_object());
|
||||
let _ = raw_json.pointer("/info/title").map(|v| v.as_str());
|
||||
let c_val = raw_json.pointer("/info/title").map(|v| v.as_str());
|
||||
black_box((a, b_val, c_val));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_search_scoring(c: &mut Criterion) {
|
||||
let raw_json = load_petstore_json();
|
||||
fn bench_search_real(c: &mut Criterion) {
|
||||
let index = load_petstore_index();
|
||||
let engine = SearchEngine::new(&index);
|
||||
let opts = SearchOptions::default();
|
||||
|
||||
// Pre-extract summaries for search simulation
|
||||
let mut summaries: Vec<String> = Vec::new();
|
||||
if let Some(paths) = raw_json.get("paths").and_then(|v| v.as_object()) {
|
||||
for (_path, methods) in paths {
|
||||
if let Some(methods_map) = methods.as_object() {
|
||||
for (_method, op) in methods_map {
|
||||
if let Some(summary) = op.get("summary").and_then(|v| v.as_str()) {
|
||||
summaries.push(summary.to_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.bench_function("search_scoring_pet", |b| {
|
||||
let query = "pet";
|
||||
c.bench_function("search_real_pet", |b| {
|
||||
b.iter(|| {
|
||||
let mut matches = 0u32;
|
||||
for summary in &summaries {
|
||||
if summary.contains(query) {
|
||||
matches += 1;
|
||||
let results = engine.search(black_box("pet"), &opts);
|
||||
black_box(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_search_multi_term(c: &mut Criterion) {
|
||||
let index = load_petstore_index();
|
||||
let engine = SearchEngine::new(&index);
|
||||
let opts = SearchOptions::default();
|
||||
|
||||
c.bench_function("search_real_multi_term", |b| {
|
||||
b.iter(|| {
|
||||
let results = engine.search(black_box("list all pets"), &opts);
|
||||
black_box(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
assert!(matches > 0);
|
||||
|
||||
fn bench_search_case_insensitive(c: &mut Criterion) {
|
||||
let index = load_petstore_index();
|
||||
let engine = SearchEngine::new(&index);
|
||||
let opts = SearchOptions {
|
||||
case_sensitive: false,
|
||||
..SearchOptions::default()
|
||||
};
|
||||
|
||||
c.bench_function("search_real_case_insensitive", |b| {
|
||||
b.iter(|| {
|
||||
let results = engine.search(black_box("PET"), &opts);
|
||||
black_box(results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_normalize_json(c: &mut Criterion) {
|
||||
let path = fixture_path("petstore.json");
|
||||
let bytes = fs::read(&path).expect("failed to read petstore.json fixture");
|
||||
|
||||
c.bench_function("normalize_json_input", |b| {
|
||||
b.iter(|| {
|
||||
let result = swagger_cli::core::indexer::normalize_to_json(
|
||||
black_box(&bytes),
|
||||
swagger_cli::core::indexer::Format::Json,
|
||||
)
|
||||
.expect("normalize failed");
|
||||
black_box(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_normalize_and_parse_pipeline(c: &mut Criterion) {
|
||||
let path = fixture_path("petstore.json");
|
||||
let bytes = fs::read(&path).expect("failed to read petstore.json fixture");
|
||||
|
||||
// New way: normalize returns both bytes and value (one parse)
|
||||
c.bench_function("pipeline_normalize_new", |b| {
|
||||
b.iter(|| {
|
||||
let (json_bytes, value) = swagger_cli::core::indexer::normalize_to_json(
|
||||
black_box(&bytes),
|
||||
swagger_cli::core::indexer::Format::Json,
|
||||
)
|
||||
.expect("normalize failed");
|
||||
black_box((&json_bytes, &value));
|
||||
});
|
||||
});
|
||||
|
||||
// Old way simulation: parse to validate + copy, then parse again
|
||||
c.bench_function("pipeline_normalize_old", |b| {
|
||||
b.iter(|| {
|
||||
// Step 1: old normalize_to_json did: parse(validate), copy bytes
|
||||
let _: serde_json::Value =
|
||||
serde_json::from_slice(black_box(&bytes)).expect("validate failed");
|
||||
let json_bytes = bytes.to_vec();
|
||||
// Step 2: caller would parse again
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_slice(&json_bytes).expect("parse failed");
|
||||
black_box((&json_bytes, &value));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_detect_format_json_no_hints(c: &mut Criterion) {
|
||||
let path = fixture_path("petstore.json");
|
||||
let bytes = fs::read(&path).expect("failed to read petstore.json fixture");
|
||||
|
||||
c.bench_function("detect_format_json_no_hints", |b| {
|
||||
b.iter(|| {
|
||||
let fmt = swagger_cli::core::indexer::detect_format(black_box(&bytes), None, None);
|
||||
black_box(fmt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_detect_format_with_hint(c: &mut Criterion) {
|
||||
let path = fixture_path("petstore.json");
|
||||
let bytes = fs::read(&path).expect("failed to read petstore.json fixture");
|
||||
|
||||
c.bench_function("detect_format_with_content_type", |b| {
|
||||
b.iter(|| {
|
||||
let fmt = swagger_cli::core::indexer::detect_format(
|
||||
black_box(&bytes),
|
||||
None,
|
||||
Some("application/json"),
|
||||
);
|
||||
black_box(fmt);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -118,9 +208,15 @@ fn bench_search_scoring(c: &mut Criterion) {
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_json_parse,
|
||||
bench_build_index,
|
||||
bench_build_index_real,
|
||||
bench_hash_computation,
|
||||
bench_json_pointer_resolution,
|
||||
bench_search_scoring,
|
||||
bench_search_real,
|
||||
bench_search_multi_term,
|
||||
bench_search_case_insensitive,
|
||||
bench_normalize_json,
|
||||
bench_normalize_and_parse_pipeline,
|
||||
bench_detect_format_json_no_hints,
|
||||
bench_detect_format_with_hint,
|
||||
);
|
||||
criterion_main!(benches);
|
||||
|
||||
Reference in New Issue
Block a user