[IMP] api: move cache_key to the Environment and cache it

Computing the `cache_key` turned out to be a big factor during the
lifespan of a `BaseModel.mapped` call and a lot of this time is spent
computing the same `cache_key` over and over.

These unnecessary computations can be easily reduced to a couple by
moving the `cache_key` method on the environment (instead of the field)
and by implementing a memo for that method.  The rationale is that the
`cache_key` of a field does not change for a given environment.

The result of this patch is up to 50% faster `Field.__get__` which in
turn means a GLOBAL gain in performance, especially for methods /
functions that rely heavily on `__get__` such as `BaseModel.mapped`.

closes odoo/odoo#42674

Signed-off-by: Adrian Torres (adt) <adt@odoo.com>
This commit is contained in:
Adrian Torres
2020-01-21 08:28:36 +00:00
committed by Raphael Collet
parent 8645e31863
commit be01db2f71
2 changed files with 38 additions and 35 deletions
+38 -7
View File
@@ -452,6 +452,7 @@ class Environment(Mapping):
self.cr, self.uid, self.context, self.su = self.args = args
self.registry = Registry(cr.dbname)
self.cache = envs.cache
self._cache_key = {} # memo {field: cache_key}
self._protected = envs.protected # proxy to shared data structure
self.all = envs
envs.add(self)
@@ -709,6 +710,36 @@ class Environment(Mapping):
""" Delay recomputations (deprecated: this is not the default behavior). """
yield
def cache_key(self, field):
""" Return the cache key corresponding to ``field.depends_context``. """
try:
return self._cache_key[field]
except KeyError:
def get(key, get_context=self.context.get):
if key == 'company':
return self.company.id
elif key == 'uid':
return (self.uid, self.su)
elif key == 'active_test':
return get_context('active_test', field.context.get('active_test', True))
else:
val = get_context(key)
try:
hash(val)
except TypeError:
raise TypeError(
"Can only create cache keys from hashable values, "
"got non-hashable value {!r} at context key {!r} "
"(dependency of field {})".format(val, key, field)
) from None # we don't need to chain the exception created 2 lines above
else:
return val
result = tuple(get(key) for key in field.depends_context)
self._cache_key[field] = result
return result
class Environments(object):
""" A common object for all environments in a request. """
@@ -742,7 +773,7 @@ class Cache(object):
def contains(self, record, field):
""" Return whether ``record`` has a value for ``field``. """
if field.depends_context:
key = field.cache_key(record.env)
key = record.env.cache_key(field)
return key in self._data.get(field, {}).get(record.id, {})
return record.id in self._data.get(field, ())
@@ -751,7 +782,7 @@ class Cache(object):
try:
value = self._data[field][record._ids[0]]
if field.depends_context:
value = value[field.cache_key(record.env)]
value = value[record.env.cache_key(field)]
return value
except KeyError:
if default is NOTHING:
@@ -761,7 +792,7 @@ class Cache(object):
def set(self, record, field, value):
""" Set the value of ``field`` for ``record``. """
if field.depends_context:
key = field.cache_key(record.env)
key = record.env.cache_key(field)
self._data[field].setdefault(record._ids[0], {})[key] = value
else:
self._data[field][record._ids[0]] = value
@@ -769,7 +800,7 @@ class Cache(object):
def update(self, records, field, values):
""" Set the values of ``field`` for several ``records``. """
if field.depends_context:
key = field.cache_key(records.env)
key = records.env.cache_key(field)
field_cache = self._data[field]
for record_id, value in zip(records._ids, values):
field_cache.setdefault(record_id, {})[key] = value
@@ -786,7 +817,7 @@ class Cache(object):
def get_values(self, records, field):
""" Return the cached values of ``field`` for ``records``. """
field_cache = self._data[field]
key = field.cache_key(records.env) if field.depends_context else None
key = records.env.cache_key(field) if field.depends_context else None
for record_id in records._ids:
try:
if key is not None:
@@ -799,7 +830,7 @@ class Cache(object):
def get_records_different_from(self, records, field, value):
""" Return the subset of ``records`` that has not ``value`` for ``field``. """
field_cache = self._data[field]
key = field.cache_key(records.env) if field.depends_context else None
key = records.env.cache_key(field) if field.depends_context else None
ids = []
for record_id in records._ids:
try:
@@ -822,7 +853,7 @@ class Cache(object):
values = self._data.get(field, {})
if record.id not in values:
continue
if field.depends_context and field.cache_key(record.env) not in values[record.id]:
if field.depends_context and record.env.cache_key(field) not in values[record.id]:
continue
yield field
-28
View File
@@ -632,34 +632,6 @@ class Field(MetaField('DummyField', (object,), {})):
Property = records.env['ir.property']
return Property.search_multi(self.name, self.model_name, operator, value)
#
# Cache key for context-dependent fields
#
def cache_key(self, env):
""" Return the cache key corresponding to ``self.depends_context``. """
def get(key, get_context=env.context.get):
if key == 'company':
return env.company.id
elif key == 'uid':
return (env.uid, env.su)
elif key == 'active_test':
return get_context('active_test', self.context.get('active_test', True))
else:
v = get_context(key)
try: hash(v)
except TypeError:
raise TypeError(
"Can only create cache keys from hashable values, "
"got non-hashable value {!r} at context key {!r} "
"(dependency of field {})".format(v, key, self)
) from None # we don't need to chain the exception created 2 lines above
else:
return v
return tuple(get(key) for key in self.depends_context)
#
# Setup of field triggers
#