Skip to content

Commit 129d04e

Browse files
committed
Optimize performance for hundreds of entities (#7115)
This commit addresses the performance bottlenecks described in issue #7115 where the Rerun viewer becomes slow with several hundred entities. Key improvements: 1. Share visualizable_entities_per_view across the frame - Added visualizable_entities_per_view to ViewerContext to avoid redundant computation - Previously, determine_visualizable_entities() was called multiple times per frame for each view: * Once in app_state.rs * Once in update_overrides() * Once or more in create_entity_add_info() for UI updates - Now computed once per frame and shared via ViewerContext 2. Update create_entity_add_info to use cached results - Modified entity_add_info.rs to use pre-computed visualizable entities from ViewerContext instead of recomputing 3. Improve cache invalidation strategy - Changed cache keys from latest_row_id to ChunkStoreGeneration - latest_row_id changes on every data insertion, invalidating cache every frame for streaming data
1 parent 0ba7dc5 commit 129d04e

File tree

4 files changed

+180
-45
lines changed

4 files changed

+180
-45
lines changed

crates/viewer/re_test_context/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,9 @@ impl TestContext {
494494
let drag_and_drop_manager =
495495
re_viewer_context::DragAndDropManager::new(ItemCollection::default());
496496

497+
// Tests don't compute visualizable entities per view, so we provide an empty map
498+
let visualizable_entities_per_view: HashMap<ViewId, re_viewer_context::PerVisualizer<re_viewer_context::VisualizableEntities>> = HashMap::default();
499+
497500
let mut context_render_state = self.egui_render_state.lock();
498501
let render_state = context_render_state.get_or_insert_with(create_egui_renderstate);
499502
let mut egui_renderer = render_state.renderer.write();
@@ -531,6 +534,7 @@ impl TestContext {
531534
storage_context: &storage_context,
532535
maybe_visualizable_entities_per_visualizer: &maybe_visualizable_entities_per_visualizer,
533536
indicated_entities_per_visualizer: &indicated_entities_per_visualizer,
537+
visualizable_entities_per_view: &visualizable_entities_per_view,
534538
query_results: &self.query_results,
535539
time_ctrl: &self.time_ctrl.read(),
536540
blueprint_time_ctrl: &Default::default(),

crates/viewer/re_viewer/src/app_state.rs

Lines changed: 161 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
use std::{borrow::Cow, str::FromStr as _};
1+
use std::{
2+
borrow::Cow,
3+
str::FromStr as _,
4+
};
25

36
use ahash::HashMap;
47
use egui::{Ui, text_edit::TextEditState, text_selection::LabelSelectionState};
58

69
use re_chunk::{Timeline, TimelineName};
710
use re_chunk_store::LatestAtQuery;
811
use re_entity_db::EntityDb;
9-
use re_log_types::{AbsoluteTimeRangeF, DataSourceMessage, StoreId, TableId};
12+
use re_log_types::{AbsoluteTimeRangeF, DataSourceMessage, EntityPath, StoreId, TableId};
1013
use re_redap_browser::RedapServers;
1114
use re_redap_client::ConnectionRegistryHandle;
1215
use re_smart_channel::ReceiveSet;
@@ -18,7 +21,7 @@ use re_viewer_context::{
1821
DragAndDropManager, FallbackProviderRegistry, GlobalContext, IndicatedEntities, Item,
1922
MaybeVisualizableEntities, PerVisualizer, SelectionChange, StorageContext, StoreContext,
2023
StoreHub, SystemCommand, SystemCommandSender as _, TableStore, TimeControl, TimeControlCommand,
21-
ViewClassRegistry, ViewId, ViewStates, ViewerContext, blueprint_timeline,
24+
ViewClassRegistry, ViewId, ViewStates, ViewerContext, VisualizableEntities, blueprint_timeline,
2225
open_url::{self, ViewerOpenUrl},
2326
};
2427
use re_viewport::ViewportUi;
@@ -72,6 +75,12 @@ pub struct AppState {
7275
#[serde(skip)]
7376
pub(crate) share_modal: crate::ui::ShareModal,
7477

78+
#[serde(skip)]
79+
view_query_cache: HashMap<ViewId, ViewQueryCacheEntry>,
80+
81+
#[serde(skip)]
82+
visualizable_entities_cache: HashMap<ViewId, VisualizableEntitiesCacheEntry>,
83+
7584
/// Test-only: single-shot callback to run at the end of the frame. Used in integration tests
7685
/// to interact with the `ViewerContext`.
7786
#[cfg(feature = "testing")]
@@ -127,6 +136,8 @@ impl Default for AppState {
127136
view_states: Default::default(),
128137
selection_state: Default::default(),
129138
focused_item: Default::default(),
139+
view_query_cache: Default::default(),
140+
visualizable_entities_cache: Default::default(),
130141

131142
#[cfg(feature = "testing")]
132143
test_hook: None,
@@ -142,6 +153,58 @@ pub(crate) struct WelcomeScreenState {
142153
pub opacity: f32,
143154
}
144155

156+
struct ViewQueryCacheEntry {
157+
key: ViewQueryCacheKey,
158+
result: DataQueryResult,
159+
}
160+
161+
#[derive(Clone, PartialEq, Eq)]
162+
struct ViewQueryCacheKey {
163+
recording_generation: re_chunk_store::ChunkStoreGeneration,
164+
blueprint_generation: re_chunk_store::ChunkStoreGeneration,
165+
blueprint_query: LatestAtQuery,
166+
}
167+
168+
impl ViewQueryCacheKey {
169+
fn new(
170+
recording_generation: re_chunk_store::ChunkStoreGeneration,
171+
blueprint_generation: re_chunk_store::ChunkStoreGeneration,
172+
blueprint_query: &LatestAtQuery,
173+
) -> Self {
174+
Self {
175+
recording_generation,
176+
blueprint_generation,
177+
blueprint_query: blueprint_query.clone(),
178+
}
179+
}
180+
}
181+
182+
struct VisualizableEntitiesCacheEntry {
183+
key: VisualizableEntitiesCacheKey,
184+
result: PerVisualizer<VisualizableEntities>,
185+
}
186+
187+
#[derive(Clone, PartialEq, Eq, Hash)]
188+
struct VisualizableEntitiesCacheKey {
189+
recording_generation: re_chunk_store::ChunkStoreGeneration,
190+
blueprint_generation: re_chunk_store::ChunkStoreGeneration,
191+
space_origin: EntityPath,
192+
}
193+
194+
impl VisualizableEntitiesCacheKey {
195+
fn new(
196+
recording_generation: re_chunk_store::ChunkStoreGeneration,
197+
blueprint_generation: re_chunk_store::ChunkStoreGeneration,
198+
space_origin: &EntityPath,
199+
) -> Self {
200+
Self {
201+
recording_generation,
202+
blueprint_generation,
203+
space_origin: space_origin.clone(),
204+
}
205+
}
206+
}
207+
145208
impl AppState {
146209
pub fn set_examples_manifest_url(&mut self, egui_ctx: &egui::Context, url: String) {
147210
self.welcome_screen.set_examples_manifest_url(egui_ctx, url);
@@ -291,39 +354,97 @@ impl AppState {
291354
.maybe_visualizable_entities_for_visualizer_systems(recording.store_id());
292355
let indicated_entities_per_visualizer =
293356
view_class_registry.indicated_entities_per_visualizer(recording.store_id());
357+
let recording_generation = recording.generation();
358+
let blueprint_generation = store_context.blueprint.generation();
359+
360+
self.view_query_cache
361+
.retain(|view_id, _| viewport_ui.blueprint.views.contains_key(view_id));
362+
self.visualizable_entities_cache
363+
.retain(|view_id, _| viewport_ui.blueprint.views.contains_key(view_id));
364+
365+
// Determine visualizable entities for each view (cached)
366+
let visualizable_entities_per_view: HashMap<ViewId, PerVisualizer<VisualizableEntities>> = {
367+
re_tracing::profile_scope!("visualizable_entities_per_view");
368+
viewport_ui
369+
.blueprint
370+
.views
371+
.values()
372+
.map(|view| {
373+
let cache_key = VisualizableEntitiesCacheKey::new(
374+
recording_generation.clone(),
375+
blueprint_generation.clone(),
376+
&view.space_origin,
377+
);
378+
379+
let visualizable_entities = if let Some(entry) = self.visualizable_entities_cache.get(&view.id)
380+
&& entry.key == cache_key
381+
{
382+
PerVisualizer(entry.result.0.clone())
383+
} else {
384+
// This is the expensive operation we're caching
385+
view.class(view_class_registry)
386+
.determine_visualizable_entities(
387+
&maybe_visualizable_entities_per_visualizer,
388+
recording,
389+
&view_class_registry
390+
.new_visualizer_collection(view.class_identifier()),
391+
&view.space_origin,
392+
)
393+
};
394+
395+
self.visualizable_entities_cache.insert(
396+
view.id,
397+
VisualizableEntitiesCacheEntry {
398+
key: cache_key,
399+
result: PerVisualizer(visualizable_entities.0.clone()),
400+
},
401+
);
402+
403+
(view.id, visualizable_entities)
404+
})
405+
.collect()
406+
};
294407

295408
// Execute the queries for every `View`
296-
let query_results = {
409+
let query_results: HashMap<_, _> = {
297410
re_tracing::profile_scope!("query_results");
298411
viewport_ui
299412
.blueprint
300413
.views
301414
.values()
302415
.map(|view| {
303-
// TODO(andreas): This needs to be done in a store subscriber that exists per view (instance, not class!).
304-
// Note that right now we determine *all* visualizable entities, not just the queried ones.
305-
// In a store subscriber set this is fine, but on a per-frame basis it's wasteful.
306-
let visualizable_entities = view
307-
.class(view_class_registry)
308-
.determine_visualizable_entities(
309-
&maybe_visualizable_entities_per_visualizer,
310-
recording,
311-
&view_class_registry
312-
.new_visualizer_collection(view.class_identifier()),
313-
&view.space_origin,
314-
);
315-
316-
(
317-
view.id,
416+
let visualizable_entities = &visualizable_entities_per_view[&view.id];
417+
418+
let cache_key = ViewQueryCacheKey::new(
419+
recording_generation.clone(),
420+
blueprint_generation.clone(),
421+
&blueprint_query,
422+
);
423+
424+
let result = if let Some(entry) = self.view_query_cache.get(&view.id)
425+
&& entry.key == cache_key
426+
{
427+
entry.result.clone()
428+
} else {
318429
view.contents.execute_query(
319430
store_context,
320431
view_class_registry,
321432
&blueprint_query,
322-
&visualizable_entities,
323-
),
324-
)
433+
visualizable_entities,
434+
)
435+
};
436+
437+
self.view_query_cache.insert(
438+
view.id,
439+
ViewQueryCacheEntry {
440+
key: cache_key,
441+
result: result.clone(),
442+
},
443+
);
444+
445+
(view.id, result)
325446
})
326-
.collect::<_>()
447+
.collect()
327448
};
328449

329450
let app_blueprint_ctx = AppBlueprintCtx {
@@ -361,6 +482,7 @@ impl AppState {
361482
maybe_visualizable_entities_per_visualizer:
362483
&maybe_visualizable_entities_per_visualizer,
363484
indicated_entities_per_visualizer: &indicated_entities_per_visualizer,
485+
visualizable_entities_per_view: &visualizable_entities_per_view,
364486
query_results: &query_results,
365487
time_ctrl,
366488
blueprint_time_ctrl: blueprint_time_control,
@@ -389,9 +511,16 @@ impl AppState {
389511
time_ctrl.timeline(),
390512
&maybe_visualizable_entities_per_visualizer,
391513
&indicated_entities_per_visualizer,
514+
&visualizable_entities_per_view,
392515
view_states,
393516
);
394517

518+
for (view_id, result) in &query_results {
519+
if let Some(entry) = self.view_query_cache.get_mut(view_id) {
520+
entry.result = result.clone();
521+
}
522+
}
523+
395524
// We need to recreate the context to appease the borrow checker. It is a bit annoying, but
396525
// it's just a bunch of refs so not really that big of a deal in practice.
397526
let ctx = ViewerContext {
@@ -417,6 +546,7 @@ impl AppState {
417546
maybe_visualizable_entities_per_visualizer:
418547
&maybe_visualizable_entities_per_visualizer,
419548
indicated_entities_per_visualizer: &indicated_entities_per_visualizer,
549+
visualizable_entities_per_view: &visualizable_entities_per_view,
420550
query_results: &query_results,
421551
time_ctrl,
422552
blueprint_time_ctrl: blueprint_time_control,
@@ -806,6 +936,7 @@ fn update_overrides(
806936
active_timeline: &Timeline,
807937
maybe_visualizable_entities_per_visualizer: &PerVisualizer<MaybeVisualizableEntities>,
808938
indicated_entities_per_visualizer: &PerVisualizer<IndicatedEntities>,
939+
visualizable_entities_per_view: &HashMap<ViewId, PerVisualizer<VisualizableEntities>>,
809940
view_states: &mut ViewStates,
810941
) -> HashMap<ViewId, DataQueryResult> {
811942
use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _};
@@ -814,6 +945,7 @@ fn update_overrides(
814945
view: &'a re_viewport_blueprint::ViewBlueprint,
815946
view_state: &'a dyn re_viewer_context::ViewState,
816947
query_result: DataQueryResult,
948+
visualizable_entities: &'a PerVisualizer<VisualizableEntities>,
817949
}
818950

819951
for view in viewport_blueprint.views.values() {
@@ -828,10 +960,12 @@ fn update_overrides(
828960
let view_state = view_states
829961
.get(view.id)
830962
.expect("View state should exist, we just called ensure_state_exists on it.");
963+
let visualizable_entities = &visualizable_entities_per_view[&view.id];
831964
OverridesUpdateTask {
832965
view,
833966
view_state,
834967
query_result,
968+
visualizable_entities,
835969
}
836970
})
837971
})
@@ -844,24 +978,15 @@ fn update_overrides(
844978
view,
845979
view_state,
846980
mut query_result,
981+
visualizable_entities,
847982
}| {
848-
// TODO(andreas): This needs to be done in a store subscriber that exists per view (instance, not class!).
849-
// Note that right now we determine *all* visualizable entities, not just the queried ones.
850-
// In a store subscriber set this is fine, but on a per-frame basis it's wasteful.
851-
let visualizable_entities = view
852-
.class(view_class_registry)
853-
.determine_visualizable_entities(
854-
maybe_visualizable_entities_per_visualizer,
855-
store_context.recording,
856-
&view_class_registry.new_visualizer_collection(view.class_identifier()),
857-
&view.space_origin,
858-
);
859-
983+
// Now using the pre-computed visualizable_entities instead of recomputing them.
984+
// This avoids the expensive determine_visualizable_entities call that was happening twice per frame.
860985
let resolver = re_viewport_blueprint::DataQueryPropertyResolver::new(
861986
view,
862987
view_class_registry,
863988
maybe_visualizable_entities_per_visualizer,
864-
&visualizable_entities,
989+
visualizable_entities,
865990
indicated_entities_per_visualizer,
866991
);
867992

crates/viewer/re_viewer_context/src/viewer_context.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ pub struct ViewerContext<'a> {
4747
/// or are we ever interested in a (definitely-)non-visualizable but archetype-matching entity?
4848
pub indicated_entities_per_visualizer: &'a PerVisualizer<IndicatedEntities>,
4949

50+
/// For each view, the set of entities that can be visualized by that view.
51+
///
52+
/// This is computed once per frame and cached to avoid redundant computation.
53+
pub visualizable_entities_per_view: &'a HashMap<ViewId, PerVisualizer<crate::VisualizableEntities>>,
54+
5055
/// All the query results for this frame.
5156
pub query_results: &'a HashMap<ViewId, DataQueryResult>,
5257

crates/viewer/re_viewport_blueprint/src/entity_add_info.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,16 @@ pub fn create_entity_add_info(
7171
) -> IntMap<EntityPath, EntityAddInfo> {
7272
let mut meta_data: IntMap<EntityPath, EntityAddInfo> = IntMap::default();
7373

74-
// TODO(andreas): This should be state that is already available because it's part of the view's state.
75-
let class = view.class(ctx.view_class_registry());
76-
let visualizable_entities = class.determine_visualizable_entities(
77-
ctx.maybe_visualizable_entities_per_visualizer,
78-
ctx.recording(),
79-
&ctx.view_class_registry()
80-
.new_visualizer_collection(view.class_identifier()),
81-
&view.space_origin,
82-
);
74+
// Use pre-computed visualizable entities from the context to avoid recomputation.
75+
// This was previously computed in app_state.rs and is now shared across the frame.
76+
let Some(visualizable_entities) = ctx.visualizable_entities_per_view.get(&view.id) else {
77+
// This should not happen in normal operation - if it does, something is wrong with the frame setup
78+
re_log::error_once!(
79+
"visualizable_entities not found for view {:?} - this is a bug",
80+
view.id
81+
);
82+
return meta_data; // Return empty metadata
83+
};
8384

8485
tree.visit_children_recursively(|entity_path| {
8586
let can_add: CanAddToView =

0 commit comments

Comments
 (0)