Skip to content

Commit

Permalink
Add support for exclusion expressions to the DataQueryBlueprint (#4380)
Browse files Browse the repository at this point in the history
### What
- Part of the #4308 stack:
  - #4310
  - #4311
  - THIS PR: #4380
  - #4381
- Resolves: #4158

The query logic is as follows because it was simple and was sufficient
to mostly match our existing add/remove semantics. It basically
prioritizes exclusions over inclusions. Anything recursively excluded
will be pruned, even if explicitly included. We likely want to change
this behavior, but we can do so in a future PR.

Expands the query evaluator to support exclusions, and UI mechanism for
editing.

![image](https://github.com/rerun-io/rerun/assets/3312232/e35fba49-bbce-48da-8e29-03d0dbb6d985)

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [app.rerun.io](https://app.rerun.io/pr/4380) (if
applicable)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG

- [PR Build Summary](https://build.rerun.io/pr/4380)
- [Docs
preview](https://rerun.io/preview/45acbc744dddc65ea6682d5c58755ed27e96db78/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/45acbc744dddc65ea6682d5c58755ed27e96db78/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
jleibs authored Nov 29, 2023
1 parent 086b681 commit ecb8e87
Show file tree
Hide file tree
Showing 7 changed files with 502 additions and 183 deletions.
439 changes: 329 additions & 110 deletions crates/re_space_view/src/blueprint/query_expressions.rs

Large diffs are not rendered by default.

163 changes: 117 additions & 46 deletions crates/re_space_view/src/data_query_blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ impl DataQueryBlueprint {
Self {
id: DataQueryId::random(),
space_view_class_name,
expressions: queries_entities
.map(|exp| exp.to_string().into())
.collect::<Vec<_>>()
.into(),
expressions: QueryExpressions {
inclusions: queries_entities
.map(|exp| exp.to_string().into())
.collect::<Vec<_>>(),
exclusions: vec![],
},
}
}

Expand Down Expand Up @@ -142,8 +144,10 @@ impl DataQuery for DataQueryBlueprint {
struct QueryExpressionEvaluator<'a> {
blueprint: &'a DataQueryBlueprint,
per_system_entity_list: &'a EntitiesPerSystem,
exact_matches: IntSet<EntityPath>,
recursive_matches: IntSet<EntityPath>,
exact_inclusions: IntSet<EntityPath>,
recursive_inclusions: IntSet<EntityPath>,
exact_exclusions: IntSet<EntityPath>,
recursive_exclusions: IntSet<EntityPath>,
allowed_prefixes: IntSet<EntityPath>,
}

Expand All @@ -152,34 +156,54 @@ impl<'a> QueryExpressionEvaluator<'a> {
blueprint: &'a DataQueryBlueprint,
per_system_entity_list: &'a EntitiesPerSystem,
) -> Self {
let expressions: Vec<EntityPathExpr> = blueprint
let inclusions: Vec<EntityPathExpr> = blueprint
.expressions
.inclusions
.iter()
.filter(|exp| !exp.as_str().is_empty())
.map(|exp| EntityPathExpr::from(exp.as_str()))
.collect();

let exclusions: Vec<EntityPathExpr> = blueprint
.expressions
.exclusions
.iter()
.filter(|exp| !exp.as_str().is_empty())
.map(|exp| EntityPathExpr::from(exp.as_str()))
.collect();

let exact_matches: IntSet<EntityPath> = expressions
let exact_inclusions: IntSet<EntityPath> = inclusions
.iter()
.filter_map(|exp| exp.exact_entity_path().cloned())
.collect();

let recursive_matches = expressions
let recursive_inclusions = inclusions
.iter()
.filter_map(|exp| exp.recursive_entity_path().cloned())
.collect();

let allowed_prefixes = expressions
let exact_exclusions: IntSet<EntityPath> = exclusions
.iter()
.filter_map(|exp| exp.exact_entity_path().cloned())
.collect();

let recursive_exclusions: IntSet<EntityPath> = exclusions
.iter()
.filter_map(|exp| exp.recursive_entity_path().cloned())
.collect();

let allowed_prefixes = inclusions
.iter()
.flat_map(|exp| EntityPath::incremental_walk(None, exp.entity_path()))
.collect();

Self {
blueprint,
per_system_entity_list,
exact_matches,
recursive_matches,
exact_inclusions,
recursive_inclusions,
exact_exclusions,
recursive_exclusions,
allowed_prefixes,
}
}
Expand All @@ -193,18 +217,22 @@ impl<'a> QueryExpressionEvaluator<'a> {
from_recursive: bool,
) -> Option<DataResultHandle> {
// If we hit a prefix that is not allowed, we terminate. This is
// a pruned branch of the tree.
// a pruned branch of the tree. Can come from either an explicit
// recursive exclusion, or an implicit missing inclusion.
// TODO(jleibs): If this space is disconnected, we should terminate here
if !(from_recursive || self.allowed_prefixes.contains(&tree.path)) {
if self.recursive_exclusions.contains(&tree.path)
|| !(from_recursive || self.allowed_prefixes.contains(&tree.path))
{
return None;
}

let entity_path = tree.path.clone();

// Pre-compute our matches
let exact_match = self.exact_matches.contains(&entity_path);
let recursive_match = self.recursive_matches.contains(&entity_path) || from_recursive;
let any_match = exact_match || recursive_match;
let exact_include = self.exact_inclusions.contains(&entity_path);
let recursive_include = self.recursive_inclusions.contains(&entity_path) || from_recursive;
let exact_exclude = self.exact_exclusions.contains(&entity_path);
let any_match = (exact_include || recursive_include) && !exact_exclude;

// Only populate view_parts if this is a match
// Note that allowed prefixes that aren't matches can still create groups
Expand Down Expand Up @@ -238,7 +266,7 @@ impl<'a> QueryExpressionEvaluator<'a> {
.join(&DataQueryBlueprint::RECURSIVE_OVERRIDES_PREFIX.into())
.join(&entity_path);

let self_leaf = if !view_parts.is_empty() || exact_match {
let self_leaf = if !view_parts.is_empty() || exact_include {
let individual_props = overrides.individual.get_opt(&entity_path);
let mut leaf_resolved_properties = resolved_properties.clone();

Expand Down Expand Up @@ -273,7 +301,7 @@ impl<'a> QueryExpressionEvaluator<'a> {
overrides,
&resolved_properties,
data_results,
recursive_match, // Once we have hit a recursive match, it's always propagated
recursive_include, // Once we have hit a recursive match, it's always propagated
)
}))
.collect();
Expand Down Expand Up @@ -450,39 +478,49 @@ mod tests {
all_recordings: vec![],
};

let scenarios: Vec<(Vec<&str>, Vec<&str>)> = vec![
(
vec!["/"],
vec![
struct Scenario {
inclusions: Vec<&'static str>,
exclusions: Vec<&'static str>,
outputs: Vec<&'static str>,
}

let scenarios: Vec<Scenario> = vec![
Scenario {
inclusions: vec!["/"],
exclusions: vec![],
outputs: vec![
"/",
"parent/",
"parent",
"parent/skipped/", // Not an exact match and not found in tree
"parent/skipped/child1", // Only child 1 has ViewParts
],
),
(
vec!["parent/skipped/"],
vec![
},
Scenario {
inclusions: vec!["parent/skipped/"],
exclusions: vec![],
outputs: vec![
"/",
"parent/", // Only included because is a prefix
"parent/skipped/", // Not an exact match and not found in tree
"parent/skipped/child1", // Only child 1 has ViewParts
],
),
(
vec!["parent", "parent/skipped/child2"],
vec![
},
Scenario {
inclusions: vec!["parent", "parent/skipped/child2"],
exclusions: vec![],
outputs: vec![
"/", // Trivial intermediate group -- could be collapsed
"parent/",
"parent",
"parent/skipped/", // Trivial intermediate group -- could be collapsed
"parent/skipped/child2",
],
),
(
vec!["parent/skipped", "parent/skipped/child2", "parent/"],
vec![
},
Scenario {
inclusions: vec!["parent/skipped", "parent/skipped/child2", "parent/"],
exclusions: vec![],
outputs: vec![
"/",
"parent/",
"parent",
Expand All @@ -491,25 +529,58 @@ mod tests {
"parent/skipped/child1", // Included because an exact match
"parent/skipped/child2",
],
),
(
vec!["not/found"],
},
Scenario {
inclusions: vec!["parent/skipped", "parent/skipped/child2", "parent/"],
exclusions: vec!["parent"],
outputs: vec![
"/",
"parent/", // Parent leaf has been excluded
"parent/skipped/",
"parent/skipped", // Included because an exact match
"parent/skipped/child1", // Included because an exact match
"parent/skipped/child2",
],
},
Scenario {
inclusions: vec!["parent/"],
exclusions: vec!["parent/skipped/"],
outputs: vec!["/", "parent"], // None of the children are hit since excluded
},
Scenario {
inclusions: vec!["parent/", "parent/skipped/child2"],
exclusions: vec!["parent/skipped/child1"],
outputs: vec![
"/",
"parent/",
"parent",
"parent/skipped/",
"parent/skipped/child2", // No child1 since skipped.
],
},
Scenario {
inclusions: vec!["not/found"],
exclusions: vec![],
// TODO(jleibs): Making this work requires merging the EntityTree walk with a minimal-coverage ExactMatchTree walk
// not crucial for now until we expose a free-form UI for entering paths.
// vec!["/", "not/", "not/found"]),
vec![],
),
outputs: vec![],
},
];

for (input, outputs) in scenarios {
for Scenario {
inclusions,
exclusions,
outputs,
} in scenarios
{
let query = DataQueryBlueprint {
id: DataQueryId::random(),
space_view_class_name: "3D".into(),
expressions: input
.into_iter()
.map(|s| s.into())
.collect::<Vec<_>>()
.into(),
expressions: QueryExpressions {
inclusions: inclusions.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
exclusions: exclusions.into_iter().map(|s| s.into()).collect::<Vec<_>>(),
},
};

let query_result = query.execute_query(&resolver, &ctx, &entities_per_system_per_class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ table QueryExpressions (
"attr.rust.derive": "PartialEq, Eq",
"attr.rust.override_crate": "re_space_view"
) {
/// A set of strings that can be parsed as `EntityPathExpression`s.
expressions: [string] (order: 100);
/// A set of strings representing `EntityPathExpression`s to be included.
inclusions: [string] (order: 100);

/// A set of strings representing `EntityPathExpression`s to be excluded.
exclusions: [string] (order: 200);
}
16 changes: 11 additions & 5 deletions crates/re_viewer/src/ui/selection_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,16 +417,22 @@ fn blueprint_ui(

if let Some(space_view) = viewport.blueprint.space_view(space_view_id) {
if let Some(query) = space_view.queries.first() {
let expressions = query.expressions.expressions.join("\n");
let mut edited_expressions = expressions.clone();
let inclusions = query.expressions.inclusions.join("\n");
let mut edited_inclusions = inclusions.clone();
let exclusions = query.expressions.exclusions.join("\n");
let mut edited_exclusions = exclusions.clone();

ui.text_edit_multiline(&mut edited_expressions);
ui.label("Inclusion expressions");
ui.text_edit_multiline(&mut edited_inclusions);
ui.label("Exclusion expressions");
ui.text_edit_multiline(&mut edited_exclusions);

if edited_expressions != expressions {
if edited_inclusions != inclusions || edited_exclusions != exclusions {
let timepoint = TimePoint::timeless();

let expressions_component = QueryExpressions {
expressions: edited_expressions.split('\n').map(|s| s.into()).collect(),
inclusions: edited_inclusions.split('\n').map(|s| s.into()).collect(),
exclusions: edited_exclusions.split('\n').map(|s| s.into()).collect(),
};

let row = DataRow::from_cells1_sized(
Expand Down
25 changes: 22 additions & 3 deletions rerun_cpp/src/rerun/blueprint/query_expressions.cpp

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

16 changes: 5 additions & 11 deletions rerun_cpp/src/rerun/blueprint/query_expressions.hpp

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

Loading

0 comments on commit ecb8e87

Please sign in to comment.