re_chunk/range.rs
1use re_log_types::{AbsoluteTimeRange, TimeInt, TimelineName};
2use re_types_core::ComponentIdentifier;
3
4use crate::Chunk;
5
6// --- Range ---
7
8/// A query over a time range, for a given timeline.
9///
10/// Get all the data within this time interval, plus the latest one before the start of the
11/// interval.
12///
13/// Motivation: all data is considered alive until the next logging to the same component path.
14#[derive(Clone, PartialEq, Eq, Hash)]
15pub struct RangeQuery {
16 pub timeline: TimelineName,
17 pub range: AbsoluteTimeRange,
18 pub options: RangeQueryOptions,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub struct RangeQueryOptions {
23 /// Should the results contain all extra timeline information available in the [`Chunk`]?
24 ///
25 /// While this information can be useful in some cases, it comes at a performance cost.
26 pub keep_extra_timelines: bool,
27
28 /// Should the results contain all extra component information available in the [`Chunk`]?
29 ///
30 /// While this information can be useful in some cases, it comes at a performance cost.
31 pub keep_extra_components: bool,
32
33 /// If true, the results will include one extra tick on each side of the range.
34 ///
35 /// Note: this is different from simply subtracting/adding one to the time range of the query,
36 /// as this will work even with non-contiguous time values, and even if these non-contiguous
37 /// jumps happen across multiple chunks.
38 ///
39 /// Consider for example this data:
40 /// ```text
41 /// ┌──────────────────────────────────┬───────────────┬──────────────────────┐
42 /// │ RowId ┆ frame_nr ┆ Scalar │
43 /// ╞══════════════════════════════════╪═══════════════╪══════════════════════╡
44 /// │ 17E9C11C655B21A9006568024DA10857 ┆ 0 ┆ [2] │
45 /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
46 /// │ 17E9C11C6560E8A6006568024DA10859 ┆ 2 ┆ [2.04] │
47 /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
48 /// │ 17E9C11C656504F0006568024DA1085B ┆ 4 ┆ [2.08] │
49 /// ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
50 /// │ 17E9C11C65693204006568024DA1085D ┆ 6 ┆ [2.12] │
51 /// └──────────────────────────────────┴───────────────┴──────────────────────┘
52 /// ```
53 ///
54 /// * A `RangeQuery(#2, #4)` would yield frames #2 and #4.
55 /// * A `RangeQuery(#1, #5)` would still only yield frames #2 and #4.
56 /// * A `RangeQuery(#2, #4, include_extended_bounds=true)`, on the other hand, would yield all of
57 /// frames #0, #2, #4 and #6.
58 pub include_extended_bounds: bool,
59}
60
61impl RangeQueryOptions {
62 pub const DEFAULT: Self = Self {
63 keep_extra_timelines: false,
64 keep_extra_components: false,
65 include_extended_bounds: false,
66 };
67}
68
69impl Default for RangeQueryOptions {
70 #[inline]
71 fn default() -> Self {
72 Self::DEFAULT
73 }
74}
75
76impl std::fmt::Debug for RangeQuery {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 let Self {
79 timeline,
80 range,
81 options,
82 } = self;
83
84 let RangeQueryOptions {
85 keep_extra_timelines,
86 keep_extra_components,
87 include_extended_bounds,
88 } = options;
89
90 f.write_fmt(format_args!(
91 "<ranging {:?}..={:?} on {:?}{}{}{}",
92 range.min(),
93 range.max(),
94 timeline,
95 if *keep_extra_timelines {
96 " keep_extra_timelines"
97 } else {
98 ""
99 },
100 if *keep_extra_components {
101 " keep_extra_components"
102 } else {
103 ""
104 },
105 if *include_extended_bounds {
106 " include_extended_bounds"
107 } else {
108 ""
109 },
110 ))
111 }
112}
113
114impl RangeQuery {
115 /// The returned query is guaranteed to never include [`TimeInt::STATIC`].
116 #[inline]
117 pub const fn new(timeline: TimelineName, range: AbsoluteTimeRange) -> Self {
118 Self {
119 timeline,
120 range,
121 options: RangeQueryOptions::DEFAULT,
122 }
123 }
124
125 /// The returned query is guaranteed to never include [`TimeInt::STATIC`].
126 ///
127 /// Keeps all extra timelines and components around.
128 #[inline]
129 pub const fn with_extras(timeline: TimelineName, range: AbsoluteTimeRange) -> Self {
130 Self {
131 timeline,
132 range,
133 options: RangeQueryOptions {
134 keep_extra_timelines: true,
135 keep_extra_components: true,
136 include_extended_bounds: false,
137 },
138 }
139 }
140
141 #[inline]
142 pub const fn everything(timeline: TimelineName) -> Self {
143 Self {
144 timeline,
145 range: AbsoluteTimeRange::EVERYTHING,
146 options: RangeQueryOptions::DEFAULT,
147 }
148 }
149
150 /// See [`RangeQueryOptions::keep_extra_timelines`] for more information.
151 #[inline]
152 pub fn keep_extra_timelines(mut self, toggle: bool) -> Self {
153 self.options.keep_extra_timelines = toggle;
154 self
155 }
156
157 /// See [`RangeQueryOptions::keep_extra_components`] for more information.
158 #[inline]
159 pub fn keep_extra_components(mut self, toggle: bool) -> Self {
160 self.options.keep_extra_components = toggle;
161 self
162 }
163
164 /// See [`RangeQueryOptions::include_extended_bounds`] for more information.
165 #[inline]
166 pub fn include_extended_bounds(mut self, toggle: bool) -> Self {
167 self.options.include_extended_bounds = toggle;
168 self
169 }
170
171 #[inline]
172 pub fn timeline(&self) -> &TimelineName {
173 &self.timeline
174 }
175
176 #[inline]
177 pub fn range(&self) -> AbsoluteTimeRange {
178 self.range
179 }
180
181 #[inline]
182 pub fn options(&self) -> RangeQueryOptions {
183 self.options.clone()
184 }
185}
186
187// ---
188
189impl Chunk {
190 /// Runs a [`RangeQuery`] filter on a [`Chunk`].
191 ///
192 /// This behaves as a row-based filter: the result is a new [`Chunk`] that is vertically
193 /// sliced, sorted and filtered in order to only contain the row(s) relevant for the
194 /// specified `query`.
195 ///
196 /// The resulting [`Chunk`] is guaranteed to contain all the same columns has the queried
197 /// chunk: there is no horizontal slicing going on.
198 ///
199 /// An empty [`Chunk`] (i.e. 0 rows, but N columns) is returned if the `query` yields nothing.
200 ///
201 /// Because the resulting chunk doesn't discard any column information, you can find extra relevant
202 /// information by inspecting the data, for examples timestamps on other timelines.
203 /// See [`Self::timeline_sliced`] and [`Self::component_sliced`] if you do want to filter this
204 /// extra data.
205 //
206 // TODO(RR-3865): Use arrow's `ListView` to avoid cloning data when the chunk requires sorting.
207 pub fn range(&self, query: &RangeQuery, component: ComponentIdentifier) -> Self {
208 if self.is_empty() {
209 return self.clone();
210 }
211
212 re_tracing::profile_function!(format!("{query:?}"));
213
214 let RangeQueryOptions {
215 keep_extra_timelines,
216 keep_extra_components,
217 include_extended_bounds,
218 } = query.options();
219
220 // Pre-slice the data if the caller allowed us: this will make further slicing
221 // (e.g. the range query itself) much cheaper to compute.
222 use std::borrow::Cow;
223 let chunk = if !keep_extra_timelines {
224 Cow::Owned(self.timeline_sliced(*query.timeline()))
225 } else {
226 Cow::Borrowed(self)
227 };
228 let chunk = if !keep_extra_components {
229 Cow::Owned(chunk.component_sliced(component))
230 } else {
231 chunk
232 };
233
234 if chunk.is_static() {
235 // NOTE: A given component for a given entity can only have one static entry associated
236 // with it, and this entry overrides everything else, which means it is functionally
237 // equivalent to just running a latest-at query.
238 chunk.latest_at(
239 &crate::LatestAtQuery::new(*query.timeline(), TimeInt::MAX),
240 component,
241 )
242 } else {
243 let Some(is_sorted_by_time) = chunk
244 .timelines
245 .get(query.timeline())
246 .map(|time_column| time_column.is_sorted())
247 else {
248 return chunk.emptied();
249 };
250
251 let (chunk, _densified) = chunk.densified(component);
252
253 let chunk = if is_sorted_by_time {
254 // Temporal, row-sorted, time-sorted chunk
255 chunk
256 } else {
257 // Temporal, unsorted chunk
258 chunk.sorted_by_timeline_if_unsorted(query.timeline())
259 };
260
261 let Some(times) = chunk
262 .timelines
263 .get(query.timeline())
264 .map(|time_column| time_column.times_raw())
265 else {
266 return chunk.emptied();
267 };
268
269 let mut start_index =
270 times.partition_point(|&time| time < query.range().min().as_i64());
271 let mut end_index = times.partition_point(|&time| time <= query.range().max().as_i64());
272
273 // See `RangeQueryOptions::include_extended_bounds` for more information.
274 if include_extended_bounds {
275 start_index = start_index.saturating_sub(1);
276 end_index = usize::min(self.num_rows(), end_index.saturating_add(1));
277 }
278
279 chunk.row_sliced_shallow(start_index, end_index.saturating_sub(start_index))
280 }
281 }
282}