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