ifc_model/lazy.rs
1//! Lazily decoded entities: a slot per id, filled on first access.
2//!
3//! # Why
4//!
5//! Decoding every attribute of every record into owned `Value`s is most of
6//! the cost of reading a file: on real IFC exports the allocator alone was
7//! 41% of the read and `Model::insert` another 12%. A viewer that opens a
8//! file to show a type census, or the properties of a few elements, pays for
9//! millions of values it never looks at.
10//!
11//! A codec may instead register each entity as a byte span of its source
12//! plus its type name, and hand the model an [`EntitySource`] that decodes a
13//! span on demand. [`Model::get`](crate::Model::get) then decodes an entity
14//! the first time anyone asks for it and keeps the result, so every later
15//! access is a plain lookup and the returned reference is stable.
16//!
17//! # Contract
18//!
19//! The codec validates every record when it builds the model -- syntax and
20//! every conversion that could fail -- so decoding a registered span cannot
21//! fail while the source is unchanged. A source that changes underneath the
22//! model (a memory-mapped file edited by another process) breaks that
23//! promise; [`EntitySource::decode`] then panics with a message saying so
24//! rather than returning a different entity.
25
26use std::ops::Range;
27use std::sync::OnceLock;
28
29use crate::entity::Entity;
30
31/// Decodes the entities of a lazily loaded [`Model`](crate::Model).
32///
33/// Implemented by codecs; the model never knows what format a span is in.
34pub trait EntitySource: Send + Sync + std::fmt::Debug {
35 /// Decodes the entity whose record occupies `span` of the source.
36 ///
37 /// # Panics
38 ///
39 /// When the record no longer decodes, which means the source changed
40 /// after the codec validated it. A codec must never register a span it
41 /// has not validated.
42 fn decode(&self, span: Range<usize>) -> Entity;
43}
44
45/// One entity: decoded already, or a span still to decode.
46#[derive(Debug, Clone)]
47pub(crate) struct Slot {
48 entity: OnceLock<Entity>,
49 /// The record's bytes in the model's source; empty for an entity that
50 /// was inserted decoded.
51 span: Range<usize>,
52}
53
54impl Slot {
55 pub(crate) fn decoded(entity: Entity) -> Self {
56 Self {
57 entity: OnceLock::from(entity),
58 span: 0..0,
59 }
60 }
61
62 pub(crate) fn lazy(span: Range<usize>) -> Self {
63 Self {
64 entity: OnceLock::new(),
65 span,
66 }
67 }
68
69 /// The entity, decoding it on first access.
70 pub(crate) fn get(&self, source: Option<&dyn EntitySource>) -> &Entity {
71 self.entity.get_or_init(|| {
72 source
73 .expect("a lazy slot exists only in a model with a source")
74 .decode(self.span.clone())
75 })
76 }
77
78 /// The entity for editing, decoding it first when needed.
79 pub(crate) fn get_mut(&mut self, source: Option<&dyn EntitySource>) -> &mut Entity {
80 self.get(source);
81 self.entity
82 .get_mut()
83 .expect("`get` just initialised the slot")
84 }
85
86 /// The entity by value, decoding it first when needed.
87 pub(crate) fn into_entity(self, source: Option<&dyn EntitySource>) -> Entity {
88 self.get(source);
89 self.entity
90 .into_inner()
91 .expect("`get` just initialised the slot")
92 }
93
94 /// Whether the entity has been decoded.
95 pub(crate) fn is_decoded(&self) -> bool {
96 self.entity.get().is_some()
97 }
98}