Skip to content

Commit

Permalink
Detect whether we need to do a gc after sweeping the cache. Document …
Browse files Browse the repository at this point in the history
…why we would need to or not. Add tests to make sure it works (previously only in the ZODB tests.)
  • Loading branch information
jamadden committed Apr 9, 2015
1 parent fad042f commit 27c8784
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 8 deletions.
25 changes: 21 additions & 4 deletions persistent/picklecache.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
##############################################################################
import gc
import weakref
import sys

from zope.interface import implementer

Expand All @@ -27,6 +28,24 @@
_CACHEABLE_TYPES = (type, 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):
# 32 byte fixed size wrapper.
__slots__ = ('object', 'next', 'prev')
Expand Down Expand Up @@ -371,10 +390,8 @@ def _sweep(self, target, target_size_bytes=0):
ejected += 1
self.__remove_from_ring(node)
node = node.next
if ejected:
# TODO: Only do this on PyPy/Jython?
# Even on CPython, though, it could trigger a lot of Persistent
# object deallocations and dictionary mutations
if ejected and _SWEEP_NEEDS_GC:
# See comments on _SWEEP_NEEDS_GC
gc.collect()
return ejected

Expand Down
14 changes: 10 additions & 4 deletions persistent/tests/test_picklecache.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ def setUp(self):
self.orig_types = persistent.picklecache._CACHEABLE_TYPES
persistent.picklecache._CACHEABLE_TYPES += (DummyPersistent,)

self.orig_sweep_gc = persistent.picklecache._SWEEP_NEEDS_GC
persistent.picklecache._SWEEP_NEEDS_GC = True # coverage

def tearDown(self):
import persistent.picklecache
persistent.picklecache._CACHEABLE_TYPES = self.orig_types
persistent.picklecache._SWEEP_NEEDS_GC = self.orig_sweep_gc

def _getTargetClass(self):
from persistent.picklecache import PickleCache
Expand Down Expand Up @@ -1000,15 +1004,13 @@ def test_cache_garbage_collection_bytes_also_deactivates_object(self):
# size; if we don't get deactivated by sweeping, the cache size
# won't shrink so this also validates that _p_deactivate gets
# called when ejecting an object.
o._p_deactivate = lambda: cache.update_object_size_estimation(oid,
-1)


o._p_deactivate = lambda: cache.update_object_size_estimation(oid, -1)
self.assertEqual(cache.cache_non_ghost_count, 100)

# A GC at this point does nothing
cache.incrgc()
self.assertEqual(cache.cache_non_ghost_count, 100)
self.assertEqual(len(cache), 100)

# Now if we set a byte target:

Expand All @@ -1024,6 +1026,10 @@ def test_cache_garbage_collection_bytes_also_deactivates_object(self):
# sanity check
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):
from persistent._compat import _b
KEY = _b('pclass')
Expand Down

0 comments on commit 27c8784

Please sign in to comment.