Skip to content

Commit

Permalink
perf: Remove weakref instance state that slowed down instantiation by…
Browse files Browse the repository at this point in the history
… factors of about 400% for simple classes.
  • Loading branch information
matthewwardrop committed Jun 4, 2024
1 parent c59cd91 commit a1ebf69
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 71 deletions.
47 changes: 30 additions & 17 deletions spec_classes/methods/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
from typing import Any, Callable, Iterable, Optional

from spec_classes.errors import FrozenInstanceError
from spec_classes.methods.scalar import WithAttrMethod
from spec_classes.types import MISSING, Attr
from spec_classes.utils.method_builder import MethodBuilder
from spec_classes.utils.mutation import (
invalidate_attrs,
mutate_attr,
prepare_attr_value,
protect_via_deepcopy,
)

Expand Down Expand Up @@ -45,9 +45,9 @@ def init(spec_cls, self, **kwargs):

# Initialise any non-local spec attributes via parent constructors
if instance_metadata.owner is spec_cls:
self.__spec_class_state__.initialized = False
self.__spec_class_state__.frozen = False

self.__setattr__(
"__spec_class_initializing__", True, force=True, skip_invalidation=True
)
for parent in reversed(spec_cls.mro()[1:]):
parent_metadata = getattr(parent, "__spec_class__", None)
if parent_metadata:
Expand Down Expand Up @@ -77,7 +77,7 @@ def init(spec_cls, self, **kwargs):
)

# For each attribute owned by this spec_cls in `instance_metadata`,
# initalize the attribute.
# initialize the attribute.
for attr, attr_spec in instance_metadata.attrs.items():
if (
not attr_spec.init
Expand All @@ -100,7 +100,7 @@ def init(spec_cls, self, **kwargs):
if value is not MISSING:
if copy_required:
value = protect_via_deepcopy(value)
setattr(self, attr, value)
self.__setattr__(attr, value, force=True, skip_invalidation=True)

# Finalize initialisation by storing overflow attrs and restoring frozen
# status.
Expand All @@ -121,8 +121,9 @@ def init(spec_cls, self, **kwargs):
if instance_metadata.post_init:
instance_metadata.post_init(self)

self.__spec_class_state__.initialized = True
self.__spec_class_state__.frozen = instance_metadata.frozen
self.__delattr__(
"__spec_class_initializing__", force=True, skip_invalidation=True
)

def build_method(self) -> Callable:
spec_class_key = self.spec_cls.__spec_class__.key
Expand Down Expand Up @@ -203,12 +204,20 @@ class SetAttrMethod(MethodDescriptor):
method_name = "__setattr__"

def build_method(self) -> Callable:
def __setattr__(self, attr, value, force=False):
def __setattr__(self, attr, value, force=False, skip_invalidation=False):
attr_spec = self.__spec_class__.attrs.get(attr)
if attr_spec:
WithAttrMethod.with_attr(attr_spec, self, value, _inplace=True)
return
mutate_attr(self, attr=attr, value=value, inplace=True, force=force)
mutate_attr(
obj=self,
attr=attr,
value=prepare_attr_value(
attr_spec=attr_spec, instance=self, value=value
)
if attr_spec
else value,
inplace=True,
force=force,
skip_invalidation=skip_invalidation,
)

# Add reference to original __setattr__.
__setattr__.__raw__ = getattr(
Expand All @@ -234,8 +243,11 @@ class DelAttrMethod(MethodDescriptor):
method_name = "__delattr__"

def build_method(self) -> Callable:
def __delattr__(self, attr, force=False):
if self.__spec_class_state__.frozen:
def __delattr__(self, attr, force=False, skip_invalidation=False):
if (
not (force or getattr(self, "__spec_class_initializing__", False))
and self.__spec_class__.frozen
):
raise FrozenInstanceError(
f"Cannot mutate attribute `{attr}` of frozen spec class `{self.__class__.__name__}`."
)
Expand All @@ -249,7 +261,8 @@ def __delattr__(self, attr, force=False):
or attr_spec.is_masked
):
self.__delattr__.__raw__(self, attr)
invalidate_attrs(self, attr)
if not skip_invalidation:
invalidate_attrs(self, attr)
return None

return mutate_attr(
Expand All @@ -258,6 +271,7 @@ def __delattr__(self, attr, force=False):
value=protect_via_deepcopy(attr_spec.default), # handle default factory
inplace=True,
force=True,
skip_invalidation=skip_invalidation,
)

# Add reference to original __delattr__
Expand Down Expand Up @@ -461,7 +475,6 @@ def deepcopy(self, memo):
new.__dict__[attr] = value
else:
new.__dict__[attr] = protect_via_deepcopy(value, memo)
self.__spec_class__.instance_state[new] = self.__spec_class_state__
return new

def build_method(self) -> Callable:
Expand Down
46 changes: 0 additions & 46 deletions spec_classes/spec_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,6 @@ def bootstrap(self, spec_cls: type):

# Finalize metadata and remove bootstrapper from class.
spec_cls.__spec_class__ = metadata
spec_cls.__spec_class_state__ = __spec_class_state__
spec_cls.__dataclass_fields__ = metadata.attrs

# Register class-level methods and validate constructor/etc.
Expand Down Expand Up @@ -640,51 +639,6 @@ def invalidation_map(self):
return invalidation_map


@dataclasses.dataclass
class SpecClassState:
"""
A container for the instance state of a spec-class. It is used to control
certain runtime behaviors, like whether a spec-class should be treated as
frozen and/or whether invalidation should be applied.
Attributes:
spec_class: The spec-class instance.
initialized: Whether the spec-class has finished initialization.
frozen: Whether the spec-class should be treated as frozen.
"""

metadata: SpecClassMetadata
initialized: Optional[bool] = None
frozen: Optional[bool] = None

def __post_init__(self):
if self.initialized is None:
self.initialized = True
if self.frozen is None:
return self.metadata.frozen

@property
def invalidation_enabled(self) -> bool:
"""
Whether invalidation logic should be applied at this stage in the
spec-class instance's life-cycle.
"""
return self.initialized and not self.frozen


@property
def __spec_class_state__(self):
"""
The Spec Class instance state. This is primarily used to distinguish between
pre- and post-initialisation phases. Objects sent across process boundaries
(or otherwise deserialized) will not persist this object. It should not
contain anything critical to class function.
"""
if self not in self.__spec_class__.instance_state:
self.__spec_class__.instance_state[self] = SpecClassState(self.__spec_class__)
return self.__spec_class__.instance_state[self]


@dataclasses.dataclass
class _SpecClassMetadataPlaceholder:
"""
Expand Down
15 changes: 7 additions & 8 deletions spec_classes/utils/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def mutate_attr(
inplace: bool = False,
type_check: bool = True,
force: bool = False,
skip_invalidation: bool = False,
) -> Any:
"""
Set attribute `attr` of `obj` to `value`, and return the mutated
Expand All @@ -100,7 +101,11 @@ def mutate_attr(

if metadata:
# Abort if class is frozen.
if not force and inplace and obj.__spec_class_state__.frozen:
if (
not (force or getattr(obj, "__spec_class_initializing__", False))
and inplace
and obj.__spec_class__.frozen
):
raise FrozenInstanceError(
f"Cannot mutate attribute `{attr}` of frozen spec class `{obj.__class__.__name__}`."
)
Expand Down Expand Up @@ -135,19 +140,13 @@ def mutate_attr(
raise

# Invalidate any caches depending on this attribute
if (
metadata
and obj.__spec_class_state__.invalidation_enabled
and metadata.invalidation_map
):
if not skip_invalidation and metadata and metadata.invalidation_map:
invalidate_attrs(obj, attr, metadata.invalidation_map)

return obj


def invalidate_attrs(obj: Any, attr: str, invalidation_map: Dict[str, Set[str]] = None):
if not obj.__spec_class_state__.invalidation_enabled:
return
if invalidation_map is None:
invalidation_map = obj.__spec_class__.invalidation_map
if not invalidation_map:
Expand Down

0 comments on commit a1ebf69

Please sign in to comment.