Skip to main content

ifc_lite_core/decoder/
caches.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! `EntityDecoder`'s cache management.
6//!
7//! Split from `decoder.rs` under the house rule (AGENTS.md). These are one
8//! cohesive concern — what is retained, what is handed across rayon workers,
9//! and what a caller may drop to bound memory — and they read better together
10//! than scattered among the decode paths that populate them.
11
12use rustc_hash::FxHashMap;
13use std::sync::Arc;
14
15use super::EntityDecoder;
16use crate::DecodedEntity;
17
18impl EntityDecoder<'_> {
19    /// #3987: validate an indexed record fully, but materialize only a string
20    /// attribute. Discovery must not allocate discarded property/reference trees.
21    /// Honor both requested-id and parsed-id memoized values without adding any.
22    pub(crate) fn decode_string_by_id_transient(
23        &mut self, id: u32, attribute: usize,
24    ) -> crate::Result<Option<String>> {
25        if let Some(entity) = self.cache.get(&id) {
26            return Ok(entity.get_string(attribute).map(str::to_owned));
27        }
28        self.build_index();
29        let (start, end) = self.entity_index.as_ref().and_then(|index| index.lookup(id))
30            .ok_or_else(|| crate::Error::parse(0, format!("Entity #{} not found", id)))?;
31        // The SAME parser checks unused fields too. A malformed tail must fail
32        // even when Name was already readable or the parsed id is memoized.
33        let (parsed_id, _, tokens) = self.parse_at(start, end, "decode_at_uncached")?;
34        if let Some(entity) = self.cache.get(&parsed_id) {
35            return Ok(entity.get_string(attribute).map(str::to_owned));
36        }
37        Ok(match tokens.get(attribute) {
38            Some(token @ crate::parser::Token::String(_)) => {
39                match crate::AttributeValue::from_token(token) {
40                    crate::AttributeValue::String(value) => Some(value),
41                    _ => None,
42                }
43            }
44            _ => None,
45        })
46    }
47
48    /// Drain the populated cache out of this decoder for sharing across
49    /// rayon tasks. After calling this, the decoder is empty (cache
50    /// moved out); callers typically then drop the decoder.
51    pub fn drain_cache(&mut self) -> FxHashMap<u32, Arc<DecodedEntity>> {
52        std::mem::take(&mut self.cache)
53    }
54
55    /// Clear all caches to free memory
56    pub fn clear_cache(&mut self) {
57        self.cache.clear();
58        self.point_cache.clear();
59        self.placement_transform_cache.clear();
60    }
61
62    /// Clear only the decoded-entity cache, keeping the placement memo.
63    ///
64    /// For a caller that drains on a size trigger while resolving placements.
65    /// The two caches have very different value per byte: an entry in the
66    /// entity cache saves one re-decode of one entity, while an entry in the
67    /// placement memo can save re-walking a chain that thousands of elements
68    /// share — a site or building transform is resolved once and read by every
69    /// product under it. Dropping the memo to bound the entity cache trades the
70    /// expensive one away to bound the cheap one, and does it invisibly: output
71    /// stays correct and large models simply get slower.
72    pub fn clear_entity_cache(&mut self) {
73        self.cache.clear();
74    }
75
76    /// Clear only the point coordinate cache (used after BREP preprocessing).
77    /// The entity cache is preserved for subsequent geometry processing.
78    pub fn clear_point_cache(&mut self) {
79        self.point_cache.clear();
80    }
81
82    /// Move the CartesianPoint coordinate cache OUT of this decoder, leaving it
83    /// empty. Paired with [`Self::set_point_cache`] to hoist the cache across the
84    /// per-element decoders a single worker builds within one batch: the cache is
85    /// pure memoization of `content` + point id -> coords, so reusing it across
86    /// elements is byte-identical (speed only). Cheap: moves the map header, not
87    /// its contents.
88    pub fn take_point_cache(&mut self) -> FxHashMap<u32, (f64, f64, f64)> {
89        std::mem::take(&mut self.point_cache)
90    }
91
92    /// Install a previously-accumulated point cache (see [`Self::take_point_cache`]).
93    /// Does not reset the hit/miss counters, which stay per-decoder so each job's
94    /// [`Self::point_cache_stats`] reflect only that job's activity.
95    pub fn set_point_cache(&mut self, cache: FxHashMap<u32, (f64, f64, f64)>) {
96        self.point_cache = cache;
97    }
98
99    /// `(hits, misses)` served by [`Self::get_polyloop_coords_cached`] over this
100    /// decoder's lifetime. A hit is a CartesianPoint served from the cache; a miss
101    /// is one parsed for the first time. Non-zero hits after processing more than
102    /// one faceted part with a shared point list prove cross-element memoization.
103    pub fn point_cache_stats(&self) -> (u64, u64) {
104        (self.point_cache_hits, self.point_cache_misses)
105    }
106
107    /// Move the placement-transform memo OUT of this decoder, leaving it empty.
108    /// Paired with [`Self::set_placement_transform_cache`] to hoist the cache
109    /// across the per-element decoders a single worker builds within one batch,
110    /// exactly like [`Self::take_point_cache`]. The memo is a pure function of
111    /// `content` + placement id (deterministic `parent * local` composition), so
112    /// reusing it across elements is byte-identical (speed only). Cheap: moves
113    /// the map header, not its contents.
114    pub fn take_placement_transform_cache(&mut self) -> FxHashMap<u32, [f64; 16]> {
115        std::mem::take(&mut self.placement_transform_cache)
116    }
117
118    /// Install a previously-accumulated placement-transform memo (see
119    /// [`Self::take_placement_transform_cache`]).
120    pub fn set_placement_transform_cache(&mut self, cache: FxHashMap<u32, [f64; 16]>) {
121        self.placement_transform_cache = cache;
122    }
123
124    /// Read a memoized placement world transform by placement id. Returns a copy
125    /// (`[f64; 16]` is `Copy`) so the caller can drop the borrow before
126    /// reconstructing its `Matrix4`. The array is the opaque column-major layout
127    /// written by [`Self::cache_placement_transform`].
128    pub fn get_placement_transform_cached(&self, id: u32) -> Option<[f64; 16]> {
129        self.placement_transform_cache.get(&id).copied()
130    }
131
132    /// Memoize a placement world transform under its placement id.
133    ///
134    /// This is an unconditional insert, and last write wins. It does not
135    /// validate `transform`, does not compare it against any entry already held
136    /// for `id`, and has no way to tell a complete world transform from a
137    /// partial one — nothing in this crate computes placement transforms, so
138    /// nothing here can.
139    ///
140    /// Whether the memo is a pure function of the placement id is therefore a
141    /// property of the CALLERS, not of this method. The geometry router is the
142    /// one that owes it: it stores only fully composed local/linear/grid
143    /// transforms, and in particular never stores one composed from a walk its
144    /// depth guard cut short, because what such a walk composed depends on the
145    /// depth it was entered at rather than on the placement id (#3012). A caller
146    /// that breaks that discipline does not fail here — it makes whichever
147    /// reader happens to query the id first decide the answer for every reader
148    /// after it, including across workers via
149    /// [`Self::take_placement_transform_cache`].
150    pub fn cache_placement_transform(&mut self, id: u32, transform: [f64; 16]) {
151        self.placement_transform_cache.insert(id, transform);
152    }
153
154    /// Get cache size
155    pub fn cache_size(&self) -> usize {
156        self.cache.len()
157    }
158}
159
160#[cfg(test)]
161#[path = "transient_projection_tests.rs"]
162mod transient_projection_tests;