diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 7228c0d8d3f..29d2f890038 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -510,8 +510,8 @@ class TestCustomFields(common.TransactionCase): }) query_count = self.cr.sql_log_count - query_count - # create the related field, and assert it only takes 3 extra queries - with self.assertQueryCount(query_count + 3): + # create the related field, and assert it only takes 1 extra query + with self.assertQueryCount(query_count + 1): self.env['ir.model.fields'].create({ 'model_id': self.env['ir.model']._get_id('res.partner'), 'name': 'x_oh_boy', diff --git a/odoo/fields.py b/odoo/fields.py index 1d9c1292bf5..3f4dbd90576 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -217,6 +217,8 @@ class Field(MetaField('DummyField', (object,), {})): _sequence = None # absolute ordering of the field _base_fields = () # the fields defining self, in override order _extra_keys = () # unknown attributes set on the field + _direct = False # whether self may be used directly (shared) + _toplevel = False # whether self is on the model's registry class automatic = False # whether the field is automatically created ("magic" field) inherited = False # whether the field is inherited (_inherits) @@ -291,6 +293,25 @@ class Field(MetaField('DummyField', (object,), {})): # runtime on the registry class of the model. The occurrences of the field # are given to the new field as the parameter '_base_fields'; it is a list # of fields in override order (or reverse MRO). + # + # In order to save memory, a field should avoid having field.args and/or + # many attributes when possible. We call "direct" a field that can be set + # up directly from its definition class. Direct fields are non-related + # fields defined on models, and can be shared across registries. We call + # "toplevel" a field that is put on the model's registry class, and is + # therefore specific to the registry. + # + # Toplevel field are set up once, and are no longer set up from scratch + # after that. Those fields can save memory by discarding field.args and + # field._base_fields once set up, because those are no longer necessary. + # + # Non-toplevel non-direct fields are the fields on definition classes that + # may not be shared. In other words, those fields are never used directly, + # and are always recreated as toplevel fields. On those fields, the base + # setup is useless, because only field.args is used for setting up other + # fields. We therefore skip the base setup for those fields. The only + # attributes of those fields are: '_sequence', 'args', 'model_name', 'name' + # and '_module', which makes their __dict__'s size minimal. def __set_name__(self, owner, name): """ Perform the base setup of a field. @@ -305,7 +326,15 @@ class Field(MetaField('DummyField', (object,), {})): # only for fields on definition classes, not registry classes self._module = owner._module owner._field_definitions.append(self) - self._setup_attrs(owner, name) + + if not self.args.get('related'): + self._direct = True + if self._direct or self._toplevel: + self._setup_attrs(owner, name) + if self._toplevel: + # free memory, self.args and self._base_fields are no longer useful + self.__dict__.pop('args', None) + self.__dict__.pop('_base_fields', None) # # Setup field parameter attributes diff --git a/odoo/models.py b/odoo/models.py index 70729fb097e..119fc1c4eff 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -55,7 +55,7 @@ from .exceptions import AccessError, MissingError, ValidationError, UserError from .osv.query import Query from .tools import frozendict, lazy_classproperty, ormcache, \ Collector, LastOrderedSet, OrderedSet, IterableGenerator, \ - groupby + groupby, discardattr from .tools.config import config from .tools.func import frame_codeinfo from .tools.misc import CountingStream, clean_context, DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT, get_lang @@ -324,11 +324,27 @@ VALID_AGGREGATE_FUNCTIONS = { # depends on the presence or absence of each definition class, which itself # depends on the modules loaded in the registry. # -# In order to avoid any ambiguity in a field's definition, all the fields of a -# model are recreated on its registry class. When a field is set up, it -# retrieves its definition(s) from its model's ancestor classes. It also -# collects other information from its model's (registry) class, like compute -# dependencies. +# By design, the registry class has access to all the fields on the model's +# definition classes. When possible, the field is used directly from the +# model's registry class. There are a number of cases where the field cannot be +# used directly: +# - the field is related (and bits may not be shared); +# - the field is overridden on definition classes; +# - the field is defined for another model (and accessible by mixin). +# +# The last case prevents sharing the field, because the field object is specific +# to a model, and is used as a key in several key dictionaries, like the record +# cache and pending computations. +# +# Setting up a field on its definition class helps saving memory and time. +# Indeed, when sharing is possible, the field's setup is almost entirely done +# where the field was defined. It is thus done when the definition class was +# created, and it may be reused across registries. +# +# In the example below, the field 'foo' appears once on its model's definition +# classes. Assuming that it is not related, that field can be set up directly +# on its definition class. If the model appears in several registries, the +# field 'foo' is effectively shared across registries. # # class A1(Model): Model # _name = 'a' / \ @@ -338,20 +354,12 @@ VALID_AGGREGATE_FUNCTIONS = { # class A2(Model): \ / # _inherit = 'a' \ / # bar = ... a -# foo # bar # -# -# SHARING FIELDS -# -# The registry class of a model is actually specific to a given registry. In -# order to reduce the memory footprint of a registry, the fields on registry -# classes can be shared across registries. A field on a registry class may -# actually be shared with another registry class, provided it is not related and -# the definition classes in the MRO of the registry classes are the same. -# Related fields are banned from sharing, because one of their attribute refers -# to another field from their registry, which is likely to not be shared across -# the given registries. +# On the other hand, the field 'bar' is overridden in its model's definition +# classes. In that case, the framework recreates the field on the model's +# registry class. The field's setup will be based on its definitions, and will +# not be shared across registries. def is_definition_class(cls): """ Return whether ``cls`` is a model definition class. """ @@ -511,6 +519,7 @@ class BaseModel(metaclass=MetaModel): if not isinstance(getattr(cls, name, field), Field): _logger.warning("In model %r, field %r overriding existing value", cls._name, name) setattr(cls, name, field) + field._toplevel = True field.__set_name__(cls, name) cls._fields[name] = field @@ -521,8 +530,7 @@ class BaseModel(metaclass=MetaModel): """ cls = type(self) field = cls._fields.pop(name, None) - if hasattr(cls, name): - delattr(cls, name) + discardattr(cls, name) if cls._rec_name == name: # fixup _rec_name and display_name's dependencies cls._rec_name = None @@ -2852,10 +2860,7 @@ class BaseModel(metaclass=MetaModel): # reset those attributes on the model's class for _setup_fields() below for attr in ('_rec_name', '_active_name'): - try: - delattr(cls, attr) - except AttributeError: - pass + discardattr(cls, attr) @api.model def _setup_base(self): @@ -2870,7 +2875,7 @@ class BaseModel(metaclass=MetaModel): # retrieve fields from parent classes, and duplicate them on cls to # avoid clashes with inheritance between different models for name in cls._fields: - delattr(cls, name) + discardattr(cls, name) cls._fields.clear() # collect the definitions of each field (base definition + overrides) @@ -2880,7 +2885,10 @@ class BaseModel(metaclass=MetaModel): for field in klass._field_definitions: definitions[field.name].append(field) for name, fields_ in definitions.items(): - self._add_field(name, fields_[-1].new(_base_fields=fields_)) + if len(fields_) == 1 and fields_[0]._direct and fields_[0].model_name == cls._name: + cls._fields[name] = fields_[0] + else: + self._add_field(name, fields_[-1].new(_base_fields=fields_)) self._add_magic_fields() diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index c1b353e5ce1..c71bff86c90 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -697,6 +697,13 @@ def attrgetter(*items): return tuple(resolve_attr(obj, attr) for attr in items) return g +def discardattr(obj, key): + """ Perform a ``delattr(obj, key)`` but without crashing if ``key`` is not present. """ + try: + delattr(obj, key) + except AttributeError: + pass + # --------------------------------------------- # String management # ---------------------------------------------