xref: /xnu-10063.121.3/tools/lldbmacros/core/caching.py (revision 2c2f96dc2b9a4408a43d3150ae9c105355ca3daa)
1"""
2A basic caching module for xnu debug macros to use.
3
4
5When to use caching?
6~~~~~~~~~~~~~~~~~~~~
7
8Very often you do not need to: LLDB already provides extensive data caching.
9
10The most common things that need caching are:
11- types (the gettype() function provides this)
12- globals (kern.globals / kern.GetGlobalVariable() provides this)
13
14
15If your macro is slow to get some data, before slapping a caching decorator,
16please profile your code using `xnudebug profile`. Very often slowness happens
17due to the usage of CreateValueFromExpression() which spins a full compiler
18to parse relatively trivial expressions and is easily 10-100x as slow as
19alternatives like CreateValueFromAddress().
20
21Only use caching once you have eliminated those obvious performance hogs
22and an A/B shows meaningful speed improvements over the base line.
23
24
25I really need caching how does it work?
26~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27
28This module provides function decorators to easily cache the result of
29functions based on their parameters, while keeping the caches separated
30per lldb target.
31
32@cache_statically can be used to cache data once per target,
33because it won't change if the process is resumed and stopped again.
34For example: types, constant values, global addresses, ...
35
36@cache_dynamically can be used to cache data that is expensive to compute
37but which content depends on the state of the process. It will be
38automatically invalidated for you if the process is resumed and
39then stops or hits a breakpoint.
40
41@dyn_cached_property can be used on instances to turn a member into
42a per-target cached dynamic property that will be cleaned up when
43the object dies or if the process is resumed and then stops
44or hits a breakpoint.
45
46Functions using these decorators, must have a `target=None` named argument
47that no caller of those functions need to pass explicitly, the decorator
48will take care of passing the proper current target value (equivalent to
49LazyTarget.GetTarget() except more efficiently).
50
51
52Cache Invalidation
53~~~~~~~~~~~~~~~~~~
54
55This module make the crucial assumption that no XNU lldb macro
56will step/resume/... processes as the invalidation only happens
57when a new command is being run.
58
59If some macro needs to step/resume it has to provide its own
60cache invalidation, by e.g. pushing its own ImplicitContext()
61around such process state manipulations.
62"""
63
64# Private Routines and objects
65from collections import namedtuple
66
67import functools
68import gc
69import inspect
70import sys
71import weakref
72
73import lldb
74
75from .configuration import config
76from . import lldbwrap
77
78
79class _Registry(list):
80    """ Private class that holds a list of _Cache instances """
81
82    __slots__ = ('name')
83
84    def __init__(self, name):
85        super(_Registry, self).__init__()
86        self.name = name
87
88    @property
89    def size(self):
90        return sys.getsizeof(self)
91
92    def invalidate(self, pid):
93        for cache in self:
94            cache.invalidate(pid)
95
96    def clear(self):
97        for cache in self:
98            cache.clear()
99
100    def __repr__(self):
101        return "_Registry({}, {})".format(
102            self.name, super(_Registry, self).__repr__())
103
104    def __str__(self):
105        return "_Registry({}, {} caches, size {})".format(
106            self.name, len(self), self.size)
107
108
109class _Cache(dict):
110    """ Private class that implements a given global function _Cache
111
112        Those are created with the @cache_statically/@cache_dynamically
113        decorators
114    """
115
116    __slots__ = ('name')
117
118    def __init__(self, name, registry):
119        super(_Cache, self).__init__()
120        self.name = name
121        registry.append(self)
122
123    @property
124    def size(self):
125        return sys.getsizeof(self)
126
127    def invalidate(self, pid):
128        if pid in self: del self[pid]
129
130    def __repr__(self):
131        return "_Cache({}, {})".format(
132            self.name, super(_Cache, self).__repr__())
133
134    def __str__(self):
135        return "_Cache({}, {} entries, size {})".format(
136            self.name, len(self), self.size)
137
138
139_static_registry     = _Registry('static')
140_dynamic_registry    = _Registry('dynamic')
141_dynamic_keys        = {}
142
143_implicit_target     = None
144_implicit_process    = None
145_implicit_exe_id     = None
146_implicit_dynkey     = None
147
148
149class _DynamicKey(object):
150    """ Wraps a process StopID as a key that can be used as caches keys """
151
152    def __init__(self, exe_id, stop_id):
153        self.exe_id  = exe_id
154        self.stop_id = stop_id
155
156    def __hash__(self):
157        return id(self)
158
159
160class LazyTarget(object):
161    """ A common object that lazy-evaluates and caches the lldb.SBTarget
162        and lldb.SBProcess for the current interactive debugging session.
163    """
164
165    @staticmethod
166    def _CacheUpdateAnchor(exe_id, process):
167        global _dynamic_keys, _dynamic_registry
168
169        stop_id = process.GetStopID()
170        dyn_key = _dynamic_keys.get(exe_id)
171
172        if dyn_key is None:
173            _dynamic_keys[exe_id] = dyn_key = _DynamicKey(exe_id, stop_id)
174        elif dyn_key.stop_id != stop_id:
175            _dynamic_registry.invalidate(exe_id)
176            _dynamic_keys[exe_id] = dyn_key = _DynamicKey(exe_id, stop_id)
177            gc.collect()
178
179        return dyn_key
180
181    @staticmethod
182    def _CacheGC():
183        global _dynamic_keys, _dynamic_registry, _static_registry
184
185        exe_ids = _dynamic_keys.keys() - set(
186            tg.GetProcess().GetUniqueID()
187            for tg in lldb.debugger
188        )
189
190        for exe_id in exe_ids:
191            _static_registry.invalidate(exe_id)
192            _dynamic_registry.invalidate(exe_id)
193            del _dynamic_keys[exe_id]
194
195        if len(exe_ids):
196            gc.collect()
197
198    @staticmethod
199    def _CacheGetDynamicKey():
200        """ Get a _DynamicKey for the most likely current process
201        """
202        global _implicit_dynkey
203
204        dyn_key = _implicit_dynkey
205        if dyn_key is None:
206            process = lldbwrap.GetProcess()
207            exe_id  = process.GetUniqueID()
208            dyn_key = LazyTarget._CacheUpdateAnchor(exe_id, process)
209
210        return dyn_key
211
212    @staticmethod
213    def _CacheClear():
214        """ remove all cached data.
215        """
216        global _dynamic_registry, _static_registry, _dynamic_keys
217
218        _dynamic_registry.clear()
219        _static_registry.clear()
220        _dynamic_keys.clear()
221
222    @staticmethod
223    def _CacheSize():
224        """ Returns number of bytes held in cache.
225            returns:
226                int - size of cache including static and dynamic
227        """
228        global _dynamic_registry, _static_registry
229
230        return _dynamic_registry.size + _static_registry.size
231
232
233    @staticmethod
234    def GetTarget():
235        """ Get the SBTarget that is the most likely current target
236        """
237        global _implicit_target
238
239        return _implicit_target or lldbwrap.GetTarget()
240
241    @staticmethod
242    def GetProcess():
243        """ Get an SBProcess for the most likely current process
244        """
245        global _implicit_process
246
247        return _implicit_process or lldbwrap.GetProcess()
248
249
250class ImplicitContext(object):
251    """ This class sets up the implicit target/process
252        being used by the XNu lldb macros system.
253
254        In order for lldb macros to function properly, such a context
255        must be used around code being run, otherwise macros will try
256        to infer it from the current lldb selected target which is
257        incorrect in certain contexts.
258
259        typical usage is:
260
261            with ImplicitContext(thing):
262                # code
263
264        where @c thing is any of an SBExecutionContext, an SBValue,
265        an SBBreakpoint, an SBProcess, or an SBTarget.
266    """
267
268    __slots__ = ('target', 'process', 'exe_id', 'old_ctx')
269
270    def __init__(self, arg):
271        if isinstance(arg, lldb.SBExecutionContext):
272            exe_ctx = lldbwrap.SBExecutionContext(arg)
273            target  = exe_ctx.GetTarget()
274            process = exe_ctx.GetProcess()
275        elif isinstance(arg, lldb.SBValue):
276            target  = lldbwrap.SBTarget(arg.GetTarget())
277            process = target.GetProcess()
278        elif isinstance(arg, lldb.SBBreakpoint):
279            bpoint  = lldbwrap.SBBreakpoint(arg)
280            target  = bpoint.GetTarget()
281            process = target.GetProcess()
282        elif isinstance(arg, lldb.SBProcess):
283            process = lldbwrap.SBProcess(arg)
284            target  = process.GetTarget()
285        elif isinstance(arg, lldb.SBTarget):
286            target  = lldbwrap.SBTarget(arg)
287            process = target.GetProcess()
288        else:
289            raise TypeError("argument type unsupported {}".format(
290                arg.__class__.__name__))
291
292        self.target  = target
293        self.process = process
294        self.exe_id  = process.GetUniqueID()
295        self.old_ctx = None
296
297    def __enter__(self):
298        global _implicit_target, _implicit_process, _implicit_exe_id
299        global _implicit_dynkey, _dynamic_keys
300
301        self.old_ctx = (_implicit_target, _implicit_process, _implicit_exe_id)
302
303        _implicit_target  = self.target
304        _implicit_process = process = self.process
305        _implicit_exe_id  = exe_id = self.exe_id
306        _implicit_dynkey  = LazyTarget._CacheUpdateAnchor(exe_id, process)
307
308        if len(_dynamic_keys) > 1:
309            LazyTarget._CacheGC()
310
311    def __exit__(self, *args):
312        global _implicit_target, _implicit_process, _implicit_exe_id
313        global _implicit_dynkey, _dynamic_keys
314
315        target, process, exe_id = self.old_ctx
316        self.old_ctx = None
317
318        _implicit_target  = target
319        _implicit_process = process
320        _implicit_exe_id  = exe_id
321
322        if process:
323            _implicit_dynkey = LazyTarget._CacheUpdateAnchor(exe_id, process)
324        else:
325            _implicit_dynkey = None
326
327
328class _HashedSeq(list):
329    """ This class guarantees that hash() will be called no more than once
330        per element.  This is important because the lru_cache() will hash
331        the key multiple times on a cache miss.
332
333        Inspired by python3's lru_cache decorator implementation
334    """
335
336    __slots__ = 'hashvalue'
337
338    def __init__(self, tup, hash=hash):
339        self[:] = tup
340        self.hashvalue = hash(tup)
341
342    def __hash__(self):
343        return self.hashvalue
344
345    @classmethod
346    def make_key(cls, args, kwds, kwd_mark = (object(),),
347        fasttypes = {int, str}, tuple=tuple, type=type, len=len):
348
349        """ Inspired from python3's cache implementation """
350
351        key = args
352        if kwds:
353            key += kwd_mark
354            key += tuple(kwd.items())
355        elif len(key) == 0:
356            return None
357        elif len(key) == 1 and type(key[0]) in fasttypes:
358            return key[0]
359        return cls(key)
360
361
362def _cache_with_registry(fn, registry, maxsize=128, sentinel=object()):
363    """ Internal function """
364
365    nokey = False
366
367    if hasattr(inspect, 'signature'): # PY3
368        sig = inspect.signature(fn)
369        tg  = sig.parameters.get('target')
370        if not tg or tg.default is not None:
371            raise ValueError("function doesn't have a 'target=None' argument")
372
373        nokey = len(sig.parameters) == 1
374        cache = _Cache(fn.__qualname__, registry)
375    else:
376        spec = inspect.getargspec(fn)
377        try:
378            index = spec.args.index('target')
379            offs  = len(spec.args) - len(spec.defaults)
380            if index < offs or spec.defaults[index - offs] is not None:
381                raise ValueError
382        except:
383            raise ValueError("function doesn't have a 'target=None' argument")
384
385        nokey = len(spec.args) == 1 and spec.varargs is None and spec.keywords is None
386        cache = _Cache(fn.__name__, registry)
387
388    c_setdef = cache.setdefault
389    c_get    = cache.get
390    make_key = _HashedSeq.make_key
391    getdynk  = LazyTarget._CacheGetDynamicKey
392    gettg    = LazyTarget.GetTarget
393
394    if nokey:
395        def caching_wrapper(*args, **kwds):
396            global _implicit_exe_id, _implicit_target
397
398            key = _implicit_exe_id or getdynk().exe_id
399            result = c_get(key, sentinel)
400            if result is not sentinel:
401                return result
402
403            kwds['target'] = _implicit_target or gettg()
404            return c_setdef(key, fn(*args, **kwds))
405
406        def cached(*args, **kwds):
407            global _implicit_exe_id
408
409            return c_get(_implicit_exe_id or getdynk().exe_id, sentinel) != sentinel
410    else:
411        def caching_wrapper(*args, **kwds):
412            global _implicit_exe_id, _implicit_target
413
414            tg_d   = c_setdef(_implicit_exe_id or getdynk().exe_id, {})
415            c_key  = make_key(args, kwds)
416            result = tg_d.get(c_key, sentinel)
417            if result is not sentinel:
418                return result
419
420            #
421            # Blunt policy to avoid exploding memory,
422            # that is simpler than an actual LRU.
423            #
424            # TODO: be smarter?
425            #
426            if len(tg_d) >= maxsize: tg_d.clear()
427
428            kwds['target'] = _implicit_target or gettg()
429            return tg_d.setdefault(c_key, fn(*args, **kwds))
430
431        def cached(*args, **kwds):
432            global _implicit_exe_id
433
434            tg_d = c_get(_implicit_exe_id or getdynk().exe_id)
435            return tg_d and tg_d.get(make_key(args, kwds), sentinel) != sentinel
436
437    caching_wrapper.cached = cached
438    return functools.update_wrapper(caching_wrapper, fn)
439
440
441def cache_statically(fn):
442    """ Decorator to cache the results statically
443
444        This basically makes the decorated function cache its result based
445        on its arguments with an automatic static per target cache
446
447        The function must have a named parameter called 'target' defaulting
448        to None, with no clients ever passing it explicitly.  It will be
449        passed the proper SBTarget when called.
450
451        @cache_statically(user_function)
452            Cache the results of this function automatically per target,
453            using the arguments of the function as the cache key.
454    """
455
456    return _cache_with_registry(fn, _static_registry)
457
458
459def cache_dynamically(fn):
460    """ Decorator to cache the results dynamically
461
462        This basically makes the decorated function cache its result based
463        on its arguments with an automatic dynamic cache that is reset
464        every time the process state changes
465
466        The function must have a named parameter called 'target' defaulting
467        to None, with no clients ever passing it explicitly.  It will be
468        passed the proper SBTarget when called.
469
470        @cache_dynamically(user_function)
471            Cache the results of this function automatically per target,
472            using the arguments of the function as the cache key.
473    """
474
475    return _cache_with_registry(fn, _dynamic_registry)
476
477
478def dyn_cached_property(fn, sentinel=object()):
479    """ Decorator to make a class or method property cached per instance
480
481        The method must have the prototype:
482
483            def foo(self, target=None)
484
485        and will generate the property "foo".
486    """
487
488    if hasattr(inspect, 'signature'): # PY3
489        if list(inspect.signature(fn).parameters) != ['self', 'target']:
490            raise ValueError("function signature must be (self, target=None)")
491    else:
492        spec = inspect.getargspec(fn)
493        if spec.args != ['self', 'target'] or \
494                spec.varargs is not None or spec.keywords is not None:
495            raise ValueError("function signature must be (self, target=None)")
496
497    getdynk  = LazyTarget._CacheGetDynamicKey
498    gettg    = LazyTarget.GetTarget
499    c_attr   = "_dyn_key__" + fn.__name__
500
501    def dyn_cached_property_wrapper(self, target=None):
502        global _implicit_dynkey, _implicit_target
503
504        cache = getattr(self, c_attr, None)
505        if cache is None:
506            cache = weakref.WeakKeyDictionary()
507            setattr(self, c_attr, cache)
508
509        c_key  = _implicit_dynkey or getdynk()
510        result = cache.get(c_key, sentinel)
511        if result is not sentinel:
512            return result
513
514        return cache.setdefault(c_key, fn(self, _implicit_target or gettg()))
515
516    return property(functools.update_wrapper(dyn_cached_property_wrapper, fn))
517
518
519ClearAllCache = LazyTarget._CacheClear
520
521GetSizeOfCache = LazyTarget._CacheSize
522
523__all__ = [
524    LazyTarget.__name__,
525    ImplicitContext.__name__,
526
527    cache_statically.__name__,
528    cache_dynamically.__name__,
529    dyn_cached_property.__name__,
530
531    ClearAllCache.__name__,
532    GetSizeOfCache.__name__,
533]
534