Summary
While profiling the engine in-process (in-memory parser) on a 54 MB IFC model against IFC106 (Resource entities should be referenced by rooted entity — identical logic to IFC105), the rule took ~128 s, almost entirely in two framework-level issues that affect every rule. I see IVS-879_Improve_Performance_of_ifc-gherkin-rules already targets part of this, so this issue is to (a) share the profiling data, (b) flag two gaps and one bug in that branch, and (c) point out the biggest win it doesn't yet touch. Happy to send PRs for any/all of these.
Profile (cProfile, sorted by self time)
Step-level timing for IFC106 on the 54 MB model, before vs. after the fixes below:
| Step |
Before |
After |
Given a traversal over the full model … |
57.4 s |
17.9 s |
Given an entity instance |
5.7 s |
5.6 s |
Given [its entity type] is not 'IfcRoot' … |
42.1 s |
19.6 s |
Then it must be referenced by … IfcRoot … |
13.9 s |
9.4 s |
| rule total |
128.1 s |
59.7 s (2.1×) |
Top self-time before: numpy._unique_hash 24.0 s, then ~12 s across inspect._signature_from_function / inspect.getargs / compile. Both vanish after the changes; outputs are byte-identical (24,863 outcomes).
1. model_traversal.py — visited set is O(n²) (not addressed by IVS-879)
features/steps/steps/model_traversal.py#L80 uses misc.ContiguousSet as the visited set for the full-model traversal. ContiguousSet.commit() runs np.union1d(self._arr, pend_arr) over the entire growing array on every batch (misc.py#L303) — effectively O(n²/batch). On this model that's the 24 s of numpy._unique_hash and the dominant cost of the 57 s traversal step.
The traversal only needs membership (inst.id() not in context.visited_instances), so a plain set[int] is O(1) per op. Swapping it took the traversal step 57 s → 18 s, identical results:
visited = set()
def visit(inst):
inst_id = inst.id()
if inst_id in visited:
return
visited.add(inst_id)
...
for inst in context.model.by_type(entity_name):
visit(inst)
context.visited_instances = visited # drop visited.commit()
(Also lets you drop the now-unused path accumulation in visit.) If ContiguousSet is kept elsewhere for the rocksdb/lazy path, this single call site can still use a plain set — it's membership-only.
2. IVS-879 signature cache misses the inspect.getargs sites
The branch adds _SIGNATURE_CACHE / get_cached_signature(fn) for the apply_operation site (#L170) — nice. But three other per-call introspection sites remain uncached and run per-instance:
These together were ~half of the ~12 s introspection cost. A small memoized helper covers all sites (signature + getargs), keyed on fn / fn.__code__:
@functools.lru_cache(maxsize=None)
def _fn_accepts_param(fn, name):
try: return name in inspect.signature(fn).parameters
except (TypeError, ValueError): return False
@functools.lru_cache(maxsize=None)
def _code_accepts_arg(code, name):
try: return name in inspect.getargs(code).args
except (TypeError, ValueError): return False
3. Bug in IVS-879's ContiguousSet.commit()
The branch's rewrite of commit() assigns the result of numpy.ndarray.sort(), which sorts in place and returns None:
# IVS-879 version
self._arr = pend_arr.sort() # -> None
...
self._arr = np.unique(np.concatenate((self._arr, pend_arr))).sort() # -> None
After the first commit self._arr is None, which will break subsequent _in_array / __contains__. np.unique(...) already returns sorted, so:
if self._arr.size == 0:
self._arr = np.sort(pend_arr) # or np.unique(pend_arr)
else:
self._arr = np.unique(np.concatenate((self._arr, pend_arr)))
(Note: even fixed, commit() per batch is still O(n log n) over the whole array; for the membership-only visited use in #1, a plain set avoids this entirely.)
Repro / notes
Summary
While profiling the engine in-process (in-memory parser) on a 54 MB IFC model against IFC106 (Resource entities should be referenced by rooted entity — identical logic to IFC105), the rule took ~128 s, almost entirely in two framework-level issues that affect every rule. I see
IVS-879_Improve_Performance_of_ifc-gherkin-rulesalready targets part of this, so this issue is to (a) share the profiling data, (b) flag two gaps and one bug in that branch, and (c) point out the biggest win it doesn't yet touch. Happy to send PRs for any/all of these.Profile (cProfile, sorted by self time)
Step-level timing for IFC106 on the 54 MB model, before vs. after the fixes below:
Given a traversal over the full model …Given an entity instanceGiven [its entity type] is not 'IfcRoot' …Then it must be referenced by … IfcRoot …Top self-time before:
numpy._unique_hash24.0 s, then ~12 s acrossinspect._signature_from_function/inspect.getargs/compile. Both vanish after the changes; outputs are byte-identical (24,863 outcomes).1.
model_traversal.py—visitedset is O(n²) (not addressed by IVS-879)features/steps/steps/model_traversal.py#L80usesmisc.ContiguousSetas thevisitedset for the full-model traversal.ContiguousSet.commit()runsnp.union1d(self._arr, pend_arr)over the entire growing array on every batch (misc.py#L303) — effectively O(n²/batch). On this model that's the 24 s ofnumpy._unique_hashand the dominant cost of the 57 s traversal step.The traversal only needs membership (
inst.id() not in context.visited_instances), so a plainset[int]is O(1) per op. Swapping it took the traversal step 57 s → 18 s, identical results:(Also lets you drop the now-unused
pathaccumulation invisit.) IfContiguousSetis kept elsewhere for the rocksdb/lazy path, this single call site can still use a plain set — it's membership-only.2.
IVS-879signature cache misses theinspect.getargssitesThe branch adds
_SIGNATURE_CACHE/get_cached_signature(fn)for theapply_operationsite (#L170) — nice. But three other per-call introspection sites remain uncached and run per-instance:validation_handling.py#L133—'inst' not in inspect.getargs(fn.__code__).args#L247/#L249—'path'/'npath' in inspect.getargs(fn.__code__).argsThese together were ~half of the ~12 s introspection cost. A small memoized helper covers all sites (signature + getargs), keyed on
fn/fn.__code__:3. Bug in
IVS-879'sContiguousSet.commit()The branch's rewrite of
commit()assigns the result ofnumpy.ndarray.sort(), which sorts in place and returnsNone:After the first commit
self._arr is None, which will break subsequent_in_array/__contains__.np.unique(...)already returns sorted, so:(Note: even fixed,
commit()per batch is still O(n log n) over the whole array; for the membership-onlyvisiteduse in #1, a plain set avoids this entirely.)Repro / notes
mainor folded intoIVS-879.