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