-
Notifications
You must be signed in to change notification settings - Fork 85
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HasTraits instantiation performance degradation from 6.0.0 => 6.1.1 => 6.2.0 #1648
Comments
FWIW changing properties to Traits 6.0.0 with depends_on (observe was introduced in 6.1.0)
Traits 6.1.1 with observe
Traits 6.2.0 with observe
Traits 6.3.2 with observe
|
Hi, thanks @jwiggins for the script. I realized a detail that I did not tell, sorry. We have it written now to take a default value and remove the caching, hoppla. Modified exampleClick to expand!import random
import secrets
import time
from traits.api import (
Dict,
Event,
Float,
HasStrictTraits,
Instance,
Property,
String,
Undefined,
)
class Variable(HasStrictTraits):
# data
attributes = Property
_attributes = Dict
value = Undefined
timestamp = Instance(object)
event1 = Event()
event2 = Event()
# convenience properties
attr1 = String
attr2 = String
attr3 = String
attr4 = String
attr5 = String
def _set_attributes(self, value):
self._attributes = value
def _get_attributes(self):
return self._attributes
def _attr1_default(self):
return self._attributes.get('foo')
def _attr2_default(self):
return self._attributes.get('foo')
def _attr3_default(self):
return self._attributes.get('foo')
def _attr4_default(self):
return self._attributes.get('foo')
def _attr5_default(self):
return self._attributes.get('foo')
class ObjectRoot(Variable):
value = Instance(dict, args=())
name = String()
schema_change = Event()
class NumberVariable(Variable):
value = Float()
class StringVariable(Variable):
value = String()
random_attributes = {"foo": "bar"}
def create_random_schema(name="root", level=2):
"""Create a big and deeply nested object description."""
VARS_PER_LEVEL = 5
schema = {"name": name}
level_schema = schema.setdefault("variables", {})
for idx in range(VARS_PER_LEVEL):
name_extra = f"{level}_{secrets.token_hex(2)}"
if level > 0:
name = f"subobj_{name_extra}"
level_schema[name] = create_random_schema(name=name, level=level - 1)
else:
name = f"var_{name_extra}"
vartype = "str" if idx % 2 == 0 else "num"
varval = secrets.token_hex(4) if vartype == "str" else random.random() * 100
level_schema[name] = {"name": name, "value": varval, "type": vartype}
return schema
def generate_from_schema(schema):
"""Recursively generate an instance from a schema describing it."""
def _gen_node(value):
if "variables" in value:
return generate_from_schema(value)
valtype = value["type"]
if valtype == "num":
return NumberVariable(value=value["value"],
attributes=random_attributes)
elif valtype == "str":
return StringVariable(value=value["value"],
attributes=random_attributes)
raise ValueError(f"{value}")
obj = ObjectRoot(name=schema["name"])
for key, value in schema["variables"].items():
obj.value[key] = _gen_node(value)
obj.schema_change = True
return obj
def summarize_schema(schema, level=0):
"""Show a condensed representation of the object we're generating."""
indent = " " * (level + 1) * 2
if level == 0:
print("Root:")
for key, value in schema["variables"].items():
if "variables" in value:
print(f"{indent}{key}:")
summarize_schema(value, level + 1)
else:
print(f"{indent}{key}: {value['type']}")
def main():
COUNT = 1000
schema = create_random_schema()
# summarize_schema(schema)
accum_time = 0
for _ in range(COUNT):
start = time.perf_counter()
generate_from_schema(schema)
accum_time += time.perf_counter() - start
print("Total time:", accum_time)
print("Time per instantiation:", accum_time / COUNT)
if __name__ == "__main__":
main() Timings Traits 6.3.2
Traits 6.0.0
|
Many thanks for this; the script is especially useful here. Attached are some profile results using Scriptimport random
import secrets
import pyinstrument
import traits
from traits.api import (Dict, Event, Float, HasStrictTraits, Instance,
Property, String, Undefined)
class Variable(HasStrictTraits):
# data
attributes = Property
_attributes = Dict
value = Undefined
timestamp = Instance(object)
event1 = Event()
event2 = Event()
# convenience properties
attr1 = String
attr2 = String
attr3 = String
attr4 = String
attr5 = String
def _set_attributes(self, value):
self._attributes = value
def _get_attributes(self):
return self._attributes
def _attr1_default(self):
return self._attributes.get("foo")
def _attr2_default(self):
return self._attributes.get("foo")
def _attr3_default(self):
return self._attributes.get("foo")
def _attr4_default(self):
return self._attributes.get("foo")
def _attr5_default(self):
return self._attributes.get("foo")
class ObjectRoot(Variable):
value = Instance(dict, args=())
name = String()
schema_change = Event()
class NumberVariable(Variable):
value = Float()
class StringVariable(Variable):
value = String()
random_attributes = {"foo": "bar"}
def create_random_schema(name="root", level=2):
"""Create a big and deeply nested object description."""
VARS_PER_LEVEL = 5
random.seed(12345)
schema = {"name": name}
level_schema = schema.setdefault("variables", {})
for idx in range(VARS_PER_LEVEL):
name_extra = f"{level}_{secrets.token_hex(2)}"
if level > 0:
name = f"subobj_{name_extra}"
level_schema[name] = create_random_schema(
name=name, level=level - 1
)
else:
name = f"var_{name_extra}"
vartype = "str" if idx % 2 == 0 else "num"
varval = (
secrets.token_hex(4)
if vartype == "str"
else random.random() * 100
)
level_schema[name] = {
"name": name,
"value": varval,
"type": vartype,
}
return schema
def generate_from_schema(schema):
"""Recursively generate an instance from a schema describing it."""
def _gen_node(value):
if "variables" in value:
return generate_from_schema(value)
valtype = value["type"]
if valtype == "num":
return NumberVariable(
value=value["value"], attributes=random_attributes
)
elif valtype == "str":
return StringVariable(
value=value["value"], attributes=random_attributes
)
raise ValueError(f"{value}")
obj = ObjectRoot(name=schema["name"])
for key, value in schema["variables"].items():
obj.value[key] = _gen_node(value)
obj.schema_change = True
return obj
def main():
COUNT = 10000
version = traits.__version__
schema = create_random_schema()
profiler = pyinstrument.Profiler()
profiler.start()
for _ in range(COUNT):
generate_from_schema(schema)
profiler.stop()
html_profile = profiler.output_html()
with open(f"profile_{version}.html", "w", encoding="utf-8") as f:
f.write(html_profile)
if __name__ == "__main__":
main() Profile results on 6.0.0 (as a screenshot, since GitHub won't let me upload the html file :-( ): Profile results on main: There's no one obvious single source for the performance regression, but there are a few low-hanging fruit. For one, it looks as though we're spending significant time validating keys and values for a |
Looks like we're also losing some time in As for very low-hanging fruit, this line unnecessarily looks up |
Hey @jwiggins the code provided in the example nicely represents our binding code in https://github.com/European-XFEL/Karabo/blob/main/src/pythonGui/karabogui/binding/binding_types.py#L41 At the moment, we are stuck on traits 6.0.0 due to the significant performance drain. |
This was reported to me informally by @dgoeries. He measured performance degradation of
HasTraits
instantiation on a large long-lived codebase when upgrading to Traits 6.1.1.I've done my best to replicate what the code is doing in a minimally reproducible form.
Minimal Example
expand
Timings
Traits 6.2.0
Traits 6.1.1
Traits 6.0.0
The text was updated successfully, but these errors were encountered: