microcad_lang/render/
cache.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Render cache.
5
6use crate::render::{GeometryOutput, HashId};
7
8/// An item in the [`RenderCache`].
9pub struct RenderCacheItem {
10    /// The actual item content.
11    content: GeometryOutput,
12    /// Number of times this cache item has been accessed successfully.
13    hits: u64,
14    /// Number of milliseconds this item took to create.
15    millis: f64,
16    /// Time stamp of the last access to this cache item.
17    last_access: u64,
18}
19
20impl RenderCacheItem {
21    /// Create new cache item.
22    pub fn new(content: impl Into<GeometryOutput>, millis: f64, last_access: u64) -> Self {
23        Self {
24            content: content.into(),
25            hits: 1,
26            millis,
27            last_access,
28        }
29    }
30
31    /// The cost of this cache item.
32    pub fn cost(&self, current_time_stamp: u64) -> f64 {
33        // Weighted sum of:
34        // - Recency: more recent items are more valuable
35        // - Frequency: more frequently accessed items are more valuable
36        // - Computation cost: items that are expensive to regenerate are more valuable
37
38        let recency = 1.0 / (1.0 + (current_time_stamp - self.last_access) as f64);
39        let frequency = self.hits as f64;
40        let computation_cost = self.millis;
41
42        // We can tune these weights.
43        let weight_recency = 2.3;
44        let weight_frequency = 0.5;
45        let weight_computation = 0.2;
46
47        (weight_recency * recency)
48            + (weight_frequency * frequency)
49            + (weight_computation * computation_cost)
50    }
51}
52
53/// The [`RenderCache`] owns all geometry created during the render process.
54pub struct RenderCache {
55    /// Current render cache item stamp.
56    current_time_stamp: u64,
57    /// Number of cache hits in this cycle.
58    hits: u64,
59    /// Maximum cost of a cache item before it is removed during garbage collection.
60    max_cost: f64,
61    /// The actual cache item store.
62    items: rustc_hash::FxHashMap<HashId, RenderCacheItem>,
63}
64
65impl RenderCache {
66    /// Create a new empty cache.
67    pub fn new() -> Self {
68        Self {
69            current_time_stamp: 0,
70            hits: 0,
71            items: Default::default(),
72            max_cost: std::env::var("MICROCAD_CACHE_MAX_COST")
73                .ok()
74                .and_then(|s| s.parse::<f64>().ok())
75                .unwrap_or(1.2),
76        }
77    }
78
79    /// Remove old items based on a cost function from the cache.
80    pub fn garbage_collection(&mut self) {
81        let old_count = self.items.len();
82        self.items.retain(|hash, item| {
83            let cost = item.cost(self.current_time_stamp);
84            let keep = cost > self.max_cost;
85            log::trace!(
86                "Item {hash:X} cost = {cost}: {keep}",
87                keep = if keep { "🔄" } else { "🗑" }
88            );
89            keep
90        });
91
92        let removed = old_count - self.items.len();
93        log::debug!(
94            "Removed {removed} items from cache. Cache contains {n} items. {hits} cache hits in this cycle.",
95            n = self.items.len(),
96            hits = self.hits,
97        );
98        self.current_time_stamp += 1;
99        self.hits = 0;
100    }
101
102    /// Empty cache entirely.
103    pub fn clear(&mut self) {
104        self.items.clear();
105    }
106
107    /// Get geometry output from the cache.
108    pub fn get(&mut self, hash: &HashId) -> Option<&GeometryOutput> {
109        match self.items.get_mut(hash) {
110            Some(item) => {
111                item.hits += 1;
112                self.hits += 1;
113                item.last_access = self.current_time_stamp;
114                log::trace!(
115                    "Cache hit: {hash:X}. Cost: {}",
116                    item.cost(self.current_time_stamp)
117                );
118                Some(&item.content)
119            }
120            _ => None,
121        }
122    }
123
124    /// Insert geometry output into the cache with pre-estimated cost and return inserted geometry.
125    pub fn insert_with_cost(
126        &mut self,
127        hash: impl Into<HashId>,
128        geo: impl Into<GeometryOutput>,
129        cost: f64,
130    ) -> GeometryOutput {
131        let hash: HashId = hash.into();
132        let geo: GeometryOutput = geo.into();
133        self.items.insert(
134            hash,
135            RenderCacheItem::new(geo.clone(), cost, self.current_time_stamp),
136        );
137        geo
138    }
139}
140
141impl Default for RenderCache {
142    fn default() -> Self {
143        Self::new()
144    }
145}