From 397791fceac225fb030c625604dd91b2db152d27 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Sat, 14 Nov 2015 09:17:40 -0800 Subject: [PATCH] Added in-memory cache for AsyncLocalStorage Reviewed By: tadeuzagallo Differential Revision: D2641705 fb-gh-sync-id: 40b96b3084b82779e16f8845f9faeb0e3638d189 --- React/Modules/RCTAsyncLocalStorage.m | 100 ++++++++++++++++++++------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/React/Modules/RCTAsyncLocalStorage.m b/React/Modules/RCTAsyncLocalStorage.m index cacf6a0bb..273ef40c6 100644 --- a/React/Modules/RCTAsyncLocalStorage.m +++ b/React/Modules/RCTAsyncLocalStorage.m @@ -122,6 +122,23 @@ static dispatch_queue_t RCTGetMethodQueue() return queue; } +static NSCache *RCTGetCache() +{ + // We want all instances to share the same cache since they will be reading/writing the same files. + static NSCache *cache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cache = [NSCache new]; + cache.totalCostLimit = 2 * 1024 * 1024; // 2MB + + // Clear cache in the event of a memory warning + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:nil usingBlock:^(__unused NSNotification *note) { + [cache removeAllObjects]; + }]; + }); + return cache; +} + static BOOL RCTHasCreatedStorageDirectory = NO; static NSDictionary *RCTDeleteStorageDirectory() { @@ -139,7 +156,7 @@ static NSDictionary *RCTDeleteStorageDirectory() // The manifest is a dictionary of all keys with small values inlined. Null values indicate values that are stored // in separate files (as opposed to nil values which don't exist). The manifest is read off disk at startup, and // written to disk after all mutations. - NSMutableDictionary *_manifest; + NSMutableDictionary *_manifest; } RCT_EXPORT_MODULE() @@ -152,7 +169,8 @@ RCT_EXPORT_MODULE() - (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); }); } @@ -160,6 +178,7 @@ RCT_EXPORT_MODULE() + (void)clearAllData { dispatch_async(RCTGetMethodQueue(), ^{ + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); }); } @@ -167,10 +186,11 @@ RCT_EXPORT_MODULE() - (void)invalidate { if (_clearOnInvalidate) { + [RCTGetCache() removeAllObjects]; RCTDeleteStorageDirectory(); } _clearOnInvalidate = NO; - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; _haveSetup = NO; } @@ -245,15 +265,25 @@ RCT_EXPORT_MODULE() - (NSString *)_getValueForKey:(NSString *)key errorOut:(NSDictionary **)errorOut { - id value = _manifest[key]; // nil means missing, null means there is a data file, else: NSString + NSString *value = _manifest[key]; // nil means missing, null means there may be a data file, else: NSString if (value == (id)kCFNull) { - NSString *filePath = [self _filePathForKey:key]; - value = RCTReadFile(filePath, key, errorOut); + value = [RCTGetCache() objectForKey:key]; + if (!value) { + NSString *filePath = [self _filePathForKey:key]; + value = RCTReadFile(filePath, key, errorOut); + if (value) { + [RCTGetCache() setObject:value forKey:key cost:value.length]; + } else { + // file does not exist after all, so remove from manifest (no need to save + // manifest immediately though, as cost of checking again next time is negligible) + [_manifest removeObjectForKey:key]; + } + } } return value; } -- (NSDictionary *)_writeEntry:(NSArray *)entry +- (NSDictionary *)_writeEntry:(NSArray *)entry changedManifest:(BOOL *)changedManifest { if (entry.count != 2) { return RCTMakeAndLogError(@"Entries must be arrays of the form [key: string, value: string], got: ", entry, nil); @@ -267,18 +297,22 @@ RCT_EXPORT_MODULE() NSString *filePath = [self _filePathForKey:key]; NSError *error; if (value.length <= RCTInlineValueThreshold) { - if (_manifest[key] && _manifest[key] != (id)kCFNull) { + if (_manifest[key] == (id)kCFNull) { // If the value already existed but wasn't inlined, remove the old file. [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; } + *changedManifest = YES; _manifest[key] = value; return nil; } [value writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error]; + [RCTGetCache() setObject:value forKey:key cost:value.length]; if (error) { errorOut = RCTMakeError(@"Failed to write value.", error, @{@"key": key}); - } else { - _manifest[key] = (id)kCFNull; // Mark existence of file with null, any other value is inline data. + } else if (_manifest[key] != (id)kCFNull) { + *changedManifest = YES; + _manifest[key] = (id)kCFNull; } return errorOut; } @@ -296,7 +330,9 @@ RCT_EXPORT_METHOD(multiGet:(NSStringArray *)keys NSMutableArray *errors; NSMutableArray *> *result = [[NSMutableArray alloc] initWithCapacity:keys.count]; for (NSString *key in keys) { - NSDictionary *keyError = [self _appendItemForKey:key toArray:result]; + id keyError; + id value = [self _getValueForKey:key errorOut:&keyError]; + [result addObject:@[key, RCTNullIfNil(value)]]; RCTAppendError(keyError, &errors); } callback(@[RCTNullIfNil(errors), result]); @@ -310,12 +346,15 @@ RCT_EXPORT_METHOD(multiSet:(NSStringArrayArray *)kvPairs callback(@[@[errorOut]]); return; } + BOOL changedManifest = NO; NSMutableArray *errors; for (NSArray *entry in kvPairs) { - NSDictionary *keyError = [self _writeEntry:entry]; + NSDictionary *keyError = [self _writeEntry:entry changedManifest:&changedManifest]; RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; + if (changedManifest) { + [self _writeManifest:&errors]; + } callback(@[RCTNullIfNil(errors)]); } @@ -327,13 +366,12 @@ RCT_EXPORT_METHOD(multiMerge:(NSStringArrayArray *)kvPairs callback(@[@[errorOut]]); return; } + BOOL changedManifest = NO; NSMutableArray *errors; for (__strong NSArray *entry in kvPairs) { NSDictionary *keyError; NSString *value = [self _getValueForKey:entry[0] errorOut:&keyError]; - if (keyError) { - RCTAppendError(keyError, &errors); - } else { + if (!keyError) { if (value) { NSError *jsonError; NSMutableDictionary *mergedVal = RCTJSONParseMutable(value, &jsonError); @@ -345,12 +383,14 @@ RCT_EXPORT_METHOD(multiMerge:(NSStringArrayArray *)kvPairs } } if (!keyError) { - keyError = [self _writeEntry:entry]; + keyError = [self _writeEntry:entry changedManifest:&changedManifest]; } - RCTAppendError(keyError, &errors); } + RCTAppendError(keyError, &errors); + } + if (changedManifest) { + [self _writeManifest:&errors]; } - [self _writeManifest:&errors]; callback(@[RCTNullIfNil(errors)]); } @@ -363,22 +403,34 @@ RCT_EXPORT_METHOD(multiRemove:(NSStringArray *)keys return; } NSMutableArray *errors; + BOOL changedManifest = NO; for (NSString *key in keys) { NSDictionary *keyError = RCTErrorForKey(key); if (!keyError) { - NSString *filePath = [self _filePathForKey:key]; - [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; - [_manifest removeObjectForKey:key]; + if (_manifest[key] == (id)kCFNull) { + NSString *filePath = [self _filePathForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + [RCTGetCache() removeObjectForKey:key]; + // remove the key from manifest, but no need to mark as changed just for + // this, as the cost of checking again next time is negligible. + [_manifest removeObjectForKey:key]; + } else if (_manifest[key]) { + changedManifest = YES; + [_manifest removeObjectForKey:key]; + } } RCTAppendError(keyError, &errors); } - [self _writeManifest:&errors]; + if (changedManifest) { + [self _writeManifest:&errors]; + } callback(@[RCTNullIfNil(errors)]); } RCT_EXPORT_METHOD(clear:(RCTResponseSenderBlock)callback) { - _manifest = [NSMutableDictionary new]; + [_manifest removeAllObjects]; + [RCTGetCache() removeAllObjects]; NSDictionary *error = RCTDeleteStorageDirectory(); callback(@[RCTNullIfNil(error)]); }