Skip to content

Commit 56add26

Browse files
authored
Chunkified text-log view with multi-timeline display (#7027)
Title. This brings back the multi-timeline display feature, which had regressed when we introduced the new `ChunkStore`. - Fixes #6611 - DNM: requires #7023 ![image](https://github.com/user-attachments/assets/15a148b5-92d7-4548-b3e4-fc535f742915)
1 parent 2713062 commit 56add26

File tree

6 files changed

+146
-70
lines changed

6 files changed

+146
-70
lines changed

Cargo.lock

+2-1
Original file line numberDiff line numberDiff line change
@@ -5065,12 +5065,13 @@ version = "0.18.0-alpha.1+dev"
50655065
dependencies = [
50665066
"egui",
50675067
"egui_extras",
5068+
"itertools 0.13.0",
50685069
"re_chunk_store",
50695070
"re_data_ui",
50705071
"re_entity_db",
50715072
"re_log",
50725073
"re_log_types",
5073-
"re_query",
5074+
"re_query2",
50745075
"re_renderer",
50755076
"re_space_view",
50765077
"re_tracing",

crates/store/re_chunk/src/iter.rs

+76-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use arrow2::{
1010
};
1111
use itertools::{izip, Itertools};
1212

13-
use re_log_types::{TimeInt, Timeline};
13+
use re_log_types::{TimeInt, TimePoint, Timeline};
1414
use re_types_core::{ArrowBuffer, ArrowString, Component, ComponentName};
1515

1616
use crate::{Chunk, ChunkTimeline, RowId};
@@ -97,6 +97,81 @@ impl Chunk {
9797
}
9898
}
9999

100+
/// Returns an iterator over the [`TimePoint`]s of a [`Chunk`].
101+
///
102+
/// See also:
103+
/// * [`Self::iter_component_timepoints`].
104+
#[inline]
105+
pub fn iter_timepoints(&self) -> impl Iterator<Item = TimePoint> + '_ {
106+
let mut timelines = self
107+
.timelines
108+
.values()
109+
.map(|time_chunk| (time_chunk.timeline, time_chunk.times()))
110+
.collect_vec();
111+
112+
std::iter::from_fn(move || {
113+
let mut timepoint = TimePoint::default();
114+
for (timeline, times) in &mut timelines {
115+
timepoint.insert(*timeline, times.next()?);
116+
}
117+
Some(timepoint)
118+
})
119+
}
120+
121+
/// Returns an iterator over the [`TimePoint`]s of a [`Chunk`], for a given component.
122+
///
123+
/// This is different than [`Self::iter_timepoints`] in that it will only yield timepoints for rows
124+
/// at which there is data for the specified `component_name`.
125+
///
126+
/// See also [`Self::iter_timepoints`].
127+
pub fn iter_component_timepoints(
128+
&self,
129+
component_name: &ComponentName,
130+
) -> impl Iterator<Item = TimePoint> + '_ {
131+
let Some(list_array) = self.components.get(component_name) else {
132+
return Either::Left(std::iter::empty());
133+
};
134+
135+
if let Some(validity) = list_array.validity() {
136+
let mut timelines = self
137+
.timelines
138+
.values()
139+
.map(|time_chunk| {
140+
(
141+
time_chunk.timeline,
142+
time_chunk
143+
.times()
144+
.enumerate()
145+
.filter(|(i, _)| validity.get_bit(*i))
146+
.map(|(_, time)| time),
147+
)
148+
})
149+
.collect_vec();
150+
151+
Either::Right(Either::Left(std::iter::from_fn(move || {
152+
let mut timepoint = TimePoint::default();
153+
for (timeline, times) in &mut timelines {
154+
timepoint.insert(*timeline, times.next()?);
155+
}
156+
Some(timepoint)
157+
})))
158+
} else {
159+
let mut timelines = self
160+
.timelines
161+
.values()
162+
.map(|time_chunk| (time_chunk.timeline, time_chunk.times()))
163+
.collect_vec();
164+
165+
Either::Right(Either::Right(std::iter::from_fn(move || {
166+
let mut timepoint = TimePoint::default();
167+
for (timeline, times) in &mut timelines {
168+
timepoint.insert(*timeline, times.next()?);
169+
}
170+
Some(timepoint)
171+
})))
172+
}
173+
}
174+
100175
/// Returns an iterator over the offsets (`(offset, len)`) of a [`Chunk`], for a given
101176
/// component.
102177
///

crates/viewer/re_space_view/src/results_ext2.rs

+19-21
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,25 @@ pub trait RangeResultsExt {
227227
/// For results that are aware of the blueprint, overrides, store results, and defaults will be
228228
/// considered.
229229
fn get_optional_chunks(&self, component_name: &ComponentName) -> Cow<'_, [Chunk]>;
230+
231+
/// Returns a zero-copy iterator over all the results for the given `(timeline, component)` pair.
232+
///
233+
/// Call one of the following methods on the returned [`HybridResultsChunkIter`]:
234+
/// * [`HybridResultsChunkIter::primitive`]
235+
/// * [`HybridResultsChunkIter::primitive_array`]
236+
/// * [`HybridResultsChunkIter::string`]
237+
fn iter_as(
238+
&self,
239+
timeline: Timeline,
240+
component_name: ComponentName,
241+
) -> HybridResultsChunkIter<'_> {
242+
let chunks = self.get_optional_chunks(&component_name);
243+
HybridResultsChunkIter {
244+
chunks,
245+
timeline,
246+
component_name,
247+
}
248+
}
230249
}
231250

232251
impl RangeResultsExt for LatestAtResults {
@@ -465,24 +484,3 @@ impl<'a> HybridResultsChunkIter<'a> {
465484
})
466485
}
467486
}
468-
469-
impl<'a> HybridResults<'a> {
470-
/// Returns a zero-copy iterator over all the results for the given `(timeline, component)` pair.
471-
///
472-
/// Call one of the following methods on the returned [`HybridResultsChunkIter`]:
473-
/// * [`HybridResultsChunkIter::primitive`]
474-
/// * [`HybridResultsChunkIter::primitive_array`]
475-
/// * [`HybridResultsChunkIter::string`]
476-
pub fn iter_as(
477-
&'a self,
478-
timeline: Timeline,
479-
component_name: ComponentName,
480-
) -> HybridResultsChunkIter<'a> {
481-
let chunks = self.get_optional_chunks(&component_name);
482-
HybridResultsChunkIter {
483-
chunks,
484-
timeline,
485-
component_name,
486-
}
487-
}
488-
}

crates/viewer/re_space_view_text_log/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ re_data_ui.workspace = true
2424
re_entity_db.workspace = true
2525
re_log_types.workspace = true
2626
re_log.workspace = true
27-
re_query.workspace = true
27+
re_query2.workspace = true
2828
re_renderer.workspace = true
2929
re_space_view.workspace = true
3030
re_tracing.workspace = true
@@ -34,3 +34,4 @@ re_viewer_context.workspace = true
3434

3535
egui_extras.workspace = true
3636
egui.workspace = true
37+
itertools.workspace = true

crates/viewer/re_space_view_text_log/src/space_view_class.rs

+10-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::BTreeMap;
22

33
use re_data_ui::item_ui;
4-
use re_log_types::{EntityPath, TimePoint, Timeline};
4+
use re_log_types::{EntityPath, Timeline};
55
use re_types::View;
66
use re_types::{components::TextLogLevel, SpaceViewClassIdentifier};
77
use re_ui::UiExt as _;
@@ -279,20 +279,12 @@ fn table_ui(
279279
entries: &[&Entry],
280280
scroll_to_row: Option<usize>,
281281
) {
282-
let timelines = vec![*ctx.rec_cfg.time_ctrl.read().timeline()];
283-
284-
// TODO(#6611): This regressed because adding a metadata registry in the store is an antipattern.
285-
//
286-
// We'll bring back the multi-timeline display once we get rid of the native cache and start
287-
// exposing chunks directly instead.
288-
// Since chunks embed the data for all associated timelines, there'll be no extra work needed
289-
// to get that information out.
290-
// let timelines = state
291-
// .filters
292-
// .col_timelines
293-
// .iter()
294-
// .filter_map(|(timeline, visible)| visible.then_some(timeline))
295-
// .collect::<Vec<_>>();
282+
let timelines = state
283+
.filters
284+
.col_timelines
285+
.iter()
286+
.filter_map(|(timeline, visible)| visible.then_some(timeline))
287+
.collect::<Vec<_>>();
296288

297289
use egui_extras::Column;
298290

@@ -366,17 +358,17 @@ fn table_ui(
366358
let entry = &entries[row.index()];
367359

368360
// timeline(s)
369-
let timepoint: TimePoint = [(global_timeline, entry.time)].into();
370361
for timeline in &timelines {
371362
row.col(|ui| {
372-
let row_time = timepoint
363+
let row_time = entry
364+
.timepoint
373365
.get(timeline)
374366
.copied()
375367
.unwrap_or(re_log_types::TimeInt::STATIC);
376368
item_ui::time_button(ctx, ui, timeline, row_time);
377369

378370
if let Some(global_time) = global_time {
379-
if timeline == &global_timeline {
371+
if *timeline == &global_timeline {
380372
#[allow(clippy::comparison_chain)]
381373
if global_time < row_time {
382374
// We've past the global time - it is thus above this row.

crates/viewer/re_space_view_text_log/src/visualizer_system.rs

+37-28
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use itertools::izip;
12
use re_chunk_store::ResolvedTimeRange;
23
use re_chunk_store::RowId;
34
use re_entity_db::EntityPath;
45
use re_log_types::TimeInt;
5-
use re_query::{clamped_zip_1x2, range_zip_1x2};
6-
use re_space_view::{range_with_blueprint_resolved_data, RangeResultsExt};
6+
use re_log_types::TimePoint;
7+
use re_query2::{clamped_zip_1x2, range_zip_1x2};
8+
use re_space_view::{range_with_blueprint_resolved_data2, RangeResultsExt2};
79
use re_types::{
810
archetypes::TextLog,
911
components::{Color, Text, TextLogLevel},
@@ -19,6 +21,7 @@ pub struct Entry {
1921
pub row_id: RowId,
2022
pub entity_path: EntityPath,
2123
pub time: TimeInt,
24+
pub timepoint: TimePoint,
2225
pub color: Option<Color>,
2326
pub body: Text,
2427
pub level: Option<TextLogLevel>,
@@ -53,13 +56,7 @@ impl VisualizerSystem for TextLogSystem {
5356
re_chunk_store::RangeQuery::new(view_query.timeline, ResolvedTimeRange::EVERYTHING);
5457

5558
for data_result in view_query.iter_visible_data_results(ctx, Self::identifier()) {
56-
if let Err(err) = self.process_entity(ctx, &query, data_result) {
57-
re_log::error_once!(
58-
"Error visualizing text logs for {:?}: {:?}",
59-
data_result.entity_path,
60-
err
61-
);
62-
}
59+
self.process_entity(ctx, &query, data_result);
6360
}
6461

6562
{
@@ -86,36 +83,49 @@ impl TextLogSystem {
8683
ctx: &ViewContext<'_>,
8784
query: &re_chunk_store::RangeQuery,
8885
data_result: &re_viewer_context::DataResult,
89-
) -> Result<(), SpaceViewSystemExecutionError> {
86+
) {
9087
re_tracing::profile_function!();
91-
let resolver = ctx.recording().resolver();
9288

93-
let results = range_with_blueprint_resolved_data(
89+
let results = range_with_blueprint_resolved_data2(
9490
ctx,
9591
None,
9692
query,
9793
data_result,
9894
[Text::name(), TextLogLevel::name(), Color::name()],
9995
);
10096

101-
let Some(all_texts) = results
102-
.get_required_component_dense::<Text>(resolver)
103-
.transpose()?
104-
else {
105-
return Ok(());
97+
let Some(all_text_chunks) = results.get_required_chunks(&Text::name()) else {
98+
return;
10699
};
107100

108-
let all_levels = results.get_or_empty_dense::<TextLogLevel>(resolver)?;
109-
let all_colors = results.get_or_empty_dense::<Color>(resolver)?;
101+
// TODO(cmc): It would be more efficient (both space and compute) to do this lazily as
102+
// we're rendering the table by indexing back into the original chunk etc.
103+
// Let's keep it simple for now, until we have data suggested we need the extra perf.
104+
let all_timepoints = all_text_chunks
105+
.iter()
106+
.flat_map(|chunk| chunk.iter_component_timepoints(&Text::name()));
107+
108+
let timeline = query.timeline();
109+
let all_texts = results.iter_as(timeline, Text::name());
110+
let all_levels = results.iter_as(timeline, TextLogLevel::name());
111+
let all_colors = results.iter_as(timeline, Color::name());
112+
110113
let all_frames = range_zip_1x2(
111-
all_texts.range_indexed(),
112-
all_levels.range_indexed(),
113-
all_colors.range_indexed(),
114+
all_texts.string(),
115+
all_levels.component(),
116+
all_colors.primitive::<u32>(),
114117
);
115118

116-
for (&(data_time, row_id), bodies, levels, colors) in all_frames {
117-
let levels = levels.unwrap_or(&[]).iter().cloned().map(Some);
118-
let colors = colors.unwrap_or(&[]).iter().copied().map(Some);
119+
let all_frames = izip!(all_timepoints, all_frames);
120+
121+
for (timepoint, ((data_time, row_id), bodies, levels, colors)) in all_frames {
122+
let levels = levels.as_deref().unwrap_or(&[]).iter().cloned().map(Some);
123+
let colors = colors
124+
.unwrap_or(&[])
125+
.iter()
126+
.copied()
127+
.map(Into::into)
128+
.map(Some);
119129

120130
let level_default_fn = || None;
121131
let color_default_fn = || None;
@@ -128,14 +138,13 @@ impl TextLogSystem {
128138
row_id,
129139
entity_path: data_result.entity_path.clone(),
130140
time: data_time,
141+
timepoint: timepoint.clone(),
131142
color,
132-
body: text.clone(),
143+
body: text.clone().into(),
133144
level,
134145
});
135146
}
136147
}
137-
138-
Ok(())
139148
}
140149
}
141150

0 commit comments

Comments
 (0)