[py-svn] r8029 - py/dist/py/misc
hpk at codespeak.net
hpk at codespeak.net
Sun Jan 2 11:59:38 CET 2005
Author: hpk
Date: Sun Jan 2 11:59:37 2005
New Revision: 8029
Modified:
py/dist/py/misc/cache.py
py/dist/py/misc/test_cache.py
Log:
refactored the internal experimental cache implementation,
now there are two somewhat working caches:
BuildcostAccessCache will prune entries that have the lowest
build-cost * num-accesses
weight. build-cost is measured in seconds.
AgingCache will prune entries if they exceed their
lifetime.
Modified: py/dist/py/misc/cache.py
==============================================================================
--- py/dist/py/misc/cache.py (original)
+++ py/dist/py/misc/cache.py Sun Jan 2 11:59:37 2005
@@ -10,16 +10,17 @@
These are the current cache implementations:
- BTACache tracks building-time and accesses. Evicts
+ BuildcostAccessCache tracks building-time and accesses. Evicts
by product of num-accesses * build-time.
"""
import py
+gettime = py.std.time.time
-class WValue(object):
+class WeightedCountingEntry(object):
def __init__(self, value, oneweight):
self.num = 1
- self.value = value
+ self._value = value
self.oneweight = oneweight
def weight():
@@ -28,63 +29,126 @@
return property(fget, None, None, "cumulative weight")
weight = weight()
- def get(self):
- # you need to protect against mt-access at caller side!
- self.num += 1
- return self.value
+ 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()
+
+class BasicCache(object):
+ def __init__(self, maxentries=128):
+ self.maxentries = maxentries
+ self.prunenum = maxentries - int(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()
-class BTACache(object):
+ def getorbuild(self, key, builder, *args, **kwargs):
+ entry = self.getentry(key)
+ if entry is None:
+ entry = self.build(key, builder, *args, **kwargs)
+ 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 size threshold is superceded.
+ 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 = py.std.time.time
+ _time = gettime
- def __init__(self, size=64):
- self.size = size
- self.prunenum = int(size / 8)
- self._dict = {}
- self._lock = py.std.threading.RLock()
+ def __init__(self, maxentries=64):
+ super(BuildcostAccessCache, self).__init__(maxentries)
- def getorbuild(self, key, builder, *args, **kwargs):
- try:
- return self[key]
- except KeyError:
- entry = self.build(builder, *args, **kwargs)
- self._lock.acquire()
+ def build(self, key, builder, *args, **kwargs):
+ start = self._time()
+ val = builder(*args, **kwargs)
+ 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:
- self._prune()
- self._dict[key] = entry
- return entry.get()
- finally:
- self._lock.release()
-
- def __getitem__(self, key):
- lock = self._lock
- lock.acquire()
- try:
- return self._dict[key].get()
+ entry = self._dict[key]
+ except KeyError:
+ entry = None
+ else:
+ if entry.isexpired():
+ del self._dict[key]
+ entry = None
+ return entry
finally:
- lock.release()
+ self._lock.release()
- def build(self, builder, *args, **kwargs):
- start = self._time()
+ def build(self, key, builder, *args, **kwargs):
+ ctime = gettime()
val = builder(*args, **kwargs)
- end = self._time()
- return WValue(val, end-start)
+ entry = AgingEntry(val, ctime + self.maxseconds)
+ self.putentry(key, entry)
+ return entry
- def _prune(self):
- # must be called with acquired lock
- if len(self._dict) >= self.size:
- # evict according to entry's weight
- items = [(entry.weight, key) for key, entry in self._dict.iteritems()]
- items.sort()
- for weight, key in items[:-(self.size-self.prunenum)]:
- del self._dict[key]
+class AgingEntry(object):
+ def __init__(self, value, expirationtime):
+ self.value = value
+ self.expirationtime = expirationtime
+
+ def isexpired(self):
+ t = py.std.time.time()
+ return t >= self.expirationtime
Modified: py/dist/py/misc/test_cache.py
==============================================================================
--- py/dist/py/misc/test_cache.py (original)
+++ py/dist/py/misc/test_cache.py Sun Jan 2 11:59:37 2005
@@ -1,27 +1,55 @@
import py
-from cache import BTACache
+from cache import BuildcostAccessCache, AgingCache
-def test_cache_works_somewhat_simple():
- cache = BTACache()
- for x in range(cache.size):
- y = cache.getorbuild(x, lambda: x)
- assert x == y
- for x in range(cache.size):
- assert cache.getorbuild(x, None) == x
- for x in range(cache.size/2):
- assert cache.getorbuild(x, None) == x
- assert cache.getorbuild(x, None) == x
- assert cache.getorbuild(x, None) == x
- val = cache.getorbuild(cache.size * 2, lambda: 42)
- assert val == 42
- # check that recently used ones are still there
- # and are not build again
- for x in range(cache.size/2):
- assert cache.getorbuild(x, None) == x
- assert cache.getorbuild(cache.size*2, None) == 42
-
-def test_cache_get_key_error():
- cache = BTACache()
- cache.getorbuild(0, lambda: 0)
- py.test.raises(KeyError, "cache[1]")
-
+class BasicCacheAPITest:
+ cache = None
+ def test_getorbuild(self):
+ val = self.cache.getorbuild(-42, lambda: 42)
+ assert val == 42
+ val = self.cache.getorbuild(-42, lambda: 23)
+ assert val == 42
+
+ def test_cache_get_key_error(self):
+ assert self.cache.getentry(-23) == None
+
+ def test_delentry_non_raising(self):
+ val = self.cache.getorbuild(100, lambda: 100)
+ self.cache.delentry(100)
+ assert self.cache.getentry(100) is None
+
+ def test_delentry_raising(self):
+ val = self.cache.getorbuild(100, lambda: 100)
+ self.cache.delentry(100)
+ py.test.raises(KeyError, "self.cache.delentry(100, raising=True)")
+
+class TestBuildcostAccess(BasicCacheAPITest):
+ cache = BuildcostAccessCache(maxentries=128)
+
+ def test_cache_works_somewhat_simple(self):
+ cache = BuildcostAccessCache()
+ for x in range(cache.maxentries):
+ y = cache.getorbuild(x, lambda: x)
+ assert x == y
+ for x in range(cache.maxentries):
+ assert cache.getorbuild(x, None) == x
+ 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)
+ 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
+
+
+class TestAging(BasicCacheAPITest):
+ maxsecs = 0.02
+ cache = AgingCache(maxentries=128, maxseconds=maxsecs)
+
+ def test_cache_eviction(self):
+ self.cache.getorbuild(17, lambda: 17)
+ py.std.time.sleep(self.maxsecs*1.1)
+ assert self.cache.getentry(17) is None
More information about the pytest-commit
mailing list