/*
******************************************************************************
-* Copyright (C) 2014, International Business Machines
+* Copyright (C) 2015, International Business Machines
* Corporation and others. All Rights Reserved.
******************************************************************************
* sharedobject.cpp
*/
#include "sharedobject.h"
+#include "uassert.h"
U_NAMESPACE_BEGIN
+
SharedObject::~SharedObject() {}
+UnifiedCacheBase::~UnifiedCacheBase() {}
+
void
-SharedObject::addRef() const {
+SharedObject::addRef(UBool fromWithinCache) const {
umtx_atomic_inc(&totalRefCount);
+
+ // Although items in use may not be correct immediately, it
+ // will be correct eventually.
+ if (umtx_atomic_inc(&hardRefCount) == 1 && cachePtr != NULL) {
+ // If this object is cached, and the hardRefCount goes from 0 to 1,
+ // then the increment must happen from within the cache while the
+ // cache global mutex is locked. In this way, we can be rest assured
+ // that data races can't happen if the cache performs some task if
+ // the hardRefCount is zero while the global cache mutex is locked.
+ U_ASSERT(fromWithinCache);
+ cachePtr->incrementItemsInUse();
+ }
}
void
-SharedObject::removeRef() const {
- if(umtx_atomic_dec(&totalRefCount) == 0) {
+SharedObject::removeRef(UBool fromWithinCache) const {
+ UBool decrementItemsInUse = (umtx_atomic_dec(&hardRefCount) == 0);
+ UBool allReferencesGone = (umtx_atomic_dec(&totalRefCount) == 0);
+
+ // Although items in use may not be correct immediately, it
+ // will be correct eventually.
+ if (decrementItemsInUse && cachePtr != NULL) {
+ if (fromWithinCache) {
+ cachePtr->decrementItemsInUse();
+ } else {
+ cachePtr->decrementItemsInUseWithLockingAndEviction();
+ }
+ }
+ if (allReferencesGone) {
delete this;
}
}
void
SharedObject::addSoftRef() const {
- addRef();
- umtx_atomic_inc(&softRefCount);
+ umtx_atomic_inc(&totalRefCount);
+ ++softRefCount;
}
void
SharedObject::removeSoftRef() const {
- umtx_atomic_dec(&softRefCount);
- removeRef();
-}
-
-UBool
-SharedObject::allSoftReferences() const {
- return umtx_loadAcquire(totalRefCount) == umtx_loadAcquire(softRefCount);
+ --softRefCount;
+ if (umtx_atomic_dec(&totalRefCount) == 0) {
+ delete this;
+ }
}
int32_t
}
int32_t
-SharedObject::getSoftRefCount() const {
- return umtx_loadAcquire(softRefCount);
+SharedObject::getHardRefCount() const {
+ return umtx_loadAcquire(hardRefCount);
}
void
/*
******************************************************************************
-* Copyright (C) 2014, International Business Machines
+* Copyright (C) 2015, International Business Machines
* Corporation and others. All Rights Reserved.
******************************************************************************
* sharedobject.h
U_NAMESPACE_BEGIN
+/**
+ * Base class for unified cache exposing enough methods to SharedObject
+ * instances to allow their addRef() and removeRef() methods to
+ * update cache metrics. No other part of ICU, except for SharedObject,
+ * should directly call the methods of this base class.
+ */
+class UnifiedCacheBase : public UObject {
+public:
+ UnifiedCacheBase() { }
+
+ /**
+ * Called by addRefWhileHoldingCacheLock() when the hard reference count
+ * of its instance goes from 0 to 1.
+ */
+ virtual void incrementItemsInUse() const = 0;
+
+ /**
+ * Called by removeRef() when the hard reference count of its instance
+ * drops from 1 to 0.
+ */
+ virtual void decrementItemsInUseWithLockingAndEviction() const = 0;
+
+ /**
+ * Called by removeRefWhileHoldingCacheLock() when the hard reference
+ * count of its instance drops from 1 to 0.
+ */
+ virtual void decrementItemsInUse() const = 0;
+ virtual ~UnifiedCacheBase();
+private:
+ UnifiedCacheBase(const UnifiedCacheBase &);
+ UnifiedCacheBase &operator=(const UnifiedCacheBase &);
+};
+
/**
* Base class for shared, reference-counted, auto-deleted objects.
* Subclasses can be immutable.
class U_COMMON_API SharedObject : public UObject {
public:
/** Initializes totalRefCount, softRefCount to 0. */
- SharedObject() : totalRefCount(0), softRefCount(0) {}
+ SharedObject() :
+ totalRefCount(0),
+ softRefCount(0),
+ hardRefCount(0),
+ cachePtr(NULL) {}
/** Initializes totalRefCount, softRefCount to 0. */
- SharedObject(const SharedObject &other)
- : UObject(other),
- totalRefCount(0),
- softRefCount(0) {}
+ SharedObject(const SharedObject &other) :
+ UObject(other),
+ totalRefCount(0),
+ softRefCount(0),
+ hardRefCount(0),
+ cachePtr(NULL) {}
virtual ~SharedObject();
/**
* Increments the number of references to this object. Thread-safe.
*/
- void addRef() const;
+ void addRef() const { addRef(FALSE); }
+
+ /**
+ * Increments the number of references to this object.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
+ */
+ void addRefWhileHoldingCacheLock() const { addRef(TRUE); }
/**
- * Increments the number of soft references to this object. Thread-safe.
+ * Increments the number of soft references to this object.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
*/
void addSoftRef() const;
/**
* Decrements the number of references to this object. Thread-safe.
*/
- void removeRef() const;
+ void removeRef() const { removeRef(FALSE); }
/**
- * Decrements the number of soft references to this object. Thread-safe.
+ * Decrements the number of references to this object.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
+ */
+ void removeRefWhileHoldingCacheLock() const { removeRef(TRUE); }
+
+ /**
+ * Decrements the number of soft references to this object.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
*/
void removeSoftRef() const;
int32_t getRefCount() const;
/**
- * Returns the count of soft references only. Uses a memory barrier.
+ * Returns the count of soft references only.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
+ */
+ int32_t getSoftRefCount() const { return softRefCount; }
+
+ /**
+ * Returns the count of hard references only. Uses a memory barrier.
* Used for testing the cache. Regular clients won't need this.
*/
- int32_t getSoftRefCount() const;
+ int32_t getHardRefCount() const;
+
+ /**
+ * If noHardReferences() == TRUE then this object has no hard references.
+ * Must be called only from within the internals of UnifiedCache.
+ */
+ inline UBool noHardReferences() const { return getHardRefCount() == 0; }
/**
- * If allSoftReferences() == TRUE then this object has only soft
- * references. The converse is not necessarily true.
+ * If hasHardReferences() == TRUE then this object has hard references.
+ * Must be called only from within the internals of UnifiedCache.
*/
- UBool allSoftReferences() const;
+ inline UBool hasHardReferences() const { return getHardRefCount() != 0; }
+
+ /**
+ * If noSoftReferences() == TRUE then this object has no soft references.
+ * Must be called only from within the internals of UnifiedCache and
+ * only while the cache global mutex is held.
+ */
+ UBool noSoftReferences() const { return (softRefCount == 0); }
/**
* Deletes this object if it has no references or soft references.
*/
void deleteIfZeroRefCount() const;
+ /**
+ * @internal For UnifedCache use only to register this object with itself.
+ * Must be called before this object is exposed to multiple threads.
+ */
+ void registerWithCache(const UnifiedCacheBase *ptr) const {
+ cachePtr = ptr;
+ }
+
/**
* Returns a writable version of ptr.
* If there is exactly one owner, then ptr itself is returned as a
private:
mutable u_atomic_int32_t totalRefCount;
- mutable u_atomic_int32_t softRefCount;
+
+ // Any thread modifying softRefCount must hold the global cache mutex
+ mutable int32_t softRefCount;
+
+ mutable u_atomic_int32_t hardRefCount;
+ mutable const UnifiedCacheBase *cachePtr;
+ void addRef(UBool withCacheLock) const;
+ void removeRef(UBool withCacheLock) const;
+
};
U_NAMESPACE_END
/*
******************************************************************************
-* Copyright (C) 1997-2014, International Business Machines
+* Copyright (C) 1997-2015, International Business Machines
* Corporation and others. All Rights Reserved.
******************************************************************************
* Date Name Description
* hashcode. During iteration an element may be deleted by calling
* uhash_removeElement(); iteration may safely continue thereafter.
* The uhash_remove() function may also be safely called in
- * mid-iteration. However, if uhash_put() is called during iteration
- * then the iteration will be out of sync. Under no circumstances
- * should the UHashElement returned by uhash_nextElement be modified
- * directly.
+ * mid-iteration. If uhash_put() is called during iteration,
+ * the iteration is still guaranteed to terminate reasonably, but
+ * there is no guarantee that every element will be returned or that
+ * some won't be returned more than once.
+ *
+ * Under no circumstances should the UHashElement returned by
+ * uhash_nextElement be modified directly.
*
* By default, the hashtable grows when necessary, but never shrinks,
* even if all items are removed. For most applications this is
/*
******************************************************************************
-* Copyright (C) 2014, International Business Machines Corporation and
+* Copyright (C) 2015, International Business Machines Corporation and
* others. All Rights Reserved.
******************************************************************************
*
static UMutex gCacheMutex = U_MUTEX_INITIALIZER;
static UConditionVar gInProgressValueAddedCond = U_CONDITION_INITIALIZER;
static icu::UInitOnce gCacheInitOnce = U_INITONCE_INITIALIZER;
+static const int32_t MAX_EVICT_ITERATIONS = 10;
+
+static int32_t DEFAULT_MAX_UNUSED = 1000;
+static int32_t DEFAULT_PERCENTAGE_OF_IN_USE = 100;
+
U_CDECL_BEGIN
static UBool U_CALLCONV unifiedcache_cleanup() {
gNoValue->addSoftRef();
}
-const UnifiedCache *UnifiedCache::getInstance(UErrorCode &status) {
+UnifiedCache *UnifiedCache::getInstance(UErrorCode &status) {
umtx_initOnce(gCacheInitOnce, &cacheInit, status);
if (U_FAILURE(status)) {
return NULL;
return gCache;
}
-UnifiedCache::UnifiedCache(UErrorCode &status) {
+UnifiedCache::UnifiedCache(UErrorCode &status) :
+ fHashtable(NULL),
+ fEvictPos(UHASH_FIRST),
+ fItemsInUseCount(0),
+ fMaxUnused(DEFAULT_MAX_UNUSED),
+ fMaxPercentageOfInUse(DEFAULT_PERCENTAGE_OF_IN_USE),
+ fAutoEvictedCount(0) {
if (U_FAILURE(status)) {
return;
}
uhash_setKeyDeleter(fHashtable, &ucache_deleteKey);
}
+void UnifiedCache::setEvictionPolicy(
+ int32_t count, int32_t percentageOfInUseItems, UErrorCode &status) {
+ if (U_FAILURE(status)) {
+ return;
+ }
+ if (count < 0 || percentageOfInUseItems < 0) {
+ status = U_ILLEGAL_ARGUMENT_ERROR;
+ return;
+ }
+ Mutex lock(&gCacheMutex);
+ fMaxUnused = count;
+ fMaxPercentageOfInUse = percentageOfInUseItems;
+}
+
+int32_t UnifiedCache::unusedCount() const {
+ Mutex lock(&gCacheMutex);
+ return uhash_count(fHashtable) - fItemsInUseCount;
+}
+
+int64_t UnifiedCache::autoEvictedCount() const {
+ Mutex lock(&gCacheMutex);
+ return fAutoEvictedCount;
+}
+
int32_t UnifiedCache::keyCount() const {
Mutex lock(&gCacheMutex);
return uhash_count(fHashtable);
// other cache items making those additional cache items eligible for
// flushing.
while (_flush(FALSE));
- umtx_condBroadcast(&gInProgressValueAddedCond);
}
#ifdef UNIFIED_CACHE_DEBUG
(const SharedObject *) element->value.pointer;
const CacheKeyBase *key =
(const CacheKeyBase *) element->key.pointer;
- if (!sharedObject->allSoftReferences()) {
+ if (sharedObject->hasHardReferences()) {
++cnt;
fprintf(
stderr,
uhash_close(fHashtable);
}
+// Returns the next element in the cache round robin style.
+// On entry, gCacheMutex must be held.
+const UHashElement *
+UnifiedCache::_nextElement() const {
+ const UHashElement *element = uhash_nextElement(fHashtable, &fEvictPos);
+ if (element == NULL) {
+ fEvictPos = UHASH_FIRST;
+ return uhash_nextElement(fHashtable, &fEvictPos);
+ }
+ return element;
+}
+
// Flushes the contents of the cache. If cache values hold references to other
// cache values then _flush should be called in a loop until it returns FALSE.
// On entry, gCacheMutex must be held.
-// On exit, those values with only soft references are flushed. If all is true
-// then every value is flushed even if hard references are held.
+// On exit, those values with are evictable are flushed. If all is true
+// then every value is flushed even if it is not evictable.
// Returns TRUE if any value in cache was flushed or FALSE otherwise.
UBool UnifiedCache::_flush(UBool all) const {
UBool result = FALSE;
- int32_t pos = UHASH_FIRST;
- const UHashElement *element = uhash_nextElement(fHashtable, &pos);
- for (; element != NULL; element = uhash_nextElement(fHashtable, &pos)) {
- const SharedObject *sharedObject =
- (const SharedObject *) element->value.pointer;
- if (all || sharedObject->allSoftReferences()) {
+ int32_t origSize = uhash_count(fHashtable);
+ for (int32_t i = 0; i < origSize; ++i) {
+ const UHashElement *element = _nextElement();
+ if (all || _isEvictable(element)) {
+ const SharedObject *sharedObject =
+ (const SharedObject *) element->value.pointer;
uhash_removeElement(fHashtable, element);
sharedObject->removeSoftRef();
result = TRUE;
return result;
}
+// Computes how many items should be evicted.
+// On entry, gCacheMutex must be held.
+// Returns number of items that should be evicted or a value <= 0 if no
+// items need to be evicted.
+int32_t UnifiedCache::_computeCountOfItemsToEvict() const {
+ int32_t maxPercentageOfInUseCount =
+ fItemsInUseCount * fMaxPercentageOfInUse / 100;
+ int32_t maxUnusedCount = fMaxUnused;
+ if (maxUnusedCount < maxPercentageOfInUseCount) {
+ maxUnusedCount = maxPercentageOfInUseCount;
+ }
+ return uhash_count(fHashtable) - fItemsInUseCount - maxUnusedCount;
+}
+
+// Run an eviction slice.
+// On entry, gCacheMutex must be held.
+// _runEvictionSlice runs a slice of the evict pipeline by examining the next
+// 10 entries in the cache round robin style evicting them if they are eligible.
+void UnifiedCache::_runEvictionSlice() const {
+ int32_t maxItemsToEvict = _computeCountOfItemsToEvict();
+ if (maxItemsToEvict <= 0) {
+ return;
+ }
+ for (int32_t i = 0; i < MAX_EVICT_ITERATIONS; ++i) {
+ const UHashElement *element = _nextElement();
+ if (_isEvictable(element)) {
+ const SharedObject *sharedObject =
+ (const SharedObject *) element->value.pointer;
+ uhash_removeElement(fHashtable, element);
+ sharedObject->removeSoftRef();
+ ++fAutoEvictedCount;
+ if (--maxItemsToEvict == 0) {
+ break;
+ }
+ }
+ }
+}
+
+
// Places a new value and creationStatus in the cache for the given key.
// On entry, gCacheMutex must be held. key must not exist in the cache.
// On exit, value and creation status placed under key. Soft reference added
status = U_MEMORY_ALLOCATION_ERROR;
return;
}
- keyToAdopt->creationStatus = creationStatus;
+ keyToAdopt->fCreationStatus = creationStatus;
+ if (value->noSoftReferences()) {
+ _registerMaster(keyToAdopt, value);
+ }
uhash_put(fHashtable, keyToAdopt, (void *) value, &status);
if (U_SUCCESS(status)) {
value->addSoftRef();
UErrorCode putError = U_ZERO_ERROR;
// best-effort basis only.
_putNew(key, value, status, putError);
- return;
+ } else {
+ _put(element, value, status);
}
- _put(element, value, status);
+ // Run an eviction slice. This will run even if we added a master entry
+ // which doesn't increase the unused count, but that is still o.k
+ _runEvictionSlice();
}
// Attempts to fetch value and status for key from cache.
// On exit. value and status set to what is in cache at key or on cache
// miss the key's createObject() is called and value and status are set to
// the result of that. In this latter case, best effort is made to add the
-// value and status to the cache. value will be set to NULL instead of
-// gNoValue. Caller must call removeRef on value if non NULL.
+// value and status to the cache. If createObject() fails to create a value,
+// gNoValue is stored in cache, and value is set to NULL. Caller must call
+// removeRef on value if non NULL.
void UnifiedCache::_get(
const CacheKeyBase &key,
const SharedObject *&value,
return;
}
value = key.createObject(creationContext, status);
- U_ASSERT(value == NULL || !value->allSoftReferences());
+ U_ASSERT(value == NULL || value->hasHardReferences());
U_ASSERT(value != NULL || status != U_ZERO_ERROR);
if (value == NULL) {
SharedObject::copyPtr(gNoValue, value);
}
}
+void UnifiedCache::decrementItemsInUseWithLockingAndEviction() const {
+ Mutex mutex(&gCacheMutex);
+ decrementItemsInUse();
+ _runEvictionSlice();
+}
+
+void UnifiedCache::incrementItemsInUse() const {
+ ++fItemsInUseCount;
+}
+
+void UnifiedCache::decrementItemsInUse() const {
+ --fItemsInUseCount;
+}
+
+// Register a master cache entry.
+// On entry, gCacheMutex must be held.
+// On exit, items in use count incremented, entry is marked as a master
+// entry, and value registered with cache so that subsequent calls to
+// addRef() and removeRef() on it correctly updates items in use count
+void UnifiedCache::_registerMaster(
+ const CacheKeyBase *theKey, const SharedObject *value) const {
+ theKey->fIsMaster = TRUE;
+ ++fItemsInUseCount;
+ value->registerWithCache(this);
+}
+
// Store a value and error in given hash entry.
// On entry, gCacheMutex must be held. Hash entry element must be in progress.
// value must be non NULL.
void UnifiedCache::_put(
const UHashElement *element,
const SharedObject *value,
- const UErrorCode status) {
+ const UErrorCode status) const {
U_ASSERT(_inProgress(element));
const CacheKeyBase *theKey = (const CacheKeyBase *) element->key.pointer;
const SharedObject *oldValue = (const SharedObject *) element->value.pointer;
- theKey->creationStatus = status;
+ theKey->fCreationStatus = status;
+ if (value->noSoftReferences()) {
+ _registerMaster(theKey, value);
+ }
value->addSoftRef();
UHashElement *ptr = const_cast<UHashElement *>(element);
ptr->value.pointer = (void *) value;
umtx_condBroadcast(&gInProgressValueAddedCond);
}
+void
+UnifiedCache::copyPtr(const SharedObject *src, const SharedObject *&dest) {
+ if(src != dest) {
+ if(dest != NULL) {
+ dest->removeRefWhileHoldingCacheLock();
+ }
+ dest = src;
+ if(src != NULL) {
+ src->addRefWhileHoldingCacheLock();
+ }
+ }
+}
+
+void
+UnifiedCache::clearPtr(const SharedObject *&ptr) {
+ if (ptr != NULL) {
+ ptr->removeRefWhileHoldingCacheLock();
+ ptr = NULL;
+ }
+}
+
+
// Fetch value and error code from a particular hash entry.
// On entry, gCacheMutex must be held. value must be either NULL or must be
// included in the ref count of the object to which it points.
const SharedObject *&value,
UErrorCode &status) {
const CacheKeyBase *theKey = (const CacheKeyBase *) element->key.pointer;
- status = theKey->creationStatus;
- SharedObject::copyPtr(
- (const SharedObject *) element->value.pointer, value);
+ status = theKey->fCreationStatus;
+
+ // Since we have the cache lock, calling regular SharedObject methods
+ // could cause us to deadlock on ourselves since they may need to lock
+ // the cache mutex.
+ UnifiedCache::copyPtr((const SharedObject *) element->value.pointer, value);
}
-
+
// Determine if given hash entry is in progress.
// On entry, gCacheMutex must be held.
UBool UnifiedCache::_inProgress(const UHashElement *element) {
const SharedObject *value = NULL;
UErrorCode status = U_ZERO_ERROR;
_fetch(element, value, status);
- UBool result = (value == gNoValue && status == U_ZERO_ERROR);
- SharedObject::clearPtr(value);
+ UBool result = _inProgress(value, status);
+
+ // Since we have the cache lock, calling regular SharedObject methods
+ // could cause us to deadlock on ourselves since they may need to lock
+ // the cache mutex.
+ UnifiedCache::clearPtr(value);
return result;
}
+// Determine if given hash entry is in progress.
+// On entry, gCacheMutex must be held.
+UBool UnifiedCache::_inProgress(
+ const SharedObject *theValue, UErrorCode creationStatus) {
+ return (theValue == gNoValue && creationStatus == U_ZERO_ERROR);
+}
+
+// Determine if given hash entry is eligible for eviction.
+// On entry, gCacheMutex must be held.
+UBool UnifiedCache::_isEvictable(const UHashElement *element) {
+ const CacheKeyBase *theKey = (const CacheKeyBase *) element->key.pointer;
+ const SharedObject *theValue =
+ (const SharedObject *) element->value.pointer;
+
+ // Entries that are under construction are never evictable
+ if (_inProgress(theValue, theKey->fCreationStatus)) {
+ return FALSE;
+ }
+
+ // We can evict entries that are either not a master or have just
+ // one reference (The one reference being from the cache itself).
+ return (!theKey->fIsMaster || (theValue->getSoftRefCount() == 1 && theValue->noHardReferences()));
+}
+
U_NAMESPACE_END
/*
******************************************************************************
-* Copyright (C) 2014, International Business Machines Corporation and
+* Copyright (C) 2015, International Business Machines Corporation and
* others. All Rights Reserved.
******************************************************************************
*
class UnifiedCache;
/**
- * A base class for all cache keys
+ * A base class for all cache keys.
*/
class U_COMMON_API CacheKeyBase : public UObject {
public:
- CacheKeyBase() : creationStatus(U_ZERO_ERROR) {}
+ CacheKeyBase() : fCreationStatus(U_ZERO_ERROR), fIsMaster(FALSE) {}
/**
* Copy constructor. Needed to support cloning.
*/
CacheKeyBase(const CacheKeyBase &other)
- : UObject(other), creationStatus(other.creationStatus) { }
+ : UObject(other), fCreationStatus(other.fCreationStatus), fIsMaster(FALSE) { }
virtual ~CacheKeyBase();
/**
return !(*this == other);
}
private:
- mutable UErrorCode creationStatus;
+ mutable UErrorCode fCreationStatus;
+ mutable UBool fIsMaster;
friend class UnifiedCache;
};
/**
* The unified cache. A singleton type.
+ * Design doc here:
+ * https://docs.google.com/document/d/1RwGQJs4N4tawNbf809iYDRCvXoMKqDJihxzYt1ysmd8/edit?usp=sharing
*/
-class U_COMMON_API UnifiedCache : public UObject {
+class U_COMMON_API UnifiedCache : public UnifiedCacheBase {
public:
/**
* @internal
+ * Do not call directly. Instead use UnifiedCache::getInstance() as
+ * there should be only one UnifiedCache in an application.
*/
UnifiedCache(UErrorCode &status);
/**
* Returns the cache instance.
*/
- static const UnifiedCache *getInstance(UErrorCode &status);
+ static UnifiedCache *getInstance(UErrorCode &status);
/**
* Fetches a value from the cache by key. Equivalent to
*/
void flush() const;
+ /**
+ * Configures at what point evcition of unused entries will begin.
+ * Eviction is triggered whenever the number of unused entries exeeds
+ * BOTH count AND (number of in-use items) * (percentageOfInUseItems / 100).
+ * Once the number of unused entries drops below one of these,
+ * eviction ceases. Because eviction happens incrementally,
+ * the actual unused entry count may exceed both these numbers
+ * from time to time.
+ *
+ * A cache entry is defined as unused if it is not essential to guarantee
+ * that for a given key X, the cache returns the same reference to the
+ * same value as long as the client already holds a reference to that
+ * value.
+ *
+ * If this method is never called, the default settings are 1000 and 100%.
+ *
+ * Although this method is thread-safe, it is designed to be called at
+ * application startup. If it is called in the middle of execution, it
+ * will have no immediate effect on the cache. However over time, the
+ * cache will perform eviction slices in an attempt to honor the new
+ * settings.
+ *
+ * If a client already holds references to many different unique values
+ * in the cache such that the number of those unique values far exeeds
+ * "count" then the cache may not be able to maintain this maximum.
+ * However, if this happens, the cache still guarantees that the number of
+ * unused entries will remain only a small percentage of the total cache
+ * size.
+ *
+ * If the parameters passed are negative, setEvctionPolicy sets status to
+ * U_ILLEGAL_ARGUMENT_ERROR.
+ */
+ void setEvictionPolicy(
+ int32_t count, int32_t percentageOfInUseItems, UErrorCode &status);
+
+
+ /**
+ * Returns how many entries have been auto evicted during the lifetime
+ * of this cache. This only includes auto evicted entries, not
+ * entries evicted because of a call to flush().
+ */
+ int64_t autoEvictedCount() const;
+
+ /**
+ * Returns the unused entry count in this cache. For testing only,
+ * Regular clients will not need this.
+ */
+ int32_t unusedCount() const;
+
+ virtual void incrementItemsInUse() const;
+ virtual void decrementItemsInUseWithLockingAndEviction() const;
+ virtual void decrementItemsInUse() const;
virtual ~UnifiedCache();
private:
UHashtable *fHashtable;
+ mutable int32_t fEvictPos;
+ mutable int32_t fItemsInUseCount;
+ int32_t fMaxUnused;
+ int32_t fMaxPercentageOfInUse;
+ mutable int64_t fAutoEvictedCount;
UnifiedCache(const UnifiedCache &other);
UnifiedCache &operator=(const UnifiedCache &other);
UBool _flush(UBool all) const;
const CacheKeyBase &key,
const SharedObject *&value,
UErrorCode &status) const;
+ const UHashElement *_nextElement() const;
+ int32_t _computeCountOfItemsToEvict() const;
+ void _runEvictionSlice() const;
+ void _registerMaster(
+ const CacheKeyBase *theKey, const SharedObject *value) const;
+ void _put(
+ const UHashElement *element,
+ const SharedObject *value,
+ const UErrorCode status) const;
#ifdef UNIFIED_CACHE_DEBUG
void _dumpContents() const;
#endif
- static void _put(
- const UHashElement *element,
- const SharedObject *value,
- const UErrorCode status);
+ static void copyPtr(const SharedObject *src, const SharedObject *&dest);
+ static void clearPtr(const SharedObject *&ptr);
static void _fetch(
const UHashElement *element,
const SharedObject *&value,
UErrorCode &status);
static UBool _inProgress(const UHashElement *element);
+ static UBool _inProgress(
+ const SharedObject *theValue, UErrorCode creationStatus);
+ static UBool _isEvictable(const UHashElement *element);
};
U_NAMESPACE_END
// Unified Cache Test
//
-static const char *gCacheLocales[] = {"en_US", "en_GB", "fr_FR", "fr"};
+// Each thread fetches a pair of objects. There are 8 distinct pairs:
+// ("en_US", "bs"), ("en_GB", "ca"), ("fr_FR", "ca_AD") etc.
+// These pairs represent 8 distinct languages
+
+// Note that only one value per language gets created in the cache.
+// In particular each cached value can have multiple keys.
+static const char *gCacheLocales[] = {
+ "en_US", "en_GB", "fr_FR", "fr",
+ "de", "sr_ME", "sr_BA", "sr_CS"};
+static const char *gCacheLocales2[] = {
+ "bs", "ca", "ca_AD", "ca_ES",
+ "en_US", "fi", "ff_CM", "ff_GN"};
+
static int32_t gObjectsCreated = 0; // protected by gCTMutex
static const int32_t CACHE_LOAD = 3;
template<> U_EXPORT
const UCTMultiThreadItem *LocaleCacheKey<UCTMultiThreadItem>::createObject(
- const void * /*unused*/, UErrorCode & /* status */) const {
+ const void *context, UErrorCode &status) const {
+ const UnifiedCache *cacheContext = (const UnifiedCache *) context;
+
+ if (uprv_strcmp(fLoc.getLanguage(), fLoc.getName()) != 0) {
+ const UCTMultiThreadItem *result = NULL;
+ if (cacheContext == NULL) {
+ UnifiedCache::getByLocale(fLoc.getLanguage(), result, status);
+ return result;
+ }
+ cacheContext->get(LocaleCacheKey<UCTMultiThreadItem>(fLoc.getLanguage()), result, status);
+ return result;
+ }
+
umtx_lock(&gCTMutex);
bool firstObject = (gObjectsCreated == 0);
if (firstObject) {
}
umtx_unlock(&gCTMutex);
- UCTMultiThreadItem *result = new UCTMultiThreadItem(fLoc.getName());
- result->addRef();
-
+ const UCTMultiThreadItem *result =
+ new UCTMultiThreadItem(fLoc.getLanguage());
+ if (result == NULL) {
+ status = U_MEMORY_ALLOCATION_ERROR;
+ } else {
+ result->addRef();
+ }
+
// Log that we created an object. The first object was already counted,
// don't do it again.
umtx_lock(&gCTMutex);
}
umtx_condBroadcast(&gCTConditionVar);
umtx_unlock(&gCTMutex);
+
return result;
}
class UnifiedCacheThread: public SimpleThread {
public:
- UnifiedCacheThread(const char *loc) : fLoc(loc) {};
+ UnifiedCacheThread(
+ const UnifiedCache *cache,
+ const char *loc,
+ const char *loc2) : fCache(cache), fLoc(loc), fLoc2(loc2) {};
~UnifiedCacheThread() {};
void run();
- const char *fLoc;
+ void exerciseByLocale(const Locale &);
+ const UnifiedCache *fCache;
+ Locale fLoc;
+ Locale fLoc2;
};
-void UnifiedCacheThread::run() {
+void UnifiedCacheThread::exerciseByLocale(const Locale &locale) {
UErrorCode status = U_ZERO_ERROR;
- const UnifiedCache *cache = UnifiedCache::getInstance(status);
- U_ASSERT(status == U_ZERO_ERROR);
- const UCTMultiThreadItem *item = NULL;
- cache->get(LocaleCacheKey<UCTMultiThreadItem>(fLoc), item, status);
- U_ASSERT(item != NULL);
- if (uprv_strcmp(fLoc, item->value)) {
- IntlTest::gTest->errln("Expected %s, got %s", fLoc, item->value);
- }
- item->removeRef();
+ const UCTMultiThreadItem *origItem = NULL;
+ fCache->get(
+ LocaleCacheKey<UCTMultiThreadItem>(locale), fCache, origItem, status);
+ U_ASSERT(U_SUCCESS(status));
+ if (uprv_strcmp(locale.getLanguage(), origItem->value)) {
+ IntlTest::gTest->errln(
+ "%s:%d Expected %s, got %s", __FILE__, __LINE__,
+ locale.getLanguage(),
+ origItem->value);
+ }
+
+ // Fetch the same item again many times. We should always get the same
+ // pointer since this client is already holding onto it
+ for (int32_t i = 0; i < 1000; ++i) {
+ const UCTMultiThreadItem *item = NULL;
+ fCache->get(
+ LocaleCacheKey<UCTMultiThreadItem>(locale), fCache, item, status);
+ if (item != origItem) {
+ IntlTest::gTest->errln(
+ "%s:%d Expected to get the same pointer",
+ __FILE__,
+ __LINE__);
+ }
+ if (item != NULL) {
+ item->removeRef();
+ }
+ }
+ origItem->removeRef();
+}
+
+void UnifiedCacheThread::run() {
+ // Run the exercise with 2 different locales so that we can exercise
+ // eviction more. If each thread exerices just one locale, then
+ // eviction can't start until the threads end.
+ exerciseByLocale(fLoc);
+ exerciseByLocale(fLoc2);
}
void MultithreadTest::TestUnifiedCache() {
+
+ // Start with our own local cache so that we have complete control
+ // and set the eviction policy to evict starting with 2 unused
+ // values
UErrorCode status = U_ZERO_ERROR;
- const UnifiedCache *cache = UnifiedCache::getInstance(status);
- U_ASSERT(cache != NULL);
- cache->flush();
+ UnifiedCache::getInstance(status);
+ UnifiedCache cache(status);
+ cache.setEvictionPolicy(2, 0, status);
+ U_ASSERT(U_SUCCESS(status));
+
gFinishedThreads = 0;
gObjectsCreated = 0;
UnifiedCacheThread *threads[CACHE_LOAD][UPRV_LENGTHOF(gCacheLocales)];
for (int32_t i=0; i<CACHE_LOAD; ++i) {
for (int32_t j=0; j<UPRV_LENGTHOF(gCacheLocales); ++j) {
- threads[i][j] = new UnifiedCacheThread(gCacheLocales[j]);
+ // Each thread works with a pair of locales.
+ threads[i][j] = new UnifiedCacheThread(
+ &cache, gCacheLocales[j], gCacheLocales2[j]);
threads[i][j]->start();
}
}
threads[i][j]->join();
}
}
- assertEquals("Objects created", UPRV_LENGTHOF(gCacheLocales), gObjectsCreated);
+ // Because of cache eviction, we can't assert exactly how many
+ // distinct objects get created over the course of this run.
+ // However we know that at least 8 objects get created because that
+ // is how many distinct languages we have in our test.
+ if (gObjectsCreated < 8) {
+ errln("%s:%d Too few objects created.", __FILE__, __LINE__);
+ }
+ // We know that each thread cannot create more than 2 objects in
+ // the cache, and there are UPRV_LENGTHOF(gCacheLocales) pairs of
+ // objects fetched from the cache
+ if (gObjectsCreated > 2 * UPRV_LENGTHOF(gCacheLocales)) {
+ errln("Too many objects created.", __FILE__, __LINE__);
+ }
+
+ assertEquals("unused values", 2, cache.unusedCount());
// clean up threads
for (int32_t i=0; i<CACHE_LOAD; ++i) {
/*
*******************************************************************************
-* Copyright (C) 2014, International Business Machines Corporation and *
+* Copyright (C) 2015, International Business Machines Corporation and *
* others. All Rights Reserved. *
*******************************************************************************
*
#include "cstring.h"
#include "intltest.h"
#include "unifiedcache.h"
+#include "unicode/datefmt.h"
class UCTItem : public SharedObject {
public:
template<> U_EXPORT
const UCTItem *LocaleCacheKey<UCTItem>::createObject(
- const void * /*unused*/, UErrorCode &status) const {
+ const void *context, UErrorCode &status) const {
+ const UnifiedCache *cacheContext = (const UnifiedCache *) context;
if (uprv_strcmp(fLoc.getName(), "zh") == 0) {
status = U_MISSING_RESOURCE_ERROR;
return NULL;
}
if (uprv_strcmp(fLoc.getLanguage(), fLoc.getName()) != 0) {
const UCTItem *item = NULL;
- UnifiedCache::getByLocale(fLoc.getLanguage(), item, status);
+ if (cacheContext == NULL) {
+ UnifiedCache::getByLocale(fLoc.getLanguage(), item, status);
+ } else {
+ cacheContext->get(LocaleCacheKey<UCTItem>(fLoc.getLanguage()), item, status);
+ }
if (U_FAILURE(status)) {
return NULL;
}
}
void runIndexedTest(int32_t index, UBool exec, const char *&name, char *par=0);
private:
+ void TestEvictionPolicy();
+ void TestBounded();
void TestBasic();
void TestError();
void TestHashEquals();
+ void TestEvictionUnderStress();
};
void UnifiedCacheTest::runIndexedTest(int32_t index, UBool exec, const char* &name, char* /*par*/) {
TESTCASE_AUTO_BEGIN;
+ TESTCASE_AUTO(TestEvictionPolicy);
+ TESTCASE_AUTO(TestBounded);
TESTCASE_AUTO(TestBasic);
TESTCASE_AUTO(TestError);
TESTCASE_AUTO(TestHashEquals);
+ TESTCASE_AUTO(TestEvictionUnderStress);
TESTCASE_AUTO_END;
}
+void UnifiedCacheTest::TestEvictionUnderStress() {
+ int32_t localeCount;
+ const Locale *locales = DateFormat::getAvailableLocales(localeCount);
+ UErrorCode status = U_ZERO_ERROR;
+ const UnifiedCache *cache = UnifiedCache::getInstance(status);
+ int64_t evictedCountBefore = cache->autoEvictedCount();
+ for (int32_t i = 0; i < localeCount; ++i) {
+ LocalPointer<DateFormat> ptr(DateFormat::createInstanceForSkeleton("yMd", locales[i], status));
+ }
+ int64_t evictedCountAfter = cache->autoEvictedCount();
+ if (evictedCountBefore == evictedCountAfter) {
+ errln("%s:%d Items should have been evicted from cache",
+ __FILE__, __LINE__);
+ }
+}
+
+void UnifiedCacheTest::TestEvictionPolicy() {
+ UErrorCode status = U_ZERO_ERROR;
+
+ // We have to call this first or else calling the UnifiedCache
+ // ctor will fail. This is by design to deter clients from using the
+ // cache API incorrectly by creating their own cache instances.
+ UnifiedCache::getInstance(status);
+
+ // We create our own local UnifiedCache instance to ensure we have
+ // complete control over it. Real clients should never ever create
+ // their own cache!
+ UnifiedCache cache(status);
+ assertSuccess("", status);
+
+ // Don't allow unused entries to exeed more than 100% of in use entries.
+ cache.setEvictionPolicy(0, 100, status);
+
+ static const char *locales[] = {
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
+ "11", "12", "13", "14", "15", "16", "17", "18", "19", "20"};
+
+ const UCTItem *usedReferences[] = {NULL, NULL, NULL, NULL, NULL};
+ const UCTItem *unusedReference = NULL;
+
+ // Add 5 in-use entries
+ for (int32_t i = 0; i < UPRV_LENGTHOF(usedReferences); i++) {
+ cache.get(
+ LocaleCacheKey<UCTItem>(locales[i]),
+ &cache,
+ usedReferences[i],
+ status);
+ }
+
+ // Add 10 not in use entries.
+ for (int32_t i = 0; i < 10; ++i) {
+ cache.get(
+ LocaleCacheKey<UCTItem>(
+ locales[i + UPRV_LENGTHOF(usedReferences)]),
+ &cache,
+ unusedReference,
+ status);
+ }
+ unusedReference->removeRef();
+
+ // unused count not to exeed in use count
+ assertEquals("", UPRV_LENGTHOF(usedReferences), cache.unusedCount());
+ assertEquals("", 2*UPRV_LENGTHOF(usedReferences), cache.keyCount());
+
+ // Free up those used entries.
+ for (int32_t i = 0; i < UPRV_LENGTHOF(usedReferences); i++) {
+ usedReferences[i]->removeRef();
+ }
+
+ // This should free up all cache items
+ assertEquals("", 0, cache.keyCount());
+
+ assertSuccess("", status);
+}
+
+
+
+void UnifiedCacheTest::TestBounded() {
+ UErrorCode status = U_ZERO_ERROR;
+
+ // We have to call this first or else calling the UnifiedCache
+ // ctor will fail. This is by design to deter clients from using the
+ // cache API incorrectly by creating their own cache instances.
+ UnifiedCache::getInstance(status);
+
+ // We create our own local UnifiedCache instance to ensure we have
+ // complete control over it. Real clients should never ever create
+ // their own cache!
+ UnifiedCache cache(status);
+ assertSuccess("", status);
+
+ // Maximum unused count is 3.
+ cache.setEvictionPolicy(3, 0, status);
+
+ // Our cache will hold up to 3 unused key-value pairs
+ // We test the following invariants:
+ // 1. unusedCount <= 3
+ // 2. cache->get(X) always returns the same reference as long as caller
+ // already holds references to that same object.
+
+ // We first add 5 key-value pairs with two distinct values, "en" and "fr"
+ // keeping all those references.
+
+ const UCTItem *en = NULL;
+ const UCTItem *enGb = NULL;
+ const UCTItem *enUs = NULL;
+ const UCTItem *fr = NULL;
+ const UCTItem *frFr = NULL;
+ cache.get(LocaleCacheKey<UCTItem>("en_US"), &cache, enUs, status);
+ cache.get(LocaleCacheKey<UCTItem>("en"), &cache, en, status);
+ assertEquals("", 1, cache.unusedCount());
+ cache.get(LocaleCacheKey<UCTItem>("en_GB"), &cache, enGb, status);
+ cache.get(LocaleCacheKey<UCTItem>("fr_FR"), &cache, frFr, status);
+ cache.get(LocaleCacheKey<UCTItem>("fr"), &cache, fr, status);
+
+ // Client holds two unique references, "en" and "fr" the other three
+ // entries are eligible for eviction.
+ assertEquals("", 3, cache.unusedCount());
+ assertEquals("", 5, cache.keyCount());
+
+ // Exercise cache more but don't hold the references except for
+ // the last one. At the end of this, we will hold references to one
+ // additional distinct value, so we will have references to 3 distinct
+ // values.
+ const UCTItem *throwAway = NULL;
+ cache.get(LocaleCacheKey<UCTItem>("zn_AA"), &cache, throwAway, status);
+ cache.get(LocaleCacheKey<UCTItem>("sr_AA"), &cache, throwAway, status);
+ cache.get(LocaleCacheKey<UCTItem>("de_AU"), &cache, throwAway, status);
+
+ const UCTItem *deAu(throwAway);
+ deAu->addRef();
+
+ // Client holds three unique references, "en", "fr", "de" although we
+ // could have a total of 8 entries in the cache maxUnusedCount == 3
+ // so we have only 6 entries.
+ assertEquals("", 3, cache.unusedCount());
+ assertEquals("", 6, cache.keyCount());
+
+ // For all the references we have, cache must continue to return
+ // those same references (#2)
+
+ cache.get(LocaleCacheKey<UCTItem>("en"), &cache, throwAway, status);
+ if (throwAway != en) {
+ errln("Expected en to resolve to the same object.");
+ }
+ cache.get(LocaleCacheKey<UCTItem>("en_US"), &cache, throwAway, status);
+ if (throwAway != enUs) {
+ errln("Expected enUs to resolve to the same object.");
+ }
+ cache.get(LocaleCacheKey<UCTItem>("en_GB"), &cache, throwAway, status);
+ if (throwAway != enGb) {
+ errln("Expected enGb to resolve to the same object.");
+ }
+ cache.get(LocaleCacheKey<UCTItem>("fr_FR"), &cache, throwAway, status);
+ if (throwAway != frFr) {
+ errln("Expected frFr to resolve to the same object.");
+ }
+ cache.get(LocaleCacheKey<UCTItem>("fr_FR"), &cache, throwAway, status);
+ cache.get(LocaleCacheKey<UCTItem>("fr"), &cache, throwAway, status);
+ if (throwAway != fr) {
+ errln("Expected fr to resolve to the same object.");
+ }
+ cache.get(LocaleCacheKey<UCTItem>("de_AU"), &cache, throwAway, status);
+ if (throwAway != deAu) {
+ errln("Expected deAu to resolve to the same object.");
+ }
+
+ assertEquals("", 3, cache.unusedCount());
+ assertEquals("", 6, cache.keyCount());
+
+ // Now we hold a references to two more distinct values. Cache size
+ // should grow to 8.
+ const UCTItem *es = NULL;
+ const UCTItem *ru = NULL;
+ cache.get(LocaleCacheKey<UCTItem>("es"), &cache, es, status);
+ cache.get(LocaleCacheKey<UCTItem>("ru"), &cache, ru, status);
+ assertEquals("", 3, cache.unusedCount());
+ assertEquals("", 8, cache.keyCount());
+
+ // Now release all the references we hold except for
+ // es, ru, and en
+ SharedObject::clearPtr(enGb);
+ SharedObject::clearPtr(enUs);
+ SharedObject::clearPtr(fr);
+ SharedObject::clearPtr(frFr);
+ SharedObject::clearPtr(deAu);
+ SharedObject::clearPtr(es);
+ SharedObject::clearPtr(ru);
+ SharedObject::clearPtr(en);
+ SharedObject::clearPtr(throwAway);
+
+ // Size of cache should magically drop to 3.
+ assertEquals("", 3, cache.unusedCount());
+ assertEquals("", 3, cache.keyCount());
+
+ // Be sure nothing happens setting the eviction policy in the middle of
+ // a run.
+ cache.setEvictionPolicy(3, 0, status);
+ assertSuccess("", status);
+
+}
+
void UnifiedCacheTest::TestBasic() {
UErrorCode status = U_ZERO_ERROR;
const UnifiedCache *cache = UnifiedCache::getInstance(status);
assertEquals("", baseCount + 5, cache->keyCount());
SharedObject::clearPtr(enGb);
cache->flush();
- assertEquals("", baseCount + 5, cache->keyCount());
+
+ // Only 2 unique values in the cache. flushing trims cache down
+ // to this minimum size.
+ assertEquals("", baseCount + 2, cache->keyCount());
SharedObject::clearPtr(enUs);
SharedObject::clearPtr(en);
cache->flush();
// With en_GB and en_US and en cleared there are no more hard references to
// the "en" object, so it gets flushed and the keys that refer to it
- // get removed from the cache.
- assertEquals("", baseCount + 2, cache->keyCount());
+ // get removed from the cache. Now we have just one unique value, fr, in
+ // the cache
+ assertEquals("", baseCount + 1, cache->keyCount());
SharedObject::clearPtr(fr);
cache->flush();
- assertEquals("", baseCount + 2, cache->keyCount());
+ assertEquals("", baseCount + 1, cache->keyCount());
SharedObject::clearPtr(frFr);
cache->flush();
assertEquals("", baseCount + 0, cache->keyCount());
+ assertSuccess("", status);
}
void UnifiedCacheTest::TestError() {