From 7fee01cdf3e07a3a1758c9462d8bf04e75a6a851 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 14:53:56 -0700 Subject: [PATCH 1/6] Adding support for automatically skipping cancelation if the amount left to be downloaded is less than the average time to first byte. --- .../Categories/NSURLSessionTask+Timing.m | 56 +++++++++++------ Source/Classes/PINRemoteImageDownloadTask.m | 10 ++++ Source/Classes/PINURLSessionManager.h | 2 + Source/Classes/PINURLSessionManager.m | 60 +++++++++++++++++++ Tests/PINRemoteImageTests.m | 52 ++++++++++++++++ 5 files changed, 162 insertions(+), 18 deletions(-) diff --git a/Source/Classes/Categories/NSURLSessionTask+Timing.m b/Source/Classes/Categories/NSURLSessionTask+Timing.m index bee1f0c2..2e113bbb 100644 --- a/Source/Classes/Categories/NSURLSessionTask+Timing.m +++ b/Source/Classes/Categories/NSURLSessionTask+Timing.m @@ -11,9 +11,13 @@ #import #import +static NSString * const kPINURLSessionTaskStateKey = @"state"; +static NSString * const kPINURLSessionTaskResponseKey = @"response"; + @interface PINURLSessionTaskObserver : NSObject @property (atomic, assign) CFTimeInterval startTime; +@property (atomic, assign) CFTimeInterval timeOfFirstByte; @property (atomic, assign) CFTimeInterval endTime; - (instancetype)init NS_UNAVAILABLE; @@ -28,30 +32,46 @@ - (instancetype)initWithTask:(NSURLSessionTask *)task if (self = [super init]) { _startTime = 0; _endTime = 0; - [task addObserver:self forKeyPath:@"state" options:0 context:nil]; + [task addObserver:self forKeyPath:kPINURLSessionTaskStateKey options:0 context:nil]; + [task addObserver:self forKeyPath:kPINURLSessionTaskResponseKey options:0 context:nil]; } return self; } +- (void)removeObservers:(NSURLSessionTask *)task +{ + [task removeObserver:self forKeyPath:kPINURLSessionTaskStateKey]; + [task removeObserver:self forKeyPath:kPINURLSessionTaskResponseKey]; +} + + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSURLSessionTask *task = (NSURLSessionTask *)object; - switch (task.state) { - case NSURLSessionTaskStateRunning: - if (self.startTime == 0) { - self.startTime = CACurrentMediaTime(); - } - break; - - case NSURLSessionTaskStateCompleted: - NSAssert(self.startTime != 0, @"Expect that task was started before it's completed."); - if (self.endTime == 0) { - self.endTime = CACurrentMediaTime(); - } - break; - - default: - break; + if ([keyPath isEqualToString:kPINURLSessionTaskStateKey]) { + switch (task.state) { + case NSURLSessionTaskStateRunning: + if (self.startTime == 0) { + self.startTime = CACurrentMediaTime(); + } + break; + + case NSURLSessionTaskStateCompleted: + NSAssert(self.startTime != 0, @"Expect that task was started before it's completed."); + if (self.endTime == 0) { + self.endTime = CACurrentMediaTime(); + } + break; + + default: + break; + } + } else if ([keyPath isEqualToString:kPINURLSessionTaskResponseKey]) { + if (task.response != nil) { + NSAssert(self.startTime != 0, @"Expect that task has started."); + NSAssert(self.endTime == 0, @"Expect that task has not completed."); + self.timeOfFirstByte = CACurrentMediaTime(); + } } } @@ -74,7 +94,7 @@ - (void)PIN_setupSessionTaskObserver //remove state observer PINURLSessionTaskObserver *observer = objc_getAssociatedObject((__bridge id)obj, @selector(PIN_setupSessionTaskObserver)); if (observer) { - [(__bridge id)obj removeObserver:observer forKeyPath:@"state"]; + [observer removeObservers:(__bridge NSURLSessionTask *)obj]; } } diff --git a/Source/Classes/PINRemoteImageDownloadTask.m b/Source/Classes/PINRemoteImageDownloadTask.m index ababb4ec..23025733 100644 --- a/Source/Classes/PINRemoteImageDownloadTask.m +++ b/Source/Classes/PINRemoteImageDownloadTask.m @@ -96,6 +96,16 @@ - (BOOL)cancelWithUUID:(NSUUID *)UUID resume:(PINResume * _Nullable * _Nullable) { __block BOOL noMoreCompletions; [self.lock lockWithBlock:^{ + if (resume) { + //consider skipping cancelation if there's a request for resume data and the time to start the connection is greater than + //the time remaining to download. + NSTimeInterval timeToFirstByte = [self.manager.sessionManager weightedTimeToFirstByteForHost:_progressImage.dataTask.originalRequest.URL.host]; + if (_progressImage.estimatedRemainingTime <= timeToFirstByte) { + noMoreCompletions = NO; + return; + } + } + noMoreCompletions = [super l_cancelWithUUID:UUID resume:resume]; if (noMoreCompletions) { diff --git a/Source/Classes/PINURLSessionManager.h b/Source/Classes/PINURLSessionManager.h index f7f7dac6..4ba5f91f 100644 --- a/Source/Classes/PINURLSessionManager.h +++ b/Source/Classes/PINURLSessionManager.h @@ -35,6 +35,8 @@ typedef void (^PINURLSessionDataTaskCompletion)(NSURLSessionTask * _Nonnull task @property (atomic, weak, nullable) id delegate; +- (NSTimeInterval)weightedTimeToFirstByteForHost:(nonnull NSString *)host; + #if DEBUG - (void)concurrentDownloads:(void (^_Nullable)(NSUInteger concurrentDownloads))concurrentDownloadsCompletion; #endif diff --git a/Source/Classes/PINURLSessionManager.m b/Source/Classes/PINURLSessionManager.m index 1cc32e44..518254f1 100644 --- a/Source/Classes/PINURLSessionManager.m +++ b/Source/Classes/PINURLSessionManager.m @@ -11,6 +11,9 @@ NSString * const PINURLErrorDomain = @"PINURLErrorDomain"; @interface PINURLSessionManager () +{ + NSCache *_timeToFirstByteCache; +} @property (nonatomic, strong) NSLock *sessionManagerLock; @property (nonatomic, strong) NSURLSession *session; @@ -35,6 +38,9 @@ - (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)config self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.operationQueue]; self.completions = [[NSMutableDictionary alloc] init]; self.delegateQueues = [[NSMutableDictionary alloc] init]; + + _timeToFirstByteCache = [[NSCache alloc] init]; + _timeToFirstByteCache.countLimit = 25; } return self; } @@ -187,6 +193,60 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp }); } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics +{ + NSDate *requestStart = [NSDate distantFuture]; + NSDate *firstByte = [NSDate distantPast]; + + BOOL valid = YES; + + for (NSURLSessionTaskTransactionMetrics *metric in metrics.transactionMetrics) { + if (metric.requestStartDate == nil || metric.responseStartDate == nil || metric.responseEndDate == nil) { + //Only evaluate requests which completed their first byte. + valid = NO; + break; + } + if ([requestStart compare:metric.requestStartDate] != NSOrderedAscending) { + requestStart = metric.requestStartDate; + } + if ([firstByte compare:metric.responseStartDate] != NSOrderedDescending) { + firstByte = metric.responseStartDate; + } + } + + if (valid) { + [self storeTimeToFirstByte:[firstByte timeIntervalSinceDate:requestStart] forHost:task.originalRequest.URL.host]; + } +} + +- (void)storeTimeToFirstByte:(NSTimeInterval)timeToFirstByte forHost:(NSString *)host +{ + [self lock]; + NSNumber *existingTimeToFirstByte = [_timeToFirstByteCache objectForKey:host]; + if (existingTimeToFirstByte) { + //We're obviously seriously weighting the latest result by doing this. Seems reasonable in + //possibly changing network conditions. + existingTimeToFirstByte = @( (timeToFirstByte + [existingTimeToFirstByte doubleValue]) / 2.0 ); + } else { + existingTimeToFirstByte = [NSNumber numberWithDouble:timeToFirstByte]; + } + [_timeToFirstByteCache setObject:existingTimeToFirstByte forKey:host]; + [self unlock]; +} + +- (NSTimeInterval)weightedTimeToFirstByteForHost:(NSString *)host +{ + NSTimeInterval timeToFirstByte; + [self lock]; + timeToFirstByte = [[_timeToFirstByteCache objectForKey:host] doubleValue]; + if (timeToFirstByte <= 0 + DBL_EPSILON) { + //return 0 if we're not sure. + timeToFirstByte = 0; + } + [self unlock]; + return timeToFirstByte; +} + #if DEBUG - (void)concurrentDownloads:(void (^_Nullable)(NSUInteger concurrentDownloads))concurrentDownloadsCompletion { diff --git a/Tests/PINRemoteImageTests.m b/Tests/PINRemoteImageTests.m index fca2e36c..df1a70f4 100644 --- a/Tests/PINRemoteImageTests.m +++ b/Tests/PINRemoteImageTests.m @@ -47,6 +47,7 @@ - (NSString *)resumeCacheKeyForURL:(NSURL *)url; @interface PINURLSessionManager () @property (nonatomic, strong) NSURLSession *session; +- (void)storeTimeToFirstByte:(NSTimeInterval)timeToFirstByte forHost:(NSString *)host; @end #endif @@ -1162,4 +1163,55 @@ - (void)testResume XCTAssert([nonResumedImageData isEqualToData:resumedImageData], @"Resumed image data and non resumed image data should be the same."); } +- (void)testResumeSkipCancelation +{ + //Test that images aren't canceled if the cost of resuming is high (i.e. time to first byte is longer than the time left to download) + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + weakify(self); + [self.imageManager setEstimatedRemainingTimeThresholdForProgressiveDownloads:0.001 completion:^{ + strongify(self); + [self.imageManager setProgressiveRendersMaxProgressiveRenderSize:CGSizeMake(10000, 10000) completion:^{ + dispatch_semaphore_signal(semaphore); + }]; + }]; + dispatch_semaphore_wait(semaphore, [self timeout]); + + XCTestExpectation *progressExpectation = [self expectationWithDescription:@"progress is rendered"]; + progressExpectation.assertForOverFulfill = NO; + + [self.imageManager.sessionManager storeTimeToFirstByte:0 forHost:[[self progressiveURL] host]]; + + [self.imageManager downloadImageWithURL:[self progressiveURL] + options:PINRemoteImageManagerDownloadOptionsNone + progressImage:^(PINRemoteImageManagerResult * _Nonnull result) { + [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; + [progressExpectation fulfill]; + dispatch_semaphore_signal(semaphore); + } + completion:^(PINRemoteImageManagerResult * _Nonnull result) { + XCTAssert(result.image == nil, @"should not complete download: %@", result); + }]; + + dispatch_semaphore_wait(semaphore, [self timeout]); + + XCTestExpectation *progress2Expectation = [self expectationWithDescription:@"progress is rendered"]; + progress2Expectation.assertForOverFulfill = NO; + XCTestExpectation *completedExpectation = [self expectationWithDescription:@"image is completed"]; + + [self.imageManager.sessionManager storeTimeToFirstByte:10 forHost:[[self progressiveURL] host]]; + + [self.imageManager downloadImageWithURL:[self progressiveURL] + options:PINRemoteImageManagerDownloadOptionsNone + progressImage:^(PINRemoteImageManagerResult * _Nonnull result) { + [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; + [progress2Expectation fulfill]; + } + completion:^(PINRemoteImageManagerResult * _Nonnull result) { + [completedExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:[self timeoutTimeInterval] handler:nil]; +} + @end From aedd21e85e55d4013929a8be25597364cdd3d010 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 14:56:11 -0700 Subject: [PATCH 2/6] Cleaning up unused properties --- Source/Classes/Categories/NSURLSessionTask+Timing.m | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Source/Classes/Categories/NSURLSessionTask+Timing.m b/Source/Classes/Categories/NSURLSessionTask+Timing.m index 2e113bbb..4c7191b2 100644 --- a/Source/Classes/Categories/NSURLSessionTask+Timing.m +++ b/Source/Classes/Categories/NSURLSessionTask+Timing.m @@ -12,12 +12,10 @@ #import static NSString * const kPINURLSessionTaskStateKey = @"state"; -static NSString * const kPINURLSessionTaskResponseKey = @"response"; @interface PINURLSessionTaskObserver : NSObject @property (atomic, assign) CFTimeInterval startTime; -@property (atomic, assign) CFTimeInterval timeOfFirstByte; @property (atomic, assign) CFTimeInterval endTime; - (instancetype)init NS_UNAVAILABLE; @@ -33,7 +31,6 @@ - (instancetype)initWithTask:(NSURLSessionTask *)task _startTime = 0; _endTime = 0; [task addObserver:self forKeyPath:kPINURLSessionTaskStateKey options:0 context:nil]; - [task addObserver:self forKeyPath:kPINURLSessionTaskResponseKey options:0 context:nil]; } return self; } @@ -41,7 +38,6 @@ - (instancetype)initWithTask:(NSURLSessionTask *)task - (void)removeObservers:(NSURLSessionTask *)task { [task removeObserver:self forKeyPath:kPINURLSessionTaskStateKey]; - [task removeObserver:self forKeyPath:kPINURLSessionTaskResponseKey]; } @@ -66,12 +62,6 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N default: break; } - } else if ([keyPath isEqualToString:kPINURLSessionTaskResponseKey]) { - if (task.response != nil) { - NSAssert(self.startTime != 0, @"Expect that task has started."); - NSAssert(self.endTime == 0, @"Expect that task has not completed."); - self.timeOfFirstByte = CACurrentMediaTime(); - } } } From 7dedb415e1bf1d8c9fc6b114b9a4c5e7d0bb7a58 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 14:58:02 -0700 Subject: [PATCH 3/6] More missed stuff left in --- Source/Classes/PINURLSessionManager.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Classes/PINURLSessionManager.m b/Source/Classes/PINURLSessionManager.m index 518254f1..9e634350 100644 --- a/Source/Classes/PINURLSessionManager.m +++ b/Source/Classes/PINURLSessionManager.m @@ -201,7 +201,7 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFini BOOL valid = YES; for (NSURLSessionTaskTransactionMetrics *metric in metrics.transactionMetrics) { - if (metric.requestStartDate == nil || metric.responseStartDate == nil || metric.responseEndDate == nil) { + if (metric.requestStartDate == nil || metric.responseStartDate == nil) { //Only evaluate requests which completed their first byte. valid = NO; break; From bbce106b9ccae1e5eadb8f730a3968fe24016d5a Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 15:29:23 -0700 Subject: [PATCH 4/6] Added CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d36ee651..b542a01f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [new] Added support (in iOS 10) for skipping cancelation if the estimated amount of time to complete the download is less than the average time to first byte for a host. [#364](https://github.com/pinterest/PINRemoteImage/pull/364) [garrettmoon](http://github.com/garrettmoon) - [fixed] Fixes an issue where PINResume would assert because the server didn't return an expected content length. - [fixed] Fixed bytes per second on download tasks (which could affect if an image is progressively rendered) [#360](https://github.com/pinterest/PINRemoteImage/pull/360) [garrettmoon](https://github.com/garrettmoon) - [new] Added request configuration handler to allow customizing HTTP headers per request [#355](https://github.com/pinterest/PINRemoteImage/pull/355) [zachwaugh](https://github.com/zachwaugh) From 31b6a419a36ac3afa0643777eedbdf4d90dec18a Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 16:17:39 -0700 Subject: [PATCH 5/6] Remove assertForOverFulfill, which is bizarly not supported? --- Tests/PINRemoteImageTests.m | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Tests/PINRemoteImageTests.m b/Tests/PINRemoteImageTests.m index df1a70f4..0012fcad 100644 --- a/Tests/PINRemoteImageTests.m +++ b/Tests/PINRemoteImageTests.m @@ -258,6 +258,10 @@ - (void)testCustomRequestHeaderIsAddedToImageRequests [self.imageManager setValue:@"Custom Request Header 2" forHTTPHeaderField:@"X-Custom-Request-Header-2"]; [self.imageManager setValue:nil forHTTPHeaderField:@"X-Custom-Request-Header-2"]; self.imageManager.sessionManager.delegate = self; + + //sleep for a moment so values have a chance to asynchronously set. + usleep(10000); + [self.imageManager downloadImageWithURL:[self progressiveURL] options:PINRemoteImageManagerDownloadOptionsNone completion:^(PINRemoteImageManagerResult *result) @@ -1178,16 +1182,19 @@ - (void)testResumeSkipCancelation dispatch_semaphore_wait(semaphore, [self timeout]); XCTestExpectation *progressExpectation = [self expectationWithDescription:@"progress is rendered"]; - progressExpectation.assertForOverFulfill = NO; [self.imageManager.sessionManager storeTimeToFirstByte:0 forHost:[[self progressiveURL] host]]; + __block BOOL canceled = NO; [self.imageManager downloadImageWithURL:[self progressiveURL] options:PINRemoteImageManagerDownloadOptionsNone progressImage:^(PINRemoteImageManagerResult * _Nonnull result) { - [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; - [progressExpectation fulfill]; - dispatch_semaphore_signal(semaphore); + if (canceled == NO) { + canceled = YES; + [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; + [progressExpectation fulfill]; + dispatch_semaphore_signal(semaphore); + } } completion:^(PINRemoteImageManagerResult * _Nonnull result) { XCTAssert(result.image == nil, @"should not complete download: %@", result); @@ -1195,17 +1202,23 @@ - (void)testResumeSkipCancelation dispatch_semaphore_wait(semaphore, [self timeout]); - XCTestExpectation *progress2Expectation = [self expectationWithDescription:@"progress is rendered"]; - progress2Expectation.assertForOverFulfill = NO; + //Remove any progress + [self.imageManager.cache removeObjectForKey:[self.imageManager resumeCacheKeyForURL:[self progressiveURL]]]; + + XCTestExpectation *progress2Expectation = [self expectationWithDescription:@"progress 2 is rendered"]; XCTestExpectation *completedExpectation = [self expectationWithDescription:@"image is completed"]; [self.imageManager.sessionManager storeTimeToFirstByte:10 forHost:[[self progressiveURL] host]]; + canceled = NO; [self.imageManager downloadImageWithURL:[self progressiveURL] options:PINRemoteImageManagerDownloadOptionsNone progressImage:^(PINRemoteImageManagerResult * _Nonnull result) { - [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; - [progress2Expectation fulfill]; + if (canceled == NO) { + canceled = YES; + [self.imageManager cancelTaskWithUUID:result.UUID storeResumeData:YES]; + [progress2Expectation fulfill]; + } } completion:^(PINRemoteImageManagerResult * _Nonnull result) { [completedExpectation fulfill]; From 99c6b91a75d197062024572a7765b3246e1101f0 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 25 May 2017 17:13:05 -0700 Subject: [PATCH 6/6] Addressing @jparise's comments. --- .../Categories/NSURLSessionTask+Timing.m | 2 +- Source/Classes/PINURLSessionManager.h | 6 +++ Source/Classes/PINURLSessionManager.m | 43 ++++++++----------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Source/Classes/Categories/NSURLSessionTask+Timing.m b/Source/Classes/Categories/NSURLSessionTask+Timing.m index 4c7191b2..3a3e2867 100644 --- a/Source/Classes/Categories/NSURLSessionTask+Timing.m +++ b/Source/Classes/Categories/NSURLSessionTask+Timing.m @@ -53,7 +53,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N break; case NSURLSessionTaskStateCompleted: - NSAssert(self.startTime != 0, @"Expect that task was started before it's completed."); + NSAssert(self.startTime > 0, @"Expect that task was started before it's completed."); if (self.endTime == 0) { self.endTime = CACurrentMediaTime(); } diff --git a/Source/Classes/PINURLSessionManager.h b/Source/Classes/PINURLSessionManager.h index 4ba5f91f..1077b1bc 100644 --- a/Source/Classes/PINURLSessionManager.h +++ b/Source/Classes/PINURLSessionManager.h @@ -35,6 +35,12 @@ typedef void (^PINURLSessionDataTaskCompletion)(NSURLSessionTask * _Nonnull task @property (atomic, weak, nullable) id delegate; +/* + Returns a weighted average of time to first byte for the specified host. + More specifically, we get the time to first byte for every task that completes + and add it to an existing average: newAverage = (existingAverage + newTimeToFirstByte / 2) + This is all done on a per host basis. + */ - (NSTimeInterval)weightedTimeToFirstByteForHost:(nonnull NSString *)host; #if DEBUG diff --git a/Source/Classes/PINURLSessionManager.m b/Source/Classes/PINURLSessionManager.m index 9e634350..7dbb7595 100644 --- a/Source/Classes/PINURLSessionManager.m +++ b/Source/Classes/PINURLSessionManager.m @@ -198,13 +198,10 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFini NSDate *requestStart = [NSDate distantFuture]; NSDate *firstByte = [NSDate distantPast]; - BOOL valid = YES; - for (NSURLSessionTaskTransactionMetrics *metric in metrics.transactionMetrics) { if (metric.requestStartDate == nil || metric.responseStartDate == nil) { //Only evaluate requests which completed their first byte. - valid = NO; - break; + return; } if ([requestStart compare:metric.requestStartDate] != NSOrderedAscending) { requestStart = metric.requestStartDate; @@ -214,36 +211,32 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFini } } - if (valid) { - [self storeTimeToFirstByte:[firstByte timeIntervalSinceDate:requestStart] forHost:task.originalRequest.URL.host]; - } + [self storeTimeToFirstByte:[firstByte timeIntervalSinceDate:requestStart] forHost:task.originalRequest.URL.host]; } +/* We don't bother locking around the timeToFirstByteCache because NSCache itself is + threadsafe and we're not concerned about dropping or overwriting a result. */ - (void)storeTimeToFirstByte:(NSTimeInterval)timeToFirstByte forHost:(NSString *)host { - [self lock]; - NSNumber *existingTimeToFirstByte = [_timeToFirstByteCache objectForKey:host]; - if (existingTimeToFirstByte) { - //We're obviously seriously weighting the latest result by doing this. Seems reasonable in - //possibly changing network conditions. - existingTimeToFirstByte = @( (timeToFirstByte + [existingTimeToFirstByte doubleValue]) / 2.0 ); - } else { - existingTimeToFirstByte = [NSNumber numberWithDouble:timeToFirstByte]; - } - [_timeToFirstByteCache setObject:existingTimeToFirstByte forKey:host]; - [self unlock]; + NSNumber *existingTimeToFirstByte = [_timeToFirstByteCache objectForKey:host]; + if (existingTimeToFirstByte) { + //We're obviously seriously weighting the latest result by doing this. Seems reasonable in + //possibly changing network conditions. + existingTimeToFirstByte = @( (timeToFirstByte + [existingTimeToFirstByte doubleValue]) / 2.0 ); + } else { + existingTimeToFirstByte = [NSNumber numberWithDouble:timeToFirstByte]; + } + [_timeToFirstByteCache setObject:existingTimeToFirstByte forKey:host]; } - (NSTimeInterval)weightedTimeToFirstByteForHost:(NSString *)host { NSTimeInterval timeToFirstByte; - [self lock]; - timeToFirstByte = [[_timeToFirstByteCache objectForKey:host] doubleValue]; - if (timeToFirstByte <= 0 + DBL_EPSILON) { - //return 0 if we're not sure. - timeToFirstByte = 0; - } - [self unlock]; + timeToFirstByte = [[_timeToFirstByteCache objectForKey:host] doubleValue]; + if (timeToFirstByte <= 0 + DBL_EPSILON) { + //return 0 if we're not sure. + timeToFirstByte = 0; + } return timeToFirstByte; }