1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4# decorator makes wrappers that have the same API as their wrapped function 5from collections import Counter, defaultdict 6from decorator import decorator 7from inspect import signature 8import logging 9 10unsafe_eval = eval 11 12_logger = logging.getLogger(__name__) 13 14 15class ormcache_counter(object): 16 """ Statistic counters for cache entries. """ 17 __slots__ = ['hit', 'miss', 'err'] 18 19 def __init__(self): 20 self.hit = 0 21 self.miss = 0 22 self.err = 0 23 24 @property 25 def ratio(self): 26 return 100.0 * self.hit / (self.hit + self.miss or 1) 27 28# statistic counters dictionary, maps (dbname, modelname, method) to counter 29STAT = defaultdict(ormcache_counter) 30 31 32class ormcache(object): 33 """ LRU cache decorator for model methods. 34 The parameters are strings that represent expressions referring to the 35 signature of the decorated method, and are used to compute a cache key:: 36 37 @ormcache('model_name', 'mode') 38 def _compute_domain(self, model_name, mode="read"): 39 ... 40 41 For the sake of backward compatibility, the decorator supports the named 42 parameter `skiparg`:: 43 44 @ormcache(skiparg=1) 45 def _compute_domain(self, model_name, mode="read"): 46 ... 47 48 Methods implementing this decorator should never return a Recordset, 49 because the underlying cursor will eventually be closed and raise a 50 `psycopg2.OperationalError`. 51 """ 52 def __init__(self, *args, **kwargs): 53 self.args = args 54 self.skiparg = kwargs.get('skiparg') 55 56 def __call__(self, method): 57 self.method = method 58 self.determine_key() 59 lookup = decorator(self.lookup, method) 60 lookup.clear_cache = self.clear 61 return lookup 62 63 def determine_key(self): 64 """ Determine the function that computes a cache key from arguments. """ 65 if self.skiparg is None: 66 # build a string that represents function code and evaluate it 67 args = str(signature(self.method))[1:-1] 68 if self.args: 69 code = "lambda %s: (%s,)" % (args, ", ".join(self.args)) 70 else: 71 code = "lambda %s: ()" % (args,) 72 self.key = unsafe_eval(code) 73 else: 74 # backward-compatible function that uses self.skiparg 75 self.key = lambda *args, **kwargs: args[self.skiparg:] 76 77 def lru(self, model): 78 counter = STAT[(model.pool.db_name, model._name, self.method)] 79 return model.pool._Registry__cache, (model._name, self.method), counter 80 81 def lookup(self, method, *args, **kwargs): 82 d, key0, counter = self.lru(args[0]) 83 key = key0 + self.key(*args, **kwargs) 84 try: 85 r = d[key] 86 counter.hit += 1 87 return r 88 except KeyError: 89 counter.miss += 1 90 value = d[key] = self.method(*args, **kwargs) 91 return value 92 except TypeError: 93 _logger.warning("cache lookup error on %r", key, exc_info=True) 94 counter.err += 1 95 return self.method(*args, **kwargs) 96 97 def clear(self, model, *args): 98 """ Clear the registry cache """ 99 model.pool._clear_cache() 100 101 102class ormcache_context(ormcache): 103 """ This LRU cache decorator is a variant of :class:`ormcache`, with an 104 extra parameter ``keys`` that defines a sequence of dictionary keys. Those 105 keys are looked up in the ``context`` parameter and combined to the cache 106 key made by :class:`ormcache`. 107 """ 108 def __init__(self, *args, **kwargs): 109 super(ormcache_context, self).__init__(*args, **kwargs) 110 self.keys = kwargs['keys'] 111 112 def determine_key(self): 113 """ Determine the function that computes a cache key from arguments. """ 114 assert self.skiparg is None, "ormcache_context() no longer supports skiparg" 115 # build a string that represents function code and evaluate it 116 sign = signature(self.method) 117 args = str(sign)[1:-1] 118 cont_expr = "(context or {})" if 'context' in sign.parameters else "self._context" 119 keys_expr = "tuple(%s.get(k) for k in %r)" % (cont_expr, self.keys) 120 if self.args: 121 code = "lambda %s: (%s, %s)" % (args, ", ".join(self.args), keys_expr) 122 else: 123 code = "lambda %s: (%s,)" % (args, keys_expr) 124 self.key = unsafe_eval(code) 125 126 127class ormcache_multi(ormcache): 128 """ This LRU cache decorator is a variant of :class:`ormcache`, with an 129 extra parameter ``multi`` that gives the name of a parameter. Upon call, the 130 corresponding argument is iterated on, and every value leads to a cache 131 entry under its own key. 132 """ 133 def __init__(self, *args, **kwargs): 134 super(ormcache_multi, self).__init__(*args, **kwargs) 135 self.multi = kwargs['multi'] 136 137 def determine_key(self): 138 """ Determine the function that computes a cache key from arguments. """ 139 assert self.skiparg is None, "ormcache_multi() no longer supports skiparg" 140 assert isinstance(self.multi, str), "ormcache_multi() parameter multi must be an argument name" 141 142 super(ormcache_multi, self).determine_key() 143 144 # key_multi computes the extra element added to the key 145 sign = signature(self.method) 146 args = str(sign)[1:-1] 147 code_multi = "lambda %s: %s" % (args, self.multi) 148 self.key_multi = unsafe_eval(code_multi) 149 150 # self.multi_pos is the position of self.multi in args 151 self.multi_pos = list(sign.parameters).index(self.multi) 152 153 def lookup(self, method, *args, **kwargs): 154 d, key0, counter = self.lru(args[0]) 155 base_key = key0 + self.key(*args, **kwargs) 156 ids = self.key_multi(*args, **kwargs) 157 result = {} 158 missed = [] 159 160 # first take what is available in the cache 161 for i in ids: 162 key = base_key + (i,) 163 try: 164 result[i] = d[key] 165 counter.hit += 1 166 except Exception: 167 counter.miss += 1 168 missed.append(i) 169 170 if missed: 171 # call the method for the ids that were not in the cache; note that 172 # thanks to decorator(), the multi argument will be bound and passed 173 # positionally in args. 174 args = list(args) 175 args[self.multi_pos] = missed 176 result.update(method(*args, **kwargs)) 177 178 # store those new results back in the cache 179 for i in missed: 180 key = base_key + (i,) 181 d[key] = result[i] 182 183 return result 184 185 186class dummy_cache(object): 187 """ Cache decorator replacement to actually do no caching. """ 188 def __init__(self, *l, **kw): 189 pass 190 191 def __call__(self, fn): 192 fn.clear_cache = self.clear 193 return fn 194 195 def clear(self, *l, **kw): 196 pass 197 198 199def log_ormcache_stats(sig=None, frame=None): 200 """ Log statistics of ormcache usage by database, model, and method. """ 201 from odoo.modules.registry import Registry 202 import threading 203 204 me = threading.currentThread() 205 me_dbname = getattr(me, 'dbname', 'n/a') 206 207 for dbname, reg in sorted(Registry.registries.d.items()): 208 # set logger prefix to dbname 209 me.dbname = dbname 210 entries = Counter(k[:2] for k in reg._Registry__cache.d) 211 # show entries sorted by model name, method name 212 for key in sorted(entries, key=lambda key: (key[0], key[1].__name__)): 213 model, method = key 214 stat = STAT[(dbname, model, method)] 215 _logger.info( 216 "%6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s", 217 entries[key], stat.hit, stat.miss, stat.err, stat.ratio, model, method.__name__, 218 ) 219 220 me.dbname = me_dbname 221 222 223def get_cache_key_counter(bound_method, *args, **kwargs): 224 """ Return the cache, key and stat counter for the given call. """ 225 model = bound_method.__self__ 226 ormcache = bound_method.clear_cache.__self__ 227 cache, key0, counter = ormcache.lru(model) 228 key = key0 + ormcache.key(model, *args, **kwargs) 229 return cache, key, counter 230 231# For backward compatibility 232cache = ormcache 233