Commit Graph

45 Commits

Author SHA1 Message Date
Dion Moult a92bebd73e 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
2026-09-14 19:34:08 +10:00
Dion Moult 91622b97b3 ifcparse: keep unresolved references in the attribute slots
The parser could not resolve a #name when it read it, because the
instance may be defined further down the file, so it left the slot empty
and appended (owner, attribute, name) to a side table that a second pass
walked. The table held one entry per reference for the whole read: 64 MB
on a 58 MB model, the high-water mark of opening.

Now the reference stays where the tokenizer put it: the attribute slot
holds the instance_reference, or the reference_or_simple_type aggregate
for a list (mixed with inline typed values or not), until every instance
has been read, and resolve_instance_references() walks each instance's
slots and swaps names for instances. Ordering is what the tokenizer
produced; nothing is re-derived. A missing name becomes null in a scalar
and is dropped from an aggregate, as before; the error keeps its offset.
The three transient alternatives are appended to the attribute pack and
to argument_type in lock step and are never visible once a file is
loaded. Simple type instances read inline (IfcPropertySetDefinitionSet)
have their own slots, so their references need no diversion.

The table remains for the header entities and for streaming consumers of
instance_streamer::references(), which leave resolve_references_in_place
off.

TXG 58 MB / 210_King 147 MB / OKgate22 231 MB, single thread: time
unchanged (1.05 / 2.69 / 4.99 s), memory after the parse 287 -> 274,
698 -> 654, 1086 -> 1036 MB, peak 400 -> 365, 965 -> 871, 1471 -> 1347 MB.

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
2026-09-14 19:33:48 +10:00
Dion Moult 9829ebf001 ifcparse: give the tokenizer a compile-time policy for what it decodes
spf_lexer::next() becomes next<Policy>(). full_tokens, the default, is
what the parser has always had. index_tokens is what the lazy index
needs: a string is ended but not decoded, and a number, enumeration or
binary comes back as Token_LITERAL with only its position; names,
keywords and operators are read as before. Each policy compiles to its
own loop from the one implementation, so there is no second tokenizer.

character_decoder gains skip(): the same state machine as the
conversion with the collection compiled out, so an escape such as \S\'
(an apostrophe as the page character) ends the string at the same byte
under both policies. A byte-level scan would have ended it early.

Also fixes a comment that follows a token without whitespace, ",/* x */",
which skip_comment() never saw because the slash had been consumed.

TXG (58 MB), single thread: tokenizing the whole file 194 MB/s with
full_tokens, 249 MB/s with index_tokens; through 64 KB pages 196 and
205 MB/s. The parse itself is unchanged.

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
2026-09-14 19:33:48 +10:00
Dion Moult 64805b409f ifcparse: store the GlobalId index in a hash map with inline keys
byguid_ was a std::map<std::string, ...>: a red-black node plus a
heap-allocated 22-character string per rooted instance, and a lookup that
walks ~18 levels of string comparisons on a 200k-entry file.

guid_map keeps keys of up to 23 characters inline in an unordered_map node
(every valid GlobalId is 22), and routes anything longer to an ordered map
so invalid files still work. Same std::string-keyed interface as before.

Parse, C++ file constructor, on top of the previous commits:
  TXG            58 MB   1.08 s -> 1.03 s   341 -> 335 MB
  210_King      148 MB   2.80 s -> 2.72 s   836 -> 830 MB
  OKgate22      232 MB   4.25 s -> 3.94 s  1271 -> 1253 MB

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
2026-09-14 19:33:47 +10:00
Dion Moult e0b6122d17 ifcparse: inline the file reader accessors and pre-size the inverse index
full_buffer_impl::size(), get(), get_u32() and get_u64() (and their mmap_impl
twins) were defined out of line, so every character the lexer read crossed a
call boundary with its own bounds check. Callgrind put the three at 5.6% of
parse self time; inlining them lets the compiler hoist the checks out of the
scanning loops, which is worth more than their own cost.

Also reserve the streamer's inverse vector from the file size (about one
record per 32 bytes of SPF on real models) and shrink it once the bulk load
is sorted, so the doubling copies and the capacity slack go away.

Parse time, C++ file constructor, 12-core Linux box:
  TXG            58 MB   1.35 s -> 1.15 s
  210_King      148 MB   3.70 s -> 3.09 s
  OKgate22      232 MB   6.18 s -> 5.30 s
Python ifcopenshell.open(): 1.40 -> 1.22, 3.70 -> 3.24, 6.27 -> 5.52 s.
Memory unchanged. The removed exported symbols mean the Python wrapper must
be rebuilt against this library.

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
2026-09-14 19:33:47 +10:00
Dion Moult e1be433207 ifcparse: unregister a deleted instance's inverse records via its attributes (#9467)
process_deletion_inverse() called inverse_index::remove_source(), which
walked every record in the file's inverse index to find the ones whose
source is the deleted instance: O(R) per deletion, the dominant cost of
file.remove() on large files now that the lookup side no longer re-sorts.

The records a deleted instance contributed are exactly the entity
references in its own attributes, so walk those with the same visitor
build_inverses_() uses for registration and remove each record with a
targeted binary search instead. remove_source() has no callers left and
is deleted.

Also use the ordered view of batch_deletion_ids_ (a boost multi_index
that already had one) for the is-this-referencer-also-being-deleted
check in process_deletion_(), which was a linear std::find over the
sequenced view: O(b) per referencing instance made batch deletion of b
instances quadratic.

file.remove on 300 IfcPropertySet of a 155 MB IFC4 model (201k IfcRoot)
drops from 3.15 ms to 0.17 ms per call, batched removal of 2000 from
3.34 ms to 0.17 ms per call, root.remove_product on 100 walls from
332 ms to 131 ms per call.


Claude-Session: https://claude.ai/code/session_01HNrXDmR88wKPCYwGE21SyH
(cherry picked from commit 938442303f)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 08:04:58 +10:00
Dion Moult ba9810f459 [AI-generated, unverified] ifcparse: stop re-sorting the inverse index on every read after a write (#9460) 2026-09-10 07:31:45 +10:00
Andrej730 104591a80b Normalize whitespaces in the codebase 2026-08-19 20:17:50 +05:00
Thomas Krijnen 4597929df9 Remove _t suffixes from public types
Rename header-scope aliases, enums, and helper types while retaining descriptive names where dropping the suffix would create a collision.

Generated with the assistance of an AI coding tool.
2026-08-08 14:58:26 +02:00
Thomas Krijnen 02481b3247 Wrap more classes into ifcopenshell:: namespace 2026-08-08 13:58:39 +02:00
Thomas Krijnen af58eaf79f Last minute refactoring 2026-08-08 07:42:45 +02:00
Petru Conduraru d5076bded3 ifcparse: store integer attribute values as int64_t to allow out-of-range timestamps
Setting an IfcInteger/IfcTimeStamp typed attribute (e.g. IfcOwnerHistory.CreationDate)
outside the signed 32-bit range corrupted the value instead of raising, since the
Python wrapper's set_attribute_value_py() truncated it with a plain static_cast<int>
before handing it to the C++ storage. Unix timestamps before 1901-12-13 or after
2038-01-19 silently wrapped around (e.g. 3000000000 became -1294967296) rather than
being rejected or stored correctly. Fixes #3058, equivalent to PR #8683 but ported to
this branch's rewritten ifcparse (snake_case files, variant_array/instance_data
storage, SWIG PyObject-based attribute setter) instead of the old IfcEntityInstanceData
sources, which no longer exist here.

The scalar slot of the attribute variant (Argument_INT) becomes int64_t. Integer
aggregates (Argument_AGGREGATE_OF_INT, e.g. CoordIndex) and instance/reference
identifiers stay 32-bit, since neither is the value that overflows here; this narrow
scope is kept on its own technical merits (aggregates and identifiers were never the
source of the bug, and widening them would be a much larger, riskier change for no
benefit) even though aothms said compatibility isn't a concern on this v0.9-track
branch. express::Base::set_attribute_value promotes the schema-generated int to
int64_t at a single choke point, so the generated setters keep compiling unchanged.
The STEP lexer, writer, and SWIG wrapper (set_attribute_value_py, pythonize) are all
widened together, since widening only the Python-facing setter would have silently
wrapped the value on file write instead of raising.

Verified in a build (IFC2X3 and IFC4, BUILD_IFCGEOM off, no kernels): pre-1901,
post-2038, both 32-bit boundaries, and a 9e12 value all round trip exactly both in
memory and through STEP text serialization (write then reopen). A value outside the
64-bit range now raises a clean exception instead of corrupting data. Ordinary
in-range integers and integer aggregates (e.g. IfcTriangulatedFaceSet.CoordIndex) are
unaffected. The existing util/test_attribute.py and test_file.py suites pass
unchanged; test_entity_instance.py has 5 pre-existing failures unrelated to this
change (confirmed identical on an unfixed build of this branch, caused by a missing
get_info_2 binding and _patch_swig_comparisons never being implemented here).

Generated with the assistance of an AI coding tool.
2026-07-19 13:54:16 +02:00
Thomas Krijnen 561a23cfbc After-merge clean-ups 2026-07-09 22:01:21 +02:00
Thomas Krijnen 7fc2d9a998 Merge remote-tracking branch 'origin/v0.8.0' into ifcviewer-wgpu 2026-07-09 13:21:39 +02:00
Thomas Krijnen 347a3c80bb More logger changes 2026-06-11 21:09:56 +02:00
Bruno Postle 3f9b25d4e0 Use version preprocessor guards for RocksDB unique_ptr API, retain unique_ptr internally 2026-06-11 18:45:18 +02:00
Bruno Postle 78697582f7 Add missing standard library includes for self-sufficient headers
Fixes builds with newer GCC/libstdc++ that no longer provide <cstdint>,
<cstring>, <cfloat>, <memory>, <algorithm> etc. transitively. Also
disambiguates visit<> calls in taxonomy.h with the full namespace and
casts the character value in IfcCharacterDecoder to uint32_t to silence
ambiguous overload warnings.
2026-06-11 18:41:07 +02:00
Thomas Krijnen 4c13e2424c Configurable pointer type; std::from_chars(); aggregate inverses in vector; skip parse_context 2026-06-11 15:51:40 +02:00
Bruno Postle 24a241addc Use version preprocessor guards for RocksDB unique_ptr API, retain unique_ptr internally 2026-06-05 14:22:07 +02:00
Bruno Postle bd264f1d85 Add missing standard library includes for self-sufficient headers
Fixes builds with newer GCC/libstdc++ that no longer provide <cstdint>,
<cstring>, <cfloat>, <memory>, <algorithm> etc. transitively. Also
disambiguates visit<> calls in taxonomy.h with the full namespace and
casts the character value in IfcCharacterDecoder to uint32_t to silence
ambiguous overload warnings.
2026-06-05 08:54:27 +02:00
Thomas Krijnen a1efdccb4b Add back mutex 2026-05-08 10:07:34 +02:00
Dion Moult 0bb6df6a6b ifcparse: skip flush+compact for read-only RocksDB on destruction
Read-only handles reject Flush/CompactRange, so the destructor's status
assertion always fired on shutdown when the streamer's sidecar was
opened with read_only=true. Track the flag and skip the write path; also
guard against a null db when the initial open failed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 10:16:15 +10:00
Dion Moult 543d9f8588 Local hacks to compile and monkey patch issues in the Python world
All AI generated slop. Do NOT trust these "fixes". It's just to get it
working on my machine.
2026-04-23 21:32:24 +10:00
Thomas Krijnen 9b13dc8dd6 Get rid of parse context pool 2026-04-22 12:03:58 +02:00
Thomas Krijnen 13c71d7a8a symbol visibility 2026-04-21 21:48:54 +02:00
Thomas Krijnen 11006f0ef5 deque 2026-04-13 21:18:58 +02:00
Thomas Krijnen b2fc0c00cc Hierarchical index for inverses 2026-04-10 14:54:47 +02:00
Thomas Krijnen 20ccf2b455 Parameter naming 2026-03-31 18:23:13 +02:00
Thomas Krijnen a07f56db6f Restructure and rename 2026-03-31 15:32:36 +02:00
Thomas Krijnen 603cedc487 Try some things: (a) fewer allocations - parse context pool; lexer string pool (b) SWAR process multiple chars at once in keywords/enums/strs/stc. 2026-03-27 20:45:13 +01:00
Thomas Krijnen 8e42f35db3 Rework variable length token storage to use string pool; eliminate need for rereads 2026-03-26 15:49:28 +01:00
Thomas Krijnen f2f4d626a5 Fix examples mostly 2026-01-07 13:51:48 +01:00
Thomas Krijnen 7098beb819 Work towards v1.0 data model with encapsulated weak_ptr as basis for instances 2026-01-05 21:42:01 +01:00
Thomas Krijnen 0494bd9677 Option to bypass storing types when opening model 2025-10-24 12:06:59 +02:00
Thomas Krijnen 4d688170e0 Storage rework WIP 2025-10-13 20:47:43 +02:00
Thomas Krijnen e15f222a19 Address segfault #7215 #7185 2025-10-08 14:17:47 +02:00
Thomas Krijnen 5e10e61aca Clean-up parsed simple types #7185 2025-10-03 14:18:07 +02:00
Thomas Krijnen ce91d296b6 dllimport/export #6926 2025-09-26 14:24:49 +02:00
Thomas Krijnen 59a5bf2344 RocksDB zstd encryption; cache tuning; readonly open mode 2025-09-03 11:11:55 +02:00
Thomas Krijnen df726a2d61 Initialization in various other constructor forms 2025-08-29 14:13:21 +02:00
Thomas Krijnen b6712602e2 Retain exact attribute counts in rdb as parsed 2025-08-28 11:15:05 +02:00
Thomas Krijnen e5fe2df552 Template, typename, others to make GCC happy 2025-08-26 15:59:05 +02:00
Thomas Krijnen 96336d6f68 Ugly conditional compilation for without rdb support 2025-08-26 13:56:03 +02:00
Thomas Krijnen 862aa71a77 Left-overs after merge 2025-08-26 10:20:08 +02:00
Thomas Krijnen d2c7c1532c Implement streaming scan through file and use in rocksdb serializer and python 2025-08-25 12:45:10 +02:00