Skip to content
This repository

Significant performance enhancement for ASIDownloadCache's -clearCachedResponsesForStoragePolicy: #347

Open
wants to merge 4 commits into from

1 participant

Mark Allen
Mark Allen

Instead of walking each file and deleting it on the current thread, we move the cache directory to a uniquely-named directory in the temp directory and delete its contents on a background thread. For whatever reason the iOS filesystem can perform a rename very quickly but deletes - especially for larger files, e.g. images - can be very slow (orders of magnitude slower) on old devices like the iPad 1.

This approach should be safe even if the background deletion thread is unable to complete normally (e.g. the application is unexpected terminated during execution) because we can rely on the system to clean up for us eventually since we're operating on files in the temp directory.

ASIHTTPRequest is a great library and I have used it to build some high-traffic apps. Unfortunately I was really bitten by the cache-clearing performance on older devices. Users were unable to start up the app due to being killed by the iOS watchdog since the deletes took so long to execute. In our app, this call might have taken 20 seconds in the pathological case (!) on an iPad 1. Now it takes about 100ms.

Another way to address this would be to allow client code to specify ASIDownloadCache size/item limits, with some kind of LRU eviction. But this was a quicker, less invasive win for me.

revetkn added some commits
Mark Allen revetkn Significant performance enhancement for -clearCachedResponsesForStora…
…gePolicy:

Instead of walking each file and deleting it on the current thread, we move the cache directory to a uniquely-named directory in the temp directory and delete its contents on a background thread.  For whatever reason the iOS filesystem can perform a rename very quickly but deletes - especially for larger files, e.g. images - can be very slow (orders of magnitude slower) on old devices like the iPad 1.

This approach should be safe even if the background deletion thread is unable to complete normally (e.g. the application is unexpected terminated during execution) because we can rely on the system to clean up for us eventually since we're operating on files in the temp directory.

ASIHTTPRequest is a great library and I have used it to build some high-traffic apps.  Unfortunately I was really bitten by the cache-clearing performance on older devices.  Users were unable to start up the app due to being killed by the iOS watchdog since the deletes took so long to execute.  In our app, this call might have taken 20 seconds in the pathological case (!) on an iPad 1.  Now it takes about 100ms.

Another way to address this would be to allow client code to specify ASIDownloadCache size/item limits, with some kind of LRU eviction.  But this was a quicker, less invasive win for me.
9aadd7c
Mark Allen revetkn NSStreamEventErrorOccurred -> NSStreamStatusError fix 93ca247
Mark Allen revetkn Deprecated NSDate -addTimeInterval: replaced with -dateByAddingTimeIn…
…terval:
3a1eac4
Mark Allen revetkn Fix for deprecated methods 7cf10bf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 4 unique commits by 1 author.

Nov 19, 2012
Mark Allen revetkn Significant performance enhancement for -clearCachedResponsesForStora…
…gePolicy:

Instead of walking each file and deleting it on the current thread, we move the cache directory to a uniquely-named directory in the temp directory and delete its contents on a background thread.  For whatever reason the iOS filesystem can perform a rename very quickly but deletes - especially for larger files, e.g. images - can be very slow (orders of magnitude slower) on old devices like the iPad 1.

This approach should be safe even if the background deletion thread is unable to complete normally (e.g. the application is unexpected terminated during execution) because we can rely on the system to clean up for us eventually since we're operating on files in the temp directory.

ASIHTTPRequest is a great library and I have used it to build some high-traffic apps.  Unfortunately I was really bitten by the cache-clearing performance on older devices.  Users were unable to start up the app due to being killed by the iOS watchdog since the deletes took so long to execute.  In our app, this call might have taken 20 seconds in the pathological case (!) on an iPad 1.  Now it takes about 100ms.

Another way to address this would be to allow client code to specify ASIDownloadCache size/item limits, with some kind of LRU eviction.  But this was a quicker, less invasive win for me.
9aadd7c
Sep 30, 2013
Mark Allen revetkn NSStreamEventErrorOccurred -> NSStreamStatusError fix 93ca247
Mark Allen revetkn Deprecated NSDate -addTimeInterval: replaced with -dateByAddingTimeIn…
…terval:
3a1eac4
Oct 01, 2013
Mark Allen revetkn Fix for deprecated methods 7cf10bf
This page is out of date. Refresh to see the latest.
10 Classes/ASIAuthenticationDialog.m
@@ -217,9 +217,9 @@ - (UITextField *)domainField
217 217 + (void)dismiss
218 218 {
219 219 if ([sharedDialog respondsToSelector:@selector(presentingViewController)])
220   - [[sharedDialog presentingViewController] dismissModalViewControllerAnimated:YES];
  220 + [[sharedDialog presentingViewController] dismissViewControllerAnimated:YES completion:^{}];
221 221 else
222   - [[sharedDialog parentViewController] dismissModalViewControllerAnimated:YES];
  222 + [[sharedDialog parentViewController] dismissViewControllerAnimated:YES completion:^{}];
223 223 }
224 224
225 225 - (void)viewDidDisappear:(BOOL)animated
@@ -237,9 +237,9 @@ - (void)dismiss
237 237 [[self class] dismiss];
238 238 } else {
239 239 if ([self respondsToSelector:@selector(presentingViewController)])
240   - [[self presentingViewController] dismissModalViewControllerAnimated:YES];
  240 + [[self presentingViewController] dismissViewControllerAnimated:YES completion:^{}];
241 241 else
242   - [[self parentViewController] dismissModalViewControllerAnimated:YES];
  242 + [[self parentViewController] dismissViewControllerAnimated:YES completion:^{}];
243 243 }
244 244 }
245 245
@@ -315,7 +315,7 @@ - (void)show
315 315 }
316 316 #endif
317 317
318   - [[self presentingController] presentModalViewController:self animated:YES];
  318 + [[self presentingController] presentViewController:self animated:YES completion:^{}];
319 319 }
320 320
321 321 #pragma mark button callbacks
4 Classes/ASIDataCompressor.m
@@ -161,7 +161,7 @@ + (BOOL)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinati
161 161 readLength = [inputStream read:inputData maxLength:DATA_CHUNK_SIZE];
162 162
163 163 // Make sure nothing went wrong
164   - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) {
  164 + if ([inputStream streamStatus] == NSStreamStatusError) {
165 165 if (err) {
166 166 *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Compression of %@ failed because we were unable to read from the source data file",sourcePath],NSLocalizedDescriptionKey,[inputStream streamError],NSUnderlyingErrorKey,nil]];
167 167 }
@@ -187,7 +187,7 @@ + (BOOL)compressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destinati
187 187 [outputStream write:(const uint8_t *)[outputData bytes] maxLength:[outputData length]];
188 188
189 189 // Make sure nothing went wrong
190   - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) {
  190 + if ([inputStream streamStatus] == NSStreamStatusError) {
191 191 if (err) {
192 192 *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Compression of %@ failed because we were unable to write to the destination data file at %@",sourcePath,destinationPath],NSLocalizedDescriptionKey,[outputStream streamError],NSUnderlyingErrorKey,nil]];
193 193 }
4 Classes/ASIDataDecompressor.m
@@ -158,7 +158,7 @@ + (BOOL)uncompressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destina
158 158 readLength = [inputStream read:inputData maxLength:DATA_CHUNK_SIZE];
159 159
160 160 // Make sure nothing went wrong
161   - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) {
  161 + if ([inputStream streamStatus] == NSStreamStatusError) {
162 162 if (err) {
163 163 *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Decompression of %@ failed because we were unable to read from the source data file",sourcePath],NSLocalizedDescriptionKey,[inputStream streamError],NSUnderlyingErrorKey,nil]];
164 164 }
@@ -184,7 +184,7 @@ + (BOOL)uncompressDataFromFile:(NSString *)sourcePath toFile:(NSString *)destina
184 184 [outputStream write:(Bytef*)[outputData bytes] maxLength:[outputData length]];
185 185
186 186 // Make sure nothing went wrong
187   - if ([inputStream streamStatus] == NSStreamEventErrorOccurred) {
  187 + if ([inputStream streamStatus] == NSStreamStatusError) {
188 188 if (err) {
189 189 *err = [NSError errorWithDomain:NetworkRequestErrorDomain code:ASICompressionError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Decompression of %@ failed because we were unable to write to the destination data file at %@",sourcePath,destinationPath],NSLocalizedDescriptionKey,[outputStream streamError],NSUnderlyingErrorKey,nil]];
190 190 }
91 Classes/ASIDownloadCache.m
@@ -19,6 +19,7 @@
19 19 @interface ASIDownloadCache ()
20 20 + (NSString *)keyForURL:(NSURL *)url;
21 21 - (NSString *)pathToFile:(NSString *)file;
  22 +- (NSString *)generateUniqueIdentifier;
22 23 @end
23 24
24 25 @implementation ASIDownloadCache
@@ -393,35 +394,69 @@ - (void)setDefaultCachePolicy:(ASICachePolicy)cachePolicy
393 394
394 395 - (void)clearCachedResponsesForStoragePolicy:(ASICacheStoragePolicy)storagePolicy
395 396 {
396   - [[self accessLock] lock];
397   - if (![self storagePath]) {
398   - [[self accessLock] unlock];
399   - return;
400   - }
401   - NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
402   -
403   - NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
  397 + [[self accessLock] lock];
  398 + if (![self storagePath]) {
  399 + [[self accessLock] unlock];
  400 + return;
  401 + }
  402 + NSString *path = [[self storagePath] stringByAppendingPathComponent:(storagePolicy == ASICacheForSessionDurationCacheStoragePolicy ? sessionCacheFolder : permanentCacheFolder)];
  403 +
  404 + NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease];
  405 +
  406 + BOOL isDirectory = NO;
  407 + BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
  408 + if (!exists || !isDirectory) {
  409 + [[self accessLock] unlock];
  410 + return;
  411 + }
  412 +
  413 + // It is significantly faster to perform a move than a delete on the iOS filesystem, especially on older devices.
  414 + // We move the existing cache directory so it lives in the temp directory and has a unique name.
  415 + // We clear the contents of the moved directory in a background thread.
  416 + NSString *renamedPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[self generateUniqueIdentifier]];
  417 +
  418 + NSError *error = nil;
  419 + BOOL renamed = [fileManager moveItemAtPath:path toPath:renamedPath error:&error];
  420 + if (!renamed) {
  421 + [[self accessLock] unlock];
  422 + [NSException raise:@"FailedToRenameCacheDirectory" format:@"Renaming cache directory failed at path '%@'",path];
  423 + }
  424 +
  425 + BOOL recreated = [fileManager createDirectoryAtPath:path withIntermediateDirectories:YES attributes:nil error:&error];
  426 + if (!recreated) {
  427 + [[self accessLock] unlock];
  428 + [NSException raise:@"FailedToRecreateCacheDirectory" format:@"Recreating cache directory failed at path '%@'",path];
  429 + }
  430 +
  431 + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
  432 + NSError *backgroundError = nil;
  433 + NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:renamedPath error:&backgroundError];
  434 + if (backgroundError) {
  435 + [NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",renamedPath];
  436 + }
  437 + for (NSString *file in cacheFiles) {
  438 + [fileManager removeItemAtPath:[renamedPath stringByAppendingPathComponent:file] error:&backgroundError];
  439 + if (backgroundError) {
  440 + [NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",renamedPath];
  441 + }
  442 + }
  443 +
  444 + // Remove the now-empty temporary directory
  445 + [fileManager removeItemAtPath:renamedPath error:&backgroundError];
  446 + if (backgroundError) {
  447 + [NSException raise:@"FailedToRemoveCacheDirectory" format:@"Failed to remove cached directory at path '%@'",renamedPath];
  448 + }
  449 + });
  450 +
  451 + [[self accessLock] unlock];
  452 +}
404 453
405   - BOOL isDirectory = NO;
406   - BOOL exists = [fileManager fileExistsAtPath:path isDirectory:&isDirectory];
407   - if (!exists || !isDirectory) {
408   - [[self accessLock] unlock];
409   - return;
410   - }
411   - NSError *error = nil;
412   - NSArray *cacheFiles = [fileManager contentsOfDirectoryAtPath:path error:&error];
413   - if (error) {
414   - [[self accessLock] unlock];
415   - [NSException raise:@"FailedToTraverseCacheDirectory" format:@"Listing cache directory failed at path '%@'",path];
416   - }
417   - for (NSString *file in cacheFiles) {
418   - [fileManager removeItemAtPath:[path stringByAppendingPathComponent:file] error:&error];
419   - if (error) {
420   - [[self accessLock] unlock];
421   - [NSException raise:@"FailedToRemoveCacheFile" format:@"Failed to remove cached data at path '%@'",path];
422   - }
423   - }
424   - [[self accessLock] unlock];
  454 +- (NSString *)generateUniqueIdentifier
  455 +{
  456 + CFUUIDRef uniqueIdentifier = CFUUIDCreate(NULL);
  457 + CFStringRef uniqueIdentifierString = CFUUIDCreateString(NULL, uniqueIdentifier);
  458 + CFRelease(uniqueIdentifier);
  459 + return [(NSString *)uniqueIdentifierString autorelease];
425 460 }
426 461
427 462 + (BOOL)serverAllowsResponseCachingForRequest:(ASIHTTPRequest *)request
2  Classes/ASIHTTPRequest.m
@@ -4866,7 +4866,7 @@ + (NSDate *)expiryDateForRequest:(ASIHTTPRequest *)request maxAge:(NSTimeInterva
4866 4866
4867 4867 // RFC 2612 says max-age must override any Expires header
4868 4868 if (maxAge) {
4869   - return [[NSDate date] addTimeInterval:maxAge];
  4869 + return [[NSDate date] dateByAddingTimeInterval:maxAge];
4870 4870 } else {
4871 4871 NSString *expires = [responseHeaders objectForKey:@"Expires"];
4872 4872 if (expires) {

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.