Skip to content

Commit a553619

Browse files
authored
Significantly improve transform ingestion speed (#11655)
1 parent 1994530 commit a553619

File tree

10 files changed

+1048
-632
lines changed

10 files changed

+1048
-632
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9651,6 +9651,7 @@ version = "0.27.0-alpha.1+dev"
96519651
dependencies = [
96529652
"ahash",
96539653
"bitflags 2.9.4",
9654+
"criterion",
96549655
"glam",
96559656
"insta",
96569657
"itertools 0.14.0",
@@ -11935,6 +11936,17 @@ dependencies = [
1193511936
"rerun",
1193611937
]
1193711938

11939+
[[package]]
11940+
name = "test_out_of_order_transforms"
11941+
version = "0.27.0-alpha.1+dev"
11942+
dependencies = [
11943+
"anyhow",
11944+
"clap",
11945+
"glam",
11946+
"re_log",
11947+
"rerun",
11948+
]
11949+
1193811950
[[package]]
1193911951
name = "test_ui_wakeup"
1194011952
version = "0.27.0-alpha.1+dev"

crates/store/re_grpc_client/src/write.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ async fn message_proxy_client(
391391
Some(Cmd::Flush { on_done }) => {
392392
// Messages are received in order, so once we receive a `flush`
393393
// we know we've sent all messages before that flush through already.
394-
re_log::debug!("Flush requested");
394+
re_log::trace!("Flush requested");
395395
if on_done.send(()).is_err() {
396396
// Flush channel may already be closed for non-blocking flush, so this isn't an error.
397397
re_log::debug!("Failed to respond to flush: flush report channel was closed");

crates/store/re_tf/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,12 @@ thiserror.workspace = true
3636
vec1 = { workspace = true, features = ["smallvec-v1"] }
3737

3838
[dev-dependencies]
39+
criterion.workspace = true
3940
insta.workspace = true
41+
42+
[lib]
43+
bench = false
44+
45+
[[bench]]
46+
name = "transform_resolution_cache_bench"
47+
harness = false
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#![expect(clippy::unwrap_used)] // acceptable in benchmarks
2+
3+
use criterion::{Criterion, criterion_group, criterion_main};
4+
use itertools::Itertools as _;
5+
use std::sync::Arc;
6+
7+
use re_chunk_store::{Chunk, ChunkStoreEvent};
8+
use re_entity_db::EntityDb;
9+
use re_log_types::{EntityPath, StoreId, TimePoint, Timeline, TimelineName};
10+
use re_types::{RowId, archetypes};
11+
12+
use re_tf::{TransformFrameIdHash, TransformResolutionCache};
13+
14+
const NUM_TIMELINES: usize = 4;
15+
const NUM_TIMEPOINTS: usize = 1000;
16+
const NUM_TIMEPOINTS_PER_ENTITY: usize = 50;
17+
const NUM_ENTITIES: usize = 100;
18+
19+
fn setup_store() -> (EntityDb, Vec<ChunkStoreEvent>) {
20+
let mut entity_db = EntityDb::new(StoreId::random(
21+
re_log_types::StoreKind::Recording,
22+
"test_app",
23+
));
24+
25+
let timelines = (0..NUM_TIMELINES)
26+
.map(|i| Timeline::new(format!("timeline{i}"), re_log_types::TimeType::Sequence))
27+
.collect_vec();
28+
29+
let mut events = Vec::new();
30+
for entity_idx in 0..NUM_ENTITIES {
31+
for batch in 0..(NUM_TIMEPOINTS / NUM_TIMEPOINTS_PER_ENTITY) {
32+
let chunk_base_time = batch * NUM_TIMEPOINTS_PER_ENTITY;
33+
34+
let mut builder = Chunk::builder(EntityPath::from(format!("entity{entity_idx}")));
35+
for t in 0..NUM_TIMEPOINTS_PER_ENTITY {
36+
let mut timepoint = TimePoint::default();
37+
for timeline in &timelines {
38+
#[expect(clippy::cast_possible_wrap)]
39+
timepoint.insert(*timeline, (chunk_base_time + t) as i64);
40+
}
41+
builder = builder.with_archetype(
42+
RowId::new(),
43+
timepoint,
44+
&archetypes::Transform3D::from_translation([1.0, 2.0, 3.0])
45+
.with_scale(2.0)
46+
.with_quaternion(glam::Quat::from_xyzw(0.0, 2.0, 3.0, 1.0))
47+
.with_mat3x3(glam::Mat3::IDENTITY),
48+
);
49+
}
50+
let chunk = builder.build().unwrap();
51+
52+
events.extend(entity_db.add_chunk(&Arc::new(chunk)).unwrap().into_iter());
53+
}
54+
}
55+
(entity_db, events)
56+
}
57+
58+
fn transform_resolution_cache_query(c: &mut Criterion) {
59+
let (entity_db, events) = setup_store();
60+
61+
c.bench_function("build_from_entitydb", |b| {
62+
b.iter(|| {
63+
let mut cache = TransformResolutionCache::default();
64+
cache.process_store_events(events.iter());
65+
cache
66+
});
67+
});
68+
69+
let query = re_chunk_store::LatestAtQuery::new(TimelineName::new("timeline2"), 123);
70+
let queried_frame = TransformFrameIdHash::from_entity_path(&EntityPath::from("entity2"));
71+
72+
c.bench_function("query_uncached_frame", |b| {
73+
b.iter_batched(
74+
|| {
75+
let mut cache = TransformResolutionCache::default();
76+
cache.process_store_events(events.iter());
77+
cache
78+
},
79+
|mut cold_cache| {
80+
let frame_transforms = cold_cache
81+
.transforms_for_timeline(query.timeline())
82+
.frame_transforms(queried_frame)
83+
.unwrap();
84+
frame_transforms
85+
.latest_at_transform(&entity_db, &query)
86+
.unwrap()
87+
},
88+
criterion::BatchSize::PerIteration,
89+
);
90+
});
91+
92+
let mut warm_cache = TransformResolutionCache::default();
93+
warm_cache.process_store_events(events.iter());
94+
warm_cache
95+
.transforms_for_timeline(query.timeline())
96+
.frame_transforms(queried_frame)
97+
.unwrap()
98+
.latest_at_transform(&entity_db, &query);
99+
100+
c.bench_function("query_cached_frame", |b| {
101+
b.iter(|| {
102+
let frame_transforms = warm_cache
103+
.transforms_for_timeline(query.timeline())
104+
.frame_transforms(queried_frame)
105+
.unwrap();
106+
frame_transforms
107+
.latest_at_transform(&entity_db, &query)
108+
.unwrap()
109+
});
110+
});
111+
112+
// TODO(andreas): Additional benchmarks for iterative invalidation would be great!
113+
}
114+
115+
criterion_group!(benches, transform_resolution_cache_query);
116+
criterion_main!(benches);

crates/store/re_tf/src/transform_forest.rs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use nohash_hasher::IntMap;
33
use vec1::smallvec_v1::SmallVec1;
44

55
use re_chunk_store::LatestAtQuery;
6-
use re_entity_db::{EntityPath, EntityTree};
6+
use re_entity_db::{EntityDb, EntityPath, EntityTree};
77
use re_types::ArchetypeName;
88

99
use crate::{
@@ -239,7 +239,7 @@ impl TransformForest {
239239
/// entities are transformed relative to it.
240240
pub fn new(
241241
recording: &re_entity_db::EntityDb,
242-
transform_cache: &TransformResolutionCache,
242+
transform_cache: &mut TransformResolutionCache,
243243
time_query: &LatestAtQuery,
244244
) -> Self {
245245
re_tracing::profile_function!();
@@ -256,6 +256,7 @@ impl TransformForest {
256256
};
257257
tree.entity_tree_gather_transforms_recursive(
258258
entity_tree,
259+
recording,
259260
time_query,
260261
// Ignore potential pinhole camera at the root of the view, since it is regarded as being "above" this root.
261262
// TODO(andreas): Should we warn about that?
@@ -271,9 +272,10 @@ impl TransformForest {
271272
fn entity_tree_gather_transforms_recursive(
272273
&mut self,
273274
subtree: &EntityTree,
275+
entity_db: &EntityDb,
274276
query: &LatestAtQuery,
275277
transform_root_from_parent: TransformInfo,
276-
transforms_for_timeline: &CachedTransformsForTimeline,
278+
transforms_for_timeline: &mut CachedTransformsForTimeline,
277279
) {
278280
let root = transform_root_from_parent.root;
279281
let root_from_parent = transform_root_from_parent.target_from_source;
@@ -287,7 +289,8 @@ impl TransformForest {
287289
for child_tree in subtree.children.values() {
288290
let child_path = &child_tree.path;
289291

290-
let transforms_at_entity = transforms_at(child_path, query, transforms_for_timeline);
292+
let transforms_at_entity =
293+
transforms_at(child_path, entity_db, query, transforms_for_timeline);
291294

292295
let root_from_child =
293296
root_from_parent * transforms_at_entity.parent_from_entity_tree_transform;
@@ -313,11 +316,11 @@ impl TransformForest {
313316
// Collect & compute poses.
314317
let root_from_instances = compute_root_from_instances(
315318
target_from_source,
316-
transforms_at_entity.entity_from_instance_poses,
319+
transforms_at_entity.entity_from_instance_poses.as_ref(),
317320
);
318321
let root_from_archetype = compute_root_from_archetype(
319322
target_from_source,
320-
transforms_at_entity.entity_from_instance_poses,
323+
transforms_at_entity.entity_from_instance_poses.as_ref(),
321324
);
322325

323326
let transform_root_from_child = TransformInfo {
@@ -329,6 +332,7 @@ impl TransformForest {
329332

330333
self.entity_tree_gather_transforms_recursive(
331334
child_tree,
335+
entity_db,
332336
query,
333337
transform_root_from_child,
334338
transforms_for_timeline,
@@ -656,17 +660,18 @@ fn pinhole3d_from_image_plane(
656660

657661
/// Resolved transforms at an entity.
658662
#[derive(Default)]
659-
struct TransformsAtEntity<'a> {
663+
struct TransformsAtEntity {
660664
parent_from_entity_tree_transform: glam::Affine3A,
661-
entity_from_instance_poses: Option<&'a PoseTransformArchetypeMap>,
662-
pinhole_projection: Option<&'a ResolvedPinholeProjection>,
665+
entity_from_instance_poses: Option<PoseTransformArchetypeMap>,
666+
pinhole_projection: Option<ResolvedPinholeProjection>,
663667
}
664668

665-
fn transforms_at<'a>(
669+
fn transforms_at(
666670
entity_path: &EntityPath,
671+
entity_db: &EntityDb,
667672
query: &LatestAtQuery,
668-
transforms_for_timeline: &'a CachedTransformsForTimeline,
669-
) -> TransformsAtEntity<'a> {
673+
transforms_for_timeline: &mut CachedTransformsForTimeline,
674+
) -> TransformsAtEntity {
670675
// This is called very frequently, don't put a profile scope here.
671676

672677
let Some(entity_transforms) = transforms_for_timeline
@@ -676,13 +681,17 @@ fn transforms_at<'a>(
676681
};
677682

678683
let parent_from_entity_tree_transform = entity_transforms
679-
.latest_at_transform(query)
684+
.latest_at_transform(entity_db, query)
680685
// TODO(RR-2511): Don't ignore target frame.
681686
.map_or(glam::Affine3A::IDENTITY, |source_to_target| {
682687
source_to_target.transform
683688
});
684-
let entity_from_instance_poses = entity_transforms.latest_at_instance_poses_all(query);
685-
let pinhole_projection = entity_transforms.latest_at_pinhole(query);
689+
let entity_from_instance_poses = entity_transforms
690+
.latest_at_instance_poses(entity_db, query)
691+
.cloned();
692+
let pinhole_projection = entity_transforms
693+
.latest_at_pinhole(entity_db, query)
694+
.cloned();
686695

687696
TransformsAtEntity {
688697
parent_from_entity_tree_transform,
@@ -800,13 +809,10 @@ mod tests {
800809
fn test_simple_entity_hierarchy() {
801810
let test_scene = entity_hierarchy_test_scene();
802811
let mut transform_cache = TransformResolutionCache::default();
803-
transform_cache.add_chunks(
804-
&test_scene,
805-
test_scene.storage_engine().store().iter_chunks(),
806-
);
812+
transform_cache.add_chunks(test_scene.storage_engine().store().iter_chunks());
807813

808814
let query = LatestAtQuery::latest(TimelineName::log_tick());
809-
let transform_forest = TransformForest::new(&test_scene, &transform_cache, &query);
815+
let transform_forest = TransformForest::new(&test_scene, &mut transform_cache, &query);
810816

811817
let all_entity_paths = test_scene
812818
.entity_paths()
@@ -935,10 +941,10 @@ mod tests {
935941
.unwrap();
936942

937943
let mut transform_cache = TransformResolutionCache::default();
938-
transform_cache.add_chunks(&entity_db, entity_db.storage_engine().store().iter_chunks());
944+
transform_cache.add_chunks(entity_db.storage_engine().store().iter_chunks());
939945

940946
let query = LatestAtQuery::latest(TimelineName::log_tick());
941-
let transform_forest = TransformForest::new(&entity_db, &transform_cache, &query);
947+
let transform_forest = TransformForest::new(&entity_db, &mut transform_cache, &query);
942948

943949
let target = TransformFrameIdHash::from_entity_path(&EntityPath::from("box"));
944950
let sources = [TransformFrameIdHash::from_entity_path(&EntityPath::from(

0 commit comments

Comments
 (0)