From a0ab47ec7e232282614a07997424afb8b6bdacb8 Mon Sep 17 00:00:00 2001 From: theraven Date: Mon, 31 Aug 2009 21:45:53 +0000 Subject: [PATCH] Added NSCache (OS X 10.6) implementation. git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/base/trunk@28581 72102866-910b-0410-8b05-ffd578937521 --- ChangeLog | 4 + Headers/Foundation/Foundation.h | 1 + Headers/Foundation/NSCache.h | 112 ++++++++++++++++ Source/GNUmakefile | 2 + Source/NSCache.m | 227 ++++++++++++++++++++++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 Headers/Foundation/NSCache.h create mode 100644 Source/NSCache.m diff --git a/ChangeLog b/ChangeLog index 371cfa86b..d5c44e8f0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2009-08-31 David Chisnall + + Added NSCache implementation. + 2009-08-31 Richard Frith-Macdonald * Source/NSMessagePort.m: diff --git a/Headers/Foundation/Foundation.h b/Headers/Foundation/Foundation.h index 49403a06a..baefe0be5 100644 --- a/Headers/Foundation/Foundation.h +++ b/Headers/Foundation/Foundation.h @@ -43,6 +43,7 @@ #import #import #import +#import #import #import #import diff --git a/Headers/Foundation/NSCache.h b/Headers/Foundation/NSCache.h new file mode 100644 index 000000000..53a3690ff --- /dev/null +++ b/Headers/Foundation/NSCache.h @@ -0,0 +1,112 @@ +#if OS_API_VERSION(MAC_OS_X_VERSION_10_6, GS_API_LATEST) + +#import + +@class NSString; +@class NSMutableDictionary; +@class NSMutableArray; +#ifndef __has_feature +#define __has_feature(x) 0 +#endif +@interface NSCache : NSObject +{ +#if !__has_feature(objc_nonfragile_abi) || defined(EXPOSE_GSCACHE_IVARS) + @private + /** The maximum total cost of all cache objects. */ + NSUInteger _costLimit; + /** Total cost of currently-stored objects. */ + NSUInteger _totalCost; + /** The maximum number of objects in the cache. */ + NSUInteger _countLimit; + /** The delegate object, notified when objects are about to be evicted. */ + id _delegate; + /** Flag indicating whether discarded objects should be evicted */ + BOOL _evictsObjectsWithDiscardedContent; + /** Name of this cache. */ + NSString *_name; + /** The mapping from names to objects in this cache. */ + NSMutableDictionary *_objects; + /** LRU ordering of all potentially-evictable objects in this cache. */ + NSMutableArray *_accesses; + /** Total number of accesses to objects */ + int64_t _totalAccesses; +#endif +} +/** + * Returns the maximum number of objects that are supported by this cache. + */ +- (NSUInteger)countLimit; +/** + * Returns the cache's delegate. + */ +- (id)delegate; +/** + * Returns whether objects stored in this cache which implement the + * NSDiscardableContent protocol are removed from the cache when their contents + * are evicted. + */ +- (BOOL)evictsObjectsWithDiscardedContent; +/** + * Returns the name associated with this cache. + */ +- (NSString*)name; +/** + * Returns an object associated with the specified key in this cache. + */ +- (id)objectForKey: (id)key; +/** + * Removes all objects from this cache. + */ +- (void)removeAllObjects; +/** + * Removes the object associated with the given key. + */ +- (void)removeObjectForKey: (id)key; +/** + * Sets the maximum number of objects permitted in this cache. This limit is + * advisory; caches may choose to disregard it temporarily or permanently. A + * limit of 0 is used to indicate no limit; this is the default. + */ +- (void)setCountLimit: (NSUInteger)lim;; +/** + * Sets the delegate for this cache. The delegate will be notified when an + * object is being evicted or removed from the cache. + */ +- (void)setDelegate:(id)del; +/** + * Sets whether this cache will evict objects that conform to the + * NSDiscardableContent protocol, or simply discard their contents. + */ +- (void)setEvictsObjectsWithDiscardedContent:(BOOL)b; +/** + * Sets the name for this cache. + */ +- (void)setName: (NSString*)cacheName; +/** + * Adds an object and its associated cost. The cache will endeavor to keep the + * total cost below the value set with -setTotalCostLimit: by discarding the + * contents of objects which implement the NSDiscardableContent protocol. + */ +- (void)setObject: (id)obj forKey: (id)key cost: (NSUInteger)num; +/** + * Adds an object to the cache without associating a cost with it. + */ +- (void)setObject: (id)obj forKey: (id)key; +/** + * Sets the maximum total cost for objects stored in this cache. This limit is + * advisory; caches may choose to disregard it temporarily or permanently. A + * limit of 0 is used to indicate no limit; this is the default. + */ +- (void)setTotalCostLimit: (NSUInteger)lim; +@end +/** + * Protocol implemented by NSCache delegate objects. + */ +@protocol NSCacheDelegate +/** + * Delegate method, called just before the cache removes an object, either as + * the result of user action or due to the cache becoming full. + */ +- (void)cache: (NSCache*)cache willEvictObject: (id)obj; +@end +#endif //OS_API_VERSION(MAC_OS_X_VERSION_10_6, GS_API_LATEST) diff --git a/Source/GNUmakefile b/Source/GNUmakefile index 0e4e07b07..9f4f8a409 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -177,6 +177,7 @@ NSAssertionHandler.m \ NSAttributedString.m \ NSAutoreleasePool.m \ NSBundle.m \ +NSCache.m \ NSCachedURLResponse.m \ NSCalendarDate.m \ NSCallBacks.m \ @@ -332,6 +333,7 @@ NSAttributedString.h \ NSAutoreleasePool.h \ NSBundle.h \ NSByteOrder.h \ +NSCache.h\ NSCalendarDate.h \ NSCharacterSet.h \ NSClassDescription.h \ diff --git a/Source/NSCache.m b/Source/NSCache.m new file mode 100644 index 000000000..3438d5ee4 --- /dev/null +++ b/Source/NSCache.m @@ -0,0 +1,227 @@ +#define EXPOSE_GSCACHE_IVARS 1 +#import + +/** + * _GSCachedObject is effectively used as a structure containing the various + * things that need to be associated with objects stored in an NSCache. It is + * an NSObject subclass so that it can be used with OpenStep collection + * classes. + */ +@interface _GSCachedObject : NSObject +{ + @public + id object; + NSString *key; + int accessCount; + NSUInteger cost; + BOOL isEvictable; +} +@end + +@interface NSCache (EvictionPolicy) +/** The method controlling eviction policy in an NSCache. */ +- (void)_evictObjectsToMakeSpaceForObjectWithCost: (NSUInteger)cost; +@end + +@implementation NSCache +- (id)init +{ + if (nil == (self = [super init])) { return nil; } + _objects = [NSMutableDictionary new]; + _accesses = [NSMutableArray new]; + return self; +} +- (NSUInteger)countLimit +{ + return _countLimit; +} +- (id)delegate +{ + return _delegate; +} +- (BOOL)evictsObjectsWithDiscardedContent +{ + return _evictsObjectsWithDiscardedContent; +} +- (NSString*)name +{ + return _name; +} +- (id)objectForKey: (id)key +{ + _GSCachedObject *obj = [_objects objectForKey: key]; + if (nil == obj) + { + return nil; + } + if (obj->isEvictable) + { + // Move the object to the end of the access list. + [_accesses removeObjectIdenticalTo: obj]; + [_accesses addObject: obj]; + } + obj->accessCount++; + _totalAccesses++; + return obj->object; +} +- (void)removeAllObjects +{ + NSEnumerator *e = [_objects objectEnumerator]; + _GSCachedObject *obj; + while (nil != (obj = [e nextObject])) + { + [_delegate cache: self willEvictObject: obj->object]; + } + [_objects removeAllObjects]; + [_accesses removeAllObjects]; + _totalAccesses = 0; +} +- (void)removeObjectForKey: (id)key +{ + _GSCachedObject *obj = [_objects objectForKey: key]; + if (nil != obj) + { + [_delegate cache: self willEvictObject: obj->object]; + _totalAccesses -= obj->accessCount; + [_objects removeObjectForKey: key]; + [_accesses removeObjectIdenticalTo: obj]; + } +} +- (void)setCountLimit: (NSUInteger)lim +{ + _countLimit = lim; +} +- (void)setDelegate:(id)del +{ + _delegate = del; +} +- (void)setEvictsObjectsWithDiscardedContent:(BOOL)b +{ + _evictsObjectsWithDiscardedContent = b; +} +- (void)setName: (NSString*)cacheName +{ + ASSIGN(_name, cacheName); +} +- (void)setObject: (id)obj forKey: (id)key cost: (NSUInteger)num +{ + _GSCachedObject *old = [_objects objectForKey: key]; + if (nil != old) + { + [self removeObjectForKey: old->key]; + } + [self _evictObjectsToMakeSpaceForObjectWithCost: num]; + _GSCachedObject *new = [_GSCachedObject new]; + // Retained here, released when obj is dealloc'd + new->object = RETAIN(obj); + new->key = RETAIN(key); + new->cost = num; + if ([obj conformsToProtocol: @protocol(NSDiscardableContent)]) + { + new->isEvictable = YES; + [_accesses addObject: new]; + } + [_objects setObject: new forKey: key]; + RELEASE(obj); + _totalCost += num; +} +- (void)setObject: (id)obj forKey: (id)key +{ + [self setObject: obj forKey: key cost: 0]; +} +- (void)setTotalCostLimit: (NSUInteger)lim +{ + _costLimit = lim; +} +- (NSUInteger)totalCostLimit +{ + return _costLimit; +} +/** + * This method is the one that handles the eviction policy. This + * implementation uses a relatively simple LRU/LFU hybrid. The NSCache + * documentation from Apple makes it clear that the policy may change, so we + * could in future have a class cluster with pluggable policies for different + * caches or some other mechanism. + */ +- (void)_evictObjectsToMakeSpaceForObjectWithCost: (NSUInteger)cost +{ + NSUInteger spaceNeeded = 0; + NSUInteger count = [_objects count]; + + if (_costLimit > 0 && _totalCost + cost > _costLimit) + { + spaceNeeded = _totalCost + cost - _costLimit; + } + + // Only evict if we need the space. + if (count > 0 && (spaceNeeded > 0 || count >= _countLimit)) + { + NSMutableArray *evictedKeys = nil; + // Round up slightly. + NSUInteger averageAccesses = (_totalAccesses / count * 0.2) + 1; + NSEnumerator *e = [_accesses objectEnumerator]; + _GSCachedObject *obj; + + if (_evictsObjectsWithDiscardedContent) + { + evictedKeys = [[NSMutableArray alloc] init]; + } + while (nil != (obj = [e nextObject])) + { + // Don't evict frequently accessed objects. + if (obj->accessCount < averageAccesses && obj->isEvictable) + { + [obj->object discardContentIfPossible]; + if ([obj->object isContentDiscarded]) + { + NSUInteger cost = obj->cost; + // Evicted objects have no cost. + obj->cost = 0; + // Don't try evicting this again in future; it's gone already. + obj->isEvictable = NO; + // Remove this object as well as its contents if required + if (_evictsObjectsWithDiscardedContent) + { + [evictedKeys addObject: obj->key]; + } + _totalCost -= cost; + // If we've freed enough space, give up + if (cost > spaceNeeded) + { + break; + } + spaceNeeded -= cost; + } + } + } + // Evict all of the objects whose content we have discarded if required + if (_evictsObjectsWithDiscardedContent) + { + NSString *key; + e = [evictedKeys objectEnumerator]; + while (nil != (key = [e nextObject])) + { + [self removeObjectForKey: key]; + } + } + } +} + +- (void)dealloc +{ + [_name release]; + [_objects release]; + [_accesses release]; + [super dealloc]; +} +@end + +@implementation _GSCachedObject +- (void)dealloc +{ + [object release]; + [key release]; + [super dealloc]; +} +@end