Skip to content

Commit 3d80c8e

Browse files
authored
Significantly improves startup performance by asynchronously building… (#203)
* Improves cache miss on disk cache by two orders of magnetude on average on an iPhone 6. Essentially just adds an in memory map of what is known to be on disk. * Significantly improves startup performance by asynchronously building known state on startup. * I knew writing tests was a good idea. * Add entry to CHANGELOG * bundle up metadata as opposed to having two dicts. Thanks for the idea @appleguy :) * Use a condition lock instead. Thanks @appleguy! * Lot's of little fixes. Thanks @Adlai-Holler!
1 parent 084ad3e commit 3d80c8e

File tree

3 files changed

+186
-60
lines changed

3 files changed

+186
-60
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- [fix] Add some sane limits to the disk cache: [#201]https://github.com/pinterest/PINCache/pull/201
55
- [new] Update enumeration methods to allow a stop flag to be flipped by caller: [#204](https://github.com/pinterest/PINCache/pull/204)
66
- [performance] Improves cache miss performance by ~2 orders of magnitude on device: [#202](https://github.com/pinterest/PINCache/pull/202)
7+
- [performance] Significantly improve startup performance: [#203](https://github.com/pinterest/PINCache/pull/203)
78

89
## 3.0.1 -- Beta 5
910
- [fix] Respect small byteLimit settings by checking object size in setObject: [#198](https://github.com/pinterest/PINCache/pull/198)

Source/PINDiskCache.m

+126-59
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,22 @@ @interface PINDiskCacheMetadata : NSObject
4646
@end
4747

4848
@interface PINDiskCache () {
49-
NSConditionLock *_instanceLock;
50-
5149
PINDiskCacheSerializerBlock _serializer;
5250
PINDiskCacheDeserializerBlock _deserializer;
5351

5452
PINDiskCacheKeyEncoderBlock _keyEncoder;
5553
PINDiskCacheKeyDecoderBlock _keyDecoder;
5654
}
5755

56+
@property (assign, nonatomic) pthread_mutex_t mutex;
5857
@property (copy, nonatomic) NSString *name;
5958
@property (assign) NSUInteger byteCount;
6059
@property (strong, nonatomic) NSURL *cacheURL;
6160
@property (strong, nonatomic) PINOperationQueue *operationQueue;
6261
@property (strong, nonatomic) NSMutableDictionary <NSString *, PINDiskCacheMetadata *> *metadata;
62+
@property (assign, nonatomic) pthread_cond_t diskWritableCondition;
63+
@property (assign, nonatomic) BOOL diskWritable;
64+
@property (assign, nonatomic) pthread_cond_t diskStateKnownCondition;
6365
@property (assign, nonatomic) BOOL diskStateKnown;
6466
@end
6567

@@ -83,6 +85,14 @@ @implementation PINDiskCache
8385

8486
#pragma mark - Initialization -
8587

88+
- (void)dealloc
89+
{
90+
__unused int result = pthread_mutex_destroy(&_mutex);
91+
NSCAssert(result == 0, @"Failed to destroy lock in PINMemoryCache %p. Code: %d", (void *)self, result);
92+
pthread_cond_destroy(&_diskWritableCondition);
93+
pthread_cond_destroy(&_diskStateKnownCondition);
94+
}
95+
8696
- (instancetype)init
8797
{
8898
@throw [NSException exceptionWithName:@"Must initialize with a name" reason:@"PINDiskCache must be initialized with a name. Call initWithName: instead." userInfo:nil];
@@ -143,10 +153,12 @@ - (instancetype)initWithName:(NSString *)name
143153
@"PINDiskCache must be initialized with a encoder AND decoder.");
144154

145155
if (self = [super init]) {
156+
__unused int result = pthread_mutex_init(&_mutex, NULL);
157+
NSAssert(result == 0, @"Failed to init lock in PINMemoryCache %@. Code: %d", self, result);
158+
146159
_name = [name copy];
147160
_prefix = [prefix copy];
148161
_operationQueue = operationQueue;
149-
_instanceLock = [[NSConditionLock alloc] initWithCondition:PINDiskCacheConditionNotReady];
150162
_willAddObjectBlock = nil;
151163
_willRemoveObjectBlock = nil;
152164
_willRemoveAllObjectsBlock = nil;
@@ -195,14 +207,16 @@ - (instancetype)initWithName:(NSString *)name
195207
} else {
196208
_keyDecoder = self.defaultKeyDecoder;
197209
}
210+
211+
pthread_cond_init(&_diskWritableCondition, NULL);
212+
pthread_cond_init(&_diskStateKnownCondition, NULL);
198213

199214
//we don't want to do anything without setting up the disk cache, but we also don't want to block init, it can take a while to initialize. This must *not* be done on _operationQueue because other operations added may hold the lock and fill up the queue.
200215
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
201-
//should always be able to aquire the lock unless the below code is running.
202-
[_instanceLock lockWhenCondition:PINDiskCacheConditionNotReady];
216+
[self lock];
203217
[self _locked_createCacheDirectory];
204-
[self _locked_initializeDiskProperties];
205-
[_instanceLock unlockWithCondition:PINDiskCacheConditionReady];
218+
[self unlock];
219+
[self initializeDiskProperties];
206220
});
207221
}
208222
return self;
@@ -415,55 +429,77 @@ + (void)emptyTrash
415429

416430
- (BOOL)_locked_createCacheDirectory
417431
{
418-
if ([[NSFileManager defaultManager] fileExistsAtPath:[_cacheURL path]])
419-
return NO;
432+
BOOL created = NO;
433+
if ([[NSFileManager defaultManager] fileExistsAtPath:[_cacheURL path]] == NO) {
434+
NSError *error = nil;
435+
created = [[NSFileManager defaultManager] createDirectoryAtURL:_cacheURL
436+
withIntermediateDirectories:YES
437+
attributes:nil
438+
error:&error];
439+
PINDiskCacheError(error);
440+
}
420441

421-
NSError *error = nil;
422-
BOOL success = [[NSFileManager defaultManager] createDirectoryAtURL:_cacheURL
423-
withIntermediateDirectories:YES
424-
attributes:nil
425-
error:&error];
426-
PINDiskCacheError(error);
442+
427443

428-
return success;
444+
// while this may not be true if success is false, it's better than deadlocking later.
445+
_diskWritable = YES;
446+
pthread_cond_broadcast(&_diskWritableCondition);
447+
448+
return created;
429449
}
430450

431-
- (void)_locked_initializeDiskProperties
451+
- (void)initializeDiskProperties
432452
{
433453
NSUInteger byteCount = 0;
434454
NSArray *keys = @[ NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey ];
435455

436456
NSError *error = nil;
437-
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:_cacheURL
438-
includingPropertiesForKeys:keys
439-
options:NSDirectoryEnumerationSkipsHiddenFiles
440-
error:&error];
457+
458+
[self lock];
459+
NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:_cacheURL
460+
includingPropertiesForKeys:keys
461+
options:NSDirectoryEnumerationSkipsHiddenFiles
462+
error:&error];
463+
[self unlock];
464+
441465
PINDiskCacheError(error);
442466

443467
for (NSURL *fileURL in files) {
444468
NSString *key = [self keyForEncodedFileURL:fileURL];
445469

446470
error = nil;
447-
NSDictionary *dictionary = [fileURL resourceValuesForKeys:keys error:&error];
448-
PINDiskCacheError(error);
449471

450-
_metadata[key] = [[PINDiskCacheMetadata alloc] init];
472+
// Continually grab and release lock while processing files to avoid contention
473+
[self lock];
474+
NSDictionary *dictionary = [fileURL resourceValuesForKeys:keys error:&error];
475+
PINDiskCacheError(error);
476+
477+
if (_metadata[key] == nil) {
478+
_metadata[key] = [[PINDiskCacheMetadata alloc] init];
479+
}
451480

452-
NSDate *date = [dictionary objectForKey:NSURLContentModificationDateKey];
453-
if (date && key)
454-
_metadata[key].date = date;
481+
NSDate *date = [dictionary objectForKey:NSURLContentModificationDateKey];
482+
if (date && key)
483+
_metadata[key].date = date;
455484

456-
NSNumber *fileSize = [dictionary objectForKey:NSURLTotalFileAllocatedSizeKey];
457-
if (fileSize) {
458-
_metadata[key].size = fileSize;
459-
byteCount += [fileSize unsignedIntegerValue];
460-
}
485+
NSNumber *fileSize = [dictionary objectForKey:NSURLTotalFileAllocatedSizeKey];
486+
if (fileSize) {
487+
_metadata[key].size = fileSize;
488+
byteCount += [fileSize unsignedIntegerValue];
489+
}
490+
[self unlock];
461491
}
462492

463-
if (byteCount > 0)
464-
_byteCount = byteCount;
493+
[self lock];
494+
if (byteCount > 0)
495+
_byteCount = byteCount;
496+
497+
if (self->_byteLimit > 0 && self->_byteCount > self->_byteLimit)
498+
[self trimToSizeByDateAsync:self->_byteLimit completion:nil];
465499

466-
_diskStateKnown = YES;
500+
_diskStateKnown = YES;
501+
pthread_cond_broadcast(&_diskStateKnownCondition);
502+
[self unlock];
467503
}
468504

469505
- (void)asynchronouslySetFileModificationDate:(NSDate *)date forURL:(NSURL *)fileURL
@@ -472,7 +508,7 @@ - (void)asynchronouslySetFileModificationDate:(NSDate *)date forURL:(NSURL *)fil
472508
[self.operationQueue addOperation:^{
473509
PINDiskCache *strongSelf = weakSelf;
474510
if (strongSelf) {
475-
[strongSelf lock];
511+
[strongSelf lockForWriting];
476512
[strongSelf _locked_setFileModificationDate:date forURL:fileURL];
477513
[strongSelf unlock];
478514
}
@@ -505,7 +541,8 @@ - (BOOL)removeFileAndExecuteBlocksForKey:(NSString *)key
505541
{
506542
NSURL *fileURL = [self encodedFileURLForKey:key];
507543

508-
[self lock];
544+
// We only need to lock until writable at the top because once writable, always writable
545+
[self lockForWriting];
509546
if (!fileURL || ![[NSFileManager defaultManager] fileExistsAtPath:[fileURL path]]) {
510547
[self unlock];
511548
return NO;
@@ -546,7 +583,7 @@ - (BOOL)removeFileAndExecuteBlocksForKey:(NSString *)key
546583

547584
- (void)trimDiskToSize:(NSUInteger)trimByteCount
548585
{
549-
[self lock];
586+
[self lockForWriting];
550587
if (_byteCount > trimByteCount) {
551588
NSArray *keysSortedBySize = [_metadata keysSortedByValueUsingComparator:^NSComparisonResult(PINDiskCacheMetadata * _Nonnull obj1, PINDiskCacheMetadata * _Nonnull obj2) {
552589
return [obj1.size compare:obj2.size];
@@ -569,7 +606,7 @@ - (void)trimDiskToSize:(NSUInteger)trimByteCount
569606

570607
- (void)trimDiskToSizeByDate:(NSUInteger)trimByteCount
571608
{
572-
[self lock];
609+
[self lockForWriting];
573610
if (_byteCount > trimByteCount) {
574611
NSArray *keysSortedByDate = [_metadata keysSortedByValueUsingComparator:^NSComparisonResult(PINDiskCacheMetadata * _Nonnull obj1, PINDiskCacheMetadata * _Nonnull obj2) {
575612
return [obj1.date compare:obj2.date];
@@ -592,7 +629,7 @@ - (void)trimDiskToSizeByDate:(NSUInteger)trimByteCount
592629

593630
- (void)trimDiskToDate:(NSDate *)trimDate
594631
{
595-
[self lock];
632+
[self lockForWriting];
596633
NSArray *keysSortedByDate = [_metadata keysSortedByValueUsingComparator:^NSComparisonResult(PINDiskCacheMetadata * _Nonnull obj1, PINDiskCacheMetadata * _Nonnull obj2) {
597634
return [obj1.date compare:obj2.date];
598635
}];
@@ -650,7 +687,7 @@ - (void)lockFileAccessWhileExecutingBlockAsync:(PINCacheBlock)block
650687

651688
[self.operationQueue addOperation:^{
652689
PINDiskCache *strongSelf = weakSelf;
653-
[strongSelf lock];
690+
[strongSelf lockForWriting];
654691
block(strongSelf);
655692
[strongSelf unlock];
656693
} withPriority:PINOperationQueuePriorityLow];
@@ -693,7 +730,7 @@ - (void)fileURLForKeyAsync:(NSString *)key completion:(PINDiskCacheFileURLBlock)
693730
PINDiskCache *strongSelf = weakSelf;
694731
NSURL *fileURL = [strongSelf fileURLForKey:key];
695732

696-
[strongSelf lock];
733+
[strongSelf lockForWriting];
697734
block(key, fileURL);
698735
[strongSelf unlock];
699736
} withPriority:PINOperationQueuePriorityLow];
@@ -830,17 +867,20 @@ - (void)enumerateObjectsWithBlockAsync:(PINDiskCacheFileURLEnumerationBlock)bloc
830867
- (void)synchronouslyLockFileAccessWhileExecutingBlock:(PINCacheBlock)block
831868
{
832869
if (block) {
833-
[self lock];
870+
[self lockForWriting];
834871
block(self);
835872
[self unlock];
836873
}
837874
}
838875

839876
- (BOOL)containsObjectForKey:(NSString *)key
840877
{
841-
if (_metadata[key] != nil) {
842-
return ([self fileURLForKey:key updateFileModificationDate:NO] != nil);
843-
}
878+
[self lock];
879+
if (_metadata[key] != nil || _diskStateKnown == NO) {
880+
[self unlock];
881+
return ([self fileURLForKey:key updateFileModificationDate:NO] != nil);
882+
}
883+
[self unlock];
844884
return NO;
845885
}
846886

@@ -856,20 +896,24 @@ - (id)objectForKeyedSubscript:(NSString *)key
856896

857897
- (nullable id <NSCoding>)objectForKey:(NSString *)key fileURL:(NSURL **)outFileURL
858898
{
859-
NSDate *now = [[NSDate alloc] init];
860-
861899
[self lock];
862-
BOOL isEmpty = _metadata.count == 0;
863-
BOOL containsKey = _metadata[key] != nil;
900+
BOOL containsKey = _metadata[key] != nil || _diskStateKnown == NO;
864901
[self unlock];
865902

866-
if (!key || isEmpty || !containsKey)
903+
if (!key || !containsKey)
867904
return nil;
868905

869906
id <NSCoding> object = nil;
870907
NSURL *fileURL = [self encodedFileURLForKey:key];
871908

909+
NSDate *now = [[NSDate alloc] init];
872910
[self lock];
911+
if (self->_ttlCache) {
912+
// We actually need to know the entire disk state if we're a TTL cache.
913+
[self unlock];
914+
[self lockAndWaitForKnownState];
915+
}
916+
873917
if (!self->_ttlCache || self->_ageLimit <= 0 || fabs([_metadata[key].date timeIntervalSinceDate:now]) < self->_ageLimit) {
874918
// If the cache should behave like a TTL cache, then only fetch the object if there's a valid ageLimit and the object is still alive
875919

@@ -920,7 +964,7 @@ - (NSURL *)fileURLForKey:(NSString *)key updateFileModificationDate:(BOOL)update
920964
NSDate *now = [[NSDate alloc] init];
921965
NSURL *fileURL = [self encodedFileURLForKey:key];
922966

923-
[self lock];
967+
[self lockForWriting];
924968
if (fileURL.path && [[NSFileManager defaultManager] fileExistsAtPath:fileURL.path]) {
925969
if (updateFileModificationDate) {
926970
[self asynchronouslySetFileModificationDate:now forURL:fileURL];
@@ -979,7 +1023,7 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key fileURL:(NSURL **
9791023
return;
9801024
}
9811025

982-
[self lock];
1026+
[self lockForWriting];
9831027
PINCacheObjectBlock willAddObjectBlock = self->_willAddObjectBlock;
9841028
if (willAddObjectBlock) {
9851029
[self unlock];
@@ -1089,11 +1133,12 @@ - (void)trimToSizeByDate:(NSUInteger)trimByteCount
10891133

10901134
- (void)removeAllObjects
10911135
{
1092-
[self lock];
1136+
// We don't need to know the disk state since we're just going to remove everything.
1137+
[self lockForWriting];
10931138
PINCacheBlock willRemoveAllObjectsBlock = self->_willRemoveAllObjectsBlock;
10941139
if (willRemoveAllObjectsBlock) {
10951140
[self unlock];
1096-
willRemoveAllObjectsBlock(self);
1141+
willRemoveAllObjectsBlock(self);
10971142
[self lock];
10981143
}
10991144

@@ -1120,7 +1165,7 @@ - (void)enumerateObjectsWithBlock:(PINDiskCacheFileURLEnumerationBlock)block
11201165
if (!block)
11211166
return;
11221167

1123-
[self lock];
1168+
[self lockAndWaitForKnownState];
11241169
NSDate *now = [NSDate date];
11251170

11261171
for (NSString *key in _metadata) {
@@ -1397,20 +1442,42 @@ - (void)setWritingProtectionOption:(NSDataWritingOptions)writingProtectionOption
13971442
NSDataWritingOptions option = NSDataWritingFileProtectionMask & writingProtectionOption;
13981443

13991444
[strongSelf lock];
1400-
strongSelf->_writingProtectionOption = option;
1445+
strongSelf->_writingProtectionOption = option;
14011446
[strongSelf unlock];
14021447
} withPriority:PINOperationQueuePriorityHigh];
14031448
}
14041449
#endif
14051450

1451+
- (void)lockForWriting
1452+
{
1453+
[self lock];
1454+
1455+
// spinlock if the disk isn't writable
1456+
if (_diskWritable == NO) {
1457+
pthread_cond_wait(&_diskWritableCondition, &_mutex);
1458+
}
1459+
}
1460+
1461+
- (void)lockAndWaitForKnownState
1462+
{
1463+
[self lock];
1464+
1465+
// spinlock if the disk state isn't known
1466+
if (_diskStateKnown == NO) {
1467+
pthread_cond_wait(&_diskStateKnownCondition, &_mutex);
1468+
}
1469+
}
1470+
14061471
- (void)lock
14071472
{
1408-
[_instanceLock lockWhenCondition:PINDiskCacheConditionReady];
1473+
__unused int result = pthread_mutex_lock(&_mutex);
1474+
NSAssert(result == 0, @"Failed to lock PINDiskCache %@. Code: %d", self, result);
14091475
}
14101476

14111477
- (void)unlock
14121478
{
1413-
[_instanceLock unlockWithCondition:PINDiskCacheConditionReady];
1479+
__unused int result = pthread_mutex_unlock(&_mutex);
1480+
NSAssert(result == 0, @"Failed to unlock PINDiskCache %@. Code: %d", self, result);
14141481
}
14151482

14161483
@end

0 commit comments

Comments
 (0)