ATLAS Offline Software
AccumulatorCache.py
Go to the documentation of this file.
1 #
2 # Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
3 #
4 
5 from AthenaCommon.Logging import logging
6 _msg = logging.getLogger('AccumulatorCache')
7 
8 import functools
9 import time
10 from abc import ABC, abstractmethod
11 from copy import deepcopy
12 from collections.abc import Hashable, Iterable
13 from collections import defaultdict
14 from dataclasses import dataclass
15 
16 try:
17  from GaudiKernel.DataHandle import DataHandle
18 except ImportError:
19  class DataHandle: pass # for analysis releases
20 
21 
22 class NotHashable(Exception):
23  """Exception thrown when AccumulatorCache is applied to non-hashable function call"""
24  def __init__(self, value):
25  self.value = value
26 
27 
29  """Abstract base for classes needing custom AccumulatorCache behavior."""
30 
31  @abstractmethod
32  def _cacheEvict(self):
33  """This method is called by AccumulatorCache when an object is removed
34  from the cache. Implement this for custom cleanup actions."""
35  pass
36 
37 
38 class AccumulatorDecorator:
39  """Class for use in function decorators, implements memoization.
40 
41  Instances are callable objects that use the
42  hash value calculated from positional and keyword arguments
43  to implement memoization. Methods for suspending and
44  resuming memoization are provided.
45  """
46 
47  _memoize = True
48 
49  VERIFY_NOTHING = 0
50  VERIFY_HASH = 1
51 
52  @dataclass
53  class CacheStats:
54  hits : int = 0
55  misses: int = 0
56  t_hits: float = 0
57  t_misses: float = 0
58 
59  _stats = defaultdict(CacheStats)
60 
61  def __init__(self, func, size, verify, deepCopy):
62  """See AccumulatorCache decorator for documentation of arguments."""
63 
64  functools.update_wrapper(self , func)
65  self._maxSize = size
66  self._func = func
67  self._cache = {}
68  self._resultCache = {}
69  self._verify = verify
70  self._deepcopy = deepCopy
71 
72  if self._verify not in [self.VERIFY_NOTHING, self.VERIFY_HASH]:
73  raise RuntimeError(f"Invalid value for verify ({verify}) in AccumulatorCache for {func}")
74 
75  def getInfo(self):
76  """Return a dictionary with information about the cache size and cache usage"""
77  return {"cache_size" : len(self._cache),
78  "misses" : self._stats[self].misses,
79  "hits" : self._stats[self].hits,
80  "function" : self._func,
81  "result_cache_size" : len(self._resultCache)}
82 
83  @classmethod
84  def printStats(cls):
85  """Print cache statistics"""
86  header = "%-70s | Hits (time) | Misses (time) |" % "AccumulatorCache"
87  print("-"*len(header))
88  print(header)
89  print("-"*len(header))
90  # Print sorted by hit+miss time:
91  for func, stats in sorted(cls._stats.items(), key=lambda s:s[1].t_hits+s[1].t_misses, reverse=True):
92  name = f"{func.__module__}.{func.__name__}"
93  if len(name) > 70:
94  name = '...' + name[-67:]
95  print(f"{name:70} | {stats.hits:6} ({stats.t_hits:4.1f}s) | "
96  f"{stats.misses:6} ({stats.t_misses:4.1f}s) |")
97  print("-"*len(header))
98 
99  @classmethod
100  def suspendCaching(cls):
101  """Suspend memoization for all instances of AccumulatorDecorator."""
102  cls._memoize = False
103 
104  @classmethod
105  def resumeCaching(cls):
106  """Resume memoization for all instances of AccumulatorDecorator."""
107  cls._memoize = True
108 
109  @classmethod
110  def clearCache(cls):
111  """Clear all accumulator caches"""
112  for decor in cls._stats:
113  decor._evictAll()
114  decor._cache.clear()
115  decor._resultCache.clear()
116 
117  cls._stats.clear()
118 
119  def _getHash(x):
120  if hasattr(x, "athHash"):
121  return x.athHash()
122  elif isinstance(x, Hashable):
123  return hash(x)
124  elif isinstance(x, DataHandle):
125  return hash(repr(x))
126  raise NotHashable(x)
127 
128  def _evict(x):
129  """Called when x is removed from the cache"""
130  if isinstance(x, AccumulatorCachable):
131  x._cacheEvict()
132  elif isinstance(x, Iterable) and not isinstance(x, str):
133  for el in x:
134  AccumulatorDecorator._evict(el)
135 
136  def _evictAll(self):
137  for v in self._cache.values():
138  AccumulatorDecorator._evict(v)
139 
140  def __get__(self, obj, objtype):
141  """Support instance methods."""
142  return functools.partial(self.__call__, obj)
143 
144  def __call__(self, *args, **kwargs):
145  cacheHit = None
146  try:
147  t0 = time.perf_counter()
148  res, cacheHit = self._callImpl(*args, **kwargs)
149  return res
150  except NotHashable as e:
151  _msg.warning(f"Argument value '{repr(e.value)}' in {self._func} is not hashable. "
152  "No caching is performed!")
153  cacheHit = False
154  return self._func(*args, **kwargs) # perform regular function call
155  finally:
156  t1 = time.perf_counter()
157  if cacheHit is True:
158  self._stats[self].hits += 1
159  self._stats[self].t_hits += (t1-t0)
160  elif cacheHit is False:
161  self._stats[self].misses += 1
162  self._stats[self].t_misses += (t1-t0)
163 
164  def _callImpl(self, *args, **kwargs):
165  """Implementation of __call__.
166 
167  Returns: (result, cacheHit)
168  """
169 
170  # AccumulatorCache enabled?
171  if not AccumulatorDecorator._memoize:
172  return (self._func(*args , **kwargs), None)
173 
174  # frozen set makes the order of keyword arguments irrelevant
175  hsh = hash( (tuple(AccumulatorDecorator._getHash(a) for a in args),
176  frozenset((hash(k), AccumulatorDecorator._getHash(v)) for k,v in kwargs.items())) )
177 
178  res = self._cache.get(hsh, None)
179  if res is not None:
180  cacheHit = None
181  if AccumulatorDecorator.VERIFY_HASH == self._verify:
182  resHsh = self._resultCache[hsh]
183  chkHsh = AccumulatorDecorator._getHash(res)
184  if chkHsh != resHsh:
185  _msg.debug("Hash of function result, cached using AccumulatorDecorator, changed.")
186  cacheHit = False
187  res = self._func(*args , **kwargs)
188  self._cache[hsh] = res
189  self._resultCache[hsh] = AccumulatorDecorator._getHash(res)
190  else:
191  cacheHit = True
192  else:
193  cacheHit = True
194 
195  if self._deepcopy:
196  return deepcopy(res), cacheHit
197  else:
198  # shallow copied CA still needs to undergo merging
199  from AthenaConfiguration.ComponentAccumulator import ComponentAccumulator
200  if isinstance(res, ComponentAccumulator):
201  res._wasMerged=False
202  return res, cacheHit
203 
204  else:
205  _msg.debug('Hash not found in AccumulatorCache for function %s' , self._func)
206  if len(self._cache) >= self._maxSize:
207  _msg.warning("Cache limit (%d) reached for %s.%s",
208  self._maxSize, self._func.__module__, self._func.__name__)
209  oldest = self._cache.pop(next(iter(self._cache)))
210  AccumulatorDecorator._evict(oldest)
211 
212  res = self._func(*args , **kwargs)
213 
214  if AccumulatorDecorator.VERIFY_HASH == self._verify:
215  if len(self._resultCache) >= self._maxSize:
216  del self._resultCache[next(iter(self._resultCache))]
217  self._resultCache[hsh] = AccumulatorDecorator._getHash(res)
218  self._cache[hsh] = res
219  else:
220  self._cache[hsh] = res
221 
222  return (deepcopy(res) if self._deepcopy else res, False)
223 
224  def __del__(self):
225  self._evictAll()
226 
227 
228 def AccumulatorCache(func = None, maxSize = 128,
229  verifyResult = AccumulatorDecorator.VERIFY_NOTHING, deepCopy = True):
230  """Function decorator, implements memoization.
231 
232  Keyword arguments:
233  maxSize: maximum size for the cache associated with the function (default 128)
234  verifyResult: takes two possible values
235 
236  AccumulatorDecorator.VERIFY_NOTHING - default, the cached function result is returned with no verification
237  AccumulatorDecorator.VERIFY_HASH - before returning the cached function value, the hash of the
238  result is checked to verify if this object was not modified
239  between function calls
240  deepCopy: if True (default) a deep copy of the function result will be stored in the cache.
241 
242  Returns:
243  An instance of AccumulatorDecorator.
244  """
245 
246  def wrapper_accumulator(func):
247  return AccumulatorDecorator(func, maxSize, verifyResult, deepCopy)
248 
249  return wrapper_accumulator(func) if func else wrapper_accumulator
python.AccumulatorCache.AccumulatorDecorator.suspendCaching
def suspendCaching(cls)
Definition: AccumulatorCache.py:100
python.AccumulatorCache.DataHandle
Definition: AccumulatorCache.py:19
python.AccumulatorCache.NotHashable.__init__
def __init__(self, value)
Definition: AccumulatorCache.py:24
python.AccumulatorCache.AccumulatorDecorator._stats
_stats
Definition: AccumulatorCache.py:59
python.AccumulatorCache.AccumulatorDecorator._callImpl
def _callImpl(self, *args, **kwargs)
Definition: AccumulatorCache.py:164
python.AccumulatorCache.AccumulatorDecorator.__get__
def __get__(self, obj, objtype)
Definition: AccumulatorCache.py:140
python.AccumulatorCache.AccumulatorDecorator._getHash
def _getHash(x)
Definition: AccumulatorCache.py:119
python.AccumulatorCache.AccumulatorDecorator.resumeCaching
def resumeCaching(cls)
Definition: AccumulatorCache.py:105
python.AccumulatorCache.AccumulatorDecorator._cache
_cache
Definition: AccumulatorCache.py:67
python.AccumulatorCache.AccumulatorDecorator._resultCache
_resultCache
Definition: AccumulatorCache.py:68
python.AccumulatorCache.AccumulatorDecorator._maxSize
_maxSize
Definition: AccumulatorCache.py:65
python.Bindings.values
values
Definition: Control/AthenaPython/python/Bindings.py:797
python.AccumulatorCache.AccumulatorDecorator.clearCache
def clearCache(cls)
Definition: AccumulatorCache.py:110
python.AccumulatorCache.AccumulatorDecorator._verify
_verify
Definition: AccumulatorCache.py:69
python.AccumulatorCache.AccumulatorDecorator._memoize
bool _memoize
Definition: AccumulatorCache.py:47
python.AccumulatorCache.AccumulatorDecorator._func
_func
Definition: AccumulatorCache.py:66
fillPileUpNoiseLumi.next
next
Definition: fillPileUpNoiseLumi.py:52
python.AccumulatorCache.AccumulatorDecorator._deepcopy
_deepcopy
Definition: AccumulatorCache.py:70
python.AccumulatorCache.AccumulatorDecorator.__del__
def __del__(self)
Definition: AccumulatorCache.py:224
PyAthena::repr
std::string repr(PyObject *o)
returns the string representation of a python object equivalent of calling repr(o) in python
Definition: PyAthenaUtils.cxx:106
python.AccumulatorCache.AccumulatorDecorator.__init__
def __init__(self, func, size, verify, deepCopy)
Definition: AccumulatorCache.py:61
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename T::value_type > sorted(T begin, T end)
Helper function to create a sorted vector from an unsorted one.
python.AccumulatorCache.AccumulatorCachable._cacheEvict
def _cacheEvict(self)
Definition: AccumulatorCache.py:32
python.AccumulatorCache.AccumulatorDecorator.getInfo
def getInfo(self)
Definition: AccumulatorCache.py:75
python.AccumulatorCache.AccumulatorDecorator.CacheStats
Definition: AccumulatorCache.py:53
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:79
python.AccumulatorCache.AccumulatorDecorator
Definition: AccumulatorCache.py:38
python.AccumulatorCache.AccumulatorDecorator.VERIFY_NOTHING
int VERIFY_NOTHING
Definition: AccumulatorCache.py:49
python.AccumulatorCache.AccumulatorDecorator._evict
def _evict(x)
Definition: AccumulatorCache.py:128
VKalVrtAthena::varHolder_detail::clear
void clear(T &var)
Definition: NtupleVars.h:48
CaloCondBlobAlgs_fillNoiseFromASCII.hash
dictionary hash
Definition: CaloCondBlobAlgs_fillNoiseFromASCII.py:109
python.AccumulatorCache.AccumulatorDecorator.printStats
def printStats(cls)
Definition: AccumulatorCache.py:84
python.AccumulatorCache.NotHashable.value
value
Definition: AccumulatorCache.py:25
Muon::print
std::string print(const MuPatSegment &)
Definition: MuonTrackSteering.cxx:28
get
T * get(TKey *tobj)
get a TObject* from a TKey* (why can't a TObject be a TKey?)
Definition: hcg.cxx:127
python.AccumulatorCache.AccumulatorCachable
Definition: AccumulatorCache.py:28
python.AccumulatorCache.AccumulatorDecorator.__call__
def __call__(self, *args, **kwargs)
Definition: AccumulatorCache.py:144
python.AccumulatorCache.AccumulatorDecorator.VERIFY_HASH
int VERIFY_HASH
Definition: AccumulatorCache.py:50
python.AccumulatorCache.AccumulatorDecorator._evictAll
def _evictAll(self)
Definition: AccumulatorCache.py:136
python.AccumulatorCache.NotHashable
Definition: AccumulatorCache.py:22
python.AccumulatorCache.AccumulatorCache
def AccumulatorCache(func=None, maxSize=128, verifyResult=AccumulatorDecorator.VERIFY_NOTHING, deepCopy=True)
Definition: AccumulatorCache.py:228