Commit 27c8784c authored by Jason Madden's avatar Jason Madden

Detect whether we need to do a gc after sweeping the cache. Document why we...

Detect whether we need to do a gc after sweeping the cache. Document why we would need to or not. Add tests to make sure it works (previously only in the ZODB tests.)
parent fad042f3
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
############################################################################## ##############################################################################
import gc import gc
import weakref import weakref
import sys
from zope.interface import implementer from zope.interface import implementer
...@@ -27,6 +28,24 @@ from persistent.persistence import _estimated_size_in_24_bits ...@@ -27,6 +28,24 @@ from persistent.persistence import _estimated_size_in_24_bits
_CACHEABLE_TYPES = (type, Persistent) _CACHEABLE_TYPES = (type, Persistent)
_SWEEPABLE_TYPES = (Persistent,) _SWEEPABLE_TYPES = (Persistent,)
# The Python PickleCache implementation keeps track of the objects it
# is caching in a WeakValueDictionary. The number of objects in the
# cache (in this dictionary) is exposed as the len of the cache. Under
# non-refcounted implementations like PyPy, the weak references in
# this dictionary are only cleared when the garbage collector runs.
# Thus, after an incrgc, the len of the cache is incorrect for some
# period of time unless we ask the GC to run.
# Furthermore, evicted objects can stay in the dictionary and be returned
# from __getitem__ or possibly conflict with a new item in __setitem__.
# We determine whether or not we need to do the GC based on the ability
# to get a reference count: PyPy and Jython don't use refcounts and don't
# expose this; this is safer than blacklisting specific platforms (e.g.,
# what about IronPython?). On refcounted platforms, we don't want to
# run a GC to avoid possible performance regressions (e.g., it may
# block all threads).
# Tests may modify this
_SWEEP_NEEDS_GC = not hasattr(sys, 'getrefcount')
class RingNode(object): class RingNode(object):
# 32 byte fixed size wrapper. # 32 byte fixed size wrapper.
__slots__ = ('object', 'next', 'prev') __slots__ = ('object', 'next', 'prev')
...@@ -371,10 +390,8 @@ class PickleCache(object): ...@@ -371,10 +390,8 @@ class PickleCache(object):
ejected += 1 ejected += 1
self.__remove_from_ring(node) self.__remove_from_ring(node)
node = node.next node = node.next
if ejected: if ejected and _SWEEP_NEEDS_GC:
# TODO: Only do this on PyPy/Jython? # See comments on _SWEEP_NEEDS_GC
# Even on CPython, though, it could trigger a lot of Persistent
# object deallocations and dictionary mutations
gc.collect() gc.collect()
return ejected return ejected
......
...@@ -22,9 +22,13 @@ class PickleCacheTests(unittest.TestCase): ...@@ -22,9 +22,13 @@ class PickleCacheTests(unittest.TestCase):
self.orig_types = persistent.picklecache._CACHEABLE_TYPES self.orig_types = persistent.picklecache._CACHEABLE_TYPES
persistent.picklecache._CACHEABLE_TYPES += (DummyPersistent,) persistent.picklecache._CACHEABLE_TYPES += (DummyPersistent,)
self.orig_sweep_gc = persistent.picklecache._SWEEP_NEEDS_GC
persistent.picklecache._SWEEP_NEEDS_GC = True # coverage
def tearDown(self): def tearDown(self):
import persistent.picklecache import persistent.picklecache
persistent.picklecache._CACHEABLE_TYPES = self.orig_types persistent.picklecache._CACHEABLE_TYPES = self.orig_types
persistent.picklecache._SWEEP_NEEDS_GC = self.orig_sweep_gc
def _getTargetClass(self): def _getTargetClass(self):
from persistent.picklecache import PickleCache from persistent.picklecache import PickleCache
...@@ -1000,15 +1004,13 @@ class PickleCacheTests(unittest.TestCase): ...@@ -1000,15 +1004,13 @@ class PickleCacheTests(unittest.TestCase):
# size; if we don't get deactivated by sweeping, the cache size # size; if we don't get deactivated by sweeping, the cache size
# won't shrink so this also validates that _p_deactivate gets # won't shrink so this also validates that _p_deactivate gets
# called when ejecting an object. # called when ejecting an object.
o._p_deactivate = lambda: cache.update_object_size_estimation(oid, o._p_deactivate = lambda: cache.update_object_size_estimation(oid, -1)
-1)
self.assertEqual(cache.cache_non_ghost_count, 100) self.assertEqual(cache.cache_non_ghost_count, 100)
# A GC at this point does nothing # A GC at this point does nothing
cache.incrgc() cache.incrgc()
self.assertEqual(cache.cache_non_ghost_count, 100) self.assertEqual(cache.cache_non_ghost_count, 100)
self.assertEqual(len(cache), 100)
# Now if we set a byte target: # Now if we set a byte target:
...@@ -1024,6 +1026,10 @@ class PickleCacheTests(unittest.TestCase): ...@@ -1024,6 +1026,10 @@ class PickleCacheTests(unittest.TestCase):
# sanity check # sanity check
self.assertTrue(cache.total_estimated_size >= 0) self.assertTrue(cache.total_estimated_size >= 0)
# It also shrank the measured size of the cache;
# this would fail under PyPy if _SWEEP_NEEDS_GC was False
self.assertEqual(len(cache), 1)
def test_invalidate_persistent_class_calls_p_invalidate(self): def test_invalidate_persistent_class_calls_p_invalidate(self):
from persistent._compat import _b from persistent._compat import _b
KEY = _b('pclass') KEY = _b('pclass')
......
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