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:
@@ -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(|| {
|
||||
|
||||
Reference in New Issue
Block a user