use std::io::Write;
use std::path::PathBuf;
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use sqry_classpath::bytecode::classfile::parse_class;
use sqry_classpath::bytecode::generics::{
parse_class_signature, parse_field_signature, parse_method_signature,
};
use sqry_classpath::bytecode::scan_jar;
use sqry_classpath::stub::cache::StubCache;
use sqry_classpath::stub::index::ClasspathIndex;
use sqry_classpath::stub::model::{AccessFlags, ClassKind, ClassStub};
use zip::write::SimpleFileOptions;
fn generate_test_class(class_name: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(128);
bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&52u16.to_be_bytes());
let class_bytes = class_name.as_bytes();
let object_bytes = b"java/lang/Object";
let cp_count: u16 = 5;
bytes.extend_from_slice(&cp_count.to_be_bytes());
bytes.push(1);
#[expect(clippy::cast_possible_truncation)]
let class_len = class_bytes.len() as u16;
bytes.extend_from_slice(&class_len.to_be_bytes());
bytes.extend_from_slice(class_bytes);
bytes.push(7);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(1);
#[expect(clippy::cast_possible_truncation)]
let obj_len = object_bytes.len() as u16;
bytes.extend_from_slice(&obj_len.to_be_bytes());
bytes.extend_from_slice(object_bytes);
bytes.push(7);
bytes.extend_from_slice(&3u16.to_be_bytes());
bytes.extend_from_slice(&0x0021u16.to_be_bytes());
bytes.extend_from_slice(&2u16.to_be_bytes());
bytes.extend_from_slice(&4u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes
}
fn class_entry_name(pkg: usize, idx: usize) -> String {
format!("com/example/pkg{pkg}/Class{idx}.class")
}
fn class_fqn(pkg: usize, idx: usize) -> String {
format!("com.example.pkg{pkg}.Class{idx}")
}
fn generate_test_jar(class_count: usize) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("create tempdir");
let jar_path = dir.path().join("bench.jar");
let file = std::fs::File::create(&jar_path).expect("create JAR file");
let mut writer = zip::ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
let packages = 10;
for i in 0..class_count {
let pkg = i % packages;
let entry_name = class_entry_name(pkg, i);
let internal_name = format!("com/example/pkg{pkg}/Class{i}");
let class_bytes = generate_test_class(&internal_name);
writer
.start_file(&entry_name, options)
.expect("start ZIP entry");
writer.write_all(&class_bytes).expect("write class bytes");
}
writer.finish().expect("finish ZIP");
(dir, jar_path)
}
fn make_bench_stub(fqn: &str) -> ClassStub {
ClassStub {
fqn: fqn.to_owned(),
name: fqn.rsplit('.').next().unwrap_or(fqn).to_owned(),
kind: ClassKind::Class,
access: AccessFlags::new(0x0021),
superclass: Some("java.lang.Object".to_owned()),
interfaces: vec![],
methods: vec![],
fields: vec![],
annotations: vec![],
generic_signature: None,
inner_classes: vec![],
lambda_targets: vec![],
module: None,
record_components: vec![],
enum_constants: vec![],
source_file: None,
source_jar: None,
kotlin_metadata: None,
scala_signature: None,
}
}
fn generate_test_stubs(count: usize) -> Vec<ClassStub> {
let packages = 10;
(0..count)
.map(|i| {
let pkg = i % packages;
make_bench_stub(&class_fqn(pkg, i))
})
.collect()
}
fn bench_class_parsing(c: &mut Criterion) {
let class_bytes = generate_test_class("com/example/BenchClass");
c.bench_function("parse_single_class", |b| {
b.iter(|| parse_class(std::hint::black_box(&class_bytes)))
});
}
fn bench_generic_signatures(c: &mut Criterion) {
let mut group = c.benchmark_group("generic_signatures");
let simple = "Ljava/lang/Object;";
group.bench_with_input(BenchmarkId::new("class", "simple"), &simple, |b, sig| {
b.iter(|| parse_class_signature(std::hint::black_box(sig)))
});
let hashmap = "<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/util/AbstractMap<TK;TV;>;Ljava/util/Map<TK;TV;>;";
group.bench_with_input(BenchmarkId::new("class", "hashmap"), &hashmap, |b, sig| {
b.iter(|| parse_class_signature(std::hint::black_box(sig)))
});
let comparable = "<T:Ljava/lang/Comparable<TT;>;>Ljava/lang/Object;";
group.bench_with_input(
BenchmarkId::new("class", "recursive_bound"),
&comparable,
|b, sig| b.iter(|| parse_class_signature(std::hint::black_box(sig))),
);
let nested = "Ljava/util/Map<Ljava/lang/String;Ljava/util/List<Ljava/util/Map<Ljava/lang/String;Ljava/lang/Integer;>;>;>;";
group.bench_with_input(
BenchmarkId::new("field", "deeply_nested"),
&nested,
|b, sig| b.iter(|| parse_field_signature(std::hint::black_box(sig))),
);
let method =
"<T:Ljava/lang/Object;>(TT;Ljava/util/List<TT;>;)Ljava/util/Map<Ljava/lang/String;TT;>;";
group.bench_with_input(
BenchmarkId::new("method", "generic_params"),
&method,
|b, sig| b.iter(|| parse_method_signature(std::hint::black_box(sig))),
);
group.finish();
}
fn bench_jar_scanning(c: &mut Criterion) {
let mut group = c.benchmark_group("jar_scanning");
group.sample_size(10);
for &count in &[10, 50, 100, 500] {
let (_dir, jar_path) = generate_test_jar(count);
group.bench_with_input(BenchmarkId::new("classes", count), &jar_path, |b, path| {
b.iter(|| scan_jar(std::hint::black_box(path)))
});
}
group.finish();
}
fn bench_index_build(c: &mut Criterion) {
let mut group = c.benchmark_group("index_build");
for &count in &[100, 1_000, 5_000] {
let stubs = generate_test_stubs(count);
group.bench_with_input(BenchmarkId::new("stubs", count), &stubs, |b, input| {
b.iter(|| ClasspathIndex::build(std::hint::black_box(input.clone())))
});
}
group.finish();
}
fn bench_index_lookup(c: &mut Criterion) {
let stubs = generate_test_stubs(5_000);
let index = ClasspathIndex::build(stubs);
let mut group = c.benchmark_group("index_lookup");
let target_fqn = class_fqn(5, 2_500);
group.bench_function("fqn_hit", |b| {
b.iter(|| index.lookup_fqn(std::hint::black_box(&target_fqn)))
});
group.bench_function("fqn_miss", |b| {
b.iter(|| index.lookup_fqn(std::hint::black_box("com.nonexistent.Missing")))
});
group.bench_function("package", |b| {
b.iter(|| index.lookup_package(std::hint::black_box("com.example.pkg3")))
});
group.finish();
}
fn bench_stub_cache(c: &mut Criterion) {
let mut group = c.benchmark_group("stub_cache");
for &count in &[10, 100, 500] {
let stubs = generate_test_stubs(count);
let dir = tempfile::tempdir().expect("create tempdir");
let cache = StubCache::new(dir.path());
let (_jar_dir, jar_path) = generate_test_jar(count);
cache
.put(&jar_path, &stubs)
.expect("cache put during setup");
group.bench_with_input(
BenchmarkId::new("cache_read", count),
&(&cache, &jar_path),
|b, (cache, path)| b.iter(|| cache.get(std::hint::black_box(path))),
);
}
group.finish();
}
fn bench_stub_cache_write(c: &mut Criterion) {
let mut group = c.benchmark_group("stub_cache_write");
group.sample_size(20);
for &count in &[10, 100, 500] {
let stubs = generate_test_stubs(count);
let dir = tempfile::tempdir().expect("create tempdir");
let cache = StubCache::new(dir.path());
let (_jar_dir, jar_path) = generate_test_jar(count);
group.bench_with_input(
BenchmarkId::new("cache_write", count),
&(&cache, &jar_path, &stubs),
|b, (cache, path, stubs)| {
b.iter(|| cache.put(std::hint::black_box(path), std::hint::black_box(stubs)))
},
);
}
group.finish();
}
fn bench_index_persistence(c: &mut Criterion) {
let mut group = c.benchmark_group("index_persistence");
group.sample_size(20);
let stubs = generate_test_stubs(5_000);
let index = ClasspathIndex::build(stubs);
let dir = tempfile::tempdir().expect("create tempdir");
let index_path = dir.path().join("index.sqry");
group.bench_function("save_5000", |b| {
b.iter(|| index.save(std::hint::black_box(&index_path)))
});
index.save(&index_path).expect("save index for load bench");
group.bench_function("load_5000", |b| {
b.iter(|| ClasspathIndex::load(std::hint::black_box(&index_path)))
});
group.finish();
}
criterion_group!(
benches,
bench_class_parsing,
bench_generic_signatures,
bench_jar_scanning,
bench_index_build,
bench_index_lookup,
bench_stub_cache,
bench_stub_cache_write,
bench_index_persistence,
);
criterion_main!(benches);