From b7375323e902d0dbbb0ff1aea75753a616a037cc Mon Sep 17 00:00:00 2001 From: Thomas Adamcik Date: Wed, 8 Apr 2015 01:14:56 +0200 Subject: [PATCH] models: Memoize identical instances automatically This combined with the previous changes has brought the memory use for a 14k track test-set down from about 75MB to 17MB or so. Note that this does however, mean that copy is now lying to us as it does not such thing whenever it can avoid it. --- mopidy/models.py | 23 ++++++++++++++++++----- tests/models/test_fields.py | 2 +- tests/models/test_models.py | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/mopidy/models.py b/mopidy/models.py index c12da719..97b524ea 100644 --- a/mopidy/models.py +++ b/mopidy/models.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, unicode_literals import copy import json +import weakref # TODO: split into base models, serialization and fields? @@ -24,7 +25,7 @@ class Field(object): """ def __init__(self, default=None, type=None, choices=None): - self._name = None # Set by FieldOwner + self._name = None # Set by ImmutableObjectMeta self._choices = choices self._default = default self._type = type @@ -130,7 +131,7 @@ class Collection(Field): return self._default.__class__(value) or None -class FieldOwner(type): +class ImmutableObjectMeta(type): """Helper to automatically assign field names to descriptors.""" @@ -142,8 +143,20 @@ class FieldOwner(type): value._name = key attrs['_fields'] = fields + attrs['_instances'] = weakref.WeakValueDictionary() attrs['__slots__'] = ['_' + field for field in fields] - return super(FieldOwner, cls).__new__(cls, name, bases, attrs) + + for base in bases: + if '__weakref__' in getattr(base, '__slots__', []): + break + else: + attrs['__slots__'].append('__weakref__') + + return super(ImmutableObjectMeta, cls).__new__(cls, name, bases, attrs) + + def __call__(cls, *args, **kwargs): # noqa: N805 + instance = super(ImmutableObjectMeta, cls).__call__(*args, **kwargs) + return cls._instances.setdefault(weakref.ref(instance), instance) class ImmutableObject(object): @@ -157,7 +170,7 @@ class ImmutableObject(object): :type kwargs: any """ - __metaclass__ = FieldOwner + __metaclass__ = ImmutableObjectMeta def __init__(self, *args, **kwargs): for key, value in kwargs.items(): @@ -232,7 +245,7 @@ class ImmutableObject(object): raise TypeError( 'copy() got an unexpected keyword argument "%s"' % key) super(ImmutableObject, other).__setattr__(key, value) - return other + return self._instances.setdefault(weakref.ref(other), other) def serialize(self): data = {} diff --git a/tests/models/test_fields.py b/tests/models/test_fields.py index ff863487..1bf46b7f 100644 --- a/tests/models/test_fields.py +++ b/tests/models/test_fields.py @@ -9,7 +9,7 @@ def create_instance(field): """Create an instance of a dummy class for testing fields.""" class Dummy(object): - __metaclass__ = FieldOwner + __metaclass__ = ImmutableObjectMeta attr = field return Dummy() diff --git a/tests/models/test_models.py b/tests/models/test_models.py index ef484fa9..90701b14 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -8,6 +8,22 @@ from mopidy.models import ( TlTrack, Track, model_json_decoder) +class CachingTest(unittest.TestCase): + + def test_same_instance(self): + self.assertIs(Track(), Track()) + + def test_same_instance_with_values(self): + self.assertIs(Track(uri='test'), Track(uri='test')) + + def test_different_instance_with_different_values(self): + self.assertIsNot(Track(uri='test1'), Track(uri='test2')) + + def test_different_instance_with_copy(self): + t = Track(uri='test1') + self.assertIsNot(t, t.copy(uri='test2')) + + class GenericCopyTest(unittest.TestCase): def compare(self, orig, other):