ifcparse: lazy loading, opt in with file::lazy_loading() / open(lazy=True)

A lazy open reads the DATA section once with the tokenizer's index
policy and builds what indexes the file: a shell per instance (name and
declaration, no attribute array), the complete inverse index with
attribute indices, the GlobalId map and the by-type lists. No attribute
value is decoded. The first time an instance's attributes are touched,
ensure_loaded() seeks the retained paged reader to the instance and runs
the same load_attributes() the full parse runs, with inverse registration
off, then resolves that instance's references from its own slots. A
modified instance is materialised first, so writing works.

There is no scanner of its own: the index pass consumes next<index_tokens>()
and counts parentheses and commas on the operator tokens; a keyword where
an instance should start, or a token the tokenizer rejects, stops the
index and the file is parsed in full. The offset of each instance's
attribute list is kept in one sorted vector that exists only in lazy
mode, so a full parse pays nothing for it. Materialising from several
threads at once is not safe.

TXG 58 MB / 210_King 147 MB / OKgate22 231 MB, single thread: lazy open
0.61 / 1.73 / 2.86 s against the full parse's 1.05 / 2.69 / 4.99 s, at
141 / 374 / 534 MB against 274 / 654 / 1036 MB; reading one attribute of
every instance afterwards costs a further 0.56 / 1.44 / 4.86 s.

This commit was written by an AI coding tool and has not been verified by
a human.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
This commit is contained in:
Dion Moult
2026-09-14 07:23:22 +10:00
parent 91622b97b3
commit a92bebd73e
9 changed files with 506 additions and 7 deletions
+26
View File
@@ -585,6 +585,25 @@ namespace ifcopenshell {
// on; streaming consumers of references() leave it off.
bool resolve_references_in_place = false;
// Lazy loading (index_lazily): the file was read once through the
// tokenizer's index policy to build the instance shells, the
// inverse index, the GlobalId map and the by-type lists, and each
// instance's attributes are parsed from the retained paged source
// the first time they are accessed (instance_data::ensure_loaded).
// The offset of each instance's attribute list lives here, not in
// the instance, so a full parse pays nothing for it. Inverses were
// registered by the index, so materialisation must not register
// them again. Materialising from several threads at once is not
// safe.
struct lazy_source;
bool lazy_ = false;
bool register_inverses_ = true;
std::unique_ptr<lazy_source, void (*)(lazy_source*)> lazy_source_{nullptr, nullptr};
std::vector<unsigned> lazy_bypassed_;
std::vector<std::pair<uint32_t, uint64_t>> lazy_offsets_;
bool index_lazily(const std::string& path, const ifcopenshell::schema_definition*& schema, unsigned int& max_id, const std::set<std::string>& types_to_bypass);
void materialize(instance_data* data);
typedef std::map<const ifcopenshell::declaration*, std::vector<express::base>> entities_by_type;
typedef std::unordered_map<uint32_t, shared_pointer_type> entity_instance_by_name_storage;
typedef map_transformer<entity_instance_by_name_storage, std::function<express::base(shared_pointer_type)>> entity_instance_by_name;
@@ -658,6 +677,13 @@ namespace ifcopenshell {
shared_pointer_type load(ifcopenshell::spf_lexer<Reader>* tokens, std::optional<size_t> entity_instance_name, const ifcopenshell::declaration* declaration, const ifcopenshell::entity* entity, int attribute_index = -1, bool coerce_attribute_count = true);
template <typename Reader>
void try_read_semicolon(ifcopenshell::spf_lexer<Reader>* tokens) const;
// The attribute-reading half of load(): the tokens after the
// opening parenthesis into a fresh attribute array. Storage is
// always in_memory_attribute_storage; it is a template parameter
// only because that type is defined in a header that includes
// this one.
template <typename Reader, typename Storage>
Storage load_attributes(ifcopenshell::spf_lexer<Reader>* tokens, std::optional<size_t> entity_instance_name, const ifcopenshell::declaration* declaration, const ifcopenshell::entity* entity, int attribute_index = -1);
// Replaces the names left in `data`'s attribute slots by in-place
// reference storage with the instances they name; a name that is
// missing or bypassed becomes null in a scalar and is dropped