diff --git a/crates/re_data_store/benches/arrow2.rs b/crates/re_data_store/benches/arrow2.rs index cb3f76af92a5..29e8f7412ad0 100644 --- a/crates/re_data_store/benches/arrow2.rs +++ b/crates/re_data_store/benches/arrow2.rs @@ -5,7 +5,7 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; use std::sync::Arc; -use arrow2::array::{Array, PrimitiveArray, StructArray, UnionArray}; +use arrow2::array::{Array, FixedSizeListArray, PrimitiveArray, StructArray}; use criterion::Criterion; use itertools::Itertools; @@ -277,8 +277,8 @@ fn estimated_size_bytes(c: &mut Criterion) { ArrayKind::Primitive => { bench_downcast_first::>(&mut group, kind); } - ArrayKind::Struct => bench_downcast_first::(&mut group, kind), - ArrayKind::StructLarge => bench_downcast_first::(&mut group, kind), + ArrayKind::Struct => bench_downcast_first::(&mut group, kind), + ArrayKind::StructLarge => bench_downcast_first::(&mut group, kind), } fn bench_downcast_first( diff --git a/crates/re_log_types/src/example_components.rs b/crates/re_log_types/src/example_components.rs index 1cb8e906ef9c..5bdf898019c4 100644 --- a/crates/re_log_types/src/example_components.rs +++ b/crates/re_log_types/src/example_components.rs @@ -1,17 +1,21 @@ //! Example components to be used for tests and docs -use re_types_core::{Loggable, SizeBytes}; +use re_types_core::{components::InstanceKey, Loggable, SizeBytes}; // ---------------------------------------------------------------------------- #[derive(Debug)] pub struct MyPoints; +impl MyPoints { + pub const NUM_COMPONENTS: usize = 5; +} + impl re_types_core::Archetype for MyPoints { type Indicator = re_types_core::GenericIndicatorComponent; fn name() -> re_types_core::ArchetypeName { - "test.MyPoints".into() + "example.MyPoints".into() } fn required_components() -> ::std::borrow::Cow<'static, [re_types_core::ComponentName]> { @@ -19,7 +23,13 @@ impl re_types_core::Archetype for MyPoints { } fn recommended_components() -> std::borrow::Cow<'static, [re_types_core::ComponentName]> { - vec![MyColor::name(), MyLabel::name()].into() + vec![ + re_types_core::LoggableBatch::name(&Self::Indicator::default()), + InstanceKey::name(), + MyColor::name(), + MyLabel::name(), + ] + .into() } } @@ -32,6 +42,7 @@ pub struct MyPoint { } impl MyPoint { + #[inline] pub fn new(x: f32, y: f32) -> Self { Self { x, y } } @@ -121,7 +132,15 @@ impl Loggable for MyPoint { #[repr(transparent)] pub struct MyColor(pub u32); +impl MyColor { + #[inline] + pub fn from_rgb(r: u8, g: u8, b: u8) -> Self { + Self(u32::from_le_bytes([r, g, b, 255])) + } +} + impl From for MyColor { + #[inline] fn from(value: u32) -> Self { Self(value) } diff --git a/crates/re_query_cache/src/cache.rs b/crates/re_query_cache/src/cache.rs index 30f4b1906fa9..bebe00e5f1ac 100644 --- a/crates/re_query_cache/src/cache.rs +++ b/crates/re_query_cache/src/cache.rs @@ -18,7 +18,7 @@ use re_types_core::{ components::InstanceKey, Archetype, ArchetypeName, Component, ComponentName, SizeBytes as _, }; -use crate::{ErasedFlatVecDeque, FlatVecDeque}; +use crate::{ErasedFlatVecDeque, FlatVecDeque, LatestAtCache, RangeCache}; // --- @@ -67,6 +67,17 @@ pub struct CachesPerArchetype { // than an `ArchetypeName`: the query system doesn't care about archetypes. pub(crate) latest_at_per_archetype: RwLock>>>, + /// Which [`Archetype`] are we querying for? + /// + /// This is very important because of our data model: we not only query for components, but we + /// query for components from a specific point-of-view (the so-called primary component). + /// Different archetypes have different point-of-views, and therefore can end up with different + /// results, even from the same raw data. + // + // TODO(cmc): At some point we should probably just store the PoV and optional components rather + // than an `ArchetypeName`: the query system doesn't care about archetypes. + pub(crate) range_per_archetype: RwLock>>>, + /// Everything greater than or equal to this timestamp has been asynchronously invalidated. /// /// The next time this cache gets queried, it must remove any entry matching this criteria. @@ -134,6 +145,42 @@ impl Caches { f(&mut cache) } + /// Gives write access to the appropriate `RangeCache` according to the specified + /// query parameters. + #[inline] + pub fn with_range( + store_id: StoreId, + entity_path: EntityPath, + query: &RangeQuery, + mut f: F, + ) -> R + where + A: Archetype, + F: FnMut(&mut RangeCache) -> R, + { + let key = CacheKey::new(store_id, entity_path, query.timeline); + + let cache = + re_data_store::DataStore::with_subscriber_once(*CACHES, move |caches: &Caches| { + let mut caches = caches.0.write(); + + let caches_per_archetype = caches.entry(key.clone()).or_default(); + caches_per_archetype.handle_pending_invalidation(&key); + + let mut range_per_archetype = caches_per_archetype.range_per_archetype.write(); + let range_cache = range_per_archetype.entry(A::name()).or_default(); + + Arc::clone(range_cache) + + // Implicitly releasing all intermediary locks. + }) + // NOTE: downcasting cannot fail, this is our own private handle. + .unwrap(); + + let mut cache = cache.write(); + f(&mut cache) + } + #[inline] pub(crate) fn with R, R>(f: F) -> R { // NOTE: downcasting cannot fail, this is our own private handle. @@ -347,6 +394,9 @@ pub struct CacheBucket { /// /// This corresponds to the data time and `RowId` returned by `re_query::query_archetype`. /// + /// This is guaranteed to always be sorted and dense (i.e. there cannot be a hole in the cached + /// data, unless the raw data itself in the store has a hole at that particular point in time). + /// /// Reminder: within a single timestamp, rows are sorted according to their [`RowId`]s. pub(crate) data_times: VecDeque<(TimeInt, RowId)>, @@ -375,6 +425,18 @@ impl CacheBucket { self.data_times.iter() } + #[inline] + pub fn contains_data_time(&self, data_time: TimeInt) -> bool { + let first_time = self.data_times.front().map_or(&TimeInt::MAX, |(t, _)| t); + let last_time = self.data_times.back().map_or(&TimeInt::MIN, |(t, _)| t); + *first_time <= data_time && data_time <= *last_time + } + + #[inline] + pub fn contains_data_row(&self, data_time: TimeInt, row_id: RowId) -> bool { + self.data_times.binary_search(&(data_time, row_id)).is_ok() + } + /// Iterate over the [`InstanceKey`] batches of the point-of-view components. #[inline] pub fn iter_pov_instance_keys(&self) -> impl Iterator { @@ -554,42 +616,3 @@ impl CacheBucket { Ok(added_size_bytes) } } - -// --- - -// NOTE: Because we're working with deserialized data, everything has to be done with metaprogramming, -// which is notoriously painful in Rust (i.e., macros). -// For this reason we move as much of the code as possible into the already existing macros in `query.rs`. - -/// Caches the results of `LatestAt` archetype queries (`ArchetypeView`). -/// -/// There is one `LatestAtCache` for each unique [`CacheKey`]. -/// -/// All query steps are cached: index search, cluster key joins and deserialization. -#[derive(Default)] -pub struct LatestAtCache { - /// Organized by _query_ time. - /// - /// If the data you're looking for isn't in here, try partially running the query (i.e. run the - /// index search in order to find a data time, but don't actually deserialize and join the data) - /// and check if there is any data available for the resulting _data_ time in [`Self::per_data_time`]. - pub per_query_time: BTreeMap>>, - - /// Organized by _data_ time. - /// - /// Due to how our latest-at semantics work, any number of queries at time `T+n` where `n >= 0` - /// can result in a data time of `T`. - pub per_data_time: BTreeMap>>, - - /// Dedicated bucket for timeless data, if any. - /// - /// Query time and data time are one and the same in the timeless case, therefore we only need - /// this one bucket. - // - // NOTE: Lives separately so we don't pay the extra `Option` cost in the much more common - // timeful case. - pub timeless: Option, - - /// Total size of the data stored in this cache in bytes. - pub total_size_bytes: u64, -} diff --git a/crates/re_query_cache/src/latest_at.rs b/crates/re_query_cache/src/latest_at.rs new file mode 100644 index 000000000000..491653881106 --- /dev/null +++ b/crates/re_query_cache/src/latest_at.rs @@ -0,0 +1,222 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use parking_lot::RwLock; +use paste::paste; +use seq_macro::seq; + +use re_data_store::{DataStore, LatestAtQuery, TimeInt}; +use re_log_types::{EntityPath, RowId}; +use re_query::query_archetype; +use re_types_core::{components::InstanceKey, Archetype, Component}; + +use crate::{CacheBucket, Caches, MaybeCachedComponentData}; + +// --- Data structures --- + +/// Caches the results of `LatestAt` queries. +#[derive(Default)] +pub struct LatestAtCache { + /// Organized by _query_ time. + /// + /// If the data you're looking for isn't in here, try partially running the query and check + /// if there is any data available for the resulting _data_ time in [`Self::per_data_time`]. + pub per_query_time: BTreeMap>>, + + /// Organized by _data_ time. + /// + /// Due to how our latest-at semantics work, any number of queries at time `T+n` where `n >= 0` + /// can result in a data time of `T`. + pub per_data_time: BTreeMap>>, + + /// Dedicated bucket for timeless data, if any. + /// + /// Query time and data time are one and the same in the timeless case, therefore we only need + /// this one bucket. + // + // NOTE: Lives separately so we don't pay the extra `Option` cost in the much more common + // timeful case. + pub timeless: Option, + + /// Total size of the data stored in this cache in bytes. + pub total_size_bytes: u64, +} + +// --- Queries --- + +macro_rules! impl_query_archetype_latest_at { + (for N=$N:expr, M=$M:expr => povs=[$($pov:ident)+] comps=[$($comp:ident)*]) => { paste! { + #[doc = "Cached implementation of [`re_query::query_archetype`] and [`re_query::range_archetype`]"] + #[doc = "(combined) for `" $N "` point-of-view components and `" $M "` optional components."] + #[allow(non_snake_case)] + pub fn []<'a, A, $($pov,)+ $($comp,)* F>( + store: &'a DataStore, + query: &LatestAtQuery, + entity_path: &'a EntityPath, + mut f: F, + ) -> ::re_query::Result<()> + where + A: Archetype + 'a, + $($pov: Component + Send + Sync + 'static,)+ + $($comp: Component + Send + Sync + 'static,)* + F: FnMut( + ( + (Option, RowId), + MaybeCachedComponentData<'_, InstanceKey>, + $(MaybeCachedComponentData<'_, $pov>,)+ + $(MaybeCachedComponentData<'_, Option<$comp>>,)* + ), + ), + { + let mut iter_results = |timeless: bool, bucket: &crate::CacheBucket| -> crate::Result<()> { + re_tracing::profile_scope!("iter"); + + let it = itertools::izip!( + bucket.iter_data_times(), + bucket.iter_pov_instance_keys(), + $(bucket.iter_component::<$pov>() + .ok_or_else(|| re_query::ComponentNotFoundError(<$pov>::name()))?,)+ + $(bucket.iter_component_opt::<$comp>() + .ok_or_else(|| re_query::ComponentNotFoundError(<$comp>::name()))?,)* + ).map(|((time, row_id), instance_keys, $($pov,)+ $($comp,)*)| { + ( + ((!timeless).then_some(*time), *row_id), + MaybeCachedComponentData::Cached(instance_keys), + $(MaybeCachedComponentData::Cached($pov),)+ + $(MaybeCachedComponentData::Cached($comp),)* + ) + }); + + for data in it { + f(data); + } + + + Ok(()) + }; + + let upsert_results = | + data_time: TimeInt, + arch_view: &::re_query::ArchetypeView, + bucket: &mut crate::CacheBucket, + | -> crate::Result { + re_log::trace!(data_time=?data_time, ?data_time, "fill"); + + // Grabbing the current time is quite costly on web. + #[cfg(not(target_arch = "wasm32"))] + let now = web_time::Instant::now(); + + let mut added_size_bytes = 0u64; + added_size_bytes += bucket.[]::(data_time, &arch_view)?; + + #[cfg(not(target_arch = "wasm32"))] + { + let elapsed = now.elapsed(); + ::re_log::trace!( + store_id=%store.id(), + %entity_path, + archetype=%A::name(), + added_size_bytes, + "cached new entry in {elapsed:?} ({:0.3} entries/s)", + 1f64 / elapsed.as_secs_f64() + ); + } + + Ok(added_size_bytes) + }; + + let mut latest_at_callback = |query: &LatestAtQuery, latest_at_cache: &mut crate::LatestAtCache| { + re_tracing::profile_scope!("latest_at", format!("{query:?}")); + + let crate::LatestAtCache { per_query_time, per_data_time, timeless, total_size_bytes } = latest_at_cache; + + let query_time_bucket_at_query_time = match per_query_time.entry(query.at) { + std::collections::btree_map::Entry::Occupied(query_time_bucket_at_query_time) => { + // Fastest path: we have an entry for this exact query time, no need to look any + // further. + re_log::trace!(query_time=?query.at, "cache hit (query time)"); + return iter_results(false, &query_time_bucket_at_query_time.get().read()); + } + entry => entry, + }; + + + let arch_view = query_archetype::(store, &query, entity_path)?; + let data_time = arch_view.data_time(); + + // Fast path: we've run the query and realized that we already have the data for the resulting + // _data_ time, so let's use that to avoid join & deserialization costs. + if let Some(data_time) = data_time { // Reminder: `None` means timeless. + if let Some(data_time_bucket_at_data_time) = per_data_time.get(&data_time) { + re_log::trace!(query_time=?query.at, ?data_time, "cache hit (data time)"); + + *query_time_bucket_at_query_time.or_default() = std::sync::Arc::clone(&data_time_bucket_at_data_time); + + // We now know for a fact that a query at that data time would yield the same + // results: copy the bucket accordingly so that the next cache hit for that query + // time ends up taking the fastest path. + let query_time_bucket_at_data_time = per_query_time.entry(data_time); + *query_time_bucket_at_data_time.or_default() = std::sync::Arc::clone(&data_time_bucket_at_data_time); + + return iter_results(false, &data_time_bucket_at_data_time.read()); + } + } else { + if let Some(timeless_bucket) = timeless.as_ref() { + re_log::trace!(query_time=?query.at, "cache hit (data time, timeless)"); + return iter_results(true, timeless_bucket); + } + } + + + // Slowest path: this is a complete cache miss. + if let Some(data_time) = data_time { // Reminder: `None` means timeless. + re_log::trace!(query_time=?query.at, ?data_time, "cache miss"); + + // BEWARE: Do _not_ move this out of this scope, or a bucket would be created + // even when taking the timeless path! + let query_time_bucket_at_query_time = query_time_bucket_at_query_time.or_default(); + + { + let mut query_time_bucket_at_query_time = query_time_bucket_at_query_time.write(); + *total_size_bytes += upsert_results(data_time, &arch_view, &mut query_time_bucket_at_query_time)?; + } + + let data_time_bucket_at_data_time = per_data_time.entry(data_time); + *data_time_bucket_at_data_time.or_default() = std::sync::Arc::clone(&query_time_bucket_at_query_time); + + iter_results(false, &query_time_bucket_at_query_time.read()) + } else { + re_log::trace!(query_time=?query.at, "cache miss (timeless)"); + + let mut timeless_bucket = crate::CacheBucket::default(); + + *total_size_bytes += upsert_results(TimeInt::MIN, &arch_view, &mut timeless_bucket)?; + iter_results(true, &timeless_bucket)?; + + *timeless = Some(timeless_bucket); + + Ok(()) + } + }; + + + Caches::with_latest_at::( + store.id().clone(), + entity_path.clone(), + query, + |latest_at_cache| latest_at_callback(query, latest_at_cache), + ) + } } + }; + + // TODO(cmc): Supporting N>1 generically is quite painful due to limitations in declarative macros, + // not that we care at the moment. + (for N=1, M=$M:expr) => { + seq!(COMP in 1..=$M { + impl_query_archetype_latest_at!(for N=1, M=$M => povs=[R1] comps=[#(C~COMP)*]); + }); + }; +} + +seq!(NUM_COMP in 0..10 { + impl_query_archetype_latest_at!(for N=1, M=NUM_COMP); +}); diff --git a/crates/re_query_cache/src/lib.rs b/crates/re_query_cache/src/lib.rs index a11b8a0f6e0f..c5f95d489438 100644 --- a/crates/re_query_cache/src/lib.rs +++ b/crates/re_query_cache/src/lib.rs @@ -3,7 +3,9 @@ mod cache; mod cache_stats; mod flat_vec_deque; +mod latest_at; mod query; +mod range; pub use self::cache::{AnyQuery, Caches}; pub use self::cache_stats::{ @@ -13,13 +15,6 @@ pub use self::flat_vec_deque::{ErasedFlatVecDeque, FlatVecDeque}; pub use self::query::{ query_archetype_pov1, query_archetype_with_history_pov1, MaybeCachedComponentData, }; - -pub(crate) use self::cache::{CacheBucket, LatestAtCache}; - -pub use re_query::{QueryError, Result}; // convenience - -// TODO(cmc): Supporting N>1 generically is quite painful due to limitations in declarative macros, -// not that we care at the moment. seq_macro::seq!(NUM_COMP in 0..10 { paste::paste! { pub use self::query::{#( query_archetype_pov1_comp~NUM_COMP, @@ -27,6 +22,22 @@ seq_macro::seq!(NUM_COMP in 0..10 { paste::paste! { )*}; }}); +pub(crate) use self::cache::CacheBucket; +pub(crate) use self::latest_at::LatestAtCache; +seq_macro::seq!(NUM_COMP in 0..10 { paste::paste! { + pub(crate) use self::latest_at::{#( + query_archetype_latest_at_pov1_comp~NUM_COMP, + )*}; +}}); +pub(crate) use self::range::RangeCache; +seq_macro::seq!(NUM_COMP in 0..10 { paste::paste! { + pub(crate) use self::range::{#( + query_archetype_range_pov1_comp~NUM_COMP, + )*}; +}}); + +pub use re_query::{QueryError, Result}; // convenience + pub mod external { pub use re_query; diff --git a/crates/re_query_cache/src/query.rs b/crates/re_query_cache/src/query.rs index 6b998007dc86..13a0595d846b 100644 --- a/crates/re_query_cache/src/query.rs +++ b/crates/re_query_cache/src/query.rs @@ -4,10 +4,9 @@ use seq_macro::seq; use re_data_store::{DataStore, LatestAtQuery, RangeQuery, TimeInt, TimeRange, Timeline}; use re_entity_db::{ExtraQueryHistory, VisibleHistory}; use re_log_types::{EntityPath, RowId}; -use re_query::query_archetype; use re_types_core::{components::InstanceKey, Archetype, Component}; -use crate::{AnyQuery, Caches}; +use crate::AnyQuery; // --- @@ -106,136 +105,36 @@ macro_rules! impl_query_archetype { format!("cached={cached} arch={} pov={} comp={}", A::name(), $N, $M) ); + match &query { + AnyQuery::LatestAt(query) if !cached => { + re_tracing::profile_scope!("latest_at", format!("{query:?}")); - let mut iter_results = |timeless: bool, bucket: &crate::CacheBucket| -> crate::Result<()> { - re_tracing::profile_scope!("iter"); - - let it = itertools::izip!( - bucket.iter_data_times(), - bucket.iter_pov_instance_keys(), - $(bucket.iter_component::<$pov>() - .ok_or_else(|| re_query::ComponentNotFoundError(<$pov>::name()))?,)+ - $(bucket.iter_component_opt::<$comp>() - .ok_or_else(|| re_query::ComponentNotFoundError(<$comp>::name()))?,)* - ).map(|((time, row_id), instance_keys, $($pov,)+ $($comp,)*)| { - ( - ((!timeless).then_some(*time), *row_id), - MaybeCachedComponentData::Cached(instance_keys), - $(MaybeCachedComponentData::Cached($pov),)+ - $(MaybeCachedComponentData::Cached($comp),)* - ) - }); - - for data in it { - f(data); - } - - Ok(()) - }; + let arch_view = ::re_query::query_archetype::(store, query, entity_path)?; - let upsert_results = | - data_time: TimeInt, - arch_view: &::re_query::ArchetypeView, - bucket: &mut crate::CacheBucket, - | -> crate::Result { - re_log::trace!(data_time=?data_time, ?data_time, "fill"); - - // Grabbing the current time is quite costly on web. - #[cfg(not(target_arch = "wasm32"))] - let now = web_time::Instant::now(); - - let mut added_size_bytes = 0u64; - added_size_bytes += bucket.[]::(data_time, &arch_view)?; - - #[cfg(not(target_arch = "wasm32"))] - { - let elapsed = now.elapsed(); - ::re_log::trace!( - store_id=%store.id(), - %entity_path, - archetype=%A::name(), - added_size_bytes, - "cached new entry in {elapsed:?} ({:0.3} entries/s)", - 1f64 / elapsed.as_secs_f64() + let data = ( + (arch_view.data_time(), arch_view.primary_row_id()), + MaybeCachedComponentData::Raw(arch_view.iter_instance_keys().collect()), + $(MaybeCachedComponentData::Raw(arch_view.iter_required_component::<$pov>()?.collect()),)+ + $(MaybeCachedComponentData::Raw(arch_view.iter_optional_component::<$comp>()?.collect()),)* ); - } - - Ok(added_size_bytes) - }; - - let mut latest_at_callback = |query: &LatestAtQuery, latest_at_cache: &mut crate::LatestAtCache| { - re_tracing::profile_scope!("latest_at", format!("{query:?}")); - let crate::LatestAtCache { per_query_time, per_data_time, timeless, total_size_bytes } = latest_at_cache; - - let query_time_bucket_at_query_time = match per_query_time.entry(query.at) { - std::collections::btree_map::Entry::Occupied(query_time_bucket_at_query_time) => { - // Fastest path: we have an entry for this exact query time, no need to look any - // further. - re_log::trace!(query_time=?query.at, "cache hit (query time)"); - return iter_results(false, &query_time_bucket_at_query_time.get().read()); - } - entry @ std::collections::btree_map::Entry::Vacant(_) => entry, - }; - - let arch_view = query_archetype::(store, &query, entity_path)?; - let data_time = arch_view.data_time(); - - // Fast path: we've run the query and realized that we already have the data for the resulting - // _data_ time, so let's use that to avoid join & deserialization costs. - if let Some(data_time) = data_time { // Reminder: `None` means timeless. - if let Some(data_time_bucket_at_data_time) = per_data_time.get(&data_time) { - re_log::trace!(query_time=?query.at, ?data_time, "cache hit (data time)"); - - *query_time_bucket_at_query_time.or_default() = std::sync::Arc::clone(&data_time_bucket_at_data_time); - - // We now know for a fact that a query at that data time would yield the same - // results: copy the bucket accordingly so that the next cache hit for that query - // time ends up taking the fastest path. - let query_time_bucket_at_data_time = per_query_time.entry(data_time); - *query_time_bucket_at_data_time.or_default() = std::sync::Arc::clone(&data_time_bucket_at_data_time); + f(data); - return iter_results(false, &data_time_bucket_at_data_time.read()); - } - } else { - if let Some(timeless_bucket) = timeless.as_ref() { - re_log::trace!(query_time=?query.at, "cache hit (data time, timeless)"); - return iter_results(true, timeless_bucket); - } + Ok(()) } - let query_time_bucket_at_query_time = query_time_bucket_at_query_time.or_default(); - - // Slowest path: this is a complete cache miss. - if let Some(data_time) = data_time { // Reminder: `None` means timeless. - re_log::trace!(query_time=?query.at, ?data_time, "cache miss"); - - { - let mut query_time_bucket_at_query_time = query_time_bucket_at_query_time.write(); - *total_size_bytes += upsert_results(data_time, &arch_view, &mut query_time_bucket_at_query_time)?; - } - - let data_time_bucket_at_data_time = per_data_time.entry(data_time); - *data_time_bucket_at_data_time.or_default() = std::sync::Arc::clone(&query_time_bucket_at_query_time); - - iter_results(false, &query_time_bucket_at_query_time.read()) - } else { - re_log::trace!(query_time=?query.at, "cache miss (timeless)"); - - let mut timeless_bucket = crate::CacheBucket::default(); - - *total_size_bytes += upsert_results(TimeInt::MIN, &arch_view, &mut timeless_bucket)?; - iter_results(true, &timeless_bucket)?; + AnyQuery::LatestAt(query) => { + re_tracing::profile_scope!("latest_at", format!("{query:?}")); - *timeless = Some(timeless_bucket); - Ok(()) + crate::[]::( + store, + query, + entity_path, + f, + ) } - }; - - match &query { - // TODO(cmc): cached range support - AnyQuery::Range(query) => { + AnyQuery::Range(query) if !cached => { re_tracing::profile_scope!("range", format!("{query:?}")); // NOTE: `+ 2` because we always grab the indicator component as well as the @@ -256,31 +155,16 @@ macro_rules! impl_query_archetype { Ok(()) } - AnyQuery::LatestAt(query) if !cached => { - re_tracing::profile_scope!("latest_at", format!("{query:?}")); - - let arch_view = ::re_query::query_archetype::(store, query, entity_path)?; - - let data = ( - (arch_view.data_time(), arch_view.primary_row_id()), - MaybeCachedComponentData::Raw(arch_view.iter_instance_keys().collect()), - $(MaybeCachedComponentData::Raw(arch_view.iter_required_component::<$pov>()?.collect()),)+ - $(MaybeCachedComponentData::Raw(arch_view.iter_optional_component::<$comp>()?.collect()),)* - ); - - f(data); - - Ok(()) - } + AnyQuery::Range(query) => { + re_tracing::profile_scope!("range", format!("{query:?}")); - AnyQuery::LatestAt(query) => { - Caches::with_latest_at::( - store.id().clone(), - entity_path.clone(), + crate::[]::( + store, query, - |latest_at_cache| latest_at_callback(query, latest_at_cache), + entity_path, + f, ) - }, + } } } } }; @@ -304,9 +188,11 @@ seq!(NUM_COMP in 0..10 { /// and no optional components. /// /// Alias for [`query_archetype_with_history_pov1_comp0`]. +#[allow(clippy::too_many_arguments)] #[inline] pub fn query_archetype_with_history_pov1<'a, A, R1, F>( - cached: bool, + cached_latest_at: bool, + cached_range: bool, store: &'a DataStore, timeline: &'a Timeline, time: &'a TimeInt, @@ -326,7 +212,14 @@ where ), { query_archetype_with_history_pov1_comp0::( - cached, store, timeline, time, history, ent_path, f, + cached_latest_at, + cached_range, + store, + timeline, + time, + history, + ent_path, + f, ) } @@ -336,8 +229,10 @@ macro_rules! impl_query_archetype_with_history { (for N=$N:expr, M=$M:expr => povs=[$($pov:ident)+] comps=[$($comp:ident)*]) => { paste! { #[doc = "Cached implementation of [`re_query::query_archetype_with_history`] for `" $N "` point-of-view"] #[doc = "components and `" $M "` optional components."] + #[allow(clippy::too_many_arguments)] pub fn []<'a, A, $($pov,)+ $($comp,)* F>( - cached: bool, + cached_latest_at: bool, + cached_range: bool, store: &'a DataStore, timeline: &'a Timeline, time: &'a TimeInt, @@ -358,11 +253,6 @@ macro_rules! impl_query_archetype_with_history { ), ), { - // NOTE: not `profile_function!` because we want them merged together. - re_tracing::profile_scope!( - "query_archetype_with_history", - format!("cached={cached} arch={} pov={} comp={}", A::name(), $N, $M) - ); let visible_history = match timeline.typ() { re_log_types::TimeType::Time => history.nanos, @@ -370,20 +260,32 @@ macro_rules! impl_query_archetype_with_history { }; if !history.enabled || visible_history == VisibleHistory::OFF { + // NOTE: not `profile_function!` because we want them merged together. + re_tracing::profile_scope!( + "query_archetype_with_history", + format!("cached={cached_latest_at} arch={} pov={} comp={}", A::name(), $N, $M) + ); + let query = LatestAtQuery::new(*timeline, *time); $crate::[]::( - cached, + cached_latest_at, store, &query.clone().into(), ent_path, f, ) } else { + // NOTE: not `profile_function!` because we want them merged together. + re_tracing::profile_scope!( + "query_archetype_with_history", + format!("cached={cached_range} arch={} pov={} comp={}", A::name(), $N, $M) + ); + let min_time = visible_history.from(*time); let max_time = visible_history.to(*time); let query = RangeQuery::new(*timeline, TimeRange::new(min_time, max_time)); $crate::[]::( - cached, + cached_range, store, &query.clone().into(), ent_path, diff --git a/crates/re_query_cache/src/range.rs b/crates/re_query_cache/src/range.rs new file mode 100644 index 000000000000..a21e6b590207 --- /dev/null +++ b/crates/re_query_cache/src/range.rs @@ -0,0 +1,146 @@ +use paste::paste; +use seq_macro::seq; + +use re_data_store::{DataStore, RangeQuery, TimeInt}; +use re_log_types::{EntityPath, RowId}; +use re_types_core::{components::InstanceKey, Archetype, Component}; + +use crate::{CacheBucket, Caches, MaybeCachedComponentData}; + +// --- Data structures --- + +/// Caches the results of `Range` queries. +#[derive(Default)] +pub struct RangeCache { + // TODO(cmc): bucketize + pub bucket: CacheBucket, + + /// Total size of the data stored in this cache in bytes. + pub total_size_bytes: u64, +} + +// --- Queries --- + +macro_rules! impl_query_archetype_range { + (for N=$N:expr, M=$M:expr => povs=[$($pov:ident)+] comps=[$($comp:ident)*]) => { paste! { + #[doc = "Cached implementation of [`re_query::query_archetype`] and [`re_query::range_archetype`]"] + #[doc = "(combined) for `" $N "` point-of-view components and `" $M "` optional components."] + #[allow(non_snake_case)] + pub fn []<'a, A, $($pov,)+ $($comp,)* F>( + store: &'a DataStore, + query: &RangeQuery, + entity_path: &'a EntityPath, + mut f: F, + ) -> ::re_query::Result<()> + where + A: Archetype + 'a, + $($pov: Component + Send + Sync + 'static,)+ + $($comp: Component + Send + Sync + 'static,)* + F: FnMut( + ( + (Option, RowId), + MaybeCachedComponentData<'_, InstanceKey>, + $(MaybeCachedComponentData<'_, $pov>,)+ + $(MaybeCachedComponentData<'_, Option<$comp>>,)* + ), + ), + { + let mut iter_results = |bucket: &crate::CacheBucket| -> crate::Result<()> { + re_tracing::profile_scope!("iter"); + + let it = itertools::izip!( + bucket.iter_data_times(), + bucket.iter_pov_instance_keys(), + $(bucket.iter_component::<$pov>() + .ok_or_else(|| re_query::ComponentNotFoundError(<$pov>::name()))?,)+ + $(bucket.iter_component_opt::<$comp>() + .ok_or_else(|| re_query::ComponentNotFoundError(<$comp>::name()))?,)* + ).map(|((time, row_id), instance_keys, $($pov,)+ $($comp,)*)| { + ( + (Some(*time), *row_id), // TODO(cmc): timeless + MaybeCachedComponentData::Cached(instance_keys), + $(MaybeCachedComponentData::Cached($pov),)+ + $(MaybeCachedComponentData::Cached($comp),)* + ) + }); + + for data in it { + f(data); + } + + Ok(()) + }; + + fn upsert_results<'a, A, $($pov,)+ $($comp,)*>( + arch_views: impl Iterator>, + bucket: &mut crate::CacheBucket, + ) -> crate::Result + where + A: Archetype + 'a, + $($pov: Component + Send + Sync + 'static,)+ + $($comp: Component + Send + Sync + 'static,)* + { + re_log::trace!("fill"); + + let now = web_time::Instant::now(); + + let mut added_entries = 0u64; + let mut added_size_bytes = 0u64; + + for arch_view in arch_views { + let data_time = arch_view.data_time().unwrap_or(TimeInt::MIN); // TODO(cmc): timeless + + if bucket.contains_data_row(data_time, arch_view.primary_row_id()) { + continue; + } + + added_size_bytes += bucket.[]::(data_time, &arch_view)?; + added_entries += 1; + } + + let elapsed = now.elapsed(); + ::re_log::trace!( + archetype=%A::name(), + added_size_bytes, + "cached {added_entries} entries in {elapsed:?} ({:0.3} entries/s)", + added_entries as f64 / elapsed.as_secs_f64() + ); + + Ok(added_size_bytes) + } + + let mut range_callback = |query: &RangeQuery, range_cache: &mut crate::RangeCache| { + re_tracing::profile_scope!("range", format!("{query:?}")); + + let RangeCache { bucket, total_size_bytes } = range_cache; + + // NOTE: `+ 2` because we always grab the indicator component as well as the + // instance keys. + let arch_views = ::re_query::range_archetype::(store, query, entity_path); + *total_size_bytes += upsert_results::(arch_views, bucket)?; + + iter_results(bucket) + }; + + + Caches::with_range::( + store.id().clone(), + entity_path.clone(), + query, + |range_cache| range_callback(query, range_cache), + ) + } } + }; + + // TODO(cmc): Supporting N>1 generically is quite painful due to limitations in declarative macros, + // not that we care at the moment. + (for N=1, M=$M:expr) => { + seq!(COMP in 1..=$M { + impl_query_archetype_range!(for N=1, M=$M => povs=[R1] comps=[#(C~COMP)*]); + }); + }; +} + +seq!(NUM_COMP in 0..10 { + impl_query_archetype_range!(for N=1, M=NUM_COMP); +}); diff --git a/crates/re_query_cache/tests/latest_at.rs b/crates/re_query_cache/tests/latest_at.rs index dad991601171..4f9a86ed56df 100644 --- a/crates/re_query_cache/tests/latest_at.rs +++ b/crates/re_query_cache/tests/latest_at.rs @@ -5,13 +5,13 @@ use itertools::Itertools as _; use re_data_store::{DataStore, LatestAtQuery}; -use re_log_types::{build_frame_nr, DataRow, EntityPath, RowId, TimePoint}; -use re_query_cache::query_archetype_pov1_comp1; -use re_types::{ - archetypes::Points2D, - components::{Color, InstanceKey, Position2D}, +use re_log_types::{ + build_frame_nr, + example_components::{MyColor, MyPoint, MyPoints}, + DataRow, EntityPath, RowId, TimePoint, }; -use re_types_core::Loggable as _; +use re_query_cache::query_archetype_pov1_comp1; +use re_types_core::{components::InstanceKey, Loggable as _}; // --- @@ -27,13 +27,13 @@ fn simple_query() { let timepoint = [build_frame_nr(123.into())]; // Create some positions with implicit instances - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, positions).unwrap(); store.insert_row(&row).unwrap(); // Assign one of them a color with an explicit instance let color_instances = vec![InstanceKey(1)]; - let colors = vec![Color::from_rgb(255, 0, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; let row = DataRow::from_cells2_sized( RowId::new(), ent_path, @@ -44,7 +44,6 @@ fn simple_query() { .unwrap(); store.insert_row(&row).unwrap(); - // Retrieve the view let query = re_data_store::LatestAtQuery::new(timepoint[0].0, timepoint[0].1); query_and_compare(&store, &query, &ent_path.into()); } @@ -61,18 +60,17 @@ fn timeless_query() { let timepoint = [build_frame_nr(123.into())]; // Create some positions with implicit instances - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, positions).unwrap(); store.insert_row(&row).unwrap(); // Assign one of them a color with an explicit instance.. timelessly! let color_instances = vec![InstanceKey(1)]; - let colors = vec![Color::from_rgb(255, 0, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; let row = DataRow::from_cells2_sized(RowId::new(), ent_path, [], 1, (color_instances, colors)) .unwrap(); store.insert_row(&row).unwrap(); - // Retrieve the view let query = re_data_store::LatestAtQuery::new(timepoint[0].0, timepoint[0].1); query_and_compare(&store, &query, &ent_path.into()); } @@ -89,16 +87,15 @@ fn no_instance_join_query() { let timepoint = [build_frame_nr(123.into())]; // Create some positions with an implicit instance - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, positions).unwrap(); store.insert_row(&row).unwrap(); // Assign them colors with explicit instances - let colors = vec![Color::from_rgb(255, 0, 0), Color::from_rgb(0, 255, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0), MyColor::from_rgb(0, 255, 0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, colors).unwrap(); store.insert_row(&row).unwrap(); - // Retrieve the view let query = re_data_store::LatestAtQuery::new(timepoint[0].0, timepoint[0].1); query_and_compare(&store, &query, &ent_path.into()); } @@ -115,11 +112,10 @@ fn missing_column_join_query() { let timepoint = [build_frame_nr(123.into())]; // Create some positions with an implicit instance - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, positions).unwrap(); store.insert_row(&row).unwrap(); - // Retrieve the view let query = re_data_store::LatestAtQuery::new(timepoint[0].0, timepoint[0].1); query_and_compare(&store, &query, &ent_path.into()); } @@ -136,13 +132,13 @@ fn splatted_query() { let timepoint = [build_frame_nr(123.into())]; // Create some positions with implicit instances - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timepoint, 2, positions).unwrap(); store.insert_row(&row).unwrap(); // Assign all of them a color via splat let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(255, 0, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; let row = DataRow::from_cells2_sized( RowId::new(), ent_path, @@ -153,7 +149,6 @@ fn splatted_query() { .unwrap(); store.insert_row(&row).unwrap(); - // Retrieve the view let query = re_data_store::LatestAtQuery::new(timepoint[0].0, timepoint[0].1); query_and_compare(&store, &query, &ent_path.into()); } @@ -173,7 +168,7 @@ fn invalidation() { ); // Create some positions with implicit instances - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized( RowId::new(), ent_path, @@ -186,7 +181,7 @@ fn invalidation() { // Assign one of them a color with an explicit instance let color_instances = vec![InstanceKey(1)]; - let colors = vec![Color::from_rgb(1, 2, 3)]; + let colors = vec![MyColor::from_rgb(1, 2, 3)]; let row = DataRow::from_cells2_sized( RowId::new(), ent_path, @@ -202,7 +197,7 @@ fn invalidation() { // --- Modify present --- // Modify the PoV component - let positions = vec![Position2D::new(10.0, 20.0), Position2D::new(30.0, 40.0)]; + let positions = vec![MyPoint::new(10.0, 20.0), MyPoint::new(30.0, 40.0)]; let row = DataRow::from_cells1_sized( RowId::new(), ent_path, @@ -216,7 +211,7 @@ fn invalidation() { query_and_compare(&store, &query, &ent_path.into()); // Modify the optional component - let colors = vec![Color::from_rgb(4, 5, 6), Color::from_rgb(7, 8, 9)]; + let colors = vec![MyColor::from_rgb(4, 5, 6), MyColor::from_rgb(7, 8, 9)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, present_data_timepoint, 2, colors) .unwrap(); @@ -227,7 +222,7 @@ fn invalidation() { // --- Modify past --- // Modify the PoV component - let positions = vec![Position2D::new(100.0, 200.0), Position2D::new(300.0, 400.0)]; + let positions = vec![MyPoint::new(100.0, 200.0), MyPoint::new(300.0, 400.0)]; let row = DataRow::from_cells1_sized( RowId::new(), ent_path, @@ -241,7 +236,7 @@ fn invalidation() { query_and_compare(&store, &query, &ent_path.into()); // Modify the optional component - let colors = vec![Color::from_rgb(10, 11, 12), Color::from_rgb(13, 14, 15)]; + let colors = vec![MyColor::from_rgb(10, 11, 12), MyColor::from_rgb(13, 14, 15)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, past_data_timepoint, 2, colors) .unwrap(); @@ -252,10 +247,7 @@ fn invalidation() { // --- Modify future --- // Modify the PoV component - let positions = vec![ - Position2D::new(1000.0, 2000.0), - Position2D::new(3000.0, 4000.0), - ]; + let positions = vec![MyPoint::new(1000.0, 2000.0), MyPoint::new(3000.0, 4000.0)]; let row = DataRow::from_cells1_sized( RowId::new(), ent_path, @@ -269,7 +261,7 @@ fn invalidation() { query_and_compare(&store, &query, &ent_path.into()); // Modify the optional component - let colors = vec![Color::from_rgb(16, 17, 18)]; + let colors = vec![MyColor::from_rgb(16, 17, 18)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, future_data_timepoint, 1, colors) .unwrap(); @@ -312,19 +304,19 @@ fn invalidation() { // # Expected: points=[[1,2,3]] colors=[] // // rr.set_time(2) -// rr.log_components("points", rr.components.Color(0xFF0000)) +// rr.log_components("points", rr.components.MyColor(0xFF0000)) // // # Do second query here: LatestAt(+inf) // # Expected: points=[[1,2,3]] colors=[0xFF0000] // // rr.set_time(3) -// rr.log_components("points", rr.components.Color(0x0000FF)) +// rr.log_components("points", rr.components.MyColor(0x0000FF)) // // # Do third query here: LatestAt(+inf) // # Expected: points=[[1,2,3]] colors=[0x0000FF] // // rr.set_time(3) -// rr.log_components("points", rr.components.Color(0x00FF00)) +// rr.log_components("points", rr.components.MyColor(0x00FF00)) // // # Do fourth query here: LatestAt(+inf) // # Expected: points=[[1,2,3]] colors=[0x00FF00] @@ -345,7 +337,7 @@ fn invalidation_of_future_optionals() { let query_time = [build_frame_nr(9999.into())]; - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timeless, 2, positions).unwrap(); store.insert_row(&row).unwrap(); @@ -353,7 +345,7 @@ fn invalidation_of_future_optionals() { query_and_compare(&store, &query, &ent_path.into()); let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(255, 0, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; let row = DataRow::from_cells2_sized(RowId::new(), ent_path, frame2, 1, (color_instances, colors)) .unwrap(); @@ -363,7 +355,7 @@ fn invalidation_of_future_optionals() { query_and_compare(&store, &query, &ent_path.into()); let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(0, 0, 255)]; + let colors = vec![MyColor::from_rgb(0, 0, 255)]; let row = DataRow::from_cells2_sized(RowId::new(), ent_path, frame3, 1, (color_instances, colors)) .unwrap(); @@ -373,7 +365,7 @@ fn invalidation_of_future_optionals() { query_and_compare(&store, &query, &ent_path.into()); let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(0, 255, 0)]; + let colors = vec![MyColor::from_rgb(0, 255, 0)]; let row = DataRow::from_cells2_sized(RowId::new(), ent_path, frame3, 1, (color_instances, colors)) .unwrap(); @@ -397,7 +389,7 @@ fn invalidation_timeless() { let query_time = [build_frame_nr(9999.into())]; - let positions = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)]; + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; let row = DataRow::from_cells1_sized(RowId::new(), ent_path, timeless.clone(), 2, positions).unwrap(); store.insert_row(&row).unwrap(); @@ -406,7 +398,7 @@ fn invalidation_timeless() { query_and_compare(&store, &query, &ent_path.into()); let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(255, 0, 0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; let row = DataRow::from_cells2_sized( RowId::new(), ent_path, @@ -421,7 +413,7 @@ fn invalidation_timeless() { query_and_compare(&store, &query, &ent_path.into()); let color_instances = vec![InstanceKey::SPLAT]; - let colors = vec![Color::from_rgb(0, 0, 255)]; + let colors = vec![MyColor::from_rgb(0, 0, 255)]; let row = DataRow::from_cells2_sized( RowId::new(), ent_path, @@ -444,7 +436,7 @@ fn query_and_compare(store: &DataStore, query: &LatestAtQuery, ent_path: &Entity let mut uncached_instance_keys = Vec::new(); let mut uncached_positions = Vec::new(); let mut uncached_colors = Vec::new(); - query_archetype_pov1_comp1::( + query_archetype_pov1_comp1::( false, // cached? store, &query.clone().into(), @@ -462,7 +454,7 @@ fn query_and_compare(store: &DataStore, query: &LatestAtQuery, ent_path: &Entity let mut cached_instance_keys = Vec::new(); let mut cached_positions = Vec::new(); let mut cached_colors = Vec::new(); - query_archetype_pov1_comp1::( + query_archetype_pov1_comp1::( true, // cached? store, &query.clone().into(), @@ -476,16 +468,16 @@ fn query_and_compare(store: &DataStore, query: &LatestAtQuery, ent_path: &Entity ) .unwrap(); - let expected = re_query::query_archetype::(store, query, ent_path).unwrap(); + let expected = re_query::query_archetype::(store, query, ent_path).unwrap(); let expected_data_time = expected.data_time(); let expected_instance_keys = expected.iter_instance_keys().collect_vec(); let expected_positions = expected - .iter_required_component::() + .iter_required_component::() .unwrap() .collect_vec(); let expected_colors = expected - .iter_optional_component::() + .iter_optional_component::() .unwrap() .collect_vec(); diff --git a/crates/re_query_cache/tests/range.rs b/crates/re_query_cache/tests/range.rs new file mode 100644 index 000000000000..e551e46a4418 --- /dev/null +++ b/crates/re_query_cache/tests/range.rs @@ -0,0 +1,388 @@ +//! Contains: +//! - A 1:1 port of the tests in `crates/re_query/tests/archetype_range_tests.rs`, with caching enabled. +//! - Invalidation tests. + +use itertools::Itertools as _; + +use re_data_store::{DataStore, RangeQuery}; +use re_log_types::{ + build_frame_nr, + example_components::{MyColor, MyLabel, MyPoint, MyPoints}, + DataRow, EntityPath, RowId, TimeInt, TimeRange, +}; +use re_query_cache::query_archetype_pov1_comp2; +use re_types::components::InstanceKey; +use re_types_core::Loggable as _; + +// --- + +#[test] +// TODO(cmc): actually make cached range queries correct +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn simple_range() { + let mut store = DataStore::new( + re_log_types::StoreId::random(re_log_types::StoreKind::Recording), + InstanceKey::name(), + Default::default(), + ); + + let ent_path: EntityPath = "point".into(); + + let timepoint1 = [build_frame_nr(123.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), timepoint1, 2, positions) + .unwrap(); + store.insert_row(&row).unwrap(); + + // Assign one of them a color with an explicit instance + let color_instances = vec![InstanceKey(1)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint1, + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint2 = [build_frame_nr(223.into())]; + { + // Assign one of them a color with an explicit instance + let color_instances = vec![InstanceKey(0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint2, + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint3 = [build_frame_nr(323.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(10.0, 20.0), MyPoint::new(30.0, 40.0)]; + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), timepoint3, 2, positions) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + // --- First test: `(timepoint1, timepoint3]` --- + + // The exclusion of `timepoint1` means latest-at semantics will kick in! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); + + // --- Second test: `[timepoint1, timepoint3]` --- + + // The inclusion of `timepoint1` means latest-at semantics will _not_ kick in! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new(timepoint1[0].1, timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); +} + +#[test] +// TODO(cmc): cached range timeless support +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn timeless_range() { + let mut store = DataStore::new( + re_log_types::StoreId::random(re_log_types::StoreKind::Recording), + InstanceKey::name(), + Default::default(), + ); + + let ent_path: EntityPath = "point".into(); + + let timepoint1 = [build_frame_nr(123.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; + let mut row = + DataRow::from_cells1(RowId::new(), ent_path.clone(), timepoint1, 2, &positions) + .unwrap(); + row.compute_all_size_bytes(); + store.insert_row(&row).unwrap(); + + // Insert timelessly too! + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), [], 2, &positions).unwrap(); + store.insert_row(&row).unwrap(); + + // Assign one of them a color with an explicit instance + let color_instances = vec![InstanceKey(1)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint1, + 1, + (color_instances.clone(), colors.clone()), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + + // Insert timelessly too! + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + [], + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint2 = [build_frame_nr(223.into())]; + { + // Assign one of them a color with an explicit instance + let color_instances = vec![InstanceKey(0)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint2, + 1, + (color_instances.clone(), colors.clone()), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + + // Insert timelessly too! + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint2, + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint3 = [build_frame_nr(323.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(10.0, 20.0), MyPoint::new(30.0, 40.0)]; + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), timepoint3, 2, &positions) + .unwrap(); + store.insert_row(&row).unwrap(); + + // Insert timelessly too! + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), [], 2, &positions).unwrap(); + store.insert_row(&row).unwrap(); + } + + // --- First test: `(timepoint1, timepoint3]` --- + + // The exclusion of `timepoint1` means latest-at semantics will kick in! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); + + // --- Second test: `[timepoint1, timepoint3]` --- + + // The inclusion of `timepoint1` means latest-at semantics will fall back to timeless data! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new(timepoint1[0].1, timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); + + // --- Third test: `[-inf, +inf]` --- + + let query = + re_data_store::RangeQuery::new(timepoint1[0].0, TimeRange::new(TimeInt::MIN, TimeInt::MAX)); + + query_and_compare(&store, &query, &ent_path); +} + +#[test] +// TODO(cmc): actually make cached range queries correct +#[should_panic(expected = "assertion failed: `(left == right)`")] +fn simple_splatted_range() { + let mut store = DataStore::new( + re_log_types::StoreId::random(re_log_types::StoreKind::Recording), + InstanceKey::name(), + Default::default(), + ); + + let ent_path: EntityPath = "point".into(); + + let timepoint1 = [build_frame_nr(123.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(1.0, 2.0), MyPoint::new(3.0, 4.0)]; + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), timepoint1, 2, positions) + .unwrap(); + store.insert_row(&row).unwrap(); + + // Assign one of them a color with an explicit instance + let color_instances = vec![InstanceKey(1)]; + let colors = vec![MyColor::from_rgb(255, 0, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint1, + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint2 = [build_frame_nr(223.into())]; + { + // Assign one of them a color with a splatted instance + let color_instances = vec![InstanceKey::SPLAT]; + let colors = vec![MyColor::from_rgb(0, 255, 0)]; + let row = DataRow::from_cells2_sized( + RowId::new(), + ent_path.clone(), + timepoint2, + 1, + (color_instances, colors), + ) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + let timepoint3 = [build_frame_nr(323.into())]; + { + // Create some Positions with implicit instances + let positions = vec![MyPoint::new(10.0, 20.0), MyPoint::new(30.0, 40.0)]; + let row = + DataRow::from_cells1_sized(RowId::new(), ent_path.clone(), timepoint3, 2, positions) + .unwrap(); + store.insert_row(&row).unwrap(); + } + + // --- First test: `(timepoint1, timepoint3]` --- + + // The exclusion of `timepoint1` means latest-at semantics will kick in! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new((timepoint1[0].1.as_i64() + 1).into(), timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); + + // --- Second test: `[timepoint1, timepoint3]` --- + + // The inclusion of `timepoint1` means latest-at semantics will _not_ kick in! + + let query = re_data_store::RangeQuery::new( + timepoint1[0].0, + TimeRange::new(timepoint1[0].1, timepoint3[0].1), + ); + + query_and_compare(&store, &query, &ent_path); +} + +// --- + +fn query_and_compare(store: &DataStore, query: &RangeQuery, ent_path: &EntityPath) { + for _ in 0..3 { + let mut uncached_data_times = Vec::new(); + let mut uncached_instance_keys = Vec::new(); + let mut uncached_positions = Vec::new(); + let mut uncached_colors = Vec::new(); + query_archetype_pov1_comp2::( + false, // cached? + store, + &query.clone().into(), + ent_path, + |((data_time, _), instance_keys, positions, colors, _)| { + uncached_data_times.push(data_time); + uncached_instance_keys.push(instance_keys.to_vec()); + uncached_positions.push(positions.to_vec()); + uncached_colors.push(colors.to_vec()); + }, + ) + .unwrap(); + + let mut cached_data_times = Vec::new(); + let mut cached_instance_keys = Vec::new(); + let mut cached_positions = Vec::new(); + let mut cached_colors = Vec::new(); + query_archetype_pov1_comp2::( + true, // cached? + store, + &query.clone().into(), + ent_path, + |((data_time, _), instance_keys, positions, colors, _)| { + cached_data_times.push(data_time); + cached_instance_keys.push(instance_keys.to_vec()); + cached_positions.push(positions.to_vec()); + cached_colors.push(colors.to_vec()); + }, + ) + .unwrap(); + + let mut expected_data_times = Vec::new(); + let mut expected_instance_keys = Vec::new(); + let mut expected_positions = Vec::new(); + let mut expected_colors = Vec::new(); + let expected = re_query::range_archetype::( + store, query, ent_path, + ); + for arch_view in expected { + expected_data_times.push(arch_view.data_time()); + expected_instance_keys.push(arch_view.iter_instance_keys().collect_vec()); + expected_positions.push( + arch_view + .iter_required_component::() + .unwrap() + .collect_vec(), + ); + expected_colors.push( + arch_view + .iter_optional_component::() + .unwrap() + .collect_vec(), + ); + } + + // Keep this around for the next unlucky chap. + // eprintln!("(expected={expected_data_times:?}, uncached={uncached_data_times:?}, cached={cached_data_times:?})"); + + similar_asserts::assert_eq!(expected_data_times, uncached_data_times); + similar_asserts::assert_eq!(expected_instance_keys, uncached_instance_keys); + similar_asserts::assert_eq!(expected_positions, uncached_positions); + similar_asserts::assert_eq!(expected_colors, uncached_colors); + + similar_asserts::assert_eq!(expected_data_times, cached_data_times); + similar_asserts::assert_eq!(expected_instance_keys, cached_instance_keys); + similar_asserts::assert_eq!(expected_positions, cached_positions); + similar_asserts::assert_eq!(expected_colors, cached_colors); + } +} diff --git a/crates/re_space_view_spatial/src/visualizers/entity_iterator.rs b/crates/re_space_view_spatial/src/visualizers/entity_iterator.rs index b7fdc5f51f0a..2ca7c8e366bd 100644 --- a/crates/re_space_view_spatial/src/visualizers/entity_iterator.rs +++ b/crates/re_space_view_spatial/src/visualizers/entity_iterator.rs @@ -124,7 +124,6 @@ macro_rules! impl_process_archetype { query: &ViewQuery<'_>, view_ctx: &ViewContextCollection, default_depth_offset: DepthOffset, - cached: bool, mut f: F, ) -> Result<(), SpaceViewSystemExecutionError> where @@ -146,7 +145,7 @@ macro_rules! impl_process_archetype { // NOTE: not `profile_function!` because we want them merged together. re_tracing::profile_scope!( "process_archetype", - format!("cached={cached} arch={} pov={} comp={}", A::name(), $N, $M) + format!("arch={} pov={} comp={}", A::name(), $N, $M) ); let transforms = view_ctx.get::()?; @@ -185,7 +184,8 @@ macro_rules! impl_process_archetype { }; ::re_query_cache::[]::( - cached, + ctx.app_options.experimental_primary_caching_latest_at, + ctx.app_options.experimental_primary_caching_range, ctx.entity_db.store(), &query.timeline, &query.latest_at, diff --git a/crates/re_space_view_spatial/src/visualizers/points2d.rs b/crates/re_space_view_spatial/src/visualizers/points2d.rs index 6ab02ffce96d..ed77a2fb265f 100644 --- a/crates/re_space_view_spatial/src/visualizers/points2d.rs +++ b/crates/re_space_view_spatial/src/visualizers/points2d.rs @@ -272,7 +272,6 @@ impl VisualizerSystem for Points2DVisualizer { query, view_ctx, view_ctx.get::()?.points, - ctx.app_options.experimental_primary_caching_point_clouds, |_ctx, ent_path, _ent_props, diff --git a/crates/re_space_view_spatial/src/visualizers/points3d.rs b/crates/re_space_view_spatial/src/visualizers/points3d.rs index 8e4ff59ef6f6..35d5562dc85b 100644 --- a/crates/re_space_view_spatial/src/visualizers/points3d.rs +++ b/crates/re_space_view_spatial/src/visualizers/points3d.rs @@ -204,7 +204,6 @@ impl VisualizerSystem for Points3DVisualizer { query, view_ctx, view_ctx.get::()?.points, - ctx.app_options.experimental_primary_caching_point_clouds, |_ctx, ent_path, _ent_props, diff --git a/crates/re_space_view_text_log/src/visualizer_system.rs b/crates/re_space_view_text_log/src/visualizer_system.rs index fd63d9460da9..e8bd9290af43 100644 --- a/crates/re_space_view_text_log/src/visualizer_system.rs +++ b/crates/re_space_view_text_log/src/visualizer_system.rs @@ -68,7 +68,7 @@ impl VisualizerSystem for TextLogSystem { re_data_store::RangeQuery::new(query.timeline, TimeRange::EVERYTHING); re_query_cache::query_archetype_pov1_comp2::( - ctx.app_options.experimental_primary_caching_series, + ctx.app_options.experimental_primary_caching_range, store, &timeline_query.clone().into(), &data_result.entity_path, diff --git a/crates/re_space_view_time_series/src/visualizer_system.rs b/crates/re_space_view_time_series/src/visualizer_system.rs index 16a833d31019..ba2318af15e8 100644 --- a/crates/re_space_view_time_series/src/visualizer_system.rs +++ b/crates/re_space_view_time_series/src/visualizer_system.rs @@ -167,7 +167,7 @@ impl TimeSeriesSystem { Text, _, >( - ctx.app_options.experimental_primary_caching_series, + ctx.app_options.experimental_primary_caching_range, store, &query.clone().into(), &data_result.entity_path, diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 16b1989db180..ebf125d3527f 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -294,18 +294,18 @@ fn experimental_feature_ui( re_ui .checkbox( ui, - &mut app_options.experimental_primary_caching_point_clouds, - "Primary caching: 2D & 3D point clouds", + &mut app_options.experimental_primary_caching_latest_at, + "Primary caching: latest-at queries", ) - .on_hover_text("Toggle primary caching for the 2D & 3D point cloud space views."); + .on_hover_text("Toggle primary caching for latest-at queries.\nApplies to the 2D/3D point cloud, text log and time series space views."); re_ui .checkbox( ui, - &mut app_options.experimental_primary_caching_series, - "Primary caching: TextLogs & TimeSeries", + &mut app_options.experimental_primary_caching_range, + "Primary caching: range queries", ) - .on_hover_text("Toggle primary caching for the time series & text logs space views."); + .on_hover_text("Toggle primary caching for range queries.\nApplies to the 2D/3D point cloud, text log and time series space views."); re_ui .checkbox( diff --git a/crates/re_viewer_context/src/app_options.rs b/crates/re_viewer_context/src/app_options.rs index ccf834840cb1..50ca23c2c29a 100644 --- a/crates/re_viewer_context/src/app_options.rs +++ b/crates/re_viewer_context/src/app_options.rs @@ -25,11 +25,15 @@ pub struct AppOptions { /// Enable the experimental support for the container addition workflow. pub experimental_additive_workflow: bool, - /// Toggle primary caching for the 2D & 3D point cloud space views. - pub experimental_primary_caching_point_clouds: bool, + /// Toggle primary caching for latest-at queries. + /// + /// Applies to the 2D/3D point cloud, text log and time series space views. + pub experimental_primary_caching_latest_at: bool, - /// Toggle primary caching for the time series & text logs space views. - pub experimental_primary_caching_series: bool, + /// Toggle primary caching for range queries. + /// + /// Applies to the 2D/3D point cloud, text log and time series space views. + pub experimental_primary_caching_range: bool, /// Displays an overlay for debugging picking. pub show_picking_debug_overlay: bool, @@ -60,8 +64,8 @@ impl Default for AppOptions { experimental_additive_workflow: cfg!(debug_assertions), - experimental_primary_caching_point_clouds: true, - experimental_primary_caching_series: true, + experimental_primary_caching_latest_at: true, + experimental_primary_caching_range: false, show_picking_debug_overlay: false,