/* -*-objc-*- */ /** Implementation of GSCache for GNUStep Copyright (C) 2005 Free Software Foundation, Inc. Written by: Richard Frith-Macdonald Date: October 2005 This file is part of the Performance Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA. $Date$ $Revision$ */ #include #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "GSCache.h" #import "GSTicker.h" #if !defined(GNUSTEP) #include #if (MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_4) #define class_getInstanceSize(isa) ((struct objc_class *)isa)->instance_size #endif #import "NSObject+GSExtensions.h" #endif @interface GSCache (Threading) + (void) _becomeThreaded: (NSNotification*)n; - (void) _createLock; @end @interface GSCacheItem : NSObject { @public GSCacheItem *next; GSCacheItem *prev; unsigned life; unsigned warn; unsigned when; NSUInteger size; id key; id object; } + (GSCacheItem*) newWithObject: (id)anObject forKey: (id)aKey; @end @implementation GSCacheItem + (GSCacheItem*) newWithObject: (id)anObject forKey: (id)aKey { GSCacheItem *i; i = (GSCacheItem*)NSAllocateObject(self, 0, NSDefaultMallocZone()); i->object = [anObject retain]; i->key = [aKey copy]; return i; } - (void) dealloc { [key release]; [object release]; [super dealloc]; } - (NSUInteger) sizeInBytesExcluding: (NSHashTable*)exclude { NSUInteger bytes = [super sizeInBytesExcluding: exclude]; if (bytes > 0) { bytes += [key sizeInBytesExcluding: exclude]; bytes += [object sizeInBytesExcluding: exclude]; } return bytes; } @end @implementation GSCache static NSHashTable *allCaches = 0; static NSRecursiveLock *allCachesLock = nil; static int itemOffset = 0; typedef struct { id delegate; void (*refresh)(id, SEL, id, id, unsigned, unsigned); BOOL (*replace)(id, SEL, id, id, unsigned, unsigned); unsigned currentObjects; NSUInteger currentSize; unsigned lifetime; unsigned maxObjects; NSUInteger maxSize; unsigned hits; unsigned misses; NSMapTable *contents; GSCacheItem *first; NSString *name; NSHashTable *exclude; NSRecursiveLock *lock; } Item; #define my ((Item*)((void*)self + itemOffset)) /* * Add item to linked list starting at *first */ static void appendItem(GSCacheItem *item, GSCacheItem **first) { if (*first == nil) { item->next = item->prev = item; *first = item; } else { (*first)->prev->next = item; item->prev = (*first)->prev; (*first)->prev = item; item->next = *first; } } /* * Remove item from linked list starting at *first */ static void removeItem(GSCacheItem *item, GSCacheItem **first) { if (*first == item) { if (item->next == item) { *first = nil; } else { *first = item->next; } } item->next->prev = item->prev; item->prev->next = item->next; item->prev = item->next = item; } + (NSArray*) allInstances { NSArray *a; [allCachesLock lock]; a = NSAllHashTableObjects(allCaches); [allCachesLock unlock]; return a; } + (id) alloc { return [self allocWithZone: NSDefaultMallocZone()]; } + (id) allocWithZone: (NSZone*)z { GSCache *c; c = (GSCache*)NSAllocateObject(self, sizeof(Item), z); return c; } + (NSString*) description { NSMutableString *ms; NSHashEnumerator e; GSCache *c; ms = [NSMutableString stringWithString: [super description]]; [allCachesLock lock]; e = NSEnumerateHashTable(allCaches); while ((c = (GSCache*)NSNextHashEnumeratorItem(&e)) != nil) { [ms appendFormat: @"\n%@", [c description]]; } NSEndHashTableEnumeration(&e); [allCachesLock unlock]; return ms; } + (void) initialize { if (allCaches == 0) { itemOffset = class_getInstanceSize(self); allCaches = NSCreateHashTable(NSNonRetainedObjectHashCallBacks, 0); if ([NSThread isMultiThreaded] == YES) { [self _becomeThreaded: nil]; } else { /* If and when we become multi-threaded, the +_becomeThreaded: * method will remove us as an observer and will create a lock * for the table of all caches, then ask each cache to create * its own lock. */ [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(_becomeThreaded:) name: NSWillBecomeMultiThreadedNotification object: nil]; } GSTickerTimeNow(); } } - (unsigned) currentObjects { return my->currentObjects; } - (NSUInteger) currentSize { return my->currentSize; } - (void) dealloc { [allCachesLock lock]; NSHashRemove(allCaches, (void*)self); [allCachesLock unlock]; if (my->contents != 0) { [self shrinkObjects: 0 andSize: 0]; NSFreeMapTable(my->contents); } [my->exclude release]; [my->name release]; [my->lock release]; [super dealloc]; } - (id) delegate { return my->delegate; } - (NSString*) description { NSString *n; [my->lock lock]; n = my->name; if (n == nil) { n = [super description]; } n = [NSString stringWithFormat: @" %@\n" @" Items: %u(%u)\n" @" Size: %"PRIuPTR"(%"PRIuPTR")\n" @" Life: %u\n" @" Hit: %u\n" @" Miss: %u\n", n, my->currentObjects, my->maxObjects, my->currentSize, my->maxSize, my->lifetime, my->hits, my->misses]; [my->lock unlock]; return n; } - (id) init { if (nil != (self = [super init])) { if ([NSThread isMultiThreaded] == YES) { [self _createLock]; } my->contents = NSCreateMapTable(NSObjectMapKeyCallBacks, NSObjectMapValueCallBacks, 0); [allCachesLock lock]; NSHashInsert(allCaches, (void*)self); [allCachesLock unlock]; } return self; } - (unsigned) lifetime { return my->lifetime; } - (unsigned) maxObjects { return my->maxObjects; } - (NSUInteger) maxSize { return my->maxSize; } - (NSString*) name { NSString *n; [my->lock lock]; n = [my->name retain]; [my->lock unlock]; return [n autorelease]; } - (id) objectForKey: (id)aKey { id object; GSCacheItem *item; unsigned when = GSTickerTimeTick(); [my->lock lock]; item = (GSCacheItem*)NSMapGet(my->contents, aKey); if (item == nil) { my->misses++; [my->lock unlock]; return nil; } if (item->when > 0 && item->when < when) { BOOL keep = NO; if (0 != my->replace) { GSCacheItem *orig = [item retain]; [my->lock unlock]; NS_DURING { keep = (*(my->replace))(my->delegate, @selector(shouldKeepItem:withKey:lifetime:after:), item->object, aKey, item->life, when - item->when); } NS_HANDLER { [my->lock unlock]; [localException raise]; } NS_ENDHANDLER [my->lock lock]; if (keep == YES) { GSCacheItem *current; /* Refetch in case delegate changed it. */ current = (GSCacheItem*)NSMapGet(my->contents, aKey); if (current == nil) { /* Delegate must have deleted the item even though * it returned YES to say we should keep it ... * we count this as a miss. */ my->misses++; [my->lock unlock]; [orig release]; return nil; } else if (orig == current) { /* Delegate told us to keep the original item so we * update its expiry time. */ item->when = when + item->life; item->warn = when + item->life / 2; } else { /* Delegate replaced the item with another and told * us to keep that one. */ item = current; } } [orig release]; } if (keep == NO) { removeItem(item, &my->first); my->currentObjects--; if (my->maxSize > 0) { my->currentSize -= item->size; } NSMapRemove(my->contents, (void*)item->key); my->misses++; [my->lock unlock]; return nil; // Lifetime expired. } } else if (item->warn > 0 && item->warn < when) { item->warn = 0; // Don't warn again. if (0 != my->refresh) { GSCacheItem *orig = [item retain]; GSCacheItem *current; [my->lock unlock]; NS_DURING { (*(my->refresh))(my->delegate, @selector(mayRefreshItem:withKey:lifetime:after:), item->object, aKey, item->life, when - item->when); } NS_HANDLER { [my->lock unlock]; [localException raise]; } NS_ENDHANDLER [my->lock lock]; /* Refetch in case delegate changed it. */ current = (GSCacheItem*)NSMapGet(my->contents, aKey); if (current == nil) { /* Delegate must have deleted the item! * So we count this as a miss. */ my->misses++; [my->lock unlock]; [orig release]; return nil; } else { item = current; } [orig release]; } } // Least recently used ... move to end of list. removeItem(item, &my->first); appendItem(item, &my->first); my->hits++; object = [item->object retain]; [my->lock unlock]; return [object autorelease]; } - (void) purge { if (my->contents != 0) { unsigned when = GSTickerTimeTick(); NSMapEnumerator e; GSCacheItem *i; id k; [my->lock lock]; e = NSEnumerateMapTable(my->contents); while (NSNextMapEnumeratorPair(&e, (void**)&k, (void**)&i) != 0) { if (i->when > 0 && i->when < when) { removeItem(i, &my->first); my->currentObjects--; if (my->maxSize > 0) { my->currentSize -= i->size; } NSMapRemove(my->contents, (void*)i->key); } } NSEndMapTableEnumeration(&e); [my->lock unlock]; } } - (oneway void) release { /* We lock the table while checking, to prevent * another thread from grabbing this object while we are * checking it. * If we are going to deallocate the object, we first remove * it from the table so that no other thread will find it * and try to use it while it is being deallocated. */ [allCachesLock lock]; if (NSDecrementExtraRefCountWasZero(self)) { NSHashRemove(allCaches, (void*)self); [allCachesLock unlock]; [self dealloc]; } else { [allCachesLock unlock]; } } - (void) setDelegate: (id)anObject { [my->lock lock]; my->delegate = anObject; if ([my->delegate respondsToSelector: @selector(shouldKeepItem:withKey:lifetime:after:)]) { my->replace = (BOOL (*)(id,SEL,id,id,unsigned,unsigned)) [my->delegate methodForSelector: @selector(shouldKeepItem:withKey:lifetime:after:)]; } else { my->replace = 0; } if ([my->delegate respondsToSelector: @selector(mayRefreshItem:withKey:lifetime:after:)]) { my->refresh = (void (*)(id,SEL,id,id,unsigned,unsigned)) [my->delegate methodForSelector: @selector(mayRefreshItem:withKey:lifetime:after:)]; } else { my->refresh = 0; } [my->lock unlock]; } - (void) setLifetime: (unsigned)max { my->lifetime = max; } - (void) setMaxObjects: (unsigned)max { [my->lock lock]; my->maxObjects = max; if (my->currentObjects > my->maxObjects) { [self shrinkObjects: my->maxObjects andSize: my->maxSize]; } [my->lock unlock]; } - (void) setMaxSize: (NSUInteger)max { [my->lock lock]; if (max > 0 && my->maxSize == 0) { NSMapEnumerator e = NSEnumerateMapTable(my->contents); GSCacheItem *i; id k; NSUInteger size = 0; if (nil == my->exclude) { my->exclude = NSCreateHashTable(NSNonOwnedPointerHashCallBacks, 0); } while (NSNextMapEnumeratorPair(&e, (void**)&k, (void**)&i) != 0) { if (i->size == 0) { i->size = [i->object sizeInBytesExcluding: my->exclude]; [my->exclude removeAllObjects]; } if (i->size > max) { /* * Item in cache is too big for new size limit ... * Remove it. */ removeItem(i, &my->first); NSMapRemove(my->contents, (void*)i->key); my->currentObjects--; continue; } size += i->size; } NSEndMapTableEnumeration(&e); my->currentSize = size; } else if (max == 0) { my->currentSize = 0; } my->maxSize = max; if (my->currentSize > my->maxSize) { [self shrinkObjects: my->maxObjects andSize: my->maxSize]; } [my->lock unlock]; } - (void) setName: (NSString*)name { [my->lock lock]; [name retain]; [my->name release]; my->name = name; [my->lock unlock]; } - (void) setObject: (id)anObject forKey: (id)aKey { [self setObject: anObject forKey: aKey lifetime: my->lifetime]; } - (void) setObject: (id)anObject forKey: (id)aKey lifetime: (unsigned)lifetime { GSCacheItem *item; unsigned maxObjects; NSUInteger maxSize; unsigned addObjects = (anObject == nil ? 0 : 1); NSUInteger addSize = 0; [my->lock lock]; maxObjects = my->maxObjects; maxSize = my->maxSize; item = (GSCacheItem*)NSMapGet(my->contents, aKey); if (item != nil) { removeItem(item, &my->first); my->currentObjects--; if (my->maxSize > 0) { my->currentSize -= item->size; } NSMapRemove(my->contents, (void*)aKey); } if (addObjects > 0 && (maxSize > 0 || maxObjects > 0)) { if (maxSize > 0) { if (nil == my->exclude) { my->exclude = NSCreateHashTable(NSNonOwnedPointerHashCallBacks, 0); } addSize = [anObject sizeInBytesExcluding: my->exclude]; [my->exclude removeAllObjects]; if (addSize > maxSize) { addObjects = 0; // Object too big to cache. } } } if (addObjects > 0) { /* * Make room for new object. */ [self shrinkObjects: maxObjects - addObjects andSize: maxSize - addSize]; item = [GSCacheItem newWithObject: anObject forKey: aKey]; if (lifetime > 0) { unsigned tick = GSTickerTimeTick(); item->when = tick + lifetime; item->warn = tick + lifetime / 2; } item->life = lifetime; item->size = addSize; NSMapInsert(my->contents, (void*)item->key, (void*)item); appendItem(item, &my->first); my->currentObjects += addObjects; my->currentSize += addSize; [item release]; } [my->lock unlock]; } - (void) setObject: (id)anObject forKey: (id)aKey until: (NSDate*)expires { NSTimeInterval i; i = (expires == nil) ? 0.0 : [expires timeIntervalSinceReferenceDate]; i -= GSTickerTimeNow(); if (i <= 0.0) { [self setObject: nil forKey: aKey]; // Already expired } else { unsigned limit; if (i > 2415919103.0) { limit = 0; // Limit in far future. } else { limit = (unsigned)i; } [self setObject: anObject forKey: aKey lifetime: limit]; } } - (void) shrinkObjects: (unsigned)objects andSize: (NSUInteger)size { NSUInteger newSize; unsigned newObjects; [my->lock lock]; newSize = [self currentSize]; newObjects = [self currentObjects]; if (newObjects > objects || (my->maxSize > 0 && newSize > size)) { [self purge]; newSize = [self currentSize]; newObjects = [self currentObjects]; while (newObjects > objects || (my->maxSize > 0 && newSize > size)) { GSCacheItem *item; item = my->first; removeItem(item, &my->first); newObjects--; if (my->maxSize > 0) { newSize -= item->size; } NSMapRemove(my->contents, (void*)item->key); } my->currentObjects = newObjects; my->currentSize = newSize; } [my->lock unlock]; } - (NSUInteger) sizeInBytesExcluding: (NSHashTable*)exclude { NSUInteger size; [my->lock lock]; size = [super sizeInBytesExcluding: exclude]; if (size > 0) { size += sizeof(Item) + [my->contents sizeInBytesExcluding: exclude] + [my->exclude sizeInBytesExcluding: exclude] + [my->name sizeInBytesExcluding: exclude] + [my->lock sizeInBytesExcluding: exclude]; } [my->lock unlock]; return size; } @end @implementation GSCache (Threading) + (void) _becomeThreaded: (NSNotification*)n { NSHashEnumerator e; GSCache *c; [[NSNotificationCenter defaultCenter] removeObserver: self name: NSWillBecomeMultiThreadedNotification object: nil]; allCachesLock = [NSRecursiveLock new]; e = NSEnumerateHashTable(allCaches); while ((c = (GSCache*)NSNextHashEnumeratorItem(&e)) != nil) { [c _createLock]; } NSEndHashTableEnumeration(&e); } - (void) _createLock { my->lock = [NSRecursiveLock new]; } @end