1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
use std::any::TypeId;
use std::sync::Arc;
use ahash::HashMap;
use re_byte_size::{MemUsageTree, MemUsageTreeCapture};
use re_chunk_store::ChunkStoreEvent;
use re_entity_db::EntityDb;
use re_log_types::StoreId;
use re_mutex::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::{Cache, CacheEntryAccess};
/// A wrapper around a cache that allows for shared access with its own lock.
///
/// This reduces lock contention by having one lock per cache type
/// instead of a single lock for all caches.
struct SharedCache {
name: &'static str,
cache: RwLock<Box<dyn Cache>>,
}
impl SharedCache {
fn new<C: Cache + Default>() -> Self {
let cache = Box::<C>::default();
Self {
name: cache.name(),
cache: RwLock::new(cache),
}
}
fn read(&self) -> RwLockReadGuard<'_, Box<dyn Cache>> {
self.cache.read()
}
fn write(&self) -> RwLockWriteGuard<'_, Box<dyn Cache>> {
self.cache.write()
}
}
/// Does memoization of different objects for the immediate mode UI.
pub struct Memoizers {
/// The store for which these caches are caching data.
store_id: StoreId,
/// Master map from cache type to the cache itself.
///
/// The master lock is only held briefly to look up or insert a cache.
/// Each cache has its own lock for actual access.
caches: RwLock<HashMap<TypeId, Arc<SharedCache>>>,
/// How much memory we used after the last call to [`Self::purge_memory`].
memory_use_after_last_purge: u64,
}
impl Memoizers {
/// Creates a new instance of [`Memoizers`] associated with a specific store.
pub fn new(store_id: StoreId) -> Self {
Self {
caches: RwLock::new(HashMap::default()),
store_id,
memory_use_after_last_purge: 0,
}
}
/// The store for which these caches are caching data.
pub fn store_id(&self) -> &StoreId {
&self.store_id
}
/// Call once per frame to potentially flush the cache(s).
pub fn begin_frame(&self) {
re_tracing::profile_function!();
#[expect(clippy::iter_over_hash_type)] // order doesn't matter here
for cache in self.caches.read().values() {
re_tracing::profile_scope!(cache.name);
cache.write().begin_frame();
}
}
/// How much memory we used after the last call to [`Self::purge_memory`].
///
/// This is the lower bound on how much memory we need.
///
/// Some caches just cannot shrink below a certain size,
/// and we need to take that into account when budgeting for other things.
pub fn memory_use_after_last_purge(&self) -> u64 {
self.memory_use_after_last_purge
}
/// Returns a memory usage tree containing only GPU memory (VRAM) usage.
pub fn vram_usage(&self) -> MemUsageTree {
re_tracing::profile_function!();
let mut node = re_byte_size::MemUsageNode::new();
let mut cache_vram: Vec<_> = self
.caches
.read()
.values()
.map(|cache| (cache.name, cache.read().vram_usage()))
.collect();
cache_vram.sort_by_key(|(cache_name, _)| *cache_name);
for (cache_name, vram_tree) in cache_vram {
node.add(cache_name, vram_tree);
}
node.into_tree()
}
/// Attempt to free up memory.
pub fn purge_memory(&mut self) {
re_tracing::profile_function!();
#[expect(clippy::iter_over_hash_type)] // order doesn't matter here
for cache in self.caches.read().values() {
re_tracing::profile_scope!(cache.name);
cache.write().purge_memory();
}
self.memory_use_after_last_purge = self.capture_mem_usage_tree().size_bytes();
}
/// React to the chunk store's changelog, if needed.
///
/// Useful to e.g. invalidate unreachable data.
pub fn on_store_events(&self, events: &[ChunkStoreEvent], entity_db: &EntityDb) {
re_tracing::profile_function!();
let relevant_events = events
.iter()
.filter(|event| event.store_id == self.store_id)
.collect::<Vec<_>>();
if relevant_events.is_empty() {
return;
}
#[expect(clippy::iter_over_hash_type)] // order doesn't matter here
for cache in self.caches.read().values() {
re_tracing::profile_scope!(cache.name);
cache.write().on_store_events(&relevant_events, entity_db);
}
}
/// Gets or creates a shared cache for the given type.
fn shared_cache_entry<C: Cache + Default>(&self) -> Arc<SharedCache> {
let type_id = TypeId::of::<C>();
if let Some(cache) = {
re_tracing::profile_wait!("master-cache-read-lock");
// Do NOT use an upgradable read lock here, because `parking_lot` permits only one upgradable reader at a time (see https://docs.rs/lock_api/latest/lock_api/struct.RwLock.html#method.upgradable_read).
// The drawback is that multiple concurrent misses may go to the `write` path, but we expect this to be much less common.
// (and the only reason the write path is worse is that it breaks concurrent access)
self.caches.read().get(&type_id).cloned()
} {
return cache;
}
let mut caches = {
re_tracing::profile_wait!("master-cache-write-lock");
self.caches.write()
};
caches
.entry(type_id)
.or_insert_with(|| Arc::new(SharedCache::new::<C>()))
.clone()
}
/// Accesses a cache for reading and writing.
///
/// Adds the cache lazily if it wasn't already there.
pub fn entry<C: Cache + Default, R>(&self, f: impl FnOnce(&mut C) -> R) -> R {
let shared_cache = self.shared_cache_entry::<C>();
// Now lock only this specific cache.
let mut cache_guard = {
re_tracing::profile_wait!("cache-write-lock", shared_cache.name);
shared_cache.write()
};
let cache = cache_guard.as_mut();
f((cache as &mut dyn std::any::Any)
.downcast_mut::<C>()
.expect(
"Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.",
))
}
/// Accesses an existing cache for reading.
///
/// Returns `None` if the cache has not been created yet.
pub fn read<C: Cache, R>(&self, f: impl FnOnce(&C) -> R) -> Option<R> {
let shared_cache = {
re_tracing::profile_wait!("master-cache-read-lock");
let guard = self.caches.read();
guard.get(&TypeId::of::<C>()).cloned()
}?;
let cache_guard = {
re_tracing::profile_wait!("cache-read-lock", shared_cache.name);
shared_cache.read()
};
let cache = cache_guard.as_ref();
Some(f((cache as &dyn std::any::Any).downcast_ref::<C>().expect(
"Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.",
)))
}
/// Tries to read an existing memoization cache entry, then computes it through mutable access on miss.
///
/// Use this if you're working with init-only cache entries, expect your cache entry to be usually present
/// and want cache hits to share a read lock.
/// The miss path releases the read lock, takes a write lock, and checks the entry again before computing.
/// This adds overhead compared to `memoizer`, so don't use this if you expect many misses!
/// (UI code typically doesn't need to care about this optimization, since it's usually single-threaded already.)
pub fn read_or_compute<C: CacheEntryAccess<Key, Value> + Default, Key, Value>(
&self,
key: &Key,
) -> Value {
let cache_entry = self.shared_cache_entry::<C>();
// Cache hits should remain concurrent, which rules out an upgradable read lock here!
// (`parking_lot` permits only one upgradable reader at a time see https://docs.rs/lock_api/latest/lock_api/struct.RwLock.html#method.upgradable_read)
{
let cache = {
re_tracing::profile_wait!("cache-read-lock");
cache_entry.read()
};
let cache_accessor = (cache.as_ref() as &dyn std::any::Any)
.downcast_ref::<C>()
.expect(
"Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.",
);
if let Some(value) = cache_accessor.read(key) {
return value;
}
}
let mut cache = {
re_tracing::profile_wait!("cache-write-lock");
cache_entry.write()
};
let cache_accessor = (cache.as_mut() as &mut dyn std::any::Any)
.downcast_mut::<C>()
.expect(
"Downcast failed, this indicates a bug in how `Memoizers` adds new cache types.",
);
// Another thread may have populated the entry between releasing the read lock and taking the write lock.
if let Some(value) = cache_accessor.read(key) {
value
} else {
cache_accessor.compute(key)
}
}
}
impl MemUsageTreeCapture for Memoizers {
fn capture_mem_usage_tree(&self) -> MemUsageTree {
re_tracing::profile_function!();
let mut node = re_byte_size::MemUsageNode::new();
let mut cache_trees: Vec<_> = self
.caches
.read()
.values()
.map(|cache| (cache.name, cache.read().capture_mem_usage_tree()))
.collect();
cache_trees.sort_by_key(|(cache_name, _)| *cache_name);
for (cache_name, tree) in cache_trees {
node.add(cache_name, tree);
}
node.into_tree()
}
}