[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