1use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
18
19use crate::tag::{MemTag, Realm};
20
21struct Cell {
24 bytes: AtomicU64,
25 peak: AtomicU64,
26 budget: AtomicU64,
27}
28
29impl Cell {
30 const fn new() -> Self {
31 Self {
32 bytes: AtomicU64::new(0),
33 peak: AtomicU64::new(0),
34 budget: AtomicU64::new(0),
35 }
36 }
37
38 fn observe(&self, bytes: u64) {
39 self.peak.fetch_max(bytes, Relaxed);
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct TagUsage {
46 pub tag: MemTag,
47 pub realm: Realm,
48 pub bytes: u64,
49 pub peak_bytes: u64,
51 pub budget: Option<u64>,
53}
54
55impl TagUsage {
56 pub const fn empty(tag: MemTag, realm: Realm) -> Self {
60 Self {
61 tag,
62 realm,
63 bytes: 0,
64 peak_bytes: 0,
65 budget: None,
66 }
67 }
68
69 pub(crate) fn is_reported(&self) -> bool {
70 self.bytes > 0 || self.peak_bytes > 0 || self.budget.is_some()
71 }
72
73 pub fn over_budget(&self) -> bool {
74 self.budget.is_some_and(|b| self.bytes > b)
75 }
76
77 pub fn fraction(&self) -> Option<f32> {
79 self.budget
80 .filter(|&b| b > 0)
81 .map(|b| self.bytes as f32 / b as f32)
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct LedgerSnapshot {
88 entries: [TagUsage; MemTag::COUNT * Realm::COUNT],
89}
90
91impl LedgerSnapshot {
92 pub fn get(&self, tag: MemTag, realm: Realm) -> TagUsage {
94 self.entries[entry_index(tag, realm)]
95 }
96
97 pub fn reported(&self, realm: Realm) -> impl Iterator<Item = TagUsage> + '_ {
100 MemTag::ALL
101 .into_iter()
102 .map(move |tag| self.get(tag, realm))
103 .filter(TagUsage::is_reported)
104 }
105
106 pub fn realm_bytes(&self, realm: Realm) -> u64 {
110 MemTag::ALL
111 .into_iter()
112 .map(|tag| self.get(tag, realm).bytes)
113 .sum()
114 }
115
116 pub fn is_empty(&self) -> bool {
118 !self.entries.iter().any(TagUsage::is_reported)
119 }
120}
121
122impl Default for LedgerSnapshot {
123 fn default() -> Self {
124 Self {
125 entries: core::array::from_fn(|i| {
126 TagUsage::empty(MemTag::ALL[i / Realm::COUNT], Realm::ALL[i % Realm::COUNT])
127 }),
128 }
129 }
130}
131
132const fn entry_index(tag: MemTag, realm: Realm) -> usize {
133 tag.index() * Realm::COUNT + realm.index()
134}
135
136pub struct Ledger {
140 cells: [Cell; MemTag::COUNT * Realm::COUNT],
141}
142
143impl Ledger {
144 pub const fn new() -> Self {
146 Self {
147 cells: [const { Cell::new() }; MemTag::COUNT * Realm::COUNT],
148 }
149 }
150
151 fn cell(&self, tag: MemTag, realm: Realm) -> &Cell {
152 &self.cells[entry_index(tag, realm)]
153 }
154
155 pub fn add(&self, tag: MemTag, realm: Realm, bytes: u64) {
158 let cell = self.cell(tag, realm);
159 let total = cell.bytes.fetch_add(bytes, Relaxed).saturating_add(bytes);
160 cell.observe(total);
161 }
162
163 pub fn release(&self, tag: MemTag, realm: Realm, bytes: u64) {
167 let cell = self.cell(tag, realm);
168 let _ = cell
169 .bytes
170 .fetch_update(Relaxed, Relaxed, |held| Some(held.saturating_sub(bytes)));
171 }
172
173 pub fn set(&self, tag: MemTag, realm: Realm, bytes: u64) {
177 let cell = self.cell(tag, realm);
178 cell.bytes.store(bytes, Relaxed);
179 cell.observe(bytes);
180 }
181
182 pub fn set_budget(&self, tag: MemTag, realm: Realm, budget: Option<u64>) {
184 self.cell(tag, realm)
185 .budget
186 .store(budget.unwrap_or(0), Relaxed);
187 }
188
189 pub fn usage(&self, tag: MemTag, realm: Realm) -> TagUsage {
191 let cell = self.cell(tag, realm);
192 let budget = cell.budget.load(Relaxed);
193 TagUsage {
194 tag,
195 realm,
196 bytes: cell.bytes.load(Relaxed),
197 peak_bytes: cell.peak.load(Relaxed),
198 budget: (budget > 0).then_some(budget),
199 }
200 }
201
202 pub fn snapshot(&self) -> LedgerSnapshot {
204 LedgerSnapshot {
205 entries: core::array::from_fn(|i| {
206 self.usage(MemTag::ALL[i / Realm::COUNT], Realm::ALL[i % Realm::COUNT])
207 }),
208 }
209 }
210
211 pub fn clear(&self) {
214 for cell in &self.cells {
215 cell.bytes.store(0, Relaxed);
216 cell.peak.store(0, Relaxed);
217 cell.budget.store(0, Relaxed);
218 }
219 }
220}
221
222impl Default for Ledger {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn an_untouched_ledger_reports_nothing() {
234 let ledger = Ledger::new();
235 let snap = ledger.snapshot();
236 assert!(snap.is_empty());
237 assert_eq!(snap.reported(Realm::Host).count(), 0);
238 assert_eq!(snap.realm_bytes(Realm::Device), 0);
239 }
240
241 #[test]
242 fn increments_accumulate_and_release_gives_them_back() {
243 let ledger = Ledger::new();
244 ledger.add(MemTag::Audio, Realm::Host, 1024);
245 ledger.add(MemTag::Audio, Realm::Host, 512);
246 assert_eq!(ledger.usage(MemTag::Audio, Realm::Host).bytes, 1536);
247
248 ledger.release(MemTag::Audio, Realm::Host, 512);
249 assert_eq!(ledger.usage(MemTag::Audio, Realm::Host).bytes, 1024);
250 }
251
252 #[test]
255 fn releasing_more_than_held_saturates_at_zero() {
256 let ledger = Ledger::new();
257 ledger.add(MemTag::Meshes, Realm::Device, 100);
258 ledger.release(MemTag::Meshes, Realm::Device, 4096);
259 assert_eq!(ledger.usage(MemTag::Meshes, Realm::Device).bytes, 0);
260 }
261
262 #[test]
265 fn set_replaces_rather_than_accumulates() {
266 let ledger = Ledger::new();
267 ledger.set(MemTag::Textures, Realm::Device, 8_000);
268 ledger.set(MemTag::Textures, Realm::Device, 6_000);
269 assert_eq!(ledger.usage(MemTag::Textures, Realm::Device).bytes, 6_000);
270 }
271
272 #[test]
273 fn peak_holds_the_high_water_mark_across_both_report_styles() {
274 let ledger = Ledger::new();
275 ledger.set(MemTag::Textures, Realm::Device, 9_000);
276 ledger.set(MemTag::Textures, Realm::Device, 1_000);
277 ledger.add(MemTag::Textures, Realm::Device, 500);
278
279 let usage = ledger.usage(MemTag::Textures, Realm::Device);
280 assert_eq!(usage.bytes, 1_500);
281 assert_eq!(usage.peak_bytes, 9_000);
282 }
283
284 #[test]
285 fn a_budget_reads_back_and_clears() {
286 let ledger = Ledger::new();
287 ledger.set_budget(MemTag::Chunks, Realm::Device, Some(4_096));
288 assert_eq!(
289 ledger.usage(MemTag::Chunks, Realm::Device).budget,
290 Some(4_096)
291 );
292
293 ledger.set_budget(MemTag::Chunks, Realm::Device, None);
294 assert_eq!(ledger.usage(MemTag::Chunks, Realm::Device).budget, None);
295 }
296
297 #[test]
298 fn over_budget_and_fraction_track_the_ceiling() {
299 let ledger = Ledger::new();
300 ledger.set_budget(MemTag::Meshes, Realm::Device, Some(1_000));
301 ledger.set(MemTag::Meshes, Realm::Device, 500);
302 let under = ledger.usage(MemTag::Meshes, Realm::Device);
303 assert!(!under.over_budget());
304 assert_eq!(under.fraction(), Some(0.5));
305
306 ledger.set(MemTag::Meshes, Realm::Device, 1_200);
307 assert!(ledger.usage(MemTag::Meshes, Realm::Device).over_budget());
308 }
309
310 #[test]
313 fn an_unbudgeted_tag_has_no_fraction() {
314 let ledger = Ledger::new();
315 ledger.set(MemTag::Scratch, Realm::Host, 4_096);
316 let usage = ledger.usage(MemTag::Scratch, Realm::Host);
317 assert_eq!(usage.fraction(), None);
318 assert!(!usage.over_budget());
319 }
320
321 #[test]
324 fn the_realms_are_counted_separately() {
325 let ledger = Ledger::new();
326 ledger.set(MemTag::Textures, Realm::Device, 2_048);
327 ledger.set(MemTag::Textures, Realm::Host, 64);
328
329 let snap = ledger.snapshot();
330 assert_eq!(snap.get(MemTag::Textures, Realm::Device).bytes, 2_048);
331 assert_eq!(snap.get(MemTag::Textures, Realm::Host).bytes, 64);
332 assert_eq!(snap.realm_bytes(Realm::Device), 2_048);
333 assert_eq!(snap.realm_bytes(Realm::Host), 64);
334 }
335
336 #[test]
339 fn reported_lists_touched_tags_in_vocabulary_order() {
340 let ledger = Ledger::new();
341 ledger.set(MemTag::Chunks, Realm::Device, 3);
342 ledger.set(MemTag::Textures, Realm::Device, 1);
343 ledger.set(MemTag::Meshes, Realm::Device, 2);
344 ledger.set(MemTag::Audio, Realm::Host, 9);
345
346 let tags: std::vec::Vec<MemTag> = ledger
347 .snapshot()
348 .reported(Realm::Device)
349 .map(|u| u.tag)
350 .collect();
351 assert_eq!(tags, [MemTag::Textures, MemTag::Meshes, MemTag::Chunks]);
352 }
353
354 #[test]
357 fn a_tag_that_dropped_to_zero_stays_reported() {
358 let ledger = Ledger::new();
359 ledger.set(MemTag::Textures, Realm::Device, 4_096);
360 ledger.set(MemTag::Textures, Realm::Device, 0);
361
362 let snap = ledger.snapshot();
363 assert!(snap.get(MemTag::Textures, Realm::Device).is_reported());
364 assert_eq!(snap.reported(Realm::Device).count(), 1);
365 }
366
367 #[test]
368 fn clear_drops_every_report_and_budget() {
369 let ledger = Ledger::new();
370 ledger.set(MemTag::Textures, Realm::Device, 4_096);
371 ledger.set_budget(MemTag::Textures, Realm::Device, Some(8_192));
372 ledger.clear();
373
374 assert!(ledger.snapshot().is_empty());
375 assert_eq!(ledger.usage(MemTag::Textures, Realm::Device).peak_bytes, 0);
376 }
377
378 #[test]
381 fn every_tag_and_realm_addresses_its_own_cell() {
382 let ledger = Ledger::new();
383 for (i, tag) in MemTag::ALL.into_iter().enumerate() {
384 for (j, realm) in Realm::ALL.into_iter().enumerate() {
385 ledger.set(tag, realm, (i * Realm::COUNT + j + 1) as u64);
386 }
387 }
388 let snap = ledger.snapshot();
389 for (i, tag) in MemTag::ALL.into_iter().enumerate() {
390 for (j, realm) in Realm::ALL.into_iter().enumerate() {
391 let usage = snap.get(tag, realm);
392 assert_eq!(usage.bytes, (i * Realm::COUNT + j + 1) as u64);
393 assert_eq!(usage.tag, tag);
394 assert_eq!(usage.realm, realm);
395 }
396 }
397 }
398}