diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c8480c..511efebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - [new] Added a way to specify custom retry logic when network error happens [#386](https://github.com/pinterest/PINRemoteImage/pull/386) - [new] Improve disk cache migration performance [#391](https://github.com/pinterest/PINRemoteImage/pull/391) [chuganzy](https://github.com/chuganzy), [#394](https://github.com/pinterest/PINRemoteImage/pull/394) [nguyenhuy](https://github.com/nguyenhuy) +- [new] Adds support for using cell vs. wifi in leau of speed for determing which URL to download if speed is unavailable. [garrettmoon](https://github.com/garrettmoon) ## 3.0.0 Beta 11 - [fixed] Fixes a deadlock with canceling processor tasks [#374](https://github.com/pinterest/PINRemoteImage/pull/374) [zachwaugh](https://github.com/zachwaugh) diff --git a/PINRemoteImage.xcodeproj/project.pbxproj b/PINRemoteImage.xcodeproj/project.pbxproj index 678fe4ca..af63e73d 100644 --- a/PINRemoteImage.xcodeproj/project.pbxproj +++ b/PINRemoteImage.xcodeproj/project.pbxproj @@ -157,6 +157,8 @@ 6818C2851E551DA800875DB7 /* utils.h in Headers */ = {isa = PBXBuildFile; fileRef = 6818C26B1E551DA800875DB7 /* utils.h */; }; 6858C0751C9CC5BA00E420EB /* PINRemoteLock.h in Headers */ = {isa = PBXBuildFile; fileRef = 6858C0731C9CC5BA00E420EB /* PINRemoteLock.h */; }; 6858C0761C9CC5BA00E420EB /* PINRemoteLock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6858C0741C9CC5BA00E420EB /* PINRemoteLock.m */; }; + 6860CB081F578287005E886E /* PINSpeedRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 6860CB061F578287005E886E /* PINSpeedRecorder.h */; }; + 6860CB091F578287005E886E /* PINSpeedRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 6860CB071F578287005E886E /* PINSpeedRecorder.m */; }; 686D48D01ED38FC0003DB4C2 /* PINRemoteImageTask+Subclassing.h in Headers */ = {isa = PBXBuildFile; fileRef = 686D48CE1ED38FC0003DB4C2 /* PINRemoteImageTask+Subclassing.h */; }; 687D3FC91E5650790027B131 /* PINOperation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 687D3FC81E5650790027B131 /* PINOperation.framework */; }; 68A0FC1C1E523434000B552D /* PINRemoteImageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A0FC1B1E523434000B552D /* PINRemoteImageTests.m */; }; @@ -450,6 +452,8 @@ 6818C2AE1E564FF900875DB7 /* PINCache.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PINCache.xcodeproj; path = Carthage/Checkouts/PINCache/PINCache.xcodeproj; sourceTree = ""; }; 6858C0731C9CC5BA00E420EB /* PINRemoteLock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PINRemoteLock.h; sourceTree = ""; }; 6858C0741C9CC5BA00E420EB /* PINRemoteLock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PINRemoteLock.m; sourceTree = ""; }; + 6860CB061F578287005E886E /* PINSpeedRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PINSpeedRecorder.h; sourceTree = ""; }; + 6860CB071F578287005E886E /* PINSpeedRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PINSpeedRecorder.m; sourceTree = ""; }; 686D48CE1ED38FC0003DB4C2 /* PINRemoteImageTask+Subclassing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PINRemoteImageTask+Subclassing.h"; sourceTree = ""; }; 687D3FC81E5650790027B131 /* PINOperation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PINOperation.framework; path = "Carthage/Checkouts/PINCache/Carthage/Checkouts/PINOperation/build/Debug-iphoneos/PINOperation.framework"; sourceTree = ""; }; 68A0FC191E523434000B552D /* PINRemoteImageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PINRemoteImageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -866,6 +870,8 @@ 68B1F2801E679D7A00ED87C4 /* PINRemoteImageDownloadQueue.m */, 68B7E3B01E736C73000FC887 /* PINResume.h */, 68B7E3B11E736C73000FC887 /* PINResume.m */, + 6860CB061F578287005E886E /* PINSpeedRecorder.h */, + 6860CB071F578287005E886E /* PINSpeedRecorder.m */, ); name = Project; path = Classes; @@ -953,6 +959,7 @@ 6818C2421E551D7500875DB7 /* vp8enci.h in Headers */, F1B919161BCF23C900710963 /* PINRemoteImageManager.h in Headers */, 6818C2361E551D7500875DB7 /* histogram.h in Headers */, + 6860CB081F578287005E886E /* PINSpeedRecorder.h in Headers */, 9DD47F9E1C699F4B00F12CA0 /* PINImageView+PINRemoteImage.h in Headers */, 68CA92831DB19C20008BECE2 /* PINCache+PINRemoteImageCaching.h in Headers */, 6818C18C1E551CE000875DB7 /* vp8li.h in Headers */, @@ -1289,6 +1296,7 @@ 6818C1DF1E551D1D00875DB7 /* dec_mips_dsp_r2.c in Sources */, 6818C22F1E551D7500875DB7 /* cost.c in Sources */, 6818C1E91E551D1D00875DB7 /* enc_mips32.c in Sources */, + 6860CB091F578287005E886E /* PINSpeedRecorder.m in Sources */, 6818C23F1E551D7500875DB7 /* syntax.c in Sources */, 6818C1EA1E551D1D00875DB7 /* enc_neon.c in Sources */, 6818C23C1E551D7500875DB7 /* picture_tools.c in Sources */, diff --git a/Source/Classes/PINRemoteImageManager.h b/Source/Classes/PINRemoteImageManager.h index 03a9a647..f73b6c31 100644 --- a/Source/Classes/PINRemoteImageManager.h +++ b/Source/Classes/PINRemoteImageManager.h @@ -441,7 +441,7 @@ typedef void(^PINRemoteImageManagerProgressDownload)(int64_t completedBytes, int Unless setShouldUpgradeLowQualityImages is set to YES, this method checks the cache for all URLs and returns the highest quality version stored. It is possible though unlikely for a cached image to not be returned if it is still being cached while a call is made to this method and if network conditions have changed. See source for more details. - @param urls An array of NSURLs of increasing size. + @param urls An array of NSURLs of increasing cost to download. @param options PINRemoteImageManagerDownloadOptions options with which to fetch the image. @param progressImage PINRemoteImageManagerImageCompletion block which will be called to update progress of the image download. @param completion PINRemoteImageManagerImageCompletion block to call when image has been fetched from the cache or downloaded. @@ -453,6 +453,25 @@ typedef void(^PINRemoteImageManagerProgressDownload)(int64_t completedBytes, int progressImage:(nullable PINRemoteImageManagerImageCompletion)progressImage completion:(nullable PINRemoteImageManagerImageCompletion)completion; +/** + Download or retrieve from cache one of the images found at the urls in the passed in array based on current network performance. URLs should be sorted from lowest quality image URL to highest. All completions are called on an arbitrary callback queue unless called on the main thread and the result is in the memory cache (this is an optimization to allow synchronous results for the UI when an object is cached in memory). + + Unless setShouldUpgradeLowQualityImages is set to YES, this method checks the cache for all URLs and returns the highest quality version stored. It is possible though unlikely for a cached image to not be returned if it is still being cached while a call is made to this method and if network conditions have changed. See source for more details. + + @param urls An array of NSURLs of increasing cost to download. + @param options PINRemoteImageManagerDownloadOptions options with which to fetch the image. + @param progressImage PINRemoteImageManagerImageCompletion block which will be called to update progress of the image download. + @param progressDownload PINRemoteImageManagerDownloadProgress block which will be called to update progress in bytes of the image download. NOTE: For performance reasons, this block is not called on the main thread every time, if you need to update your UI ensure that you dispatch to the main thread first. + @param completion PINRemoteImageManagerImageCompletion block to call when image has been fetched from the cache or downloaded. + + @return An NSUUID which uniquely identifies this request. To be used for canceling requests and verifying that the callback is for the request you expect (see categories for example). + */ +- (nullable NSUUID *)downloadImageWithURLs:(nonnull NSArray *)urls + options:(PINRemoteImageManagerDownloadOptions)options + progressImage:(nullable PINRemoteImageManagerImageCompletion)progressImage + progressDownload:(nullable PINRemoteImageManagerProgressDownload)progressDownload + completion:(nullable PINRemoteImageManagerImageCompletion)completion; + /** Adds an image manually into the memory and disk cache. diff --git a/Source/Classes/PINRemoteImageManager.m b/Source/Classes/PINRemoteImageManager.m index e8ef7da5..e1ea96c0 100644 --- a/Source/Classes/PINRemoteImageManager.m +++ b/Source/Classes/PINRemoteImageManager.m @@ -20,12 +20,13 @@ #import "PINRemoteImageProcessorTask.h" #import "PINRemoteImageDownloadTask.h" #import "PINResume.h" -#import "PINURLSessionManager.h" #import "PINRemoteImageMemoryContainer.h" #import "PINRemoteImageCaching.h" #import "PINRequestRetryStrategy.h" #import "PINRemoteImageDownloadQueue.h" #import "PINRequestRetryStrategy.h" +#import "PINSpeedRecorder.h" +#import "PINURLSessionManager.h" #import "NSData+ImageDetectors.h" #import "PINImage+DecodedImage.h" @@ -82,15 +83,6 @@ float dataTaskPriorityWithImageManagerPriority(PINRemoteImageManagerPriority pri NSString * const PINRemoteImageCacheKeyResumePrefix = @"R-"; typedef void (^PINRemoteImageManagerDataCompletion)(NSData *data, NSURLResponse *response, NSError *error); -@interface PINTaskQOS : NSObject - -- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate; - -@property (nonatomic, strong) NSDate *endDate; -@property (nonatomic, assign) float bytesPerSecond; - -@end - @interface PINRemoteImageManager () { dispatch_queue_t _callbackQueue; @@ -116,7 +108,6 @@ @interface PINRemoteImageManager () @property (nonatomic, strong) dispatch_queue_t callbackQueue; @property (nonatomic, strong) PINOperationQueue *concurrentOperationQueue; @property (nonatomic, strong) PINRemoteImageDownloadQueue *urlSessionTaskQueue; -@property (nonatomic, strong) NSMutableArray *taskQOS; @property (nonatomic, assign) float highQualityBPSThreshold; @property (nonatomic, assign) float lowQualityBPSThreshold; @property (nonatomic, assign) BOOL shouldUpgradeLowQualityImages; @@ -125,8 +116,6 @@ @interface PINRemoteImageManager () @property (nonatomic, copy) PINRemoteImageManagerRequestConfigurationHandler requestConfigurationHandler; @property (nonatomic, strong) NSMutableDictionary *httpHeaderFields; #if DEBUG -@property (nonatomic, assign) float currentBPS; -@property (nonatomic, assign) BOOL overrideBPS; @property (nonatomic, assign) NSUInteger totalDownloads; #endif @@ -208,7 +197,6 @@ - (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfi _maxProgressiveRenderSize = CGSizeMake(1024, 1024); self.tasks = [[NSMutableDictionary alloc] init]; self.canceledTasks = [[NSHashTable alloc] initWithOptions:NSHashTableWeakMemory capacity:5]; - self.taskQOS = [[NSMutableArray alloc] initWithCapacity:5]; if (alternateRepProvider == nil) { _defaultAlternateRepresentationProvider = [[PINAlternateRepresentationProvider alloc] init]; @@ -1164,85 +1152,30 @@ - (void)didCompleteTask:(NSURLSessionTask *)task withError:(NSError *)error float bytesPerSecond = task.bytesPerSecond; if (bytesPerSecond) { - [self addTaskBPS:task.bytesPerSecond endDate:[NSDate date]]; + [[PINSpeedRecorder sharedRecorder] addTaskBPS:task.bytesPerSecond endDate:[NSDate date]]; } } } #pragma mark - QOS -- (float)currentBytesPerSecond -{ - [self lock]; - #if DEBUG - if (self.overrideBPS) { - float currentBPS = self.currentBPS; - [self unlock]; - return currentBPS; - } - #endif - - const NSTimeInterval validThreshold = 60.0; - __block NSUInteger count = 0; - __block float bps = 0; - __block BOOL valid = NO; - - NSDate *threshold = [NSDate dateWithTimeIntervalSinceNow:-validThreshold]; - [self.taskQOS enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(PINTaskQOS *taskQOS, NSUInteger idx, BOOL *stop) { - if ([taskQOS.endDate compare:threshold] == NSOrderedAscending) { - *stop = YES; - return; - } - valid = YES; - count++; - bps += taskQOS.bytesPerSecond; - - }]; - [self unlock]; - - if (valid == NO) { - return -1; - } - - return bps / (float)count; -} - -- (void)addTaskBPS:(float)bytesPerSecond endDate:(NSDate *)endDate -{ - //if bytesPerSecond is less than or equal to zero, ignore. - if (bytesPerSecond <= 0) { - return; - } - - [self lock]; - if (self.taskQOS.count >= 5) { - [self.taskQOS removeObjectAtIndex:0]; - } - - PINTaskQOS *taskQOS = [[PINTaskQOS alloc] initWithBPS:bytesPerSecond endDate:endDate]; - - [self.taskQOS addObject:taskQOS]; - [self.taskQOS sortUsingComparator:^NSComparisonResult(PINTaskQOS *obj1, PINTaskQOS *obj2) { - return [obj1.endDate compare:obj2.endDate]; - }]; - - [self unlock]; -} - -#if DEBUG -- (void)setCurrentBytesPerSecond:(float)currentBPS -{ - [self lockOnMainThread]; - _overrideBPS = YES; - _currentBPS = currentBPS; - [self unlock]; -} -#endif - - (NSUUID *)downloadImageWithURLs:(NSArray *)urls options:(PINRemoteImageManagerDownloadOptions)options progressImage:(PINRemoteImageManagerImageCompletion)progressImage completion:(PINRemoteImageManagerImageCompletion)completion +{ + return [self downloadImageWithURLs:urls + options:options + progressImage:progressImage + progressDownload:nil + completion:completion]; +} + +- (nullable NSUUID *)downloadImageWithURLs:(nonnull NSArray *)urls + options:(PINRemoteImageManagerDownloadOptions)options + progressImage:(nullable PINRemoteImageManagerImageCompletion)progressImage + progressDownload:(nullable PINRemoteImageManagerProgressDownload)progressDownload + completion:(nullable PINRemoteImageManagerImageCompletion)completion { NSUUID *UUID = [NSUUID UUID]; if (urls.count <= 1) { @@ -1253,16 +1186,16 @@ - (NSUUID *)downloadImageWithURLs:(NSArray *)urls processorKey:nil processor:nil progressImage:progressImage - progressDownload:nil + progressDownload:progressDownload completion:completion inputUUID:UUID]; return UUID; } - __weak typeof(self) weakSelf = self; + PINWeakify(self); [self.concurrentOperationQueue addOperation:^{ __block NSInteger highestQualityDownloadedIdx = -1; - typeof(self) strongSelf = weakSelf; + PINStrongify(self); //check for the highest quality image already in cache. It's possible that an image is in the process of being //cached when this is being run. In which case two things could happen: @@ -1273,12 +1206,12 @@ - (NSUUID *)downloadImageWithURLs:(NSArray *)urls // the task will still exist and our callback will be attached. In this case, no detrimental behavior will have // occurred. [urls enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) { - typeof(self) strongSelf = weakSelf; - BlockAssert([url isKindOfClass:[NSURL class]], @"url must be of type URL"); - NSString *cacheKey = [strongSelf cacheKeyForURL:url processorKey:nil]; + PINStrongify(self); + NSAssert([url isKindOfClass:[NSURL class]], @"url must be of type URL"); + NSString *cacheKey = [self cacheKeyForURL:url processorKey:nil]; //we don't actually need the object, just need to know it exists so that we can request it later - BOOL hasObject = [strongSelf.cache objectExistsForKey:cacheKey]; + BOOL hasObject = [self.cache objectExistsForKey:cacheKey]; if (hasObject) { highestQualityDownloadedIdx = idx; @@ -1286,28 +1219,21 @@ - (NSUUID *)downloadImageWithURLs:(NSArray *)urls } }]; - float currentBytesPerSecond = [strongSelf currentBytesPerSecond]; - [strongSelf lock]; - float highQualityQPSThreshold = [strongSelf highQualityBPSThreshold]; - float lowQualityQPSThreshold = [strongSelf lowQualityBPSThreshold]; - BOOL shouldUpgradeLowQualityImages = [strongSelf shouldUpgradeLowQualityImages]; - [strongSelf unlock]; + [self lock]; + float highQualityQPSThreshold = [self highQualityBPSThreshold]; + float lowQualityQPSThreshold = [self lowQualityBPSThreshold]; + BOOL shouldUpgradeLowQualityImages = [self shouldUpgradeLowQualityImages]; + [self unlock]; - NSUInteger desiredImageURLIdx; - if (currentBytesPerSecond == -1 || currentBytesPerSecond >= highQualityQPSThreshold) { - desiredImageURLIdx = urls.count - 1; - } else if (currentBytesPerSecond <= lowQualityQPSThreshold) { - desiredImageURLIdx = 0; - } else if (urls.count == 2) { - desiredImageURLIdx = roundf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 1))); - } else { - desiredImageURLIdx = ceilf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 2))); - } + NSUInteger desiredImageURLIdx = [PINSpeedRecorder appropriateImageIdxForURLsGivenHistoricalNetworkConditions:urls + lowQualityQPSThreshold:lowQualityQPSThreshold + highQualityQPSThreshold:highQualityQPSThreshold]; NSUInteger downloadIdx; //if the highest quality already downloaded is less than what currentBPS would dictate and shouldUpgrade is //set, download the new higher quality image. If no image has been cached, download the image dictated by //current bps + if ((highestQualityDownloadedIdx < desiredImageURLIdx && shouldUpgradeLowQualityImages) || highestQualityDownloadedIdx == -1) { downloadIdx = desiredImageURLIdx; } else { @@ -1316,18 +1242,18 @@ - (NSUUID *)downloadImageWithURLs:(NSArray *)urls NSURL *downloadURL = [urls objectAtIndex:downloadIdx]; - [strongSelf downloadImageWithURL:downloadURL + [self downloadImageWithURL:downloadURL options:options priority:PINRemoteImageManagerPriorityDefault processorKey:nil processor:nil progressImage:progressImage - progressDownload:nil + progressDownload:progressDownload completion:^(PINRemoteImageManagerResult *result) { - typeof(self) strongSelf = weakSelf; + PINStrongify(self) //clean out any lower quality images from the cache for (NSInteger idx = downloadIdx - 1; idx >= 0; idx--) { - [[strongSelf cache] removeObjectForKey:[strongSelf cacheKeyForURL:[urls objectAtIndex:idx] processorKey:nil]]; + [[self cache] removeObjectForKey:[self cacheKeyForURL:[urls objectAtIndex:idx] processorKey:nil]]; } if (completion) { @@ -1594,16 +1520,3 @@ - (NSUInteger)totalDownloads #endif @end - -@implementation PINTaskQOS - -- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate -{ - if (self = [super init]) { - self.endDate = endDate; - self.bytesPerSecond = bytesPerSecond; - } - return self; -} - -@end diff --git a/Source/Classes/PINSpeedRecorder.h b/Source/Classes/PINSpeedRecorder.h new file mode 100644 index 00000000..a7c11f68 --- /dev/null +++ b/Source/Classes/PINSpeedRecorder.h @@ -0,0 +1,32 @@ +// +// PINSpeedRecorder.h +// PINRemoteImage +// +// Created by Garrett Moon on 8/30/17. +// Copyright © 2017 Pinterest. All rights reserved. +// + +#import + +typedef enum : NSUInteger { + PINSpeedRecorderConnectionStatusNotReachable, + PINSpeedRecorderConnectionStatusWWAN, + PINSpeedRecorderConnectionStatusWiFi +} PINSpeedRecorderConnectionStatus; + +@interface PINSpeedRecorder : NSObject + ++ (PINSpeedRecorder *)sharedRecorder; ++ (NSUInteger)appropriateImageIdxForURLsGivenHistoricalNetworkConditions:(NSArray *)urls + lowQualityQPSThreshold:(float)lowQualityQPSThreshold + highQualityQPSThreshold:(float)highQualityQPSThreshold; + +- (void)addTaskBPS:(float)bytesPerSecond endDate:(NSDate *)endDate; +- (float)currentBytesPerSecond; +- (PINSpeedRecorderConnectionStatus)connectionStatus; + +#if DEBUG +- (void)setCurrentBytesPerSecond:(float)currentBPS; +#endif + +@end diff --git a/Source/Classes/PINSpeedRecorder.m b/Source/Classes/PINSpeedRecorder.m new file mode 100644 index 00000000..69dee30c --- /dev/null +++ b/Source/Classes/PINSpeedRecorder.m @@ -0,0 +1,240 @@ +// +// PINSpeedRecorder.m +// PINRemoteImage +// +// Created by Garrett Moon on 8/30/17. +// Copyright © 2017 Pinterest. All rights reserved. +// + +#import "PINSpeedRecorder.h" + +#import +#import + +#import "PINRemoteLock.h" + +static const NSUInteger kMaxRecordedTasks = 5; + +@interface PINTaskQOS : NSObject + +- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate; + +@property (nonatomic, strong) NSDate *endDate; +@property (nonatomic, assign) float bytesPerSecond; + +@end + +@interface PINSpeedRecorder () +{ + NSMutableArray *_taskQOS; + SCNetworkReachabilityRef _reachability; +#if DEBUG + BOOL _overrideBPS; + float _currentBPS; +#endif +} + +@property (nonatomic, strong) PINRemoteLock *lock; + +@end + +@implementation PINSpeedRecorder + ++ (PINSpeedRecorder *)sharedRecorder +{ + static dispatch_once_t onceToken; + static PINSpeedRecorder *sharedRecorder; + dispatch_once(&onceToken, ^{ + sharedRecorder = [[self alloc] init]; + }); + + return sharedRecorder; +} + +- (instancetype)init +{ + if (self = [super init]) { + _lock = [[PINRemoteLock alloc] initWithName:@"PINSpeedRecorder lock"]; + _taskQOS = [[NSMutableArray alloc] initWithCapacity:kMaxRecordedTasks]; + + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + _reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress); + } + return self; +} + +- (void)addTaskBPS:(float)bytesPerSecond endDate:(NSDate *)endDate +{ + //if bytesPerSecond is less than or equal to zero, ignore. + if (bytesPerSecond <= 0) { + return; + } + + [self.lock lockWithBlock:^{ + if (_taskQOS.count >= kMaxRecordedTasks) { + [_taskQOS removeObjectAtIndex:0]; + } + + PINTaskQOS *taskQOS = [[PINTaskQOS alloc] initWithBPS:bytesPerSecond endDate:endDate]; + + [_taskQOS addObject:taskQOS]; + [_taskQOS sortUsingComparator:^NSComparisonResult(PINTaskQOS *obj1, PINTaskQOS *obj2) { + return [obj1.endDate compare:obj2.endDate]; + }]; + }]; +} + +- (float)currentBytesPerSecond +{ + __block NSUInteger count = 0; + __block float bps = 0; + __block BOOL valid = NO; + [self.lock lockWithBlock:^{ +#if DEBUG + if (_overrideBPS) { + bps = _currentBPS; + count = 1; + valid = YES; + return; + } +#endif + + const NSTimeInterval validThreshold = 60.0; + + NSDate *threshold = [NSDate dateWithTimeIntervalSinceNow:-validThreshold]; + [_taskQOS enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(PINTaskQOS *taskQOS, NSUInteger idx, BOOL *stop) { + if ([taskQOS.endDate compare:threshold] == NSOrderedAscending) { + *stop = YES; + return; + } + valid = YES; + count++; + bps += taskQOS.bytesPerSecond; + + }]; + }]; + + if (valid == NO) { + return -1; + } + + return bps / (float)count; +} + +#if DEBUG +- (void)setCurrentBytesPerSecond:(float)currentBPS +{ + [self.lock lockWithBlock:^{ + _overrideBPS = YES; + _currentBPS = currentBPS; + }]; +} +#endif + +// Cribbed from Apple's reachability: https://developer.apple.com/library/content/samplecode/Reachability/Listings/Reachability_Reachability_m.html#//apple_ref/doc/uid/DTS40007324-Reachability_Reachability_m-DontLinkElementID_9 + +- (PINSpeedRecorderConnectionStatus)connectionStatus +{ + PINSpeedRecorderConnectionStatus status = PINSpeedRecorderConnectionStatusNotReachable; + SCNetworkReachabilityFlags flags; + + // _reachability is set on init and therefore safe to access outside the lock + if (SCNetworkReachabilityGetFlags(_reachability, &flags)) { + return [self networkStatusForFlags:flags]; + } + return status; +} + +- (PINSpeedRecorderConnectionStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags +{ + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { + // The target host is not reachable. + return PINSpeedRecorderConnectionStatusNotReachable; + } + + PINSpeedRecorderConnectionStatus connectionStatus = PINSpeedRecorderConnectionStatusNotReachable; + + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { + /* + If the target host is reachable and no connection is required then we'll assume (for now) that you're on Wi-Fi... + */ + connectionStatus = PINSpeedRecorderConnectionStatusWiFi; + } + + if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) { + /* + ... and the connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs... + */ + + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { + /* + ... and no [user] intervention is needed... + */ + connectionStatus = PINSpeedRecorderConnectionStatusWiFi; + } + } + +#if PIN_TARGET_IOS + if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN) { + /* + ... but WWAN connections are OK if the calling application is using the CFNetwork APIs. + */ + connectionStatus = PINSpeedRecorderConnectionStatusWWAN; + } +#endif + + return connectionStatus; +} + ++ (NSUInteger)appropriateImageIdxForURLsGivenHistoricalNetworkConditions:(NSArray *)urls + lowQualityQPSThreshold:(float)lowQualityQPSThreshold + highQualityQPSThreshold:(float)highQualityQPSThreshold +{ + float currentBytesPerSecond = [[PINSpeedRecorder sharedRecorder] currentBytesPerSecond]; + + NSUInteger desiredImageURLIdx; + + if (currentBytesPerSecond == -1) { + // Base it on reachability + switch ([[PINSpeedRecorder sharedRecorder] connectionStatus]) { + case PINSpeedRecorderConnectionStatusWiFi: + desiredImageURLIdx = urls.count - 1; + break; + + case PINSpeedRecorderConnectionStatusWWAN: + case PINSpeedRecorderConnectionStatusNotReachable: + desiredImageURLIdx = 0; + break; + } + } else { + if (currentBytesPerSecond >= highQualityQPSThreshold) { + desiredImageURLIdx = urls.count - 1; + } else if (currentBytesPerSecond <= lowQualityQPSThreshold) { + desiredImageURLIdx = 0; + } else if (urls.count == 2) { + desiredImageURLIdx = roundf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 1))); + } else { + desiredImageURLIdx = ceilf((currentBytesPerSecond - lowQualityQPSThreshold) / ((highQualityQPSThreshold - lowQualityQPSThreshold) / (float)(urls.count - 2))); + } + } + + return desiredImageURLIdx; +} + +@end + +@implementation PINTaskQOS + +- (instancetype)initWithBPS:(float)bytesPerSecond endDate:(NSDate *)endDate +{ + if (self = [super init]) { + self.endDate = endDate; + self.bytesPerSecond = bytesPerSecond; + } + return self; +} + +@end diff --git a/Tests/PINRemoteImageTests.m b/Tests/PINRemoteImageTests.m index 86baa193..29c5e20d 100644 --- a/Tests/PINRemoteImageTests.m +++ b/Tests/PINRemoteImageTests.m @@ -17,6 +17,7 @@ #import "PINResume.h" #import "PINRemoteImageDownloadTask.h" +#import "PINSpeedRecorder.h" #import @@ -65,9 +66,6 @@ @interface PINRemoteImageManager () @property (nonatomic, strong) PINURLSessionManager *sessionManager; @property (nonatomic, readonly) NSUInteger totalDownloads; -- (float)currentBytesPerSecond; -- (void)addTaskBPS:(float)bytesPerSecond endDate:(NSDate *)endDate; -- (void)setCurrentBytesPerSecond:(float)currentBPS; - (NSString *)resumeCacheKeyForURL:(NSURL *)url; @end @@ -799,21 +797,21 @@ - (void)testBytesPerSecond XCTestExpectation *finishExpectation = [self expectationWithDescription:@"Finished testing off the main thread."]; //currentBytesPerSecond is not public, should not be called on the main queue dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - XCTAssert([self.imageManager currentBytesPerSecond] == -1, @"Without any tasks added, should be -1"); - [self.imageManager addTaskBPS:100 endDate:[NSDate dateWithTimeIntervalSinceNow:-61]]; - XCTAssert([self.imageManager currentBytesPerSecond] == -1, @"With only old task, should be -1"); - [self.imageManager addTaskBPS:100 endDate:[NSDate date]]; - XCTAssert([self isFloat:[self.imageManager currentBytesPerSecond] equalToFloat:100.0f], @"One task should be same as added task"); - [self.imageManager addTaskBPS:50 endDate:[NSDate dateWithTimeIntervalSinceNow:-30]]; - XCTAssert([self isFloat:[self.imageManager currentBytesPerSecond] equalToFloat:75.0f], @"Two tasks should be average of both tasks"); - [self.imageManager addTaskBPS:100 endDate:[NSDate dateWithTimeIntervalSinceNow:-61]]; - XCTAssert([self isFloat:[self.imageManager currentBytesPerSecond] equalToFloat:75.0f], @"Old task shouldn't be counted"); - [self.imageManager addTaskBPS:50 endDate:[NSDate date]]; - [self.imageManager addTaskBPS:50 endDate:[NSDate date]]; - [self.imageManager addTaskBPS:50 endDate:[NSDate date]]; - [self.imageManager addTaskBPS:50 endDate:[NSDate date]]; - [self.imageManager addTaskBPS:50 endDate:[NSDate date]]; - XCTAssert([self isFloat:[self.imageManager currentBytesPerSecond] equalToFloat:50.0f], @"Only last 5 tasks should be used"); + XCTAssert([[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] == -1, @"Without any tasks added, should be -1"); + [[PINSpeedRecorder sharedRecorder] addTaskBPS:100 endDate:[NSDate dateWithTimeIntervalSinceNow:-61]]; + XCTAssert([[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] == -1, @"With only old task, should be -1"); + [[PINSpeedRecorder sharedRecorder] addTaskBPS:100 endDate:[NSDate date]]; + XCTAssert([self isFloat:[[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] equalToFloat:100.0f], @"One task should be same as added task"); + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate dateWithTimeIntervalSinceNow:-30]]; + XCTAssert([self isFloat:[[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] equalToFloat:75.0f], @"Two tasks should be average of both tasks"); + [[PINSpeedRecorder sharedRecorder] addTaskBPS:100 endDate:[NSDate dateWithTimeIntervalSinceNow:-61]]; + XCTAssert([self isFloat:[[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] equalToFloat:75.0f], @"Old task shouldn't be counted"); + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate date]]; + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate date]]; + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate date]]; + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate date]]; + [[PINSpeedRecorder sharedRecorder] addTaskBPS:50 endDate:[NSDate date]]; + XCTAssert([self isFloat:[[PINSpeedRecorder sharedRecorder] currentBytesPerSecond] equalToFloat:50.0f], @"Only last 5 tasks should be used"); [finishExpectation fulfill]; }); [self waitForExpectationsWithTimeout:[self timeoutTimeInterval] handler:nil]; @@ -852,7 +850,7 @@ - (void)testQOS // So, wait until it's actually in the cache. [self waitForImageWithURLToBeCached:[self JPEGURL_Large]]; - [self.imageManager setCurrentBytesPerSecond:5]; + [[PINSpeedRecorder sharedRecorder] setCurrentBytesPerSecond:5]; [self.imageManager downloadImageWithURLs:@[[self JPEGURL_Small], [self JPEGURL_Medium], [self JPEGURL_Large]] options:PINRemoteImageManagerDownloadOptionsNone progressImage:nil @@ -878,7 +876,7 @@ - (void)testQOS [self waitForImageWithURLToBeCached:[self JPEGURL_Small]]; - [self.imageManager setCurrentBytesPerSecond:100]; + [[PINSpeedRecorder sharedRecorder] setCurrentBytesPerSecond:100]; [self.imageManager downloadImageWithURLs:@[[self JPEGURL_Small], [self JPEGURL_Medium], [self JPEGURL_Large]] options:PINRemoteImageManagerDownloadOptionsNone progressImage:nil @@ -895,7 +893,7 @@ - (void)testQOS }]; XCTAssert(dispatch_semaphore_wait(semaphore, [self timeout]) == 0, @"Semaphore timed out."); - [self.imageManager setCurrentBytesPerSecond:7]; + [[PINSpeedRecorder sharedRecorder] setCurrentBytesPerSecond:7]; [self.imageManager downloadImageWithURLs:@[[self JPEGURL_Small], [self JPEGURL_Medium], [self JPEGURL_Large]] options:PINRemoteImageManagerDownloadOptionsNone progressImage:nil @@ -913,7 +911,7 @@ - (void)testQOS if ([[self.imageManager cache] objectFromMemoryForKey:key] == nil) { break; } - sleep(50); + usleep(100); } XCTAssert( [[self.imageManager cache] objectFromMemoryForKey:[self.imageManager cacheKeyForURL:[self JPEGURL_Small] processorKey:nil]] == nil, @"Small image should have been removed from cache"); @@ -924,7 +922,7 @@ - (void)testQOS }]; XCTAssert(dispatch_semaphore_wait(semaphore, [self timeout]) == 0, @"Semaphore timed out."); - [self.imageManager setCurrentBytesPerSecond:7]; + [[PINSpeedRecorder sharedRecorder] setCurrentBytesPerSecond:7]; [self.imageManager downloadImageWithURLs:@[[self JPEGURL_Small], [self JPEGURL_Large]] options:PINRemoteImageManagerDownloadOptionsNone progressImage:nil