Ifc5d quantify function time#7322
Conversation
Useful for quantify function optimization
|
Rather than polluting the local namespace with start/end time variables, have you seen the profile class: |
|
@sboddy i didn't know that, thanks for the hint! |
|
It's kinda basic, so just copying the import of timer and the profile class across to qto.py would be a simple way. I did notice one method in the qto.py also uses a variable from timeit import default_timer as timer
class Profiler:
"""
A python context manager timing utility
"""
def __init__(self, task):
self.task = task
def __enter__(self):
self.start = timer()
def __exit__(self, *args):
print(self.task, timer() - self.start)Then in qto.py add the import: from ifcopenshell.util.profiler import Profilerand your change would become: def quantify(ifc_file: ifcopenshell.file, elements: set[ifcopenshell.entity_instance], rules: dict) -> ResultsDict:
"""Quantify elements from a rules using preset quantification rules
Rules placed as a JSON configuration file in the ``ifc5d`` folder will be
autodetected and loaded with the module for convenience.
:param rules: Set of rules from `ifc5d.qto.rules`.
"""
with Profiler("Quantify function TIME: "):
results: ResultsDict = {}
elements_by_classes: defaultdict[str, set[ifcopenshell.entity_instance]] = defaultdict(set)
for element in elements:
elements_by_classes[element.is_a()].add(element)
for calculator, queries in rules["calculators"].items():
calculator = calculators[calculator]
# Cache schema entity names once per file
schema_entity_names = lower_case_entity_names(ifc_file.schema_identifier)
# Fast path: all queries are simple entity names
if not set(m.lower() for m in queries.keys()) - schema_entity_names:
casenorm = {k.lower(): k for k in queries.keys()}
pred = lambda inst: inst.is_a().lower()
for ty, group in itertools.groupby(sorted(elements, key=pred), key=pred):
group_elements = list(group)
# check entity type and its supertypes for matching QTO rules
for sty in entity_supertypes(ifc_file.schema_identifier, ty):
if qtos := queries.get(casenorm.get(sty)):
calculator.calculate(ifc_file, group_elements, qtos, results)
# Fallback: per-query evaluation
for query, qtos in queries.items():
# Simple entity name: use by_type but restrict to incoming elements
if query.lower() in schema_entity_names:
by_type_all = ifc_file.by_type(query)
# ensure we don't expand beyond provided elements subset
if isinstance(elements, set):
filtered_elements = [e for e in by_type_all if e in elements]
else:
elements_set = set(elements)
filtered_elements = [e for e in by_type_all if e in elements_set]
else:
filtered_elements = ifcopenshell.util.selector.filter_elements(ifc_file, query, elements)
if filtered_elements:
calculator.calculate(ifc_file, filtered_elements, qtos, results)
return resultsMaking it a common utility would mean it could be factored out of the drawing/operator.py and also used elsewhere. (Be aware, that is all just copy and pasted, and untested. Might need poking, although it looks right.) |
|
Yeah, i think creating a common utility would be a good idea. I'll go with that, thanks! Another question, do you know why the name is profile? Is it a common practice or a sort of standard? Because a more understandable name would be something related with time (like timer or similar...), right? |
|
The standard terminology for measuring function run time is "profiling". Python actually has built-in libraries for profiling: |
|
Ok...so does it make sense to use the python library instead of a self-created one? |
|
@steverugi nothing special, it allows to measure the performance of a specific function (in this case the quantify function). |
|
"Well, akshullay" ;-) this PR and my suggestion will time any arbitrary section of code. It is just a convenience wrapper around some calls to the timeit module. If you just want to log the execution time of each single call to this function it would be better to put the from ifc5d.qto import quantify
from ifcopenshell.util.profiler import Profiler
with Profiler("Quantify function TIME: "):
quantify(<<passed variables>>)And not actually touching the qto.py file. It is a much smaller, cleaner diff too. The profile module is a library for doing collecting broad statistics about how long functions/methods take. Instead of getting a single figure, it will break down each subsequently called function. (no of calls, cumulative time, time per call, etc.) There are viewers that let you look at performance visually with this captured info, i.e. See #5696 (comment) where I was profiling Bonsai exiting drawing mode back to model mode. There is also cProfile module let's you do something similar using a from ifc5d.qto import quantify
import cProfile
with cProfile.Profile() as pr:
quantify(<<passed variables>>)
pr.print_stats()This would dump something like this: This will give you much more evidence about what part of quantify is taking too long. |
|
thanks guys, much appreciated |
|
@sboddy i have added a profiler function. Could you take a look please? |
|
Making the obvious assumption that you tested it, and it worked, it looks good to me. Not sure if you'd like to clean up the drawing/operator.py file to use this too. No point in having duplicated code. |
|
@sboddy yeah you are right i was thinking about create a new issue in order to improve the visibility of the new profiler function |
Useful for quantify function optimization