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