//! h5bench-equivalent metadata workloads for clawhdf5. //! //! Measures attribute creation/read throughput and group traversal latency — //! the workloads that h5bench's `metadata` mode targets against libhdf5. use clawhdf5::{AttrValue, File, FileBuilder}; use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use tempfile::TempDir; // --------------------------------------------------------------------------- // Workload: metadata_attrs_write // Create K attributes on a single dataset. // Exercises attribute message allocation and compact → dense header transition. // --------------------------------------------------------------------------- fn bench_metadata_attrs_write(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_attrs_write"); for &k in &[4usize, 16, 64, 128] { group.throughput(Throughput::Elements(k as u64)); group.bench_with_input(BenchmarkId::new("clawhdf5", k), &k, |b, &k| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("attrs_write.h5"); b.iter(|| { let mut fb = FileBuilder::new(); let ds = fb .create_dataset("data") .with_f64_data(&[1.0, 2.0, 3.0]) .with_shape(&[3]); for i in 0..k { ds.set_attr(&format!("attr_{i:04}"), AttrValue::I64(i as i64)); } fb.write(&path).unwrap(); }); }); #[cfg(feature = "libhdf5-compare")] group.bench_with_input(BenchmarkId::new("libhdf5", k), &k, |b, &k| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("attrs_libhdf5.h5"); b.iter(|| { let file = hdf5::File::create(&path).unwrap(); let ds = file.new_dataset::().shape([3]).create("data").unwrap(); ds.write(&[1.0f64, 2.0, 3.0]).unwrap(); for i in 0..k { ds.new_attr::() .create(format!("attr_{i:04}").as_str()) .unwrap() .write_scalar(&(i as i64)) .unwrap(); } }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_attrs_read // Open a pre-built file and read all K attributes back. // --------------------------------------------------------------------------- fn bench_metadata_attrs_read(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_attrs_read"); for &k in &[4usize, 16, 64, 128] { // Build the reference file in memory. let bytes = { let mut fb = FileBuilder::new(); let ds = fb .create_dataset("data") .with_f64_data(&[1.0, 2.0, 3.0]) .with_shape(&[3]); for i in 0..k { ds.set_attr(&format!("attr_{i:04}"), AttrValue::I64(i as i64)); } fb.finish().unwrap() }; group.throughput(Throughput::Elements(k as u64)); group.bench_with_input(BenchmarkId::new("clawhdf5", k), &bytes, |b, raw| { b.iter(|| { let file = File::from_bytes(raw.clone()).unwrap(); let ds = file.dataset("data").unwrap(); ds.attrs().unwrap() }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_groups_create // Create K top-level groups (no datasets inside). // Measures link-storage allocation: compact → dense B-tree transition. // --------------------------------------------------------------------------- fn bench_metadata_groups_create(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_groups_create"); for &k in &[4usize, 16, 32, 64] { group.throughput(Throughput::Elements(k as u64)); group.bench_with_input(BenchmarkId::new("clawhdf5", k), &k, |b, &k| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("groups_create.h5"); b.iter(|| { let mut fb = FileBuilder::new(); for i in 0..k { let mut g = fb.create_group(&format!("group_{i:04}")); // Minimal dataset inside each group to make it non-trivial. g.create_dataset("x").with_f64_data(&[0.0]); let finished = g.finish(); fb.add_group(finished); } fb.write(&path).unwrap(); }); }); #[cfg(feature = "libhdf5-compare")] group.bench_with_input(BenchmarkId::new("libhdf5", k), &k, |b, &k| { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("groups_libhdf5.h5"); b.iter(|| { let file = hdf5::File::create(&path).unwrap(); for i in 0..k { let g = file.create_group(&format!("group_{i:04}")).unwrap(); g.new_dataset::() .shape([1]) .create("x") .unwrap() .write(&[0.0f64]) .unwrap(); } }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_groups_traverse // Open a pre-built file with K groups and traverse (list) the root group. // --------------------------------------------------------------------------- fn bench_metadata_groups_traverse(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_groups_traverse"); for &k in &[4usize, 16, 32, 64] { // Pre-build. let bytes = { let mut fb = FileBuilder::new(); for i in 0..k { let mut g = fb.create_group(&format!("group_{i:04}")); g.create_dataset("x").with_f64_data(&[0.0]); let finished = g.finish(); fb.add_group(finished); } fb.finish().unwrap() }; group.throughput(Throughput::Elements(k as u64)); group.bench_with_input(BenchmarkId::new("clawhdf5", k), &bytes, |b, raw| { b.iter(|| { let file = File::from_bytes(raw.clone()).unwrap(); let root = file.root(); root.groups().unwrap() }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_roundtrip_string_attrs // Write and read back K variable-length string attributes. // String attrs require a dedicated VL heap entry — distinct from numeric ones. // --------------------------------------------------------------------------- fn bench_metadata_string_attrs(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_string_attrs"); for &k in &[4usize, 16, 32] { group.throughput(Throughput::Elements(k as u64)); group.bench_with_input(BenchmarkId::new("clawhdf5", k), &k, |b, &k| { b.iter(|| { let mut fb = FileBuilder::new(); let ds = fb .create_dataset("data") .with_f64_data(&[1.0]) .with_shape(&[1]); for i in 0..k { ds.set_attr( &format!("label_{i:04}"), AttrValue::String(format!("value-{i}-some-longer-string-payload")), ); } let bytes = fb.finish().unwrap(); // Immediately read back to exercise both directions. let file = File::from_bytes(bytes).unwrap(); let ds_r = file.dataset("data").unwrap(); ds_r.attrs().unwrap() }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_open_from_disk // Open a small pre-built file from disk and resolve one attribute. Both // sides pay the OS open()/read() cost plus header-parse cost, so this is a // fair, I/O-inclusive "open a file and touch its metadata" comparison — the // honest version of the "metadata parse" claim this benchmark replaces. // --------------------------------------------------------------------------- fn bench_metadata_open_from_disk(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_open_from_disk"); group.throughput(Throughput::Elements(1)); let tmp = TempDir::new().unwrap(); let clawhdf5_path = tmp.path().join("open_clawhdf5.h5"); { let mut fb = FileBuilder::new(); let ds = fb .create_dataset("data") .with_f64_data(&[1.0, 2.0, 3.0]) .with_shape(&[3]); ds.set_attr("label", AttrValue::I64(42)); fb.write(&clawhdf5_path).unwrap(); } group.bench_function("clawhdf5", |b| { b.iter(|| { let raw = std::fs::read(&clawhdf5_path).unwrap(); let file = File::from_bytes(raw).unwrap(); let ds = file.dataset("data").unwrap(); ds.attrs().unwrap() }); }); #[cfg(feature = "libhdf5-compare")] { let libhdf5_path = tmp.path().join("open_libhdf5.h5"); { let file = hdf5::File::create(&libhdf5_path).unwrap(); let ds = file.new_dataset::().shape([3]).create("data").unwrap(); ds.write(&[1.0f64, 2.0, 3.0]).unwrap(); ds.new_attr::() .create("label") .unwrap() .write_scalar(&42i64) .unwrap(); } group.bench_function("libhdf5", |b| { b.iter(|| { let file = hdf5::File::open(&libhdf5_path).unwrap(); let ds = file.dataset("data").unwrap(); let _: i64 = ds.attr("label").unwrap().read_scalar().unwrap(); }); }); } group.finish(); } // --------------------------------------------------------------------------- // Workload: metadata_parse_in_memory (clawhdf5-only) // Times File::from_bytes() alone on bytes already resident in memory — i.e. // the header-parse cost with disk I/O excluded. There is no fair libhdf5 // equivalent (its API has no "parse from an in-memory buffer" path that // skips the OS open), so this is reported standalone, not as a speedup // multiple against libhdf5. See metadata_open_from_disk above for the // I/O-inclusive, directly comparable number. // --------------------------------------------------------------------------- fn bench_metadata_parse_in_memory(c: &mut Criterion) { let mut group = c.benchmark_group("metadata_parse_in_memory"); group.throughput(Throughput::Elements(1)); let bytes = { let mut fb = FileBuilder::new(); let ds = fb .create_dataset("data") .with_f64_data(&[1.0, 2.0, 3.0]) .with_shape(&[3]); ds.set_attr("label", AttrValue::I64(42)); fb.finish().unwrap() }; group.bench_with_input( BenchmarkId::new("clawhdf5", "in_memory"), &bytes, |b, raw| { b.iter(|| { let file = File::from_bytes(raw.clone()).unwrap(); let ds = file.dataset("data").unwrap(); ds.attrs().unwrap() }); }, ); group.finish(); } criterion_group!( meta_benches, bench_metadata_attrs_write, bench_metadata_attrs_read, bench_metadata_groups_create, bench_metadata_groups_traverse, bench_metadata_string_attrs, bench_metadata_open_from_disk, bench_metadata_parse_in_memory, ); criterion_main!(meta_benches);