use core::sync::atomic::{AtomicU64, Ordering::Relaxed};
use crate::memory::tag::{MemTag, Realm};
struct Cell {
bytes: AtomicU64,
peak: AtomicU64,
budget: AtomicU64,
}
impl Cell {
const fn new() -> Self {
Self {
bytes: AtomicU64::new(0),
peak: AtomicU64::new(0),
budget: AtomicU64::new(0),
}
}
fn observe(&self, bytes: u64) {
self.peak.fetch_max(bytes, Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TagUsage {
pub tag: MemTag,
pub realm: Realm,
pub bytes: u64,
pub peak_bytes: u64,
pub budget: Option<u64>,
}
impl TagUsage {
pub const fn empty(tag: MemTag, realm: Realm) -> Self {
Self {
tag,
realm,
bytes: 0,
peak_bytes: 0,
budget: None,
}
}
pub(crate) fn is_reported(&self) -> bool {
self.bytes > 0 || self.peak_bytes > 0 || self.budget.is_some()
}
pub fn over_budget(&self) -> bool {
self.budget.is_some_and(|b| self.bytes > b)
}
pub fn fraction(&self) -> Option<f32> {
self.budget
.filter(|&b| b > 0)
.map(|b| self.bytes as f32 / b as f32)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LedgerSnapshot {
entries: [TagUsage; MemTag::COUNT * Realm::COUNT],
}
impl LedgerSnapshot {
pub fn get(&self, tag: MemTag, realm: Realm) -> TagUsage {
self.entries[entry_index(tag, realm)]
}
pub fn reported(&self, realm: Realm) -> impl Iterator<Item = TagUsage> + '_ {
MemTag::ALL
.into_iter()
.map(move |tag| self.get(tag, realm))
.filter(TagUsage::is_reported)
}
pub fn realm_bytes(&self, realm: Realm) -> u64 {
MemTag::ALL
.into_iter()
.map(|tag| self.get(tag, realm).bytes)
.sum()
}
pub fn is_empty(&self) -> bool {
!self.entries.iter().any(TagUsage::is_reported)
}
}
impl Default for LedgerSnapshot {
fn default() -> Self {
Self {
entries: core::array::from_fn(|i| {
TagUsage::empty(MemTag::ALL[i / Realm::COUNT], Realm::ALL[i % Realm::COUNT])
}),
}
}
}
const fn entry_index(tag: MemTag, realm: Realm) -> usize {
tag.index() * Realm::COUNT + realm.index()
}
pub struct Ledger {
cells: [Cell; MemTag::COUNT * Realm::COUNT],
}
impl Ledger {
pub const fn new() -> Self {
Self {
cells: [const { Cell::new() }; MemTag::COUNT * Realm::COUNT],
}
}
fn cell(&self, tag: MemTag, realm: Realm) -> &Cell {
&self.cells[entry_index(tag, realm)]
}
pub fn add(&self, tag: MemTag, realm: Realm, bytes: u64) {
let cell = self.cell(tag, realm);
let total = cell.bytes.fetch_add(bytes, Relaxed).saturating_add(bytes);
cell.observe(total);
}
pub fn release(&self, tag: MemTag, realm: Realm, bytes: u64) {
let cell = self.cell(tag, realm);
let _ = cell
.bytes
.fetch_update(Relaxed, Relaxed, |held| Some(held.saturating_sub(bytes)));
}
pub fn set(&self, tag: MemTag, realm: Realm, bytes: u64) {
let cell = self.cell(tag, realm);
cell.bytes.store(bytes, Relaxed);
cell.observe(bytes);
}
pub fn set_budget(&self, tag: MemTag, realm: Realm, budget: Option<u64>) {
self.cell(tag, realm)
.budget
.store(budget.unwrap_or(0), Relaxed);
}
pub fn usage(&self, tag: MemTag, realm: Realm) -> TagUsage {
let cell = self.cell(tag, realm);
let budget = cell.budget.load(Relaxed);
TagUsage {
tag,
realm,
bytes: cell.bytes.load(Relaxed),
peak_bytes: cell.peak.load(Relaxed),
budget: (budget > 0).then_some(budget),
}
}
pub fn snapshot(&self) -> LedgerSnapshot {
LedgerSnapshot {
entries: core::array::from_fn(|i| {
self.usage(MemTag::ALL[i / Realm::COUNT], Realm::ALL[i % Realm::COUNT])
}),
}
}
pub fn clear(&self) {
for cell in &self.cells {
cell.bytes.store(0, Relaxed);
cell.peak.store(0, Relaxed);
cell.budget.store(0, Relaxed);
}
}
}
impl Default for Ledger {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_untouched_ledger_reports_nothing() {
let ledger = Ledger::new();
let snap = ledger.snapshot();
assert!(snap.is_empty());
assert_eq!(snap.reported(Realm::Host).count(), 0);
assert_eq!(snap.realm_bytes(Realm::Device), 0);
}
#[test]
fn increments_accumulate_and_release_gives_them_back() {
let ledger = Ledger::new();
ledger.add(MemTag::Audio, Realm::Host, 1024);
ledger.add(MemTag::Audio, Realm::Host, 512);
assert_eq!(ledger.usage(MemTag::Audio, Realm::Host).bytes, 1536);
ledger.release(MemTag::Audio, Realm::Host, 512);
assert_eq!(ledger.usage(MemTag::Audio, Realm::Host).bytes, 1024);
}
#[test]
fn releasing_more_than_held_saturates_at_zero() {
let ledger = Ledger::new();
ledger.add(MemTag::Meshes, Realm::Device, 100);
ledger.release(MemTag::Meshes, Realm::Device, 4096);
assert_eq!(ledger.usage(MemTag::Meshes, Realm::Device).bytes, 0);
}
#[test]
fn set_replaces_rather_than_accumulates() {
let ledger = Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 8_000);
ledger.set(MemTag::Textures, Realm::Device, 6_000);
assert_eq!(ledger.usage(MemTag::Textures, Realm::Device).bytes, 6_000);
}
#[test]
fn peak_holds_the_high_water_mark_across_both_report_styles() {
let ledger = Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 9_000);
ledger.set(MemTag::Textures, Realm::Device, 1_000);
ledger.add(MemTag::Textures, Realm::Device, 500);
let usage = ledger.usage(MemTag::Textures, Realm::Device);
assert_eq!(usage.bytes, 1_500);
assert_eq!(usage.peak_bytes, 9_000);
}
#[test]
fn a_budget_reads_back_and_clears() {
let ledger = Ledger::new();
ledger.set_budget(MemTag::Chunks, Realm::Device, Some(4_096));
assert_eq!(
ledger.usage(MemTag::Chunks, Realm::Device).budget,
Some(4_096)
);
ledger.set_budget(MemTag::Chunks, Realm::Device, None);
assert_eq!(ledger.usage(MemTag::Chunks, Realm::Device).budget, None);
}
#[test]
fn over_budget_and_fraction_track_the_ceiling() {
let ledger = Ledger::new();
ledger.set_budget(MemTag::Meshes, Realm::Device, Some(1_000));
ledger.set(MemTag::Meshes, Realm::Device, 500);
let under = ledger.usage(MemTag::Meshes, Realm::Device);
assert!(!under.over_budget());
assert_eq!(under.fraction(), Some(0.5));
ledger.set(MemTag::Meshes, Realm::Device, 1_200);
assert!(ledger.usage(MemTag::Meshes, Realm::Device).over_budget());
}
#[test]
fn an_unbudgeted_tag_has_no_fraction() {
let ledger = Ledger::new();
ledger.set(MemTag::Scratch, Realm::Host, 4_096);
let usage = ledger.usage(MemTag::Scratch, Realm::Host);
assert_eq!(usage.fraction(), None);
assert!(!usage.over_budget());
}
#[test]
fn the_realms_are_counted_separately() {
let ledger = Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 2_048);
ledger.set(MemTag::Textures, Realm::Host, 64);
let snap = ledger.snapshot();
assert_eq!(snap.get(MemTag::Textures, Realm::Device).bytes, 2_048);
assert_eq!(snap.get(MemTag::Textures, Realm::Host).bytes, 64);
assert_eq!(snap.realm_bytes(Realm::Device), 2_048);
assert_eq!(snap.realm_bytes(Realm::Host), 64);
}
#[test]
fn reported_lists_touched_tags_in_vocabulary_order() {
let ledger = Ledger::new();
ledger.set(MemTag::Chunks, Realm::Device, 3);
ledger.set(MemTag::Textures, Realm::Device, 1);
ledger.set(MemTag::Meshes, Realm::Device, 2);
ledger.set(MemTag::Audio, Realm::Host, 9);
let tags: std::vec::Vec<MemTag> = ledger
.snapshot()
.reported(Realm::Device)
.map(|u| u.tag)
.collect();
assert_eq!(tags, [MemTag::Textures, MemTag::Meshes, MemTag::Chunks]);
}
#[test]
fn a_tag_that_dropped_to_zero_stays_reported() {
let ledger = Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 4_096);
ledger.set(MemTag::Textures, Realm::Device, 0);
let snap = ledger.snapshot();
assert!(snap.get(MemTag::Textures, Realm::Device).is_reported());
assert_eq!(snap.reported(Realm::Device).count(), 1);
}
#[test]
fn clear_drops_every_report_and_budget() {
let ledger = Ledger::new();
ledger.set(MemTag::Textures, Realm::Device, 4_096);
ledger.set_budget(MemTag::Textures, Realm::Device, Some(8_192));
ledger.clear();
assert!(ledger.snapshot().is_empty());
assert_eq!(ledger.usage(MemTag::Textures, Realm::Device).peak_bytes, 0);
}
#[test]
fn every_tag_and_realm_addresses_its_own_cell() {
let ledger = Ledger::new();
for (i, tag) in MemTag::ALL.into_iter().enumerate() {
for (j, realm) in Realm::ALL.into_iter().enumerate() {
ledger.set(tag, realm, (i * Realm::COUNT + j + 1) as u64);
}
}
let snap = ledger.snapshot();
for (i, tag) in MemTag::ALL.into_iter().enumerate() {
for (j, realm) in Realm::ALL.into_iter().enumerate() {
let usage = snap.get(tag, realm);
assert_eq!(usage.bytes, (i * Realm::COUNT + j + 1) as u64);
assert_eq!(usage.tag, tag);
assert_eq!(usage.realm, realm);
}
}
}
}