Commit c0ef0335 authored by Jason Madden's avatar Jason Madden

Checkpoint on a CFFI version of the cache MRU ring. Currently fails due to lifetime issues.

parent add499ab
...@@ -27,6 +27,8 @@ from persistent.timestamp import _ZERO ...@@ -27,6 +27,8 @@ from persistent.timestamp import _ZERO
from persistent._compat import copy_reg from persistent._compat import copy_reg
from persistent._compat import intern from persistent._compat import intern
from . import ring
_INITIAL_SERIAL = _ZERO _INITIAL_SERIAL = _ZERO
...@@ -54,7 +56,7 @@ _SPECIAL_NAMES = set(SPECIAL_NAMES) ...@@ -54,7 +56,7 @@ _SPECIAL_NAMES = set(SPECIAL_NAMES)
class Persistent(object): class Persistent(object):
""" Pure Python implmentation of Persistent base class """ Pure Python implmentation of Persistent base class
""" """
__slots__ = ('__jar', '__oid', '__serial', '__flags', '__size') __slots__ = ('__jar', '__oid', '__serial', '__flags', '__size', '__ring', '__ring_handle')
def __new__(cls, *args, **kw): def __new__(cls, *args, **kw):
inst = super(Persistent, cls).__new__(cls) inst = super(Persistent, cls).__new__(cls)
...@@ -67,6 +69,8 @@ class Persistent(object): ...@@ -67,6 +69,8 @@ class Persistent(object):
_OSA(inst, '_Persistent__serial', None) _OSA(inst, '_Persistent__serial', None)
_OSA(inst, '_Persistent__flags', None) _OSA(inst, '_Persistent__flags', None)
_OSA(inst, '_Persistent__size', 0) _OSA(inst, '_Persistent__size', 0)
_OSA(inst, '_Persistent__ring', None)
_OSA(inst, '_Persistent__ring_handle', None)
return inst return inst
# _p_jar: see IPersistent. # _p_jar: see IPersistent.
...@@ -483,6 +487,9 @@ class Persistent(object): ...@@ -483,6 +487,9 @@ class Persistent(object):
jar = oga(self, '_Persistent__jar') jar = oga(self, '_Persistent__jar')
if jar is None: if jar is None:
return return
myring = oga(self, '_Persistent__ring')
if ring is None:
return
oid = oga(self, '_Persistent__oid') oid = oga(self, '_Persistent__oid')
if oid is None: if oid is None:
return return
...@@ -490,6 +497,7 @@ class Persistent(object): ...@@ -490,6 +497,7 @@ class Persistent(object):
if flags is None: # ghost if flags is None: # ghost
return return
# The KeyError arises in ZODB: ZODB.serialize.ObjectWriter # The KeyError arises in ZODB: ZODB.serialize.ObjectWriter
# can assign a jar and an oid to newly seen persistent objects, # can assign a jar and an oid to newly seen persistent objects,
# but because they are newly created, they aren't in the # but because they are newly created, they aren't in the
...@@ -497,10 +505,11 @@ class Persistent(object): ...@@ -497,10 +505,11 @@ class Persistent(object):
# that at this level, all we can do is catch it. # that at this level, all we can do is catch it.
# The AttributeError arises in ZODB test cases # The AttributeError arises in ZODB test cases
try: try:
jar._cache.mru(oid) ring.move_to_head(jar._cache.ring_home, myring)
except (AttributeError,KeyError): except (AttributeError,KeyError):
pass pass
def _p_is_in_cache(self): def _p_is_in_cache(self):
oid = self.__oid oid = self.__oid
if not oid: if not oid:
...@@ -511,6 +520,10 @@ class Persistent(object): ...@@ -511,6 +520,10 @@ class Persistent(object):
if cache is not None: if cache is not None:
return cache.get(oid) is self return cache.get(oid) is self
def __del__(self):
if self._p_is_in_cache():
ring.del_(self._Persistent__ring)
def _estimated_size_in_24_bits(value): def _estimated_size_in_24_bits(value):
if value > 1073741696: if value > 1073741696:
return 16777215 return 16777215
......
...@@ -71,6 +71,8 @@ def _sweeping_ring(f): ...@@ -71,6 +71,8 @@ def _sweeping_ring(f):
from collections import deque from collections import deque
from . import ring
@implementer(IPickleCache) @implementer(IPickleCache)
class PickleCache(object): class PickleCache(object):
...@@ -100,11 +102,7 @@ class PickleCache(object): ...@@ -100,11 +102,7 @@ class PickleCache(object):
self.non_ghost_count = 0 self.non_ghost_count = 0
self.persistent_classes = {} self.persistent_classes = {}
self.data = weakref.WeakValueDictionary() self.data = weakref.WeakValueDictionary()
# oldest is on the left, newest on the right so that default self.ring_home = ring.CPersistentRingHead()
# iteration order is maintained from oldest to newest.
# Note that the remove() method is verboten: it uses equality
# comparisons, but we must use identity comparisons
self.ring = deque()
self.cache_size_bytes = cache_size_bytes self.cache_size_bytes = cache_size_bytes
# IPickleCache API # IPickleCache API
...@@ -164,7 +162,11 @@ class PickleCache(object): ...@@ -164,7 +162,11 @@ class PickleCache(object):
else: else:
self.data[oid] = value self.data[oid] = value
_gc_monitor(value) _gc_monitor(value)
self.mru(oid) node = ring.CPersistentRing(value)
value._Persistent__ring = node
if _OGA(value, '_p_state') != GHOST:
ring.add(self.ring_home, node)
self.non_ghost_count += 1
def __delitem__(self, oid): def __delitem__(self, oid):
""" See IPickleCache. """ See IPickleCache.
...@@ -196,21 +198,18 @@ class PickleCache(object): ...@@ -196,21 +198,18 @@ class PickleCache(object):
return False # marker return for tests return False # marker return for tests
value = self.data[oid] value = self.data[oid]
was_in_ring = self._remove_from_ring(value) id_value = id(value)
if was_in_ring:
# Compensate for decrementing the count; by was_in_ring = bool(value._Persistent__ring.r_next)
# definition it should already have been not-a-ghost ring.move_to_head(self.ring_home, value._Persistent__ring)
# so we can avoid a trip through Persistent.__getattribute__ if not was_in_ring and _OGA(value, '_p_state') != GHOST:
self.ring.append(value)
self.non_ghost_count += 1
elif _OGA(value, '_p_state') != GHOST:
self.ring.append(value)
self.non_ghost_count += 1 self.non_ghost_count += 1
def ringlen(self): def ringlen(self):
""" See IPickleCache. """ See IPickleCache.
""" """
return len(self.ring) return ring.ringlen(self.ring_home)
def items(self): def items(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -220,7 +219,11 @@ class PickleCache(object): ...@@ -220,7 +219,11 @@ class PickleCache(object):
def lru_items(self): def lru_items(self):
""" See IPickleCache. """ See IPickleCache.
""" """
return [(x._p_oid, x) for x in self.ring] result = []
for item in ring.iteritems(self.ring_home):
obj = ring.get_object(item)
result.append((obj._p_oid, obj))
return result
def klass_items(self): def klass_items(self):
""" See IPickleCache. """ See IPickleCache.
...@@ -346,17 +349,16 @@ class PickleCache(object): ...@@ -346,17 +349,16 @@ class PickleCache(object):
@_sweeping_ring @_sweeping_ring
def _sweep(self, target, target_size_bytes=0): def _sweep(self, target, target_size_bytes=0):
# lock # lock
# We can't mutate while we're iterating, so store ejections by index here ejected = 0
# (deleting by index is potentially more efficient then by value because it for here in ring.iteritems(self.ring_home):
# can use the rotate() method and not be O(n)). Also note that we do not use value = ring.get_object(here)
# self._remove_from_ring because we need to decrement the non_ghost_count if value is None:
# as we traverse the ring to be sure to meet our target continue
to_eject = [] here = here.r_next
i = -1 # Using a manual numeric counter instead of enumerate() is much faster on PyPy
for value in self.ring:
if self.non_ghost_count <= target and (self.total_estimated_size <= target_size_bytes or not target_size_bytes): if self.non_ghost_count <= target and (self.total_estimated_size <= target_size_bytes or not target_size_bytes):
break break
i += 1
if value._p_state == UPTODATE: if value._p_state == UPTODATE:
# The C implementation will only evict things that are specifically # The C implementation will only evict things that are specifically
# in the up-to-date state # in the up-to-date state
...@@ -379,16 +381,14 @@ class PickleCache(object): ...@@ -379,16 +381,14 @@ class PickleCache(object):
# they don't cooperate (without this check a bunch of test_picklecache # they don't cooperate (without this check a bunch of test_picklecache
# breaks) # breaks)
or not isinstance(value, _SWEEPABLE_TYPES)): or not isinstance(value, _SWEEPABLE_TYPES)):
to_eject.append(i) self._remove_from_ring(value)
self.non_ghost_count -= 1 self.non_ghost_count -= 1
ejected += 1
for i in reversed(to_eject): if ejected and _SWEEP_NEEDS_GC:
del self.ring[i]
if to_eject and _SWEEP_NEEDS_GC:
# See comments on _SWEEP_NEEDS_GC # See comments on _SWEEP_NEEDS_GC
gc.collect() gc.collect()
return len(to_eject) return ejected
@_sweeping_ring @_sweeping_ring
def _invalidate(self, oid): def _invalidate(self, oid):
...@@ -408,21 +408,5 @@ class PickleCache(object): ...@@ -408,21 +408,5 @@ class PickleCache(object):
pass pass
def _remove_from_ring(self, value): def _remove_from_ring(self, value):
""" if value._Persistent__ring.r_next:
Removes the previously non-ghost `value` from the ring, decrementing ring.del_(value._Persistent__ring)
the `non_ghost_count` if it's found. The value may be a ghost when
this method is called.
:return: A true value if the object was found in the ring.
"""
# Note that we do not use self.ring.remove() because that
# uses equality semantics and we don't want to call the persistent
# object's __eq__ method (which might wake it up just after we
# tried to ghost it)
i = 0 # Using a manual numeric counter instead of enumerate() is much faster on PyPy
for o in self.ring:
if o is value:
del self.ring[i]
self.non_ghost_count -= 1
return 1
i += 1
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
"""
from cffi import FFI
import pkg_resources
import os
ffi = FFI()
ffi.cdef("""
typedef struct CPersistentRingEx_struct
{
struct CPersistentRingEx_struct *r_prev;
struct CPersistentRingEx_struct *r_next;
void* object;
} CPersistentRingEx;
""")
ffi.cdef(pkg_resources.resource_string('persistent', 'ring.h'))
ring = ffi.verify("""
typedef struct CPersistentRingEx_struct
{
struct CPersistentRingEx_struct *r_prev;
struct CPersistentRingEx_struct *r_next;
void* object;
} CPersistentRingEx;
#include "ring.c"
""", include_dirs=[os.path.dirname(os.path.abspath(__file__))])
class CPersistentRing(object):
def __init__(self, obj=None):
self.handle = None
self.node = ffi.new("CPersistentRingEx*")
if obj is not None:
self.handle = self.node.object = ffi.new_handle(obj)
self._object = obj # Circular reference
def __getattr__(self, name):
return getattr(self.node, name)
def CPersistentRingHead():
head = CPersistentRing()
head.node.r_next = head.node
head.node.r_prev = head.node
return head
def _c(node):
return ffi.cast("CPersistentRing*", node.node)
def add(head, elt):
ring.ring_add(_c(head), _c(elt))
def del_(elt):
ring.ring_del(_c(elt))
def move_to_head(head, elt):
ring.ring_move_to_head(_c(head), _c(elt))
def iteritems(head):
here = head.r_next
while here != head.node:
yield here
here = here.r_next
def ringlen(head):
count = 0
for _ in iteritems(head):
count += 1
return count
def get_object(node):
return ffi.from_handle(node.object)
print CPersistentRing()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment