gooderp18绿色标准版
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
7.2KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. # decorator makes wrappers that have the same API as their wrapped function
  4. from collections import Counter, defaultdict
  5. from decorator import decorator
  6. from inspect import signature, Parameter
  7. import logging
  8. import time
  9. import warnings
  10. unsafe_eval = eval
  11. _logger = logging.getLogger(__name__)
  12. class ormcache_counter(object):
  13. """ Statistic counters for cache entries. """
  14. __slots__ = ['hit', 'miss', 'err', 'gen_time', 'cache_name']
  15. def __init__(self):
  16. self.hit = 0
  17. self.miss = 0
  18. self.err = 0
  19. self.gen_time = 0
  20. self.cache_name = None
  21. @property
  22. def ratio(self):
  23. return 100.0 * self.hit / (self.hit + self.miss or 1)
  24. # statistic counters dictionary, maps (dbname, modelname, method) to counter
  25. STAT = defaultdict(ormcache_counter)
  26. class ormcache(object):
  27. """ LRU cache decorator for model methods.
  28. The parameters are strings that represent expressions referring to the
  29. signature of the decorated method, and are used to compute a cache key::
  30. @ormcache('model_name', 'mode')
  31. def _compute_domain(self, model_name, mode="read"):
  32. ...
  33. For the sake of backward compatibility, the decorator supports the named
  34. parameter `skiparg`::
  35. @ormcache(skiparg=1)
  36. def _compute_domain(self, model_name, mode="read"):
  37. ...
  38. Methods implementing this decorator should never return a Recordset,
  39. because the underlying cursor will eventually be closed and raise a
  40. `psycopg2.InterfaceError`.
  41. """
  42. def __init__(self, *args, **kwargs):
  43. self.args = args
  44. self.skiparg = kwargs.get('skiparg')
  45. self.cache_name = kwargs.get('cache', 'default')
  46. def __call__(self, method):
  47. self.method = method
  48. self.determine_key()
  49. lookup = decorator(self.lookup, method)
  50. lookup.__cache__ = self
  51. return lookup
  52. def add_value(self, *args, cache_value=None, **kwargs):
  53. model = args[0]
  54. d, key0, counter = self.lru(model)
  55. counter.cache_name = self.cache_name
  56. key = key0 + self.key(*args, **kwargs)
  57. d[key] = cache_value
  58. def determine_key(self):
  59. """ Determine the function that computes a cache key from arguments. """
  60. if self.skiparg is None:
  61. # build a string that represents function code and evaluate it
  62. args = ', '.join(
  63. # remove annotations because lambdas can't be type-annotated,
  64. # and defaults because they are redundant (defaults are present
  65. # in the wrapper function itself)
  66. str(params.replace(annotation=Parameter.empty, default=Parameter.empty))
  67. for params in signature(self.method).parameters.values()
  68. )
  69. if self.args:
  70. code = "lambda %s: (%s,)" % (args, ", ".join(self.args))
  71. else:
  72. code = "lambda %s: ()" % (args,)
  73. self.key = unsafe_eval(code)
  74. else:
  75. # backward-compatible function that uses self.skiparg
  76. self.key = lambda *args, **kwargs: args[self.skiparg:]
  77. def lru(self, model):
  78. counter = STAT[(model.pool.db_name, model._name, self.method)]
  79. return model.pool._Registry__caches[self.cache_name], (model._name, self.method), counter
  80. def lookup(self, method, *args, **kwargs):
  81. d, key0, counter = self.lru(args[0])
  82. key = key0 + self.key(*args, **kwargs)
  83. try:
  84. r = d[key]
  85. counter.hit += 1
  86. return r
  87. except KeyError:
  88. counter.miss += 1
  89. counter.cache_name = self.cache_name
  90. start = time.time()
  91. value = d[key] = self.method(*args, **kwargs)
  92. counter.gen_time += time.time() - start
  93. return value
  94. except TypeError:
  95. _logger.warning("cache lookup error on %r", key, exc_info=True)
  96. counter.err += 1
  97. return self.method(*args, **kwargs)
  98. def clear(self, model, *args):
  99. """ Clear the registry cache """
  100. warnings.warn('Deprecated method ormcache.clear(model, *args), use registry.clear_cache() instead')
  101. model.pool.clear_all_caches()
  102. class 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. def determine_key(self):
  112. """ Determine the function that computes a cache key from arguments. """
  113. assert self.skiparg is None, "ormcache_context() no longer supports skiparg"
  114. # build a string that represents function code and evaluate it
  115. sign = signature(self.method)
  116. args = ', '.join(
  117. str(params.replace(annotation=Parameter.empty, default=Parameter.empty))
  118. for params in sign.parameters.values()
  119. )
  120. cont_expr = "(context or {})" if 'context' in sign.parameters else "self._context"
  121. keys_expr = "tuple(%s.get(k) for k in %r)" % (cont_expr, self.keys)
  122. if self.args:
  123. code = "lambda %s: (%s, %s)" % (args, ", ".join(self.args), keys_expr)
  124. else:
  125. code = "lambda %s: (%s,)" % (args, keys_expr)
  126. self.key = unsafe_eval(code)
  127. def log_ormcache_stats(sig=None, frame=None): # noqa: ARG001 (arguments are there for signals)
  128. """ Log statistics of ormcache usage by database, model, and method. """
  129. from odoo.modules.registry import Registry
  130. cache_entries = {}
  131. current_db = None
  132. cache_stats = ['Caches stats:']
  133. for (dbname, model, method), stat in sorted(STAT.items(), key=lambda k: (k[0][0] or '~', k[0][1], k[0][2].__name__)):
  134. dbname_display = dbname or "<no_db>"
  135. if current_db != dbname_display:
  136. current_db = dbname_display
  137. cache_stats.append(f"Database {dbname_display}")
  138. if dbname: # mainly for MockPool
  139. if (dbname, stat.cache_name) not in cache_entries:
  140. cache = Registry.registries.d[dbname]._Registry__caches[stat.cache_name]
  141. cache_entries[dbname, stat.cache_name] = Counter(k[:2] for k in cache.d)
  142. nb_entries = cache_entries[dbname, stat.cache_name][model, method]
  143. else:
  144. nb_entries = 0
  145. cache_name = stat.cache_name.rjust(25)
  146. cache_stats.append(
  147. f"{cache_name}, {nb_entries:6d} entries, {stat.hit:6d} hit, {stat.miss:6d} miss, {stat.err:6d} err, {stat.gen_time:10.3f}s time, {stat.ratio:6.1f}% ratio for {model}.{method.__name__}"
  148. )
  149. _logger.info('\n'.join(cache_stats))
  150. def get_cache_key_counter(bound_method, *args, **kwargs):
  151. """ Return the cache, key and stat counter for the given call. """
  152. model = bound_method.__self__
  153. ormcache = bound_method.__cache__
  154. cache, key0, counter = ormcache.lru(model)
  155. key = key0 + ormcache.key(model, *args, **kwargs)
  156. return cache, key, counter
  157. # For backward compatibility
  158. cache = ormcache
上海开阖软件有限公司 沪ICP备12045867号-1