Skip to content

Commit

Permalink
New data APIs 6: cached archetype queries (#5673)
Browse files Browse the repository at this point in the history
Introduce very high-level APIs that make it possible to query, resolve,
deserialize and cache an entire archetype all at once.

The core trait allowing this is the newly introduced `ToArchetype<A>`
trait:
```rust
pub trait ToArchetype<A: re_types_core::Archetype> {
    fn to_archetype(&self, resolver: &crate::PromiseResolver) -> crate::PromiseResult<A>;
}
```

This trait is implemented for all builtins archetypes thanks to a new
codegen pass.

Implementing such a trait is tricky: one needs to know about this trait,
archetypes, queries, caches, etc all in one single place. This is a
recipe for a very nasty circular dependency chains.
This PR makes it work for all archetypes except archetypes that are
generated in `re_viewport`. These will need some special care in a
follow-up PR (likely moving them out of there, and only reexporting them
in `re_viewport`?).
Update: related:
- #5421

Here's how it looks in practice:
```rust
let caches = re_query_cache2::Caches::new(&store);

// First, get the results for this query.
//
// They might or might not already be cached. We won't know for sure until we try to access
// each individual component's data below.
let results: CachedLatestAtResults = caches.latest_at(
    &store,
    &query,
    &entity_path.into(),
    Points2D::all_components().iter().cloned(), // no generics!
);

// Then make use of the `ToArchetype` helper trait in order to query, resolve, deserialize and
// cache an entire archetype all at once.
use re_query_cache2::ToArchetype as _;

let arch: Points2D = match results.to_archetype(&resolver) {
    PromiseResult::Pending => {
        // Handle the fact that the data isn't ready appropriately.
        return Ok(());
    }
    PromiseResult::Ready(arch) => arch,
    PromiseResult::Error(err) => return Err(err.into()),
};

// With the data now fully resolved/converted and deserialized, some joining logic can be
// applied if desired.
//
// In most cases this will be either a clamped zip, or no joining at all.

let color_default_fn = || None;
let label_default_fn = || None;

let results = clamped_zip_1x2(
    arch.positions.iter(),
    arch.colors
        .iter()
        .flat_map(|colors| colors.iter().map(Some)),
    color_default_fn,
    arch.labels
        .iter()
        .flat_map(|labels| labels.iter().map(Some)),
    label_default_fn,
)
.collect_vec();

eprintln!("results:\n{results:?}");
```


---

Part of a PR series to completely revamp the data APIs in preparation
for the removal of instance keys and the introduction of promises:
- #5573
- #5574
- #5581
- #5605
- #5606
- #5633
- #5673
- #5679
- #5687
- #5755
- TODO
- TODO

Builds on top of the static data PR series:
- #5534
  • Loading branch information
teh-cmc authored Apr 8, 2024
1 parent b66fb55 commit c53d339
Show file tree
Hide file tree
Showing 46 changed files with 2,795 additions and 46 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions crates/re_query2/benches/latest_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ fn query_and_visit_points(store: &DataStore, paths: &[EntityPath]) -> Vec<SavePo
Points2D::all_components().iter().cloned(), // no generics!
);

let points = results.get_required::<Position2D>().unwrap();
let colors = results.get_or_empty::<Color>();
let points = results.get_required(Position2D::name()).unwrap();
let colors = results.get_or_empty(Color::name());

let points = points
.iter_dense::<Position2D>(&resolver)
Expand Down Expand Up @@ -324,8 +324,8 @@ fn query_and_visit_strings(store: &DataStore, paths: &[EntityPath]) -> Vec<SaveS
Points2D::all_components().iter().cloned(), // no generics!
);

let points = results.get_required::<Position2D>().unwrap();
let colors = results.get_or_empty::<Text>();
let points = results.get_required(Position2D::name()).unwrap();
let colors = results.get_or_empty(Text::name());

let points = points
.iter_dense::<Position2D>(&resolver)
Expand Down
6 changes: 3 additions & 3 deletions crates/re_query2/examples/latest_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ fn main() -> anyhow::Result<()> {
// * `get_required` returns an error if the component batch is missing
// * `get_or_empty` returns an empty set of results if the component if missing
// * `get` returns an option
let points: &LatestAtComponentResults = results.get_required::<MyPoint>()?;
let colors: &LatestAtComponentResults = results.get_or_empty::<MyColor>();
let labels: &LatestAtComponentResults = results.get_or_empty::<MyLabel>();
let points: &LatestAtComponentResults = results.get_required(MyPoint::name())?;
let colors: &LatestAtComponentResults = results.get_or_empty(MyColor::name());
let labels: &LatestAtComponentResults = results.get_or_empty(MyLabel::name());

// Then comes the time to resolve/convert and deserialize the data.
// These steps have to be done together for efficiency reasons.
Expand Down
31 changes: 21 additions & 10 deletions crates/re_query2/src/latest_at/results.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,45 @@ impl LatestAtResults {
self.components.contains_key(&component_name.into())
}

/// Returns the [`LatestAtComponentResults`] for the specified [`Component`].
/// Returns the [`LatestAtComponentResults`] for the specified `component_name`.
#[inline]
pub fn get<C: Component>(&self) -> Option<&LatestAtComponentResults> {
self.components.get(&C::name())
pub fn get(
&self,
component_name: impl Into<ComponentName>,
) -> Option<&LatestAtComponentResults> {
self.components.get(&component_name.into())
}

/// Returns the [`LatestAtComponentResults`] for the specified [`Component`].
/// Returns the [`LatestAtComponentResults`] for the specified `component_name`.
///
/// Returns an error if the component is not present.
#[inline]
pub fn get_required<C: Component>(&self) -> crate::Result<&LatestAtComponentResults> {
if let Some(component) = self.components.get(&C::name()) {
pub fn get_required(
&self,
component_name: impl Into<ComponentName>,
) -> crate::Result<&LatestAtComponentResults> {
let component_name = component_name.into();
if let Some(component) = self.components.get(&component_name) {
Ok(component)
} else {
Err(DeserializationError::MissingComponent {
component: C::name(),
component: component_name,
backtrace: ::backtrace::Backtrace::new_unresolved(),
}
.into())
}
}

/// Returns the [`LatestAtComponentResults`] for the specified [`Component`].
/// Returns the [`LatestAtComponentResults`] for the specified `component_name`.
///
/// Returns empty results if the component is not present.
#[inline]
pub fn get_or_empty<C: Component>(&self) -> &LatestAtComponentResults {
if let Some(component) = self.components.get(&C::name()) {
pub fn get_or_empty(
&self,
component_name: impl Into<ComponentName>,
) -> &LatestAtComponentResults {
let component_name = component_name.into();
if let Some(component) = self.components.get(&component_name) {
component
} else {
static DEFAULT: LatestAtComponentResults = LatestAtComponentResults::EMPTY;
Expand Down
14 changes: 14 additions & 0 deletions crates/re_query2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,17 @@ pub enum QueryError {
}

pub type Result<T> = std::result::Result<T, QueryError>;

// ---

/// Helper extension trait to convert query results into [`re_types_core::Archetype`]s.
pub trait ToArchetype<A: re_types_core::Archetype> {
/// Converts the result into an [`re_types_core::Archetype`].
///
/// Automatically handles all aspects of the query process: deserialization, caching, promise
/// resolution, etc.
fn to_archetype(
&self,
resolver: &crate::PromiseResolver,
) -> crate::PromiseResult<crate::Result<A>>;
}
12 changes: 6 additions & 6 deletions crates/re_query2/tests/latest_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ fn simple_query() -> anyhow::Result<()> {
Some(MyColor::from_rgb(255, 0, 0)),
];

let points = results.get_required::<MyPoint>()?;
let points = results.get_required(MyPoint::name())?;
let point_data = points.iter_dense::<MyPoint>(&resolver).flatten().unwrap();

let colors = results.get_or_empty::<MyColor>();
let colors = results.get_or_empty(MyColor::name());
let color_data = colors.iter_sparse::<MyColor>(&resolver).flatten().unwrap();
let color_default_fn = || Some(MyColor::from(0xFF00FFFF));

Expand Down Expand Up @@ -130,10 +130,10 @@ fn static_query() -> anyhow::Result<()> {
Some(MyColor::from_rgb(255, 0, 0)),
];

let points = results.get_required::<MyPoint>()?;
let points = results.get_required(MyPoint::name())?;
let point_data = points.iter_dense::<MyPoint>(&resolver).flatten().unwrap();

let colors = results.get_or_empty::<MyColor>();
let colors = results.get_or_empty(MyColor::name());
let color_data = colors.iter_sparse::<MyColor>(&resolver).flatten().unwrap();
let color_default_fn = || Some(MyColor::from(0xFF00FFFF));

Expand Down Expand Up @@ -199,10 +199,10 @@ fn no_instance_join_query() -> anyhow::Result<()> {
Some(MyColor::from_rgb(0, 255, 0)),
];

let points = results.get_required::<MyPoint>()?;
let points = results.get_required(MyPoint::name())?;
let point_data = points.iter_dense::<MyPoint>(&resolver).flatten().unwrap();

let colors = results.get_or_empty::<MyColor>();
let colors = results.get_or_empty(MyColor::name());
let color_data = colors.iter_sparse::<MyColor>(&resolver).flatten().unwrap();
let color_default_fn = || Some(MyColor::from(0xFF00FFFF));

Expand Down
13 changes: 12 additions & 1 deletion crates/re_query_cache2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ all-features = true


[features]
default = []
default = ["to_archetype"]

## Implements `ToArchetype<A>` for all builtin archetypes on `CachedLatestAtResults`.
to_archetype = ["dep:re_types"]

[dependencies]
# Rerun dependencies:
Expand All @@ -31,6 +34,9 @@ re_query2.workspace = true
re_tracing.workspace = true
re_types_core.workspace = true

# Rerun dependencies (optional):
re_types = { workspace = true, optional = true }

# External dependencies:
ahash.workspace = true
anyhow.workspace = true
Expand All @@ -57,6 +63,11 @@ similar-asserts.workspace = true
bench = false


[[example]]
name = "latest_at_archetype"
required-features = ["to_archetype"]


[[bench]]
name = "flat_vec_deque"
harness = false
Expand Down
8 changes: 4 additions & 4 deletions crates/re_query_cache2/benches/latest_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ fn query_and_visit_points(
Points2D::all_components().iter().cloned(), // no generics!
);

let points = results.get_required::<Position2D>().unwrap();
let colors = results.get_or_empty::<Color>();
let points = results.get_required(Position2D::name()).unwrap();
let colors = results.get_or_empty(Color::name());

let points = points
.iter_dense::<Position2D>(&resolver)
Expand Down Expand Up @@ -347,8 +347,8 @@ fn query_and_visit_strings(
Points2D::all_components().iter().cloned(), // no generics!
);

let points = results.get_required::<Position2D>().unwrap();
let colors = results.get_or_empty::<Text>();
let points = results.get_required(Position2D::name()).unwrap();
let colors = results.get_or_empty(Text::name());

let points = points
.iter_dense::<Position2D>(&resolver)
Expand Down
6 changes: 3 additions & 3 deletions crates/re_query_cache2/examples/latest_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ fn main() -> anyhow::Result<()> {
// * `get` returns an option
//
// At this point we still don't know whether they are cached or not. That's the next step.
let points: &CachedLatestAtComponentResults = results.get_required::<MyPoint>()?;
let colors: &CachedLatestAtComponentResults = results.get_or_empty::<MyColor>();
let labels: &CachedLatestAtComponentResults = results.get_or_empty::<MyLabel>();
let points: &CachedLatestAtComponentResults = results.get_required(MyPoint::name())?;
let colors: &CachedLatestAtComponentResults = results.get_or_empty(MyColor::name());
let labels: &CachedLatestAtComponentResults = results.get_or_empty(MyLabel::name());

// Then comes the time to resolve/convert and deserialize the data.
// These steps have to be done together for efficiency reasons.
Expand Down
105 changes: 105 additions & 0 deletions crates/re_query_cache2/examples/latest_at_archetype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use itertools::Itertools;
use re_data_store::{DataStore, LatestAtQuery};
use re_log_types::{build_frame_nr, DataRow, RowId, TimeType, Timeline};
use re_types::{
archetypes::Points2D,
components::{Color, Position2D, Text},
};
use re_types_core::{Archetype as _, Loggable as _};

use re_query_cache2::{clamped_zip_1x2, CachedLatestAtResults, PromiseResolver, PromiseResult};

// ---

fn main() -> anyhow::Result<()> {
let store = store()?;
eprintln!("store:\n{}", store.to_data_table()?);

let resolver = PromiseResolver::default();

let entity_path = "points";
let timeline = Timeline::new("frame_nr", TimeType::Sequence);
let query = LatestAtQuery::latest(timeline);
eprintln!("query:{query:?}");

let caches = re_query_cache2::Caches::new(&store);

// First, get the results for this query.
//
// They might or might not already be cached. We won't know for sure until we try to access
// each individual component's data below.
let results: CachedLatestAtResults = caches.latest_at(
&store,
&query,
&entity_path.into(),
Points2D::all_components().iter().cloned(), // no generics!
);

// Then make use of the `ToArchetype` helper trait in order to query, resolve, deserialize and
// cache an entire archetype all at once.
use re_query_cache2::ToArchetype as _;

let arch: Points2D = match results.to_archetype(&resolver).flatten() {
PromiseResult::Pending => {
// Handle the fact that the data isn't ready appropriately.
return Ok(());
}
PromiseResult::Ready(arch) => arch,
PromiseResult::Error(err) => return Err(err.into()),
};

// With the data now fully resolved/converted and deserialized, some joining logic can be
// applied if desired.
//
// In most cases this will be either a clamped zip, or no joining at all.

let color_default_fn = || None;
let label_default_fn = || None;

let results = clamped_zip_1x2(
arch.positions.iter(),
arch.colors
.iter()
.flat_map(|colors| colors.iter().map(Some)),
color_default_fn,
arch.labels
.iter()
.flat_map(|labels| labels.iter().map(Some)),
label_default_fn,
)
.collect_vec();

eprintln!("results:\n{results:?}");

Ok(())
}

// ---

fn store() -> anyhow::Result<DataStore> {
let mut store = DataStore::new(
re_log_types::StoreId::random(re_log_types::StoreKind::Recording),
re_types::components::InstanceKey::name(),
Default::default(),
);

let entity_path = "points";

{
let timepoint = [build_frame_nr(123)];

let points = vec![Position2D::new(1.0, 2.0), Position2D::new(3.0, 4.0)];
let row = DataRow::from_cells1_sized(RowId::new(), entity_path, timepoint, 2, points)?;
store.insert_row(&row)?;

let colors = vec![Color::from_rgb(255, 0, 0)];
let row = DataRow::from_cells1_sized(RowId::new(), entity_path, timepoint, 1, colors)?;
store.insert_row(&row)?;

let labels = vec![Text("a".into()), Text("b".into())];
let row = DataRow::from_cells1_sized(RowId::new(), entity_path, timepoint, 2, labels)?;
store.insert_row(&row)?;
}

Ok(store)
}
2 changes: 1 addition & 1 deletion crates/re_query_cache2/src/latest_at/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ impl Caches {
re_tracing::profile_function!();

let results = self.latest_at(store, query, entity_path, [C::name()]);
let result = results.get::<C>()?;
let result = results.get(C::name())?;

let index @ (data_time, row_id) = *result.index();

Expand Down
3 changes: 3 additions & 0 deletions crates/re_query_cache2/src/latest_at/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ mod helpers;
mod query;
mod results;

#[cfg(feature = "to_archetype")]
mod to_archetype;

pub use self::helpers::CachedLatestAtMonoResult;
pub use self::query::LatestAtCache;
pub use self::results::{CachedLatestAtComponentResults, CachedLatestAtResults};
Loading

0 comments on commit c53d339

Please sign in to comment.