perf(py): datasets and groups keep their address; groups their links

Every ds[...] and g[k] resolved the path from the root again, two or
three times per open, and resolving a name in a large group scans its
links: visiting a group was O(n^2). 4000 scalar datasets in one group
took 39 s (v1 group) and 131 s (dense) to list, read and re-read; now
0.3 s each. A Dataset keeps its object address, a Group (and the file's
root) its address and, after the first lookup, its link table.

New facade API File::dataset_at(address), tested in integration_tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 09:01:54 -05:00
co-authored by Claude Opus 5.5
parent b43bd2e67f
commit f0ecae38b6
9 changed files with 349 additions and 163 deletions
+21 -20
View File
@@ -3,12 +3,11 @@
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use pyo3::exceptions::PyKeyError;
use pyo3::prelude::*;
use pyo3::types::PyList;
use crate::attrs::PyAttrs;
use crate::group::{self, PyGroup, WriteGroupState, finalize_write_group};
use crate::group::{PyGroup, ReadGroup, WriteGroupState, finalize_write_group};
use crate::{DatasetSpec, OwnedAttrValue, apply_dataset_spec, extract_numpy_data, to_py_err};
/// Internal state for write mode.
@@ -40,7 +39,8 @@ pub struct PyFile {
}
enum FileInner {
Read(Arc<clawhdf5_rs::File>),
/// The root group; it holds the file.
Read(ReadGroup),
Write(WriteState),
}
@@ -61,7 +61,7 @@ impl PyFile {
crate::no_panic(|| clawhdf5_rs::File::open(path).map_err(to_py_err))
})?;
Ok(Self {
inner: Some(FileInner::Read(Arc::new(file))),
inner: Some(FileInner::Read(root_group(Arc::new(file)))),
filename,
})
}
@@ -110,33 +110,28 @@ impl PyFile {
/// Get a child object (dataset or group) by path; `f['/']` is the root.
fn __getitem__(&self, py: Python<'_>, key: &str) -> PyResult<Py<PyAny>> {
group::get_item(py, self.read_file()?, "", key)
self.read_file()?.get_item(py, key)
}
/// `f.get(key, default=None)`.
#[pyo3(signature = (key, default=None))]
fn get(&self, py: Python<'_>, key: &str, default: Option<Py<PyAny>>) -> PyResult<Py<PyAny>> {
match group::get_item(py, self.read_file()?, "", key) {
Err(e) if e.is_instance_of::<PyKeyError>(py) => {
Ok(default.unwrap_or_else(|| py.None()))
}
other => other,
}
self.read_file()?.get(py, key, default)
}
/// List the names of all children in the root group.
fn keys(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let names = group::member_names(self.read_file()?, "")?;
let names = self.read_file()?.member_names()?;
Ok(PyList::new(py, names)?.into_any().unbind())
}
fn values(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let vals = group::values(py, self.read_file()?, "")?;
let vals = self.read_file()?.values(py)?;
Ok(PyList::new(py, vals)?.into_any().unbind())
}
fn items(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let items = group::items(py, self.read_file()?, "")?;
let items = self.read_file()?.items(py)?;
Ok(PyList::new(py, items)?.into_any().unbind())
}
@@ -145,7 +140,7 @@ impl PyFile {
}
fn __len__(&self) -> PyResult<usize> {
Ok(group::member_names(self.read_file()?, "")?.len())
Ok(self.read_file()?.member_names()?.len())
}
/// The root group's name, `/`.
@@ -211,7 +206,7 @@ impl PyFile {
#[getter]
fn attrs(&self) -> PyResult<PyAttrs> {
match self.inner.as_ref() {
Some(FileInner::Read(file)) => PyAttrs::read(Arc::clone(file), ""),
Some(FileInner::Read(root)) => root.attrs(),
Some(FileInner::Write(state)) => Ok(PyAttrs::from_write(Arc::clone(&state.root_attrs))),
None => Err(PyErr::new::<pyo3::exceptions::PyIOError, _>(
"file is closed",
@@ -221,8 +216,8 @@ impl PyFile {
fn __repr__(&self) -> String {
match &self.inner {
Some(FileInner::Read(f)) => {
format!("<HDF5 File (read, {} bytes)>", f.as_bytes().len())
Some(FileInner::Read(root)) => {
format!("<HDF5 File (read, {} bytes)>", root.file.as_bytes().len())
}
Some(FileInner::Write(s)) => {
format!("<HDF5 File (write, \"{}\")>", s.path.display())
@@ -232,12 +227,13 @@ impl PyFile {
}
fn __contains__(&self, key: &str) -> PyResult<bool> {
Ok(group::contains(self.read_file()?, "", key))
Ok(self.read_file()?.contains(key))
}
}
impl PyFile {
fn read_file(&self) -> PyResult<&Arc<clawhdf5_rs::File>> {
/// The root group of a file opened for reading.
fn read_file(&self) -> PyResult<&ReadGroup> {
match &self.inner {
Some(FileInner::Read(f)) => Ok(f),
Some(FileInner::Write(_)) => Err(PyErr::new::<pyo3::exceptions::PyIOError, _>(
@@ -275,6 +271,11 @@ fn parse_compression(
}
}
fn root_group(file: Arc<clawhdf5_rs::File>) -> ReadGroup {
let root = file.superblock().root_group_address;
ReadGroup::new(file, String::new(), root)
}
/// Build and write the HDF5 file from accumulated write state.
fn finalize_write(state: WriteState) -> PyResult<()> {
crate::no_panic(|| {