Skip to main content

microcad_lang/render/
cache.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Render cache.
5
6use crate::render::GeometryOutput;
7use microcad_core::hash::{HashId, HashMap};
8
9/// An item in the [`RenderCache`].
10pub struct RenderCacheItem<T> {
11    /// The actual item content.
12    content: T,
13    /// Number of times this cache item has been accessed successfully.
14    hits: u64,
15    /// Number of milliseconds this item took to create.
16    millis: f64,
17    /// Time stamp of the last access to this cache item.
18    last_access: u64,
19}
20
21impl<T> RenderCacheItem<T> {
22    /// Create new cache item.
23    pub fn new(content: impl Into<T>, millis: f64, last_access: u64) -> Self {
24        Self {
25            content: content.into(),
26            hits: 1,
27            millis,
28            last_access,
29        }
30    }
31
32    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<T = GeometryOutput> {
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: HashMap<HashId, RenderCacheItem<T>>,
63}
64
65impl<T> RenderCache<T> {
66    /// Create a new empty cache.
67    pub fn new() -> Self {
68        Self {
69            current_time_stamp: 0,
70            hits: 0,
71            items: HashMap::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::info!(
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<&T> {
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<T>,
129        cost: f64,
130    ) -> &T {
131        let hash: HashId = hash.into();
132        self.items.insert(
133            hash,
134            RenderCacheItem::new(geo, cost, self.current_time_stamp),
135        );
136        &self.items.get(&hash).expect("Hash").content
137    }
138}
139
140impl Default for RenderCache {
141    fn default() -> Self {
142        Self::new()
143    }
144}