lgui-core 0.2.0

Platform-neutral runtime and UI primitives for LGUI
Documentation
use std::{collections::HashMap, hash::Hash};

use super::{CacheTelemetry, CacheUsage, ResourceClass};

#[doc(hidden)]
pub struct LruCache<K, V> {
    entries: HashMap<K, Entry<V>>,
    bytes: usize,
    budget_bytes: usize,
    tick: u64,
    hits: u64,
    misses: u64,
    evictions: u64,
    class: ResourceClass,
    telemetry: CacheTelemetry,
}

struct Entry<V> {
    value: V,
    bytes: usize,
    last_used: u64,
}

impl<K, V> LruCache<K, V>
where
    K: Clone + Eq + Hash,
{
    pub fn new(budget_bytes: usize, class: ResourceClass, telemetry: CacheTelemetry) -> Self {
        let cache = Self {
            entries: HashMap::new(),
            bytes: 0,
            budget_bytes,
            tick: 0,
            hits: 0,
            misses: 0,
            evictions: 0,
            class,
            telemetry,
        };
        cache.publish();
        cache
    }

    pub fn contains_touch(&mut self, key: &K) -> bool {
        self.tick = self.tick.wrapping_add(1);
        let present = if let Some(entry) = self.entries.get_mut(key) {
            entry.last_used = self.tick;
            self.hits = self.hits.saturating_add(1);
            true
        } else {
            self.misses = self.misses.saturating_add(1);
            false
        };
        self.publish();
        present
    }

    pub fn get(&self, key: &K) -> Option<&V> {
        self.entries.get(key).map(|entry| &entry.value)
    }

    pub fn insert(&mut self, key: K, value: V, bytes: usize) -> bool {
        if bytes > self.budget_bytes {
            self.publish();
            return false;
        }
        if let Some(previous) = self.entries.remove(&key) {
            self.bytes = self.bytes.saturating_sub(previous.bytes);
        }
        self.tick = self.tick.wrapping_add(1);
        self.bytes = self.bytes.saturating_add(bytes);
        self.entries.insert(
            key,
            Entry {
                value,
                bytes,
                last_used: self.tick,
            },
        );
        self.evict_to(self.budget_bytes);
        true
    }

    pub fn can_store(&self, bytes: usize) -> bool {
        bytes <= self.budget_bytes
    }

    pub fn clear(&mut self) {
        self.evictions = self.evictions.saturating_add(self.entries.len() as u64);
        self.entries.clear();
        self.bytes = 0;
        self.publish();
    }

    pub fn trim_to(&mut self, target_bytes: usize) -> usize {
        let before = self.bytes;
        self.evict_to(target_bytes);
        before.saturating_sub(self.bytes)
    }

    pub fn set_budget(&mut self, budget_bytes: usize) {
        self.budget_bytes = budget_bytes;
        self.evict_to(self.budget_bytes);
    }

    pub fn usage(&self) -> CacheUsage {
        let mut usage = CacheUsage {
            cpu_bytes: self.bytes,
            entries: self.entries.len(),
            hits: self.hits,
            misses: self.misses,
            evictions: self.evictions,
            largest_entry_bytes: self
                .entries
                .values()
                .map(|entry| entry.bytes)
                .max()
                .unwrap_or(0),
            ..Default::default()
        };
        match self.class {
            ResourceClass::Live => usage.live_bytes = self.bytes,
            ResourceClass::Rebuildable => usage.rebuildable_bytes = self.bytes,
            ResourceClass::Cache => usage.cache_bytes = self.bytes,
            ResourceClass::Transient => {}
        }
        usage
    }

    fn evict_to(&mut self, target_bytes: usize) {
        while self.bytes > target_bytes {
            let Some(oldest) = self
                .entries
                .iter()
                .min_by_key(|(_, entry)| entry.last_used)
                .map(|(key, _)| key.clone())
            else {
                break;
            };
            if let Some(entry) = self.entries.remove(&oldest) {
                self.bytes = self.bytes.saturating_sub(entry.bytes);
                self.evictions = self.evictions.saturating_add(1);
            }
        }
        self.publish();
    }

    fn publish(&self) {
        self.telemetry.publish(self.usage());
    }
}