concurrent_read reads one shared File from 1-16 threads: every dataset in full (distinct datasets per thread) and random hyperslabs of one dataset, over a deflate and a contiguous file it generates (or reuses while manifest.json matches). It reports decoded MB/s and scaling efficiency, warm or --cold (posix_fadvise) page cache, sizes the decode pool with --decode-threads, and writes JSON. scripts/concurrent_read_h5py.py runs the same workload on the same files with h5py threads or spawned processes (same splitmix64 data and slab stream, checked at spot elements), and compare_concurrent_read.py prints one table and refuses runs with different workloads. A smoke test runs all three end to end on tiny files (h5py half honours CLAWHDF5_PYTHON / CLAWHDF5_REQUIRE_INTEROP). BENCHMARKS.md gets a "Concurrent reads" section with the commands, marked not yet measured. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
149 lines
4.4 KiB
Rust
149 lines
4.4 KiB
Rust
//! Keeps the concurrent-read harnesses working: runs `concurrent_read`, the
|
|
//! h5py script (threads and processes) and the comparison script end to end
|
|
//! on tiny files. h5py reading the files also checks, element by element at
|
|
//! spot positions, that both harnesses generate the same data and slabs.
|
|
//!
|
|
//! The h5py half is skipped when python3 with h5py is unavailable, unless
|
|
//! `CLAWHDF5_REQUIRE_INTEROP=1`; `CLAWHDF5_PYTHON` picks the interpreter.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn python() -> String {
|
|
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
|
|
}
|
|
|
|
fn interop_required() -> bool {
|
|
std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1")
|
|
}
|
|
|
|
fn python_available() -> bool {
|
|
Command::new(python())
|
|
.args(["-c", "import h5py, numpy"])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
fn scripts() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("scripts")
|
|
}
|
|
|
|
fn run(cmd: &mut Command) -> String {
|
|
let out = cmd.output().expect("spawn");
|
|
assert!(
|
|
out.status.success(),
|
|
"{cmd:?} failed\nSTDOUT:\n{}\nSTDERR:\n{}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
String::from_utf8_lossy(&out.stdout).into_owned()
|
|
}
|
|
|
|
const SMALL: [&str; 8] = [
|
|
"--threads",
|
|
"1,2",
|
|
"--slabs",
|
|
"8",
|
|
"--reps",
|
|
"1",
|
|
"--slab",
|
|
"64",
|
|
];
|
|
|
|
fn results(path: &Path) -> serde_json::Value {
|
|
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn harnesses_run_end_to_end_on_tiny_files() {
|
|
let dir = tempfile::TempDir::new().unwrap();
|
|
let data = dir.path().join("data");
|
|
let claw = dir.path().join("claw.json");
|
|
|
|
let bin = env!("CARGO_BIN_EXE_concurrent_read");
|
|
run(Command::new(bin)
|
|
.arg("--dir")
|
|
.arg(&data)
|
|
.args(["--datasets", "3", "--mib", "1"])
|
|
.args(SMALL)
|
|
.arg("--json")
|
|
.arg(&claw));
|
|
// Second run reuses the files (and exercises --cold).
|
|
let out = Command::new(bin)
|
|
.arg("--dir")
|
|
.arg(&data)
|
|
.args(["--datasets", "3", "--mib", "1", "--cold"])
|
|
.args(SMALL)
|
|
.output()
|
|
.unwrap();
|
|
assert!(out.status.success());
|
|
assert!(String::from_utf8_lossy(&out.stderr).contains("reusing"));
|
|
|
|
let doc = results(&claw);
|
|
assert_eq!(doc["tool"], "clawhdf5");
|
|
// 2 layouts x 2 modes x 2 thread counts.
|
|
assert_eq!(doc["results"].as_array().unwrap().len(), 8);
|
|
for r in doc["results"].as_array().unwrap() {
|
|
assert!(r["mb_s"].as_f64().unwrap() > 0.0, "{r}");
|
|
}
|
|
|
|
if !python_available() {
|
|
assert!(
|
|
!interop_required(),
|
|
"CLAWHDF5_REQUIRE_INTEROP=1 but {} has no h5py",
|
|
python()
|
|
);
|
|
eprintln!("skipping the h5py half: no h5py in {}", python());
|
|
return;
|
|
}
|
|
let mut jsons = vec![claw];
|
|
for executor in ["threads", "processes"] {
|
|
let out = dir.path().join(format!("h5py-{executor}.json"));
|
|
run(Command::new(python())
|
|
.arg(scripts().join("concurrent_read_h5py.py"))
|
|
.arg("--dir")
|
|
.arg(&data)
|
|
.args(["--executor", executor])
|
|
.args(SMALL)
|
|
.arg("--json")
|
|
.arg(&out));
|
|
let doc = results(&out);
|
|
assert_eq!(doc["tool"], format!("h5py-{executor}"));
|
|
assert_eq!(doc["results"].as_array().unwrap().len(), 8);
|
|
jsons.push(out);
|
|
}
|
|
let table = run(Command::new(python())
|
|
.arg(scripts().join("compare_concurrent_read.py"))
|
|
.args(&jsons));
|
|
assert!(table.contains("| deflate | same | 2 |"), "{table}");
|
|
assert!(table.contains("clawhdf5 / h5py-processes"), "{table}");
|
|
|
|
// A different workload must not be compared.
|
|
let other = dir.path().join("other.json");
|
|
run(Command::new(python())
|
|
.arg(scripts().join("concurrent_read_h5py.py"))
|
|
.arg("--dir")
|
|
.arg(&data)
|
|
.args([
|
|
"--threads",
|
|
"1",
|
|
"--slabs",
|
|
"4",
|
|
"--reps",
|
|
"1",
|
|
"--slab",
|
|
"64",
|
|
])
|
|
.arg("--json")
|
|
.arg(&other));
|
|
let out = Command::new(python())
|
|
.arg(scripts().join("compare_concurrent_read.py"))
|
|
.arg(&jsons[0])
|
|
.arg(&other)
|
|
.output()
|
|
.unwrap();
|
|
assert!(!out.status.success());
|
|
assert!(String::from_utf8_lossy(&out.stderr).contains("slabs"));
|
|
}
|