use arrow::array::ArrayRef;
use arrow::datatypes::Field;
use re_chunk::{Chunk, ChunkComponents, ChunkId, EntityPath, TimeColumn};
use crate::config::Hdf5Config;
use crate::convert;
use crate::error::Hdf5Error;
use crate::plan::{EmitUnit, Hdf5Plan, PlannedTimeline};
use crate::walk::DatasetDesc;
struct UnitColumn {
desc: DatasetDesc,
dataset: hdf5_pure::Dataset,
}
impl UnitColumn {
fn window_values(&self, start: usize, len: usize) -> Result<(Field, ArrayRef), Hdf5Error> {
convert::read_row_values(&self.dataset, &self.desc, start, len)
}
}
struct PendingData {
entity: EntityPath,
columns: Vec<UnitColumn>,
num_rows: usize,
rows_per_window: usize,
next_row: usize,
}
pub(crate) struct Hdf5ChunkIterator {
file: hdf5_pure::File,
units: std::vec::IntoIter<EmitUnit>,
timeline: Option<PlannedTimeline>,
use_structs: bool,
chunk_max_bytes: u64,
chunk_max_rows: u64,
pending: Option<PendingData>,
}
impl Hdf5ChunkIterator {
pub fn new(file: hdf5_pure::File, plan: Hdf5Plan, config: &Hdf5Config) -> Self {
Self {
file,
units: plan.units.into_iter(),
timeline: plan.timeline,
use_structs: config.use_structs,
chunk_max_bytes: config.chunk_max_bytes,
chunk_max_rows: config.chunk_max_rows,
pending: None,
}
}
}
impl Iterator for Hdf5ChunkIterator {
type Item = Result<Chunk, Hdf5Error>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self
.pending
.as_ref()
.is_some_and(|pending| pending.next_row < pending.num_rows)
{
return Some(self.emit_window());
}
self.pending = None;
match self.units.next()? {
EmitUnit::Attributes { entity, attrs } => {
return Some(build_attributes_chunk(entity, &attrs));
}
EmitUnit::StaticScalars { entity, datasets } => {
return Some(self.build_static_scalars_chunk(entity, &datasets));
}
EmitUnit::Data { entity, datasets } => {
if let Err(err) = self.begin_data_unit(entity, datasets) {
return Some(Err(err));
}
}
}
}
}
}
impl Hdf5ChunkIterator {
fn begin_data_unit(
&mut self,
entity: EntityPath,
datasets: Vec<DatasetDesc>,
) -> Result<(), Hdf5Error> {
re_tracing::profile_function!();
#[expect(clippy::cast_possible_truncation)]
let num_rows = datasets
.first()
.and_then(|desc| desc.shape.first())
.copied()
.unwrap_or(0) as usize;
#[expect(clippy::cast_possible_truncation)]
let max_rows = self.chunk_max_rows as usize;
#[expect(clippy::cast_possible_truncation)]
let max_bytes = self.chunk_max_bytes as usize;
let mut rows_per_window = max_rows;
let mut columns = Vec::with_capacity(datasets.len());
for desc in datasets {
let dataset = convert::open_dataset(&self.file, &desc)?;
rows_per_window =
rows_per_window.min((max_bytes / convert::row_byte_estimate(&desc)).max(1));
columns.push(UnitColumn { desc, dataset });
}
self.pending = Some(PendingData {
entity,
columns,
num_rows,
rows_per_window,
next_row: 0,
});
Ok(())
}
fn emit_window(&mut self) -> Result<Chunk, Hdf5Error> {
let pending = self
.pending
.as_mut()
.expect("emit_window is only called with pending rows");
let start = pending.next_row;
let len = (pending.num_rows - start).min(pending.rows_per_window);
pending.next_row += len;
let PlannedTimeline {
timeline,
times,
is_sorted,
} = self
.timeline
.as_ref()
.expect("a Data unit implies a resolved row count, hence a timeline");
let time_column = TimeColumn::new(
is_sorted.then_some(true),
*timeline,
times.slice(start, len),
);
let window_values = pending
.columns
.iter()
.map(|column| column.window_values(start, len))
.collect::<Result<Vec<_>, _>>()?;
let components: ChunkComponents = if self.use_structs && pending.columns.len() > 1 {
std::iter::once(convert::build_struct_component(window_values)?).collect()
} else {
std::iter::zip(&pending.columns, window_values)
.map(|(column, (field, values))| {
convert::values_to_component(column.desc.name(), field, values)
})
.collect::<Result<_, _>>()?
};
Ok(Chunk::from_auto_row_ids(
ChunkId::new(),
pending.entity.clone(),
std::iter::once((*timeline.name(), time_column)).collect(),
components,
)?)
}
fn build_static_scalars_chunk(
&self,
entity: EntityPath,
datasets: &[DatasetDesc],
) -> Result<Chunk, Hdf5Error> {
re_tracing::profile_function!();
let components: ChunkComponents = datasets
.iter()
.map(|dataset| convert::read_dataset_to_list(&self.file, dataset))
.collect::<Result<_, _>>()?;
Ok(Chunk::from_auto_row_ids(
ChunkId::new(),
entity,
Default::default(),
components,
)?)
}
}
fn build_attributes_chunk(
entity: EntityPath,
attrs: &[(String, hdf5_pure::AttrValue)],
) -> Result<Chunk, Hdf5Error> {
let components: ChunkComponents = attrs
.iter()
.map(|(name, value)| convert::attr_to_component(name, value))
.collect::<Result<_, _>>()?;
Ok(Chunk::from_auto_row_ids(
ChunkId::new(),
entity,
Default::default(),
components,
)?)
}