From e810e1774d29dcac956a58affd4e3cc80b58ae12 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 25 Aug 2009 16:14:20 +0200 Subject: [PATCH] simplify caching class --HG-- branch : trunk --- py/misc/cache.py | 149 ------------------ py/path/cacheutil.py | 111 +++++++++++++ py/path/svnurl.py | 2 +- .../testing/test_cacheutil.py} | 31 ++-- 4 files changed, 129 insertions(+), 164 deletions(-) delete mode 100644 py/misc/cache.py create mode 100644 py/path/cacheutil.py rename py/{misc/testing/test_cache.py => path/testing/test_cacheutil.py} (71%) diff --git a/py/misc/cache.py b/py/misc/cache.py deleted file mode 100644 index 8526d4c0a..000000000 --- a/py/misc/cache.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -This module contains multithread-safe cache implementations. - -All Caches have a - - __getitem__ and getorbuild(key, builder) method - -where the latter either just return a cached value or -first builds the value. - -""" -import py -gettime = py.std.time.time - -class WeightedCountingEntry(object): - def __init__(self, value, oneweight): - self.num = 1 - self._value = value - self.oneweight = oneweight - - def weight(): - def fget(self): - return (self.num * self.oneweight, self.num) - return property(fget, None, None, "cumulative weight") - weight = weight() - - def value(): - def fget(self): - # you need to protect against mt-access at caller side! - self.num += 1 - return self._value - return property(fget, None, None) - value = value() - - def __repr__(self): - return "<%s weight=%s>" % (self.__class__.__name__, self.weight) - -class BasicCache(object): - def __init__(self, maxentries=128): - self.maxentries = maxentries - self.prunenum = int(maxentries - maxentries/8) - self._lock = py.std.threading.RLock() - self._dict = {} - - def getentry(self, key): - lock = self._lock - lock.acquire() - try: - return self._dict.get(key, None) - finally: - lock.release() - - def putentry(self, key, entry): - self._lock.acquire() - try: - self._prunelowestweight() - self._dict[key] = entry - finally: - self._lock.release() - - def delentry(self, key, raising=False): - self._lock.acquire() - try: - try: - del self._dict[key] - except KeyError: - if raising: - raise - finally: - self._lock.release() - - def getorbuild(self, key, builder): - entry = self.getentry(key) - if entry is None: - entry = self.build(key, builder) - return entry.value - - def _prunelowestweight(self): - """ prune out entries with lowest weight. """ - # note: must be called with acquired self._lock! - numentries = len(self._dict) - if numentries >= self.maxentries: - # evict according to entry's weight - items = [(entry.weight, key) for key, entry in self._dict.iteritems()] - items.sort() - index = numentries - self.prunenum - if index > 0: - for weight, key in items[:index]: - del self._dict[key] - -class BuildcostAccessCache(BasicCache): - """ A BuildTime/Access-counting cache implementation. - the weight of a value is computed as the product of - - num-accesses-of-a-value * time-to-build-the-value - - The values with the least such weights are evicted - if the cache maxentries threshold is superceded. - For implementation flexibility more than one object - might be evicted at a time. - """ - # time function to use for measuring build-times - _time = gettime - - def build(self, key, builder): - start = self._time() - val = builder() - end = self._time() - entry = WeightedCountingEntry(val, end-start) - self.putentry(key, entry) - return entry - -class AgingCache(BasicCache): - """ This cache prunes out cache entries that are too old. - """ - def __init__(self, maxentries=128, maxseconds=10.0): - super(AgingCache, self).__init__(maxentries) - self.maxseconds = maxseconds - - def getentry(self, key): - self._lock.acquire() - try: - try: - entry = self._dict[key] - except KeyError: - entry = None - else: - if entry.isexpired(): - del self._dict[key] - entry = None - return entry - finally: - self._lock.release() - - def build(self, key, builder): - ctime = gettime() - val = builder() - entry = AgingEntry(val, ctime + self.maxseconds) - self.putentry(key, entry) - return entry - -class AgingEntry(object): - def __init__(self, value, expirationtime): - self.value = value - self.weight = expirationtime - - def isexpired(self): - t = py.std.time.time() - return t >= self.weight diff --git a/py/path/cacheutil.py b/py/path/cacheutil.py new file mode 100644 index 000000000..f10ba8cc2 --- /dev/null +++ b/py/path/cacheutil.py @@ -0,0 +1,111 @@ +""" +This module contains multithread-safe cache implementations. + +All Caches have + + getorbuild(key, builder) + delentry(key) + +methods and allow configuration when instantiating the cache class. +""" +from time import time as gettime + +class BasicCache(object): + def __init__(self, maxentries=128): + self.maxentries = maxentries + self.prunenum = int(maxentries - maxentries/8) + self._dict = {} + + def _getentry(self, key): + return self._dict[key] + + def _putentry(self, key, entry): + self._prunelowestweight() + self._dict[key] = entry + + def delentry(self, key, raising=False): + try: + del self._dict[key] + except KeyError: + if raising: + raise + + def getorbuild(self, key, builder): + try: + entry = self._getentry(key) + except KeyError: + entry = self._build(key, builder) + self._putentry(key, entry) + return entry.value + + def _prunelowestweight(self): + """ prune out entries with lowest weight. """ + numentries = len(self._dict) + if numentries >= self.maxentries: + # evict according to entry's weight + items = [(entry.weight, key) + for key, entry in self._dict.items()] + items.sort() + index = numentries - self.prunenum + if index > 0: + for weight, key in items[:index]: + # in MT situations the element might be gone + self.delentry(key, raising=False) + +class BuildcostAccessCache(BasicCache): + """ A BuildTime/Access-counting cache implementation. + the weight of a value is computed as the product of + + num-accesses-of-a-value * time-to-build-the-value + + The values with the least such weights are evicted + if the cache maxentries threshold is superceded. + For implementation flexibility more than one object + might be evicted at a time. + """ + # time function to use for measuring build-times + + def _build(self, key, builder): + start = gettime() + val = builder() + end = gettime() + return WeightedCountingEntry(val, end-start) + + +class WeightedCountingEntry(object): + def __init__(self, value, oneweight): + self._value = value + self.weight = self._oneweight = oneweight + + def value(self): + self.weight += self._oneweight + return self._value + value = property(value) + +class AgingCache(BasicCache): + """ This cache prunes out cache entries that are too old. + """ + def __init__(self, maxentries=128, maxseconds=10.0): + super(AgingCache, self).__init__(maxentries) + self.maxseconds = maxseconds + + def _getentry(self, key): + entry = self._dict[key] + if entry.isexpired(): + self.delentry(key) + raise KeyError(key) + return entry + + def _build(self, key, builder): + val = builder() + entry = AgingEntry(val, gettime() + self.maxseconds) + return entry + +class AgingEntry(object): + def __init__(self, value, expirationtime): + self.value = value + self.weight = expirationtime + + def isexpired(self): + t = gettime() + return t >= self.weight diff --git a/py/path/svnurl.py b/py/path/svnurl.py index ae8f34bb0..37ac23ed2 100644 --- a/py/path/svnurl.py +++ b/py/path/svnurl.py @@ -9,7 +9,7 @@ import py from py import path, process from py.__.path import common from py.__.path import svnwc as svncommon -from py.__.misc.cache import BuildcostAccessCache, AgingCache +from py.__.path.cacheutil import BuildcostAccessCache, AgingCache DEBUG=False diff --git a/py/misc/testing/test_cache.py b/py/path/testing/test_cacheutil.py similarity index 71% rename from py/misc/testing/test_cache.py rename to py/path/testing/test_cacheutil.py index 6b6c4302b..7dbdc508d 100644 --- a/py/misc/testing/test_cache.py +++ b/py/path/testing/test_cacheutil.py @@ -1,5 +1,5 @@ import py -from py.__.misc.cache import BuildcostAccessCache, AgingCache +from py.__.path import cacheutil class BasicCacheAPITest: cache = None @@ -10,12 +10,12 @@ class BasicCacheAPITest: assert val == 42 def test_cache_get_key_error(self): - assert self.cache.getentry(-23) == None + py.test.raises(KeyError, "self.cache._getentry(-23)") def test_delentry_non_raising(self): val = self.cache.getorbuild(100, lambda: 100) self.cache.delentry(100) - assert self.cache.getentry(100) is None + py.test.raises(KeyError, "self.cache._getentry(100)") def test_delentry_raising(self): val = self.cache.getorbuild(100, lambda: 100) @@ -23,16 +23,17 @@ class BasicCacheAPITest: py.test.raises(KeyError, "self.cache.delentry(100, raising=True)") class TestBuildcostAccess(BasicCacheAPITest): - cache = BuildcostAccessCache(maxentries=128) + cache = cacheutil.BuildcostAccessCache(maxentries=128) - def test_cache_works_somewhat_simple(self): - cache = BuildcostAccessCache() - # the default ._time() call used by + def test_cache_works_somewhat_simple(self, monkeypatch): + cache = cacheutil.BuildcostAccessCache() + # the default gettime # BuildcostAccessCache.build can # result into time()-time() == 0 which makes the below # test fail randomly. Let's rather use incrementing # numbers instead. - cache._time = py.std.itertools.count().next + monkeypatch.setattr(cacheutil, 'gettime', + py.std.itertools.count().next) for x in range(cache.maxentries): y = cache.getorbuild(x, lambda: x) assert x == y @@ -41,25 +42,27 @@ class TestBuildcostAccess(BasicCacheAPITest): for x in range(cache.maxentries/2): assert cache.getorbuild(x, None) == x assert cache.getorbuild(x, None) == x - assert cache.getorbuild(x, None) == x - val = cache.getorbuild(cache.maxentries * 2, lambda: 42) + # evict one entry + val = cache.getorbuild(-1, lambda: 42) assert val == 42 # check that recently used ones are still there # and are not build again for x in range(cache.maxentries/2): assert cache.getorbuild(x, None) == x - assert cache.getorbuild(cache.maxentries*2, None) == 42 + assert cache.getorbuild(-1, None) == 42 class TestAging(BasicCacheAPITest): maxsecs = 0.10 - cache = AgingCache(maxentries=128, maxseconds=maxsecs) + cache = cacheutil.AgingCache(maxentries=128, maxseconds=maxsecs) def test_cache_eviction(self): self.cache.getorbuild(17, lambda: 17) endtime = py.std.time.time() + self.maxsecs * 10 while py.std.time.time() < endtime: - if self.cache.getentry(17) is None: + try: + self.cache._getentry(17) + except KeyError: break py.std.time.sleep(self.maxsecs*0.3) else: @@ -67,7 +70,7 @@ class TestAging(BasicCacheAPITest): def test_prune_lowestweight(): maxsecs = 0.05 - cache = AgingCache(maxentries=10, maxseconds=maxsecs) + cache = cacheutil.AgingCache(maxentries=10, maxseconds=maxsecs) for x in range(cache.maxentries): cache.getorbuild(x, lambda: x) py.std.time.sleep(maxsecs*1.1)