Skip to main content

re_chunk/
chunk.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2
3use ahash::HashMap;
4use anyhow::Context as _;
5use arrow::array::{
6    Array as ArrowArray, ArrayRef as ArrowArrayRef, FixedSizeBinaryArray,
7    ListArray as ArrowListArray,
8};
9use arrow::buffer::{NullBuffer as ArrowNullBuffer, ScalarBuffer as ArrowScalarBuffer};
10use itertools::{Either, Itertools as _, izip};
11use nohash_hasher::IntMap;
12use re_arrow_util::{ArrowArrayDowncastRef as _, widen_binary_arrays};
13use re_byte_size::SizeBytes as _;
14use re_log::debug_assert;
15use re_log_types::{
16    AbsoluteTimeRange, EntityPath, NonMinI64, TimeInt, TimeType, Timeline, TimelineName,
17};
18use re_types_core::{
19    ArrowDataType as _, ComponentDescriptor, ComponentIdentifier, ComponentType,
20    DeserializationError, SerializationError, SerializedComponentColumn,
21};
22
23use crate::{ChunkId, RowId};
24
25// ---
26
27/// Errors that can occur when creating/manipulating a [`Chunk`]s, directly or indirectly through
28/// the use of a [`crate::ChunkBatcher`].
29#[derive(thiserror::Error, Debug)]
30pub enum ChunkError {
31    #[error("Detected malformed chunk: {reason}")]
32    Malformed { reason: String },
33
34    #[error("Arrow: {0}")]
35    Arrow(#[from] arrow::error::ArrowError),
36
37    #[error("{kind} index out of bounds: {index} (len={len})")]
38    IndexOutOfBounds {
39        kind: String,
40        len: usize,
41        index: usize,
42    },
43
44    #[error("Serialization: {0}")]
45    Serialization(#[from] SerializationError),
46
47    #[error("Deserialization: {0}")]
48    Deserialization(#[from] DeserializationError),
49
50    #[error(transparent)]
51    IndexColumn(#[from] re_sorbet::IndexColumnError),
52
53    #[error(transparent)]
54    WrongDatatypeError(#[from] re_arrow_util::WrongDatatypeError),
55
56    #[error(transparent)]
57    MismatchedChunkSchemaError(#[from] re_sorbet::MismatchedChunkSchemaError),
58
59    #[error(transparent)]
60    InvalidSorbetSchema(#[from] re_sorbet::SorbetError),
61
62    // Boxed: `DataframeToChunksError` is large (it wraps a `SorbetError`), and this variant is rare.
63    #[error(transparent)]
64    DataframeToChunks(#[from] Box<re_sorbet::DataframeToChunksError>),
65}
66
67const _: () = assert!(
68    std::mem::size_of::<ChunkError>() <= 72,
69    "Error type is too large. Try to reduce its size by boxing some of its variants.",
70);
71
72pub type ChunkResult<T> = Result<T, ChunkError>;
73
74// ---
75
76#[derive(Debug, Clone, Default, PartialEq)]
77pub struct ChunkComponents(pub IntMap<ComponentIdentifier, SerializedComponentColumn>);
78
79impl ChunkComponents {
80    /// Returns all list arrays for the given component type.
81    ///
82    /// I.e semantically equivalent to `get("MyComponent:*.*")`
83    #[inline]
84    pub fn get_by_component_type(
85        &self,
86        component_type: ComponentType,
87    ) -> impl Iterator<Item = &ArrowListArray> {
88        self.0.values().filter_map(move |column| {
89            (column.descriptor.component_type == Some(component_type)).then_some(&column.list_array)
90        })
91    }
92
93    /// Approximate equal, that ignores small numeric differences.
94    ///
95    /// Returns `Ok` if similar.
96    /// If there is a difference, a description of that difference is returned in `Err`.
97    /// We use [`anyhow`] to provide context.
98    ///
99    /// Useful for tests.
100    pub fn ensure_similar(left: &Self, right: &Self) -> anyhow::Result<()> {
101        anyhow::ensure!(left.len() == right.len());
102        for (component, left_column) in left.iter() {
103            let Some(right_column) = right.get(*component) else {
104                anyhow::bail!("rhs is missing {component:?}");
105            };
106            anyhow::ensure!(left_column.descriptor == right_column.descriptor);
107
108            let left_array = widen_binary_arrays(&left_column.list_array);
109            let right_array = widen_binary_arrays(&right_column.list_array);
110            re_arrow_util::ensure_similar(&left_array.to_data(), &right_array.to_data())
111                .with_context(|| format!("Component {component:?}"))?;
112        }
113        Ok(())
114    }
115
116    /// Whether any of the components in this chunk has the given name.
117    #[inline]
118    pub fn contains_component(&self, component: ComponentIdentifier) -> bool {
119        self.0.contains_key(&component)
120    }
121
122    /// Lists all the component descriptors in this chunk.
123    #[inline]
124    pub fn component_descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> + '_ {
125        self.0.values().map(|column| &column.descriptor)
126    }
127
128    /// Lists all the component list arrays in this chunk.
129    #[inline]
130    pub fn list_arrays(&self) -> impl Iterator<Item = &ArrowListArray> + '_ {
131        self.0.values().map(|column| &column.list_array)
132    }
133
134    /// Lists all the component list arrays in this chunk.
135    #[inline]
136    pub fn list_arrays_mut(&mut self) -> impl Iterator<Item = &mut ArrowListArray> + '_ {
137        self.0.values_mut().map(|column| &mut column.list_array)
138    }
139
140    /// Returns the array for a given component if any.
141    #[inline]
142    pub fn get_array(&self, component: ComponentIdentifier) -> Option<&ArrowListArray> {
143        self.0.get(&component).map(|column| &column.list_array)
144    }
145
146    /// Returns the descriptor for a given component if any.
147    #[inline]
148    pub fn get_descriptor(&self, component: ComponentIdentifier) -> Option<&ComponentDescriptor> {
149        self.0.get(&component).map(|column| &column.descriptor)
150    }
151
152    /// Returns the descriptor and array for a given component if any.
153    #[inline]
154    pub fn get(&self, component: ComponentIdentifier) -> Option<&SerializedComponentColumn> {
155        self.0.get(&component)
156    }
157
158    /// Unconditionally inserts a [`SerializedComponentColumn`].
159    ///
160    /// Removes and replaces the column if it already exists.
161    #[inline]
162    pub fn insert(
163        &mut self,
164        column: SerializedComponentColumn,
165    ) -> Option<SerializedComponentColumn> {
166        self.0.insert(column.descriptor.component, column)
167    }
168
169    /// Rewrites every column's outer list field into
170    /// [`re_arrow_util::canonical_component_list_field`].
171    ///
172    /// [`Chunk::new`] calls this so that every chunk in the system — however it was produced —
173    /// describes its component columns the same way. See
174    /// [`re_arrow_util::canonicalized_component_list_array`] for why that matters, and
175    /// [`Chunk::sanity_check`] for the invariant it establishes.
176    pub(crate) fn canonicalize_list_fields(&mut self) {
177        for column in self.0.values_mut() {
178            if let Some(canonical) =
179                re_arrow_util::canonicalized_component_list_array(&column.list_array)
180            {
181                column.list_array = canonical;
182            }
183        }
184    }
185}
186
187impl std::ops::Deref for ChunkComponents {
188    type Target = IntMap<ComponentIdentifier, SerializedComponentColumn>;
189
190    #[inline]
191    fn deref(&self) -> &Self::Target {
192        &self.0
193    }
194}
195
196impl std::ops::DerefMut for ChunkComponents {
197    #[inline]
198    fn deref_mut(&mut self) -> &mut Self::Target {
199        &mut self.0
200    }
201}
202
203// TODO(andreas): Remove this variant, we should let users construct `SerializedComponentColumn` directly to sharpen semantics.
204impl FromIterator<(ComponentDescriptor, ArrowListArray)> for ChunkComponents {
205    #[inline]
206    fn from_iter<T: IntoIterator<Item = (ComponentDescriptor, ArrowListArray)>>(iter: T) -> Self {
207        let mut this = Self::default();
208        {
209            for (component_desc, list_array) in iter {
210                this.insert(SerializedComponentColumn::new(list_array, component_desc));
211            }
212        }
213        this
214    }
215}
216
217impl FromIterator<SerializedComponentColumn> for ChunkComponents {
218    #[inline]
219    fn from_iter<T: IntoIterator<Item = SerializedComponentColumn>>(iter: T) -> Self {
220        let mut this = Self::default();
221        {
222            for serialized in iter {
223                this.insert(serialized);
224            }
225        }
226        this
227    }
228}
229
230/// Dense arrow-based storage of N rows of multi-component multi-temporal data for a specific entity.
231///
232/// This is our core datastructure for logging, storing, querying and transporting data around.
233///
234/// The chunk as a whole is always ascendingly sorted by [`RowId`] before it gets manipulated in any way.
235/// Its time columns might or might not be ascendingly sorted, depending on how the data was logged.
236///
237/// This is the in-memory representation of a chunk, optimized for efficient manipulation of the
238/// data within. For transport, see [`re_sorbet::ChunkBatch`] instead.
239///
240/// Each [`Chunk`] has a globally unique [`ChunkId`].
241/// Each time a new [`Chunk`] is created or modified, it should be assigned a new [`ChunkId`].
242pub struct Chunk {
243    pub(crate) id: ChunkId,
244
245    pub(crate) entity_path: EntityPath,
246
247    /// The heap size of this chunk in bytes.
248    ///
249    /// Must be cached as it is very costly to compute, and needs to be computed repeatedly on the
250    /// hot path (e.g. during garbage collection).
251    pub(crate) heap_size_bytes: AtomicU64,
252
253    /// Is the chunk as a whole sorted by [`RowId`]?
254    pub(crate) is_sorted: bool,
255
256    /// The respective [`RowId`]s for each row of data.
257    pub(crate) row_ids: FixedSizeBinaryArray,
258
259    /// The time columns.
260    ///
261    /// Each column must be the same length as `row_ids`.
262    ///
263    /// Empty if this is a static chunk.
264    pub(crate) timelines: IntMap<TimelineName, TimeColumn>,
265
266    /// A sparse `ListArray` & a [`ComponentDescriptor`] for each component.
267    ///
268    /// Each `ListArray` must be the same length as `row_ids`.
269    ///
270    /// Sparse so that we can e.g. log a `Position` at one timestamp but not a `Color`.
271    pub(crate) components: ChunkComponents,
272}
273
274/// Generates sequential [`RowId`]s derived from a [`ChunkId`].
275///
276/// A core assumption of the data model is that the value of a cell
277/// defined by (`RowId` x column descriptor) is not allowed to change,
278/// which is why we have to generate new `RowId`s before modifying
279/// components or indexes.
280fn auto_row_ids(id: ChunkId, count: usize) -> FixedSizeBinaryArray {
281    let row_ids: Vec<RowId> =
282        std::iter::successors(Some(RowId::from_tuid((*id).next())), |row_id| {
283            Some(row_id.next())
284        })
285        .take(count)
286        .collect();
287    RowId::arrow_from_slice(&row_ids)
288}
289
290impl std::fmt::Debug for Chunk {
291    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
292        let Self {
293            id,
294            entity_path,
295            heap_size_bytes,
296            is_sorted,
297            row_ids: _, // printed nicely formatted
298            timelines,
299            components,
300        } = self;
301
302        if f.alternate() {
303            f.debug_struct("Chunk")
304                .field("id", id)
305                .field("entity_path", entity_path)
306                .field("heap_size_bytes", &heap_size_bytes.load(Ordering::Relaxed))
307                .field("is_sorted", is_sorted)
308                .field("row_ids", &self.row_ids_slice())
309                .field("timelines", timelines)
310                .field("components", components)
311                .finish()
312        } else {
313            f.debug_struct("Chunk")
314                .field("id", id)
315                .field("entity_path", entity_path)
316                .finish_non_exhaustive()
317        }
318    }
319}
320
321impl PartialEq for Chunk {
322    /// NOTE: the [`ChunkId`] is _not_ compared, only the data.
323    #[inline]
324    fn eq(&self, other: &Self) -> bool {
325        let Self {
326            id: _,
327            entity_path,
328            heap_size_bytes: _,
329            is_sorted,
330            row_ids,
331            timelines,
332            components,
333        } = self;
334
335        *entity_path == other.entity_path
336            && *is_sorted == other.is_sorted
337            && *row_ids == other.row_ids
338            && *timelines == other.timelines
339            && *components == other.components
340    }
341}
342
343impl Chunk {
344    /// Returns a version of us with a new [`ChunkId`].
345    ///
346    /// The returned [`Chunk`] will re-use the exact same [`RowId`]s as `self`.
347    #[must_use]
348    #[inline]
349    pub fn with_id(mut self, id: ChunkId) -> Self {
350        self.id = id;
351        self
352    }
353
354    /// Returns `Ok` if two [`Chunk`]s are _similar_, although not byte-for-byte equal.
355    ///
356    /// In particular, this ignores chunks and row IDs, as well as `log_time` timestamps.
357    /// It also forgives small numeric inaccuracies in floating point buffers.
358    ///
359    /// If there is a difference, a description of that difference is returned in `Err`.
360    /// We use [`anyhow`] to provide context.
361    ///
362    /// Useful for tests.
363    pub fn ensure_similar(lhs: &Self, rhs: &Self) -> anyhow::Result<()> {
364        Self::ensure_similar_ignoring_timelines(lhs, rhs, &[])
365    }
366
367    /// Like [`Self::ensure_similar`], but the given timelines are ignored entirely:
368    /// their presence, absence, and values are not compared.
369    ///
370    /// Useful when comparing recordings produced with different default-timeline settings,
371    /// e.g. `log_tick`, which is opt-in.
372    pub fn ensure_similar_ignoring_timelines(
373        lhs: &Self,
374        rhs: &Self,
375        ignored_timelines: &[TimelineName],
376    ) -> anyhow::Result<()> {
377        anyhow::ensure!(lhs.num_rows() == rhs.num_rows());
378
379        let Self {
380            id: _,
381            entity_path,
382            heap_size_bytes: _,
383            is_sorted: _,
384            row_ids: _,
385            timelines,
386            components,
387        } = lhs;
388
389        let is_ignored = |timeline: &TimelineName| ignored_timelines.contains(timeline);
390
391        anyhow::ensure!(*entity_path == rhs.entity_path);
392
393        // Compare the set of timelines, disregarding any ignored timelines on either side.
394        // Compared as a sorted set, since the timeline iteration order is not meaningful.
395        let lhs_timelines = timelines
396            .keys()
397            .filter(|t| !is_ignored(t))
398            .sorted()
399            .collect_vec();
400        let rhs_timelines = rhs
401            .timelines
402            .keys()
403            .filter(|t| !is_ignored(t))
404            .sorted()
405            .collect_vec();
406        anyhow::ensure!(
407            lhs_timelines == rhs_timelines,
408            "Timelines differ: {lhs_timelines:?} vs {rhs_timelines:?}"
409        );
410
411        // Number of components must match (timelines are already checked above).
412        anyhow::ensure!(
413            components.len() == rhs.components.len(),
414            "Number of components differs: {} vs {}",
415            components.len(),
416            rhs.components.len()
417        );
418
419        for (timeline, left_time_col) in timelines {
420            if is_ignored(timeline) {
421                continue;
422            }
423            let right_time_col = rhs
424                .timelines
425                .get(timeline)
426                .ok_or_else(|| anyhow::format_err!("right is missing timeline {timeline:?}"))?;
427            if timeline == &TimelineName::log_time() {
428                continue; // We expect this to differ
429            }
430            if timeline == "sim_time" {
431                continue; // Small numeric differences
432            }
433            anyhow::ensure!(
434                left_time_col == right_time_col,
435                "Timeline differs: {timeline:?}"
436            );
437        }
438
439        // Handle edge case: recording time on segment properties should ignore start time.
440        if entity_path == &EntityPath::properties() {
441            // We're going to filter out some components on both lhs and rhs.
442            // Therefore, it's important that we first check that the number of components is the same.
443            anyhow::ensure!(components.len() == rhs.components.len());
444
445            // Copied from `rerun.archetypes.RecordingInfo`.
446            let recording_time_component: ComponentIdentifier = "RecordingInfo:start_time".into();
447
448            // Filter out the recording time component from both lhs and rhs.
449            let lhs_components = components
450                .iter()
451                .filter(|&(component, _list_array)| component != &recording_time_component)
452                .map(|(component, list_array)| (*component, list_array.clone()))
453                .collect::<IntMap<_, _>>();
454            let rhs_components = rhs
455                .components
456                .iter()
457                .filter(|&(component, _list_array)| component != &recording_time_component)
458                .map(|(component, list_array)| (*component, list_array.clone()))
459                .collect::<IntMap<_, _>>();
460
461            anyhow::ensure!(lhs_components == rhs_components);
462            Ok(())
463        } else {
464            ChunkComponents::ensure_similar(components, &rhs.components)
465        }
466    }
467
468    // Only used for tests atm
469    pub fn are_equal(&self, other: &Self) -> bool {
470        let Self {
471            id,
472            entity_path,
473            heap_size_bytes: _,
474            is_sorted,
475            row_ids,
476            timelines,
477            components,
478        } = self;
479
480        *id == other.id
481            && *entity_path == other.entity_path
482            && *is_sorted == other.is_sorted
483            && row_ids == &other.row_ids
484            && *timelines == other.timelines
485            && components.0 == other.components.0
486    }
487
488    /// Creates a new chunk with a mapped component column.
489    ///
490    /// The returned chunk always gets a new unique [`ChunkId`] and new auto-generated [`RowId`]s,
491    /// since the component data may have been modified by the closure.
492    ///
493    /// When `target` is `None` the new column will carry over the [`ComponentDescriptor`] from
494    /// the `source` column.
495    pub fn with_mapped_component<E>(
496        &self,
497        source: ComponentIdentifier,
498        target: Option<ComponentDescriptor>,
499        f: impl FnOnce(ArrowListArray) -> Result<ArrowListArray, E>,
500    ) -> Result<Self, E> {
501        let mut new_components = self.components.clone();
502        if let Some(old_entry) = new_components.remove(&source) {
503            new_components.insert(SerializedComponentColumn {
504                descriptor: target.unwrap_or(old_entry.descriptor),
505                list_array: f(old_entry.list_array)?,
506            });
507        }
508
509        let id = ChunkId::new();
510        let row_ids = auto_row_ids(id, self.num_rows());
511
512        Ok(Self {
513            id,
514            entity_path: self.entity_path.clone(),
515            heap_size_bytes: AtomicU64::new(0),
516            is_sorted: true,
517            row_ids,
518            timelines: self.timelines.clone(),
519            components: new_components,
520        })
521    }
522
523    /// Creates a new chunk with a mapped component column, keeping the original [`RowId`]s.
524    ///
525    /// The returned chunk gets a new unique [`ChunkId`] but retains the original [`RowId`]s.
526    ///
527    /// In most cases, prefer [`Self::with_mapped_component`] instead, which generates new
528    /// [`RowId`]s to maintain the data model invariants.
529    ///
530    /// # Warning
531    ///
532    /// This is **only safe** when the closure does **not** modify the actual component data
533    /// (e.g. it introduces a new component column). If an existing column is modified, the
534    /// invariant that the value of a cell defined by (`RowId` x column descriptor) is immutable
535    /// will be violated, and the caller has to handle this appropriately.
536    ///
537    /// Note: archetype information and component type information is lost on the mapped component.
538    //
539    // TODO(grtlr): This should be revised when implementing caching of mapped chunks.
540    pub fn with_shadowed_component<E>(
541        &self,
542        source: ComponentIdentifier,
543        target: ComponentIdentifier,
544        f: impl FnOnce(ArrowListArray) -> Result<ArrowListArray, E>,
545    ) -> Result<Self, E> {
546        let mut new_chunk = self.clone_with_new_id();
547        if let Some(old_entry) = new_chunk.components.remove(&source) {
548            new_chunk.components.insert(SerializedComponentColumn {
549                descriptor: ComponentDescriptor {
550                    component: target,
551                    archetype: None,
552                    component_type: None,
553                },
554                list_array: f(old_entry.list_array)?,
555            });
556        }
557
558        Ok(new_chunk)
559    }
560}
561
562impl Clone for Chunk {
563    #[inline]
564    fn clone(&self) -> Self {
565        self.clone_with_same_id()
566    }
567}
568
569impl Chunk {
570    #[inline]
571    pub fn clone_with_id(&self, id: ChunkId) -> Self {
572        Self {
573            id,
574            entity_path: self.entity_path.clone(),
575            heap_size_bytes: AtomicU64::new(self.heap_size_bytes.load(Ordering::Relaxed)),
576            is_sorted: self.is_sorted,
577            row_ids: self.row_ids.clone(),
578            timelines: self.timelines.clone(),
579            components: self.components.clone(),
580        }
581    }
582
583    #[inline]
584    pub fn clone_with_same_id(&self) -> Self {
585        self.clone_with_id(self.id)
586    }
587
588    #[inline]
589    pub fn clone_with_new_id(&self) -> Self {
590        self.clone_with_id(ChunkId::new())
591    }
592
593    #[inline]
594    pub fn clone_with_new_entity_path(&self, entity_path: EntityPath) -> Self {
595        let mut chunk = self.clone_with_id(ChunkId::new());
596        chunk.entity_path = entity_path;
597        chunk
598    }
599}
600
601impl Chunk {
602    fn reset_cached_heap_size_bytes(&self) {
603        self.heap_size_bytes.store(0, Ordering::Relaxed);
604    }
605
606    /// Clones the chunk and assign new IDs to the resulting chunk and its rows.
607    ///
608    /// `first_row_id` will become the [`RowId`] of the first row in the duplicated chunk.
609    /// Each row after that will be monotonically increasing.
610    #[inline]
611    pub fn clone_as(&self, id: ChunkId, first_row_id: RowId) -> Self {
612        let row_ids = std::iter::from_fn({
613            let mut row_id = first_row_id;
614            move || {
615                let yielded = row_id;
616                row_id = row_id.next();
617                Some(yielded)
618            }
619        })
620        .take(self.row_ids.len())
621        .collect_vec();
622
623        let new_chunk = Self {
624            row_ids: RowId::arrow_from_slice(&row_ids),
625            ..self.clone_with_id(id)
626        };
627
628        // Need to reset the cache size here as `row_ids`'s capacity could have
629        // changed here.
630        new_chunk.reset_cached_heap_size_bytes();
631
632        new_chunk
633    }
634
635    /// Clones the chunk into a new chunk without any time data.
636    #[inline]
637    pub fn into_static(mut self) -> Self {
638        self.timelines.clear();
639        self.reset_cached_heap_size_bytes();
640        self
641    }
642
643    /// Clones the chunk into a new chunk where all [`RowId`]s are [`RowId::ZERO`].
644    ///
645    /// The returned chunk always gets a new unique [`ChunkId`].
646    pub fn zeroed(self) -> Self {
647        let row_ids = vec![RowId::ZERO; self.row_ids.len()];
648
649        let row_ids = RowId::arrow_from_slice(&row_ids);
650
651        let new_chunk = Self {
652            id: ChunkId::new(),
653            row_ids,
654            ..self
655        };
656
657        // Need to reset the cache size here as `row_ids`'s capacity could have
658        // changed here.
659        new_chunk.reset_cached_heap_size_bytes();
660
661        new_chunk
662    }
663
664    /// Computes the time range covered by each individual component column on each timeline.
665    ///
666    /// This is different from the time range covered by the [`Chunk`] as a whole because component
667    /// columns are potentially sparse.
668    ///
669    /// This is crucial for indexing and queries to work properly.
670    //
671    // TODO(cmc): This needs to be stored in chunk metadata and transported across IPC.
672    #[inline]
673    pub fn time_range_per_component(
674        &self,
675    ) -> IntMap<TimelineName, IntMap<ComponentIdentifier, AbsoluteTimeRange>> {
676        re_tracing::profile_function!();
677
678        self.timelines
679            .iter()
680            .map(|(timeline_name, time_column)| {
681                (
682                    *timeline_name,
683                    time_column.time_range_per_component(&self.components),
684                )
685            })
686            .collect()
687    }
688
689    #[inline]
690    pub fn component_descriptors(&self) -> impl Iterator<Item = &ComponentDescriptor> + '_ {
691        self.components.component_descriptors()
692    }
693
694    /// The cumulative number of events in this chunk.
695    ///
696    /// I.e. how many _component batches_ ("cells") were logged in total?
697    //
698    // TODO(cmc): This needs to be stored in chunk metadata and transported across IPC.
699    #[inline]
700    pub fn num_events_cumulative(&self) -> u64 {
701        // Reminder: component columns are sparse, we must take a look at the validity bitmaps.
702        self.components
703            .list_arrays()
704            .map(|list_array| {
705                list_array.nulls().map_or_else(
706                    || list_array.len() as u64,
707                    |validity| validity.len() as u64 - validity.null_count() as u64,
708                )
709            })
710            .sum()
711    }
712
713    /// The cumulative number of events in this chunk for each _unique_ timestamp.
714    ///
715    /// I.e. how many _component batches_ ("cells") were logged in total at each timestamp?
716    ///
717    /// Keep in mind that a timestamp can appear multiple times in a [`Chunk`].
718    /// This method will do a sum accumulation to account for these cases (i.e. every timestamp in
719    /// the returned vector is guaranteed to be unique).
720    pub fn num_events_cumulative_per_unique_time(
721        &self,
722        timeline: &TimelineName,
723    ) -> Vec<(TimeInt, u64)> {
724        if self.is_static() {
725            return vec![(TimeInt::STATIC, self.num_events_cumulative())];
726        }
727
728        let Some(time_column) = self.timelines().get(timeline) else {
729            return Vec::new();
730        };
731
732        let time_range = time_column.time_range();
733        if time_range.min() == time_range.max() {
734            return vec![(time_range.min(), self.num_events_cumulative())];
735        }
736
737        let counts = if time_column.is_sorted() {
738            self.num_events_cumulative_per_unique_time_sorted(time_column)
739        } else {
740            self.num_events_cumulative_per_unique_time_unsorted(time_column)
741        };
742
743        debug_assert!(
744            counts
745                .iter()
746                .tuple_windows::<(_, _)>()
747                .all(|((time1, _), (time2, _))| time1 < time2)
748        );
749
750        counts
751    }
752
753    fn num_events_cumulative_per_unique_time_sorted(
754        &self,
755        time_column: &TimeColumn,
756    ) -> Vec<(TimeInt, u64)> {
757        debug_assert!(time_column.is_sorted());
758
759        // NOTE: This is used on some very hot paths (time panel rendering).
760        // Performance trumps readability. Optimized empirically.
761
762        // Raw, potentially duplicated counts (because timestamps aren't necessarily unique).
763        let mut counts_raw = vec![0u64; self.num_rows()];
764        {
765            self.components.list_arrays().for_each(|list_array| {
766                if let Some(validity) = list_array.nulls() {
767                    validity
768                        .iter()
769                        .enumerate()
770                        .for_each(|(i, is_valid)| counts_raw[i] += is_valid as u64);
771                } else {
772                    for count in &mut counts_raw {
773                        *count += 1;
774                    }
775                }
776            });
777        }
778
779        let mut counts = Vec::with_capacity(counts_raw.len());
780
781        let Some(mut cur_time) = time_column.times().next() else {
782            return Vec::new();
783        };
784        let mut cur_count = 0;
785        izip!(time_column.times(), counts_raw).for_each(|(time, count)| {
786            if time == cur_time {
787                cur_count += count;
788            } else {
789                counts.push((cur_time, cur_count));
790                cur_count = count;
791                cur_time = time;
792            }
793        });
794
795        if counts.last().map(|(time, _)| *time) != Some(cur_time) {
796            counts.push((cur_time, cur_count));
797        }
798
799        counts
800    }
801
802    fn num_events_cumulative_per_unique_time_unsorted(
803        &self,
804        time_column: &TimeColumn,
805    ) -> Vec<(TimeInt, u64)> {
806        debug_assert!(!time_column.is_sorted());
807
808        // NOTE: This is used on some very hot paths (time panel rendering).
809
810        let result_unordered =
811            self.components
812                .list_arrays()
813                .fold(HashMap::default(), |acc, list_array| {
814                    if let Some(validity) = list_array.nulls() {
815                        std::iter::zip(time_column.times(), validity.iter()).fold(
816                            acc,
817                            |mut acc, (time, is_valid)| {
818                                *acc.entry(time).or_default() += is_valid as u64;
819                                acc
820                            },
821                        )
822                    } else {
823                        time_column.times().fold(acc, |mut acc, time| {
824                            *acc.entry(time).or_default() += 1;
825                            acc
826                        })
827                    }
828                });
829
830        let mut result = result_unordered.into_iter().collect_vec();
831        result.sort_by_key(|val| val.0);
832        result
833    }
834
835    /// The number of events in this chunk for the specified component.
836    ///
837    /// I.e. how many _component batches_ ("cells") were logged in total for this component?
838    //
839    // TODO(cmc): This needs to be stored in chunk metadata and transported across IPC.
840    #[inline]
841    pub fn num_events_for_component(&self, component: ComponentIdentifier) -> Option<u64> {
842        // Reminder: component columns are sparse, we must check validity bitmap.
843        self.components.get_array(component).map(|list_array| {
844            list_array.nulls().map_or_else(
845                || list_array.len() as u64,
846                |validity| validity.len() as u64 - validity.null_count() as u64,
847            )
848        })
849    }
850
851    /// Computes the `RowId` range covered by each individual component column on each timeline.
852    ///
853    /// This is different from the `RowId` range covered by the [`Chunk`] as a whole because component
854    /// columns are potentially sparse.
855    ///
856    /// This is crucial for indexing and queries to work properly.
857    //
858    // TODO(cmc): This needs to be stored in chunk metadata and transported across IPC.
859    pub fn row_id_range_per_component(&self) -> IntMap<ComponentIdentifier, (RowId, RowId)> {
860        re_tracing::profile_function!();
861
862        let row_ids = self.row_ids().collect_vec();
863
864        if self.is_row_ids_sorted() {
865            self.components
866                .iter()
867                .filter_map(|(component, column)| {
868                    let mut row_id_min = None;
869                    let mut row_id_max = None;
870
871                    for (i, &row_id) in row_ids.iter().enumerate() {
872                        if column.list_array.is_valid(i) {
873                            row_id_max = Some(row_id);
874                        }
875                    }
876                    for (i, &row_id) in row_ids.iter().enumerate().rev() {
877                        if column.list_array.is_valid(i) {
878                            row_id_min = Some(row_id);
879                        }
880                    }
881
882                    Some((*component, (row_id_min?, row_id_max?)))
883                })
884                .collect()
885        } else {
886            self.components
887                .iter()
888                .filter_map(|(component, column)| {
889                    let mut row_id_min = Some(RowId::MAX);
890                    let mut row_id_max = Some(RowId::ZERO);
891
892                    for (i, &row_id) in row_ids.iter().enumerate() {
893                        if column.list_array.is_valid(i) && Some(row_id) < row_id_min {
894                            row_id_min = Some(row_id);
895                        }
896                    }
897                    for (i, &row_id) in row_ids.iter().enumerate().rev() {
898                        if column.list_array.is_valid(i) && Some(row_id) > row_id_max {
899                            row_id_max = Some(row_id);
900                        }
901                    }
902
903                    Some((*component, (row_id_min?, row_id_max?)))
904                })
905                .collect()
906        }
907    }
908}
909
910// ---
911
912#[derive(Debug, Clone, PartialEq, Eq, re_byte_size::SizeBytes)]
913pub struct TimeColumn {
914    pub(crate) timeline: Timeline,
915
916    /// Every single timestamp for this timeline.
917    ///
918    /// * This might or might not be sorted, depending on how the data was logged.
919    /// * This is guaranteed to always be dense, because chunks are split anytime a timeline is
920    ///   added or removed.
921    /// * This cannot ever contain `TimeInt::STATIC`, since static data doesn't even have timelines.
922    ///
923    /// When this buffer is converted to an arrow array, it's datatype will depend
924    /// on the timeline type, so it will either become a
925    /// [`arrow::array::Int64Array`] or a [`arrow::array::TimestampNanosecondArray`].
926    pub(crate) times: ArrowScalarBuffer<i64>,
927
928    /// Is [`Self::times`] sorted?
929    ///
930    /// This is completely independent of [`Chunk::is_sorted`]: a timeline doesn't necessarily
931    /// follow the global [`RowId`]-based order, although it does in most cases (happy path).
932    pub(crate) is_sorted: bool,
933
934    /// The time range covered by [`Self::times`].
935    ///
936    /// Not necessarily contiguous! Just the min and max value found in [`Self::times`].
937    pub(crate) time_range: AbsoluteTimeRange,
938}
939
940/// Errors when deserializing/parsing/reading a column of time data.
941#[derive(Debug, thiserror::Error)]
942pub enum TimeColumnError {
943    #[error("Time columns had nulls, but should be dense")]
944    ContainsNulls,
945
946    #[error("Unsupported data type : {0}")]
947    UnsupportedDataType(arrow::datatypes::DataType),
948}
949
950impl Chunk {
951    /// Creates a new [`Chunk`].
952    ///
953    /// This will fail if the passed in data is malformed in any way -- see [`Self::sanity_check`]
954    /// for details.
955    ///
956    /// Iff you know for sure whether the data is already appropriately sorted or not, specify `is_sorted`.
957    /// When left unspecified (`None`), it will be computed in O(n) time.
958    ///
959    /// For a row-oriented constructor, see [`Self::builder`].
960    pub fn new(
961        id: ChunkId,
962        entity_path: EntityPath,
963        is_sorted: Option<bool>,
964        row_ids: FixedSizeBinaryArray,
965        timelines: IntMap<TimelineName, TimeColumn>,
966        components: ChunkComponents,
967    ) -> ChunkResult<Self> {
968        let mut chunk = Self {
969            id,
970            entity_path,
971            heap_size_bytes: AtomicU64::new(0),
972            is_sorted: false,
973            row_ids,
974            timelines,
975            components,
976        };
977
978        // Every path that produces a `Chunk` funnels through here, which makes this the one
979        // place where the outer-list convention can be enforced for good.
980        chunk.components.canonicalize_list_fields();
981
982        chunk.is_sorted = is_sorted.unwrap_or_else(|| chunk.is_row_ids_sorted_uncached());
983
984        chunk.sanity_check()?;
985
986        Ok(chunk)
987    }
988
989    /// Creates a new [`Chunk`].
990    ///
991    /// This will fail if the passed in data is malformed in any way -- see [`Self::sanity_check`]
992    /// for details.
993    ///
994    /// Iff you know for sure whether the data is already appropriately sorted or not, specify `is_sorted`.
995    /// When left unspecified (`None`), it will be computed in O(n) time.
996    ///
997    /// For a row-oriented constructor, see [`Self::builder`].
998    pub fn from_native_row_ids(
999        id: ChunkId,
1000        entity_path: EntityPath,
1001        is_sorted: Option<bool>,
1002        row_ids: &[RowId],
1003        timelines: IntMap<TimelineName, TimeColumn>,
1004        components: ChunkComponents,
1005    ) -> ChunkResult<Self> {
1006        re_tracing::profile_function!();
1007        let row_ids = RowId::arrow_from_slice(row_ids);
1008        Self::new(id, entity_path, is_sorted, row_ids, timelines, components)
1009    }
1010
1011    /// Creates a new [`Chunk`].
1012    ///
1013    /// This will fail if the passed in data is malformed in any way -- see [`Self::sanity_check`]
1014    /// for details.
1015    ///
1016    /// The input order will NOT be respected.
1017    /// The rows will be stably reordered so that the time columns are non-decreasing
1018    /// (lexicographically across timelines, in deterministic timeline-name order).
1019    /// Sequential `RowId`s are then assigned to the reordered rows.
1020    /// This makes it less likely that we end up with "out-of-order" chunks
1021    /// (chunks where some time columns are not sorted with respect to `RowId`).
1022    pub fn from_auto_row_ids(
1023        id: ChunkId,
1024        entity_path: EntityPath,
1025        timelines: IntMap<TimelineName, TimeColumn>,
1026        components: ChunkComponents,
1027    ) -> ChunkResult<Self> {
1028        re_tracing::profile_function!();
1029
1030        let count = components
1031            .list_arrays()
1032            .next()
1033            .map_or(0, |list_array| list_array.len());
1034
1035        // Compute a stable permutation that lexicographically sorts the rows by their
1036        // time-column values. We pick a deterministic timeline order (sorted by name) so the
1037        // result does not depend on `IntMap` iteration order.
1038        let timeline_order: Vec<TimelineName> = timelines.keys().copied().sorted().collect();
1039
1040        let mut swaps: Vec<usize> = (0..count).collect();
1041        swaps.sort_by(|&a, &b| {
1042            for name in &timeline_order {
1043                let times = timelines[name].times_raw();
1044                let ord = times[a].cmp(&times[b]);
1045                if ord != std::cmp::Ordering::Equal {
1046                    return ord;
1047                }
1048            }
1049            std::cmp::Ordering::Equal
1050        });
1051
1052        // Build the chunk with placeholder sequential row_ids, then permute everything via
1053        // `shuffle_with`, then reassign sequential row_ids so the chunk is RowId-sorted again.
1054        let placeholder_row_ids = auto_row_ids(id, count);
1055        let mut chunk = Self::new(
1056            id,
1057            entity_path,
1058            Some(true),
1059            placeholder_row_ids,
1060            timelines,
1061            components,
1062        )?;
1063
1064        let already_sorted = swaps
1065            .iter()
1066            .copied()
1067            .enumerate()
1068            .all(|(to, from)| to == from);
1069        if !already_sorted {
1070            chunk.shuffle_with(&swaps);
1071            chunk.row_ids = auto_row_ids(chunk.id, count);
1072            chunk.is_sorted = true;
1073
1074            #[cfg(debug_assertions)]
1075            #[expect(clippy::unwrap_used)] // dev only
1076            chunk.sanity_check().unwrap();
1077        }
1078
1079        Ok(chunk)
1080    }
1081
1082    /// Creates a new [`Chunk`] from columnar data.
1083    ///
1084    /// Pass an empty iterator for `timelines` to create static data.
1085    ///
1086    /// The input order will NOT be respected.
1087    /// The rows will be stably reordered so that the time columns are non-decreasing
1088    /// (lexicographically across timelines, in deterministic timeline-name order).
1089    /// Sequential `RowId`s are then assigned to the reordered rows.
1090    /// This makes it less likely that we end up with "out-of-order" chunks
1091    /// (chunks where some time columns are not sorted with respect to `RowId`).
1092    pub fn from_columns(
1093        entity_path: impl Into<EntityPath>,
1094        timelines: impl IntoIterator<Item = TimeColumn>,
1095        components: impl IntoIterator<Item = (ComponentDescriptor, ArrowListArray)>,
1096    ) -> ChunkResult<Self> {
1097        let timelines: IntMap<TimelineName, TimeColumn> = timelines
1098            .into_iter()
1099            .map(|tc| (*tc.timeline().name(), tc))
1100            .collect();
1101        let components: ChunkComponents = components.into_iter().collect();
1102        Self::from_auto_row_ids(ChunkId::new(), entity_path.into(), timelines, components)
1103    }
1104
1105    /// Simple helper for [`Self::new`] for static data.
1106    ///
1107    /// For a row-oriented constructor, see [`Self::builder`].
1108    #[inline]
1109    pub fn new_static(
1110        id: ChunkId,
1111        entity_path: EntityPath,
1112        is_sorted: Option<bool>,
1113        row_ids: FixedSizeBinaryArray,
1114        components: ChunkComponents,
1115    ) -> ChunkResult<Self> {
1116        Self::new(
1117            id,
1118            entity_path,
1119            is_sorted,
1120            row_ids,
1121            Default::default(),
1122            components,
1123        )
1124    }
1125
1126    #[inline]
1127    pub fn empty(id: ChunkId, entity_path: EntityPath) -> Self {
1128        Self {
1129            id,
1130            entity_path,
1131            heap_size_bytes: Default::default(),
1132            is_sorted: true,
1133            row_ids: RowId::arrow_from_slice(&[]),
1134            timelines: Default::default(),
1135            components: Default::default(),
1136        }
1137    }
1138
1139    /// Unconditionally inserts a [`SerializedComponentColumn`].
1140    ///
1141    /// Removes and replaces the column if it already exists.
1142    ///
1143    /// This will fail if the end result is malformed in any way -- see [`Self::sanity_check`].
1144    #[inline]
1145    pub fn add_component(
1146        &mut self,
1147        component_column: SerializedComponentColumn,
1148    ) -> ChunkResult<()> {
1149        self.id = ChunkId::new();
1150        self.components.insert(component_column);
1151        self.components.canonicalize_list_fields();
1152        self.reset_cached_heap_size_bytes();
1153        self.sanity_check()
1154    }
1155
1156    /// Unconditionally inserts a [`TimeColumn`].
1157    ///
1158    /// Removes and replaces the column if it already exists.
1159    ///
1160    /// This will fail if the end result is malformed in any way -- see [`Self::sanity_check`].
1161    #[inline]
1162    pub fn add_timeline(&mut self, chunk_timeline: TimeColumn) -> ChunkResult<()> {
1163        self.id = ChunkId::new();
1164
1165        self.timelines
1166            .insert(*chunk_timeline.timeline.name(), chunk_timeline);
1167        self.reset_cached_heap_size_bytes();
1168
1169        self.sanity_check()
1170    }
1171}
1172
1173impl TimeColumn {
1174    /// Creates a new [`TimeColumn`].
1175    ///
1176    /// Iff you know for sure whether the data is already appropriately sorted or not, specify `is_sorted`.
1177    /// When left unspecified (`None`), it will be computed in O(n) time.
1178    ///
1179    /// For a row-oriented constructor, see [`Self::builder`].
1180    pub fn new(is_sorted: Option<bool>, timeline: Timeline, times: ArrowScalarBuffer<i64>) -> Self {
1181        re_tracing::profile_function_if!(1000 < times.len(), format!("{} times", times.len()));
1182
1183        let time_slice = times.as_ref();
1184
1185        let is_sorted =
1186            is_sorted.unwrap_or_else(|| time_slice.array_windows().all(|[a, b]| a <= b));
1187
1188        let time_range = if is_sorted {
1189            // NOTE: The 'or' in 'map_or' is never hit, but better safe than sorry.
1190            let min_time = time_slice
1191                .first()
1192                .copied()
1193                .map_or(TimeInt::MIN, TimeInt::new_temporal);
1194            let max_time = time_slice
1195                .last()
1196                .copied()
1197                .map_or(TimeInt::MAX, TimeInt::new_temporal);
1198            AbsoluteTimeRange::new(min_time, max_time)
1199        } else {
1200            // NOTE: Do the iteration multiple times in a cache-friendly way rather than the opposite.
1201            // NOTE: The 'or' in 'unwrap_or' is never hit, but better safe than sorry.
1202            let min_time = time_slice
1203                .iter()
1204                .min()
1205                .copied()
1206                .map_or(TimeInt::MIN, TimeInt::new_temporal);
1207            let max_time = time_slice
1208                .iter()
1209                .max()
1210                .copied()
1211                .map_or(TimeInt::MAX, TimeInt::new_temporal);
1212            AbsoluteTimeRange::new(min_time, max_time)
1213        };
1214
1215        Self {
1216            timeline,
1217            times,
1218            is_sorted,
1219            time_range,
1220        }
1221    }
1222
1223    /// Creates a new [`TimeColumn`] of sequence type.
1224    pub fn new_sequence(
1225        name: impl Into<re_log_types::TimelineName>,
1226        times: impl IntoIterator<Item = impl Into<i64>>,
1227    ) -> Self {
1228        let time_vec: Vec<_> = times.into_iter().map(|t| {
1229            let t = t.into();
1230            TimeInt::try_from(t)
1231                .unwrap_or_else(|_| {
1232                    re_log::error!(
1233                illegal_value = t,
1234                new_value = TimeInt::MIN.as_i64(),
1235                "TimeColumn::new_sequence() called with illegal value - clamped to minimum legal value"
1236            );
1237                    TimeInt::MIN
1238                })
1239                .as_i64()
1240        }).collect();
1241
1242        Self::new(
1243            None,
1244            Timeline::new_sequence(name.into()),
1245            ArrowScalarBuffer::from(time_vec),
1246        )
1247    }
1248
1249    /// Creates a new [`TimeColumn`] of duration type, in seconds.
1250    pub fn new_duration_secs(
1251        name: impl Into<re_log_types::TimelineName>,
1252        seconds: impl IntoIterator<Item = impl Into<f64>>,
1253    ) -> Self {
1254        let time_vec = seconds.into_iter().map(|seconds| {
1255            let seconds = seconds.into();
1256            let nanos = (1e9 * seconds).round();
1257            let clamped = NonMinI64::saturating_from_i64(nanos as i64);
1258            if clamped.get() as f64 != nanos {
1259                re_log::warn!(
1260                    illegal_value = nanos,
1261                    new_value = clamped.get(),
1262                    "TimeColumn::new_duration_secs() called with out-of-range value. Clamped to valid range."
1263                );
1264            }
1265            clamped.get()
1266        }).collect_vec();
1267
1268        Self::new(
1269            None,
1270            Timeline::new(name, TimeType::DurationNs),
1271            ArrowScalarBuffer::from(time_vec),
1272        )
1273    }
1274
1275    /// Creates a new [`TimeColumn`] of duration type, in seconds.
1276    pub fn new_timestamp_secs_since_epoch(
1277        name: impl Into<re_log_types::TimelineName>,
1278        seconds: impl IntoIterator<Item = impl Into<f64>>,
1279    ) -> Self {
1280        let time_vec = seconds.into_iter().map(|seconds| {
1281            let seconds = seconds.into();
1282            let nanos = (1e9 * seconds).round();
1283            let clamped = NonMinI64::saturating_from_i64(nanos as i64);
1284            if clamped.get() as f64 != nanos {
1285                re_log::warn!(
1286                    illegal_value = nanos,
1287                    new_value = clamped.get(),
1288                    "TimeColumn::new_timestamp_secs_since_epoch() called with out-of-range value. Clamped to valid range."
1289                );
1290            }
1291            clamped.get()
1292        }).collect_vec();
1293
1294        Self::new(
1295            None,
1296            Timeline::new(name, TimeType::TimestampNs),
1297            ArrowScalarBuffer::from(time_vec),
1298        )
1299    }
1300
1301    /// Creates a new [`TimeColumn`] measuring duration in nanoseconds.
1302    pub fn new_duration_nanos(
1303        name: impl Into<re_log_types::TimelineName>,
1304        nanos: impl IntoIterator<Item = impl Into<i64>>,
1305    ) -> Self {
1306        let time_vec = nanos
1307            .into_iter()
1308            .map(|nanos| {
1309                let nanos = nanos.into();
1310                NonMinI64::new(nanos)
1311                    .unwrap_or_else(|| {
1312                        re_log::error!(
1313                            illegal_value = nanos,
1314                            new_value = TimeInt::MIN.as_i64(),
1315                            "TimeColumn::new_duration_nanos() called with illegal value - clamped to minimum legal value"
1316                        );
1317                        NonMinI64::MIN
1318                    })
1319                    .get()
1320            })
1321            .collect_vec();
1322
1323        Self::new(
1324            None,
1325            Timeline::new(name, TimeType::DurationNs),
1326            ArrowScalarBuffer::from(time_vec),
1327        )
1328    }
1329
1330    /// Creates a new [`TimeColumn`] of timestamps, as nanoseconds since unix epoch.
1331    pub fn new_timestamp_nanos_since_epoch(
1332        name: impl Into<re_log_types::TimelineName>,
1333        nanos: impl IntoIterator<Item = impl Into<i64>>,
1334    ) -> Self {
1335        let time_vec = nanos
1336            .into_iter()
1337            .map(|nanos| {
1338                let nanos = nanos.into();
1339                NonMinI64::new(nanos)
1340                    .unwrap_or_else(|| {
1341                        re_log::error!(
1342                            illegal_value = nanos,
1343                            new_value = TimeInt::MIN.as_i64(),
1344                            "TimeColumn::new_timestamp_nanos_since_epoch() called with illegal value - clamped to minimum legal value"
1345                        );
1346                        NonMinI64::MIN
1347                    })
1348                    .get()
1349            })
1350            .collect_vec();
1351
1352        Self::new(
1353            None,
1354            Timeline::new(name, TimeType::TimestampNs),
1355            ArrowScalarBuffer::from(time_vec),
1356        )
1357    }
1358
1359    /// Parse the given [`ArrowArray`] as a time column.
1360    ///
1361    /// Results in an error if the array is of the wrong datatype, or if it contains nulls.
1362    pub fn read_array(array: &dyn ArrowArray) -> Result<ArrowScalarBuffer<i64>, TimeColumnError> {
1363        if array.null_count() > 0 {
1364            Err(TimeColumnError::ContainsNulls)
1365        } else {
1366            Self::read_nullable_array(array).map(|(times, _nulls)| times)
1367        }
1368    }
1369
1370    /// Parse the given [`ArrowArray`] as a time column where null values are acceptable.
1371    ///
1372    /// Results in an error if the array is of the wrong datatype.
1373    pub fn read_nullable_array(
1374        array: &dyn ArrowArray,
1375    ) -> Result<(ArrowScalarBuffer<i64>, Option<ArrowNullBuffer>), TimeColumnError> {
1376        // Sequence timelines are i64, but time columns are nanoseconds (also as i64).
1377        if let Some(times) = array.downcast_array_ref::<arrow::array::Int64Array>() {
1378            Ok((times.values().clone(), times.nulls().cloned()))
1379        } else if let Some(times) =
1380            array.downcast_array_ref::<arrow::array::TimestampNanosecondArray>()
1381        {
1382            Ok((times.values().clone(), times.nulls().cloned()))
1383        } else if let Some(times) =
1384            array.downcast_array_ref::<arrow::array::Time64NanosecondArray>()
1385        {
1386            Ok((times.values().clone(), times.nulls().cloned()))
1387        } else if let Some(times) =
1388            array.downcast_array_ref::<arrow::array::DurationNanosecondArray>()
1389        {
1390            Ok((times.values().clone(), times.nulls().cloned()))
1391        } else {
1392            Err(TimeColumnError::UnsupportedDataType(
1393                array.data_type().clone(),
1394            ))
1395        }
1396    }
1397}
1398
1399// ---
1400
1401impl Chunk {
1402    #[inline]
1403    pub fn id(&self) -> ChunkId {
1404        self.id
1405    }
1406
1407    #[inline]
1408    pub fn entity_path(&self) -> &EntityPath {
1409        &self.entity_path
1410    }
1411
1412    /// How many columns in total? Includes control, time, and component columns.
1413    #[inline]
1414    pub fn num_columns(&self) -> usize {
1415        let Self {
1416            id: _,
1417            entity_path: _, // not an actual column
1418            heap_size_bytes: _,
1419            is_sorted: _,
1420            row_ids: _,
1421            timelines,
1422            components,
1423        } = self;
1424
1425        1 /* row_ids */ + timelines.len() + components.len()
1426    }
1427
1428    #[inline]
1429    pub fn num_controls(&self) -> usize {
1430        _ = self;
1431        1 /* row_ids */
1432    }
1433
1434    #[inline]
1435    pub fn num_timelines(&self) -> usize {
1436        self.timelines.len()
1437    }
1438
1439    #[inline]
1440    pub fn num_components(&self) -> usize {
1441        self.components.len()
1442    }
1443
1444    #[inline]
1445    pub fn num_rows(&self) -> usize {
1446        self.row_ids.len()
1447    }
1448
1449    #[inline]
1450    pub fn is_empty(&self) -> bool {
1451        self.num_rows() == 0
1452    }
1453
1454    #[inline]
1455    pub fn row_ids_array(&self) -> &FixedSizeBinaryArray {
1456        &self.row_ids
1457    }
1458
1459    #[inline]
1460    pub fn row_ids_slice(&self) -> &[RowId] {
1461        RowId::slice_from_arrow(&self.row_ids)
1462    }
1463
1464    /// All the [`RowId`] in this chunk.
1465    ///
1466    /// This could be in any order if this chunk is unsorted.
1467    #[inline]
1468    pub fn row_ids(&self) -> impl ExactSizeIterator<Item = RowId> + '_ {
1469        self.row_ids_slice().iter().copied()
1470    }
1471
1472    /// Find the row index of the given [`RowId`] in this chunk, if it is present.
1473    ///
1474    /// Uses a binary search on sorted chunks (the common case), falling back to
1475    /// a linear scan otherwise.
1476    #[inline]
1477    pub fn row_index_of(&self, row_id: RowId) -> Option<usize> {
1478        if self.is_row_ids_sorted() {
1479            self.row_ids_slice().binary_search(&row_id).ok()
1480        } else {
1481            self.row_ids_slice().iter().position(|r| *r == row_id)
1482        }
1483    }
1484
1485    /// Returns an iterator over the [`RowId`]s of a [`Chunk`], for a given component.
1486    ///
1487    /// This is different than [`Self::row_ids`]: it will only yield `RowId`s for rows at which
1488    /// there is data for the specified `component`.
1489    #[inline]
1490    pub fn component_row_ids(
1491        &self,
1492        component: ComponentIdentifier,
1493    ) -> impl Iterator<Item = RowId> + '_ + use<'_> {
1494        let Some(list_array) = self.components.get_array(component) else {
1495            return Either::Left(std::iter::empty());
1496        };
1497
1498        let row_ids = self.row_ids();
1499
1500        if let Some(validity) = list_array.nulls() {
1501            Either::Right(Either::Left(
1502                row_ids
1503                    .enumerate()
1504                    .filter_map(|(i, o)| validity.is_valid(i).then_some(o)),
1505            ))
1506        } else {
1507            Either::Right(Either::Right(row_ids))
1508        }
1509    }
1510
1511    /// Returns the [`RowId`]-range covered by this [`Chunk`].
1512    ///
1513    /// `None` if the chunk `is_empty`.
1514    ///
1515    /// This is O(1) if the chunk is sorted, O(n) otherwise.
1516    #[inline]
1517    pub fn row_id_range(&self) -> Option<(RowId, RowId)> {
1518        if self.is_empty() {
1519            return None;
1520        }
1521
1522        let row_ids = self.row_ids_slice();
1523
1524        #[expect(clippy::unwrap_used)] // checked above
1525        Some(if self.is_row_ids_sorted() {
1526            (
1527                row_ids.first().copied().unwrap(),
1528                row_ids.last().copied().unwrap(),
1529            )
1530        } else {
1531            (
1532                row_ids.iter().min().copied().unwrap(),
1533                row_ids.iter().max().copied().unwrap(),
1534            )
1535        })
1536    }
1537
1538    #[inline]
1539    pub fn is_static(&self) -> bool {
1540        self.timelines.is_empty()
1541    }
1542
1543    #[inline]
1544    pub fn timelines(&self) -> &IntMap<TimelineName, TimeColumn> {
1545        &self.timelines
1546    }
1547
1548    #[inline]
1549    pub fn components_identifiers(&self) -> impl Iterator<Item = ComponentIdentifier> + '_ {
1550        self.components.keys().copied()
1551    }
1552
1553    #[inline]
1554    pub fn components(&self) -> &ChunkComponents {
1555        &self.components
1556    }
1557}
1558
1559impl std::fmt::Display for Chunk {
1560    #[inline]
1561    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1562        let batch = self.to_record_batch().map_err(|err| {
1563            re_log::error_once!("couldn't display Chunk: {err}");
1564            std::fmt::Error
1565        })?;
1566        re_arrow_util::format_record_batch_with_width(&batch, f.width(), f.sign_minus()).fmt(f)
1567    }
1568}
1569
1570impl TimeColumn {
1571    #[inline]
1572    pub fn timeline(&self) -> &Timeline {
1573        &self.timeline
1574    }
1575
1576    #[inline]
1577    pub fn name(&self) -> &str {
1578        self.timeline.name()
1579    }
1580
1581    #[inline]
1582    pub fn time_range(&self) -> AbsoluteTimeRange {
1583        self.time_range
1584    }
1585
1586    #[inline]
1587    pub fn times_buffer(&self) -> &ArrowScalarBuffer<i64> {
1588        &self.times
1589    }
1590
1591    /// Returns an array with the appropriate datatype.
1592    #[inline]
1593    pub fn times_array(&self) -> ArrowArrayRef {
1594        self.timeline.typ().make_arrow_array(self.times.clone())
1595    }
1596
1597    /// All times in a time column are guaranteed not to have the value `i64::MIN`
1598    /// (which is reserved for static data).
1599    #[inline]
1600    pub fn times_raw(&self) -> &[i64] {
1601        self.times.as_ref()
1602    }
1603
1604    /// All times in a time column are guaranteed not to have the value `i64::MIN`
1605    /// (which is reserved for static data).
1606    #[inline]
1607    pub fn times_nonmin(&self) -> impl DoubleEndedIterator<Item = NonMinI64> + '_ {
1608        self.times_raw()
1609            .iter()
1610            .copied()
1611            .map(NonMinI64::saturating_from_i64)
1612    }
1613
1614    #[inline]
1615    pub fn times(&self) -> impl DoubleEndedIterator<Item = TimeInt> + '_ {
1616        self.times_raw().iter().copied().map(TimeInt::new_temporal)
1617    }
1618
1619    #[inline]
1620    pub fn num_rows(&self) -> usize {
1621        self.times.len()
1622    }
1623
1624    #[inline]
1625    pub fn is_empty(&self) -> bool {
1626        self.num_rows() == 0
1627    }
1628
1629    /// Computes the time range covered by each individual component column.
1630    ///
1631    /// This is different from the time range covered by the [`TimeColumn`] as a whole
1632    /// because component columns are potentially sparse.
1633    ///
1634    /// This is crucial for indexing and queries to work properly.
1635    //
1636    // TODO(cmc): This needs to be stored in chunk metadata and transported across IPC.
1637    pub fn time_range_per_component(
1638        &self,
1639        components: &ChunkComponents,
1640    ) -> IntMap<ComponentIdentifier, AbsoluteTimeRange> {
1641        let times = self.times_raw();
1642        components
1643            .iter()
1644            .filter_map(|(component, column)| {
1645                if let Some(validity) = column.list_array.nulls() {
1646                    // Potentially sparse
1647
1648                    if validity.is_empty() {
1649                        return None;
1650                    }
1651
1652                    let is_dense = validity.null_count() == 0;
1653                    if is_dense {
1654                        return Some((*component, self.time_range));
1655                    }
1656
1657                    let time_min = {
1658                        let mut valid_times = times
1659                            .iter()
1660                            .enumerate()
1661                            .filter(|(i, _time)| validity.is_valid(*i));
1662                        if times.is_sorted() {
1663                            valid_times.next()
1664                        } else {
1665                            valid_times.min_by_key(|(_i, time)| *time)
1666                        }
1667                        .map_or(TimeInt::MAX, |(_i, time)| TimeInt::new_temporal(*time))
1668                    };
1669
1670                    let time_max = {
1671                        let mut valid_times_inv = times
1672                            .iter()
1673                            .enumerate()
1674                            .rev()
1675                            .filter(|(i, _time)| validity.is_valid(*i));
1676                        if times.is_sorted() {
1677                            valid_times_inv.next()
1678                        } else {
1679                            valid_times_inv.max_by_key(|(_i, time)| *time)
1680                        }
1681                        .map_or(TimeInt::MIN, |(_i, time)| TimeInt::new_temporal(*time))
1682                    };
1683
1684                    Some((*component, AbsoluteTimeRange::new(time_min, time_max)))
1685                } else {
1686                    // Dense
1687
1688                    Some((*component, self.time_range))
1689                }
1690            })
1691            .collect()
1692    }
1693
1694    /// Find the earliest time strictly after `after` in this time column.
1695    pub fn find_next_time(&self, after: TimeInt) -> Option<TimeInt> {
1696        if self.is_sorted() {
1697            let times = self.times_raw();
1698            let idx = times.partition_point(|&t| t <= after.as_i64());
1699            (idx < times.len()).then(|| TimeInt::new_temporal(times[idx]))
1700        } else {
1701            self.times_raw()
1702                .iter()
1703                .filter(|&&t| t > after.as_i64())
1704                .min()
1705                .map(|&t| TimeInt::new_temporal(t))
1706        }
1707    }
1708
1709    /// Find the latest time strictly before `before` in this time column.
1710    pub fn find_prev_time(&self, before: TimeInt) -> Option<TimeInt> {
1711        if self.is_sorted() {
1712            let times = self.times_raw();
1713            let idx = times.partition_point(|&t| t < before.as_i64());
1714            (idx > 0).then(|| TimeInt::new_temporal(times[idx - 1]))
1715        } else {
1716            self.times_raw()
1717                .iter()
1718                .filter(|&&t| t < before.as_i64())
1719                .max()
1720                .map(|&t| TimeInt::new_temporal(t))
1721        }
1722    }
1723
1724    /// Returns a new [`TimeColumn`] with all time values offset by `offset_ns` nanoseconds.
1725    ///
1726    /// Uses saturating arithmetic.
1727    pub fn offset_by_nanos(&self, offset_ns: i64) -> Self {
1728        let new_times: Vec<i64> = self
1729            .times
1730            .iter()
1731            .map(|&t| NonMinI64::saturating_from_i64(t.saturating_add(offset_ns)).get())
1732            .collect();
1733
1734        Self::new(
1735            Some(self.is_sorted),
1736            self.timeline,
1737            ArrowScalarBuffer::from(new_times),
1738        )
1739    }
1740}
1741
1742impl Chunk {
1743    fn heap_size_bytes_inner(&self) -> u64 {
1744        re_tracing::profile_function!();
1745
1746        let Self {
1747            id,
1748            entity_path,
1749            heap_size_bytes: _,
1750            is_sorted,
1751            row_ids,
1752            timelines,
1753            components,
1754        } = self;
1755
1756        id.heap_size_bytes()
1757            + entity_path.heap_size_bytes()
1758            + is_sorted.heap_size_bytes()
1759            + row_ids.heap_size_bytes()
1760            + timelines.heap_size_bytes()
1761            + components.heap_size_bytes()
1762    }
1763}
1764
1765impl re_byte_size::SizeBytes for Chunk {
1766    #[inline]
1767    fn heap_size_bytes(&self) -> u64 {
1768        let mut size_bytes = self.heap_size_bytes.load(Ordering::Relaxed);
1769
1770        if size_bytes == 0 {
1771            size_bytes = self.heap_size_bytes_inner();
1772            self.heap_size_bytes.store(size_bytes, Ordering::Relaxed);
1773        }
1774
1775        size_bytes
1776    }
1777}
1778
1779// --- Sanity checks ---
1780
1781impl Chunk {
1782    /// Warn if we find out-of-order timelines.
1783    ///
1784    /// If `RERUN_VERY_STRICT` is set, this will instead panic.
1785    ///
1786    /// This assumes the chunk is already sorted by `RowId`, like most chunks.
1787    ///
1788    /// Out-of-order timelines are sometimes unavoidable,
1789    /// but if we can avoid them we absolutely should,
1790    /// because they cause much slower queries
1791    #[track_caller]
1792    pub fn warn_if_out_of_order(&self) {
1793        if !self.is_row_ids_sorted() {
1794            // Big problem!
1795            if re_log::is_rerun_very_strict() {
1796                panic!("Found chunk that wasn't sorted by RowId. This is a bug");
1797            } else {
1798                re_log::debug_warn_once!("Found chunk that wasn't sorted by RowId. This is a bug");
1799            }
1800        }
1801
1802        let unsorted_timelines: Vec<_> = self.unsorted_timelines().collect();
1803        if !unsorted_timelines.is_empty() {
1804            if re_log::is_rerun_very_strict() {
1805                panic!(
1806                    "Found out-of-order timelines for entity '{}': {:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems",
1807                    self.entity_path, unsorted_timelines
1808                );
1809            } else {
1810                // We don't mention the entity path here to avoid log spam
1811                re_log::debug_warn_once!(
1812                    "Found out-of-order timelines: {unsorted_timelines:?}. Out-of-order timelines are sometimes unavoidable, but they may cause performance problems",
1813                );
1814            }
1815        }
1816    }
1817
1818    /// Returns an error if the Chunk's invariants are not upheld.
1819    ///
1820    /// Costly checks are only run in debug builds.
1821    #[track_caller]
1822    pub fn sanity_check(&self) -> ChunkResult<()> {
1823        re_tracing::profile_function!();
1824
1825        let Self {
1826            id: _,
1827            entity_path: _,
1828            heap_size_bytes,
1829            is_sorted,
1830            row_ids,
1831            timelines,
1832            components,
1833        } = self;
1834
1835        self.warn_if_out_of_order();
1836
1837        if cfg!(debug_assertions) {
1838            let measured = self.heap_size_bytes_inner();
1839            let advertised = heap_size_bytes.load(Ordering::Relaxed);
1840            // We use 0 as not set yet.
1841            if advertised != 0 && advertised != measured {
1842                return Err(ChunkError::Malformed {
1843                    reason: format!(
1844                        "Chunk advertises a heap size of {} but we measure {} instead",
1845                        re_format::format_bytes(advertised as _),
1846                        re_format::format_bytes(measured as _),
1847                    ),
1848                });
1849            }
1850        }
1851
1852        // Row IDs
1853        {
1854            if *row_ids.data_type() != RowId::arrow_data_type() {
1855                return Err(ChunkError::Malformed {
1856                    reason: format!(
1857                        "RowId data has the wrong datatype: expected {} but got {} instead",
1858                        RowId::arrow_data_type(),
1859                        row_ids.data_type(),
1860                    ),
1861                });
1862            }
1863
1864            #[expect(clippy::collapsible_if)] // readability
1865            if cfg!(debug_assertions) {
1866                if *is_sorted != self.is_row_ids_sorted_uncached() {
1867                    return Err(ChunkError::Malformed {
1868                        reason: format!(
1869                            "Chunk is marked as {}sorted but isn't: {row_ids:?}",
1870                            if *is_sorted { "" } else { "un" },
1871                        ),
1872                    });
1873                }
1874            }
1875        }
1876
1877        // Timelines
1878        for (timeline_name, time_column) in timelines {
1879            if time_column.times.len() != row_ids.len() {
1880                return Err(ChunkError::Malformed {
1881                    reason: format!(
1882                        "All timelines in a chunk must have the same number of timestamps, matching the number of row IDs. \
1883                         Found {} row IDs but {} timestamps for timeline '{timeline_name}'",
1884                        row_ids.len(),
1885                        time_column.times.len(),
1886                    ),
1887                });
1888            }
1889
1890            time_column.sanity_check()?;
1891        }
1892
1893        // Components
1894
1895        for (component, column) in components.iter() {
1896            let SerializedComponentColumn {
1897                list_array,
1898                descriptor,
1899            } = column;
1900
1901            if descriptor.component != *component {
1902                return Err(ChunkError::Malformed {
1903                    reason: format!(
1904                        "Component key & descriptor mismatch. Descriptor: {descriptor:?}. Key: {component:?}",
1905                    ),
1906                });
1907            }
1908
1909            // Ensure that each cell is a list (we don't support mono-components yet).
1910            if let arrow::datatypes::DataType::List(field) = list_array.data_type() {
1911                // `Chunk::new` canonicalizes the outer list field of every component column, so
1912                // anything else here means the chunk was mutated in place afterwards — via
1913                // `ChunkComponents::list_arrays_mut` or its `DerefMut`. That is a programmer
1914                // error, so we only pay for the check in debug builds.
1915                // TODO(#6819): Remove support for inner nullability.
1916                if cfg!(debug_assertions)
1917                    && let canonical =
1918                        re_arrow_util::canonical_component_list_field(field.data_type().clone())
1919                    && field.as_ref() != &canonical
1920                {
1921                    return Err(ChunkError::Malformed {
1922                        reason: format!(
1923                            "Component column {component} must describe its outer list as {canonical:?}, got {field:?}",
1924                        ),
1925                    });
1926                }
1927            } else {
1928                return Err(ChunkError::Malformed {
1929                    reason: format!(
1930                        "The inner array in a chunked component batch must be a list, got {:?}",
1931                        list_array.data_type(),
1932                    ),
1933                });
1934            }
1935
1936            if list_array.len() != row_ids.len() {
1937                return Err(ChunkError::Malformed {
1938                    reason: format!(
1939                        "All component batches in a chunk must have the same number of rows, matching the number of row IDs. \
1940                             Found {} row IDs but {} rows for component batch {component}",
1941                        row_ids.len(),
1942                        list_array.len(),
1943                    ),
1944                });
1945            }
1946
1947            let validity_is_empty = list_array
1948                .nulls()
1949                .is_some_and(|validity| validity.is_empty());
1950            if !self.is_empty() && validity_is_empty {
1951                return Err(ChunkError::Malformed {
1952                    reason: format!(
1953                        "All component batches in a chunk must contain at least one non-null entry.\
1954                             Found a completely empty column for {component}",
1955                    ),
1956                });
1957            }
1958        }
1959
1960        Ok(())
1961    }
1962}
1963
1964impl TimeColumn {
1965    /// Returns an error if the Chunk's invariants are not upheld.
1966    ///
1967    /// Costly checks are only run in debug builds.
1968    #[track_caller]
1969    pub fn sanity_check(&self) -> ChunkResult<()> {
1970        let Self {
1971            timeline: _,
1972            times,
1973            is_sorted,
1974            time_range,
1975        } = self;
1976
1977        let times = times.as_ref();
1978
1979        if cfg!(debug_assertions) && *is_sorted != times.array_windows().all(|[a, b]| a <= b) {
1980            return Err(ChunkError::Malformed {
1981                reason: format!(
1982                    "Time column is marked as {}sorted but isn't: {times:?}",
1983                    if *is_sorted { "" } else { "un" },
1984                ),
1985            });
1986        }
1987
1988        if cfg!(debug_assertions) {
1989            let is_tight_lower_bound = times.iter().any(|&time| time == time_range.min().as_i64());
1990            let is_tight_upper_bound = times.iter().any(|&time| time == time_range.max().as_i64());
1991            let is_tight_bound = is_tight_lower_bound && is_tight_upper_bound;
1992
1993            if !self.is_empty() && !is_tight_bound {
1994                return Err(ChunkError::Malformed {
1995                    reason: "Time column's cached time range isn't a tight bound.".to_owned(),
1996                });
1997            }
1998
1999            for &time in times {
2000                if time < time_range.min().as_i64() || time > time_range.max().as_i64() {
2001                    return Err(ChunkError::Malformed {
2002                        reason: format!(
2003                            "Time column's cached time range is wrong.\
2004                             Found a time value of {time} while its time range is {time_range:?}",
2005                        ),
2006                    });
2007                }
2008
2009                if time == TimeInt::STATIC.as_i64() {
2010                    return Err(ChunkError::Malformed {
2011                        reason: "A chunk's timeline should never contain a static time value."
2012                            .to_owned(),
2013                    });
2014                }
2015            }
2016        }
2017
2018        Ok(())
2019    }
2020}