Improved RCTCache performance + fixed border color crash

Summary:
RCTCache had really bad insertion performance when the cache was full due to having to LRU-sort the entries. This was making color
animations very slow.

I've fixed this in two ways:

1) by removing the sort and doing a linear search to remove old entries, which changes insertion perf to O(n) in the worst case instead of O(n log n) or even (n2).

2) by reducing the size of the color cache to 128 from 1024, which should be fine for normal use, without penalising animation performance.

Separately, border colors were not being retained, which caused crashes when the color cache was cleared. I've fixed that by retaining the border colors inside RCTView.
This commit is contained in:
Nick Lockwood 2015-07-14 04:53:54 -07:00
parent b34a85f4da
commit 0c61b49f0a
5 changed files with 145 additions and 88 deletions

View File

@ -15,7 +15,7 @@
* outside of the specified cost/count limits, and will be automatically * outside of the specified cost/count limits, and will be automatically
* cleared in the event of a memory warning. * cleared in the event of a memory warning.
*/ */
@interface RCTCache : NSCache @interface RCTCache : NSCache <NSFastEnumeration>
/** /**
* The total number of objects currently resident in the cache. * The total number of objects currently resident in the cache.
@ -33,6 +33,11 @@
- (id)objectForKeyedSubscript:(id<NSCopying>)key; - (id)objectForKeyedSubscript:(id<NSCopying>)key;
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key; - (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key;
/**
* Enumerate cached objects
*/
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block;
@end @end
@protocol RCTCacheDelegate <NSCacheDelegate> @protocol RCTCacheDelegate <NSCacheDelegate>

View File

@ -28,15 +28,6 @@
@implementation RCTCacheEntry @implementation RCTCacheEntry
+ (instancetype)entryWithObject:(id)object cost:(NSUInteger)cost sequenceNumber:(NSInteger)sequenceNumber
{
RCTCacheEntry *entry = [[self alloc] init];
entry.object = object;
entry.cost = cost;
entry.sequenceNumber = sequenceNumber;
return entry;
}
@end @end
@interface RCTCache_Private : NSObject @interface RCTCache_Private : NSObject
@ -46,24 +37,28 @@
@property (nonatomic, assign) NSUInteger totalCostLimit; @property (nonatomic, assign) NSUInteger totalCostLimit;
@property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger totalCost;
@property (nonatomic, strong) NSMutableDictionary *cache; @property (nonatomic, strong) NSMutableDictionary *cache;
@property (nonatomic, assign) BOOL delegateRespondsToWillEvictObject; @property (nonatomic, assign) NSUInteger totalCost;
@property (nonatomic, assign) BOOL delegateRespondsToShouldEvictObject;
@property (nonatomic, assign) BOOL currentlyCleaning;
@property (nonatomic, assign) NSInteger sequenceNumber; @property (nonatomic, assign) NSInteger sequenceNumber;
@property (nonatomic, strong) NSLock *lock;
@end @end
@implementation RCTCache_Private @implementation RCTCache_Private
{
BOOL _delegateRespondsToWillEvictObject;
BOOL _delegateRespondsToShouldEvictObject;
BOOL _currentlyCleaning;
NSMutableArray *_entryPool;
NSLock *_lock;
}
- (instancetype)init - (id)init
{ {
if ((self = [super init])) if ((self = [super init]))
{ {
//create storage //create storage
_cache = [[NSMutableDictionary alloc] init]; _cache = [[NSMutableDictionary alloc] init];
_entryPool = [[NSMutableArray alloc] init];
_lock = [[NSLock alloc] init]; _lock = [[NSLock alloc] init];
_totalCost = 0; _totalCost = 0;
@ -95,7 +90,7 @@
[_lock lock]; [_lock lock];
_countLimit = countLimit; _countLimit = countLimit;
[_lock unlock]; [_lock unlock];
[self cleanUp]; [self cleanUp:NO];
} }
- (void)setTotalCostLimit:(NSUInteger)totalCostLimit - (void)setTotalCostLimit:(NSUInteger)totalCostLimit
@ -103,7 +98,7 @@
[_lock lock]; [_lock lock];
_totalCostLimit = totalCostLimit; _totalCostLimit = totalCostLimit;
[_lock unlock]; [_lock unlock];
[self cleanUp]; [self cleanUp:NO];
} }
- (NSUInteger)count - (NSUInteger)count
@ -111,40 +106,51 @@
return [_cache count]; return [_cache count];
} }
- (void)cleanUp - (void)cleanUp:(BOOL)keepEntries
{ {
[_lock lock]; [_lock lock];
NSUInteger maxCount = [self countLimit] ?: INT_MAX; NSUInteger maxCount = _countLimit ?: INT_MAX;
NSUInteger maxCost = [self totalCostLimit] ?: INT_MAX; NSUInteger maxCost = _totalCostLimit ?: INT_MAX;
NSUInteger totalCount = [_cache count]; NSUInteger totalCount = _cache.count;
if (totalCount > maxCount || _totalCost > maxCost) NSMutableArray *keys = [_cache.allKeys mutableCopy];
while (totalCount > maxCount || _totalCost > maxCost)
{ {
//sort, oldest first NSInteger lowestSequenceNumber = INT_MAX;
NSArray *keys = [[_cache allKeys] sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { RCTCacheEntry *lowestEntry = nil;
RCTCacheEntry *entry1 = self.cache[key1]; id lowestKey = nil;
RCTCacheEntry *entry2 = self.cache[key2];
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
}];
//remove oldest items until within limit //remove oldest items until within limit
for (id key in keys) for (id key in keys)
{ {
if (totalCount <= maxCount && _totalCost <= maxCost)
{
break;
}
RCTCacheEntry *entry = _cache[key]; RCTCacheEntry *entry = _cache[key];
if (!_delegateRespondsToShouldEvictObject || [self.delegate cache:(RCTCache *)self shouldEvictObject:entry]) if (entry.sequenceNumber < lowestSequenceNumber)
{
lowestSequenceNumber = entry.sequenceNumber;
lowestEntry = entry;
lowestKey = key;
}
}
if (lowestKey)
{
[keys removeObject:lowestKey];
if (!_delegateRespondsToShouldEvictObject ||
[_delegate cache:(RCTCache *)self shouldEvictObject:lowestEntry.object])
{ {
if (_delegateRespondsToWillEvictObject) if (_delegateRespondsToWillEvictObject)
{ {
_currentlyCleaning = YES; _currentlyCleaning = YES;
[self.delegate cache:(RCTCache *)self willEvictObject:entry]; [self.delegate cache:(RCTCache *)self willEvictObject:lowestEntry.object];
_currentlyCleaning = NO; _currentlyCleaning = NO;
} }
[_cache removeObjectForKey:key]; [_cache removeObjectForKey:lowestKey];
_totalCost -= entry.cost; _totalCost -= lowestEntry.cost;
totalCount --; totalCount --;
if (keepEntries)
{
[_entryPool addObject:lowestEntry];
lowestEntry.object = nil;
}
} }
} }
} }
@ -161,8 +167,8 @@
{ {
//sort, oldest first (in case we want to use that information in our eviction test) //sort, oldest first (in case we want to use that information in our eviction test)
keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) { keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) {
RCTCacheEntry *entry1 = self.cache[key1]; RCTCacheEntry *entry1 = self->_cache[key1];
RCTCacheEntry *entry2 = self.cache[key2]; RCTCacheEntry *entry2 = self->_cache[key2];
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber)); return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
}]; }];
} }
@ -171,12 +177,12 @@
for (id key in keys) for (id key in keys)
{ {
RCTCacheEntry *entry = _cache[key]; RCTCacheEntry *entry = _cache[key];
if (!_delegateRespondsToShouldEvictObject || [self.delegate cache:(RCTCache *)self shouldEvictObject:entry]) if (!_delegateRespondsToShouldEvictObject || [_delegate cache:(RCTCache *)self shouldEvictObject:entry.object])
{ {
if (_delegateRespondsToWillEvictObject) if (_delegateRespondsToWillEvictObject)
{ {
_currentlyCleaning = YES; _currentlyCleaning = YES;
[self.delegate cache:(RCTCache *)self willEvictObject:entry]; [_delegate cache:(RCTCache *)self willEvictObject:entry.object];
_currentlyCleaning = NO; _currentlyCleaning = NO;
} }
[_cache removeObjectForKey:key]; [_cache removeObjectForKey:key];
@ -208,7 +214,7 @@
} }
} }
- (id)objectForKey:(id<NSCopying>)key - (id)objectForKey:(id)key
{ {
[_lock lock]; [_lock lock];
RCTCacheEntry *entry = _cache[key]; RCTCacheEntry *entry = _cache[key];
@ -227,7 +233,7 @@
return [self objectForKey:key]; return [self objectForKey:key];
} }
- (void)setObject:(id)obj forKey:(id<NSCopying>)key - (void)setObject:(id)obj forKey:(id)key
{ {
[self setObject:obj forKey:key cost:0]; [self setObject:obj forKey:key cost:0];
} }
@ -237,27 +243,44 @@
[self setObject:obj forKey:key cost:0]; [self setObject:obj forKey:key cost:0];
} }
- (void)setObject:(id)obj forKey:(id<NSCopying>)key cost:(NSUInteger)g - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
{ {
if (!obj)
{
[self removeObjectForKey:key];
return;
}
RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method."); RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
[_lock lock]; [_lock lock];
_totalCost -= [_cache[key] cost]; _totalCost -= [_cache[key] cost];
_totalCost += g; _totalCost += g;
_cache[key] = [RCTCacheEntry entryWithObject:obj cost:g sequenceNumber:_sequenceNumber++]; RCTCacheEntry *entry = _cache[key];
if (!entry) {
entry = [[RCTCacheEntry alloc] init];
_cache[key] = entry;
}
entry.object = obj;
entry.cost = g;
entry.sequenceNumber = _sequenceNumber++;
if (_sequenceNumber < 0) if (_sequenceNumber < 0)
{ {
[self resequence]; [self resequence];
} }
[_lock unlock]; [_lock unlock];
[self cleanUp]; [self cleanUp:YES];
} }
- (void)removeObjectForKey:(id<NSCopying>)key - (void)removeObjectForKey:(id)key
{ {
RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method."); RCTAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
[_lock lock]; [_lock lock];
_totalCost -= [_cache[key] cost]; RCTCacheEntry *entry = _cache[key];
[_cache removeObjectForKey:key]; if (entry) {
_totalCost -= entry.cost;
entry.object = nil;
[_entryPool addObject:entry];
[_cache removeObjectForKey:key];
}
[_lock unlock]; [_lock unlock];
} }
@ -267,20 +290,42 @@
[_lock lock]; [_lock lock];
_totalCost = 0; _totalCost = 0;
_sequenceNumber = 0; _sequenceNumber = 0;
for (RCTCacheEntry *entry in _cache.allValues)
{
entry.object = nil;
[_entryPool addObject:entry];
}
[_cache removeAllObjects]; [_cache removeAllObjects];
[_lock unlock]; [_lock unlock];
} }
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
objects:(id __unsafe_unretained [])buffer
count:(NSUInteger)len
{
[_lock lock];
NSUInteger count = [_cache countByEnumeratingWithState:state objects:buffer count:len];
[_lock unlock];
return count;
}
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
{
[_lock lock];
[_cache enumerateKeysAndObjectsUsingBlock:block];
[_lock unlock];
}
//handle unimplemented methods //handle unimplemented methods
- (BOOL)isKindOfClass:(Class)cls - (BOOL)isKindOfClass:(Class)aClass
{ {
//pretend that we're an RCTCache if anyone asks //pretend that we're an RCTCache if anyone asks
if (cls == [RCTCache class] || cls == [NSCache class]) if (aClass == [RCTCache class] || aClass == [NSCache class])
{ {
return YES; return YES;
} }
return [super isKindOfClass:cls]; return [super isKindOfClass:aClass];
} }
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
@ -308,19 +353,16 @@
@implementation RCTCache @implementation RCTCache
@dynamic count; + (id)alloc
@dynamic totalCost;
+ (instancetype)alloc
{ {
return (RCTCache *)[RCTCache_Private alloc]; return (RCTCache *)[RCTCache_Private alloc];
} }
- (id)objectForKeyedSubscript:(__unused NSNumber *)key - (id)objectForKeyedSubscript:(__unused id<NSCopying>)key { return nil; }
{
return nil;
}
- (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id<NSCopying>)key {} - (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id<NSCopying>)key {}
- (void)enumerateKeysAndObjectsUsingBlock:(__unused void (^)(id, id, BOOL *))block { }
- (NSUInteger)countByEnumeratingWithState:(__unused NSFastEnumerationState *)state
objects:(__unused __unsafe_unretained id [])buffer
count:(__unused NSUInteger)len { return 0; }
@end @end

View File

@ -396,7 +396,7 @@ RCT_CGSTRUCT_CONVERTER(CGAffineTransform, (@[
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
colorCache = [[RCTCache alloc] init]; colorCache = [[RCTCache alloc] init];
colorCache.countLimit = 1024; colorCache.countLimit = 128;
}); });
UIColor *color = colorCache[json]; UIColor *color = colorCache[json];
if (color) { if (color) {

View File

@ -64,7 +64,7 @@ typedef void (^RCTViewEventHandler)(RCTView *view);
@property (nonatomic, assign) CGFloat borderBottomRightRadius; @property (nonatomic, assign) CGFloat borderBottomRightRadius;
/** /**
* Border colors. * Border colors (actually retained).
*/ */
@property (nonatomic, assign) CGColorRef borderTopColor; @property (nonatomic, assign) CGColorRef borderTopColor;
@property (nonatomic, assign) CGColorRef borderRightColor; @property (nonatomic, assign) CGColorRef borderRightColor;

View File

@ -480,7 +480,7 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused)
_borderTopColor ?: _borderColor, _borderTopColor ?: _borderColor,
_borderLeftColor ?: _borderColor, _borderLeftColor ?: _borderColor,
_borderBottomColor ?: _borderColor, _borderBottomColor ?: _borderColor,
_borderRightColor ?: _borderColor _borderRightColor ?: _borderColor,
}; };
} }
@ -580,14 +580,15 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:unused)
#pragma mark Border Color #pragma mark Border Color
#define setBorderColor(side) \ #define setBorderColor(side) \
- (void)setBorder##side##Color:(CGColorRef)border##side##Color \ - (void)setBorder##side##Color:(CGColorRef)color \
{ \ { \
if (CGColorEqualToColor(_border##side##Color, border##side##Color)) { \ if (CGColorEqualToColor(_border##side##Color, color)) { \
return; \ return; \
} \ } \
_border##side##Color = border##side##Color; \ CGColorRelease(_border##side##Color); \
[self.layer setNeedsDisplay]; \ _border##side##Color = CGColorRetain(color); \
[self.layer setNeedsDisplay]; \
} }
setBorderColor() setBorderColor()
@ -598,14 +599,14 @@ setBorderColor(Left)
#pragma mark - Border Width #pragma mark - Border Width
#define setBorderWidth(side) \ #define setBorderWidth(side) \
- (void)setBorder##side##Width:(CGFloat)border##side##Width \ - (void)setBorder##side##Width:(CGFloat)width \
{ \ { \
if (_border##side##Width == border##side##Width) { \ if (_border##side##Width == width) { \
return; \ return; \
} \ } \
_border##side##Width = border##side##Width; \ _border##side##Width = width; \
[self.layer setNeedsDisplay]; \ [self.layer setNeedsDisplay]; \
} }
setBorderWidth() setBorderWidth()
@ -614,14 +615,14 @@ setBorderWidth(Right)
setBorderWidth(Bottom) setBorderWidth(Bottom)
setBorderWidth(Left) setBorderWidth(Left)
#define setBorderRadius(side) \ #define setBorderRadius(side) \
- (void)setBorder##side##Radius:(CGFloat)border##side##Radius \ - (void)setBorder##side##Radius:(CGFloat)radius \
{ \ { \
if (_border##side##Radius == border##side##Radius) { \ if (_border##side##Radius == radius) { \
return; \ return; \
} \ } \
_border##side##Radius = border##side##Radius; \ _border##side##Radius = radius; \
[self.layer setNeedsDisplay]; \ [self.layer setNeedsDisplay]; \
} }
setBorderRadius() setBorderRadius()
@ -630,4 +631,13 @@ setBorderRadius(TopRight)
setBorderRadius(BottomLeft) setBorderRadius(BottomLeft)
setBorderRadius(BottomRight) setBorderRadius(BottomRight)
- (void)dealloc
{
CGColorRelease(_borderColor);
CGColorRelease(_borderTopColor);
CGColorRelease(_borderRightColor);
CGColorRelease(_borderBottomColor);
CGColorRelease(_borderLeftColor);
}
@end @end