Skip to content

Commit

Permalink
Changes to address infamous issue #47 (issue 30 in owncloud/ios-app#237
Browse files Browse the repository at this point in the history
…) by trying to limit background NSURLSession usage to those times where a transfer's duration will likely exceed the app's background time:

- OCHTTP
	- addition of OCHTTPPipelineTaskMetrics to record performance metrics of
		- detailed times (using NSURLSessionTaskMetrics)
		- traffic (from NSURLSessionTask)
		- hostname + timestamp
	- OCHTTPPipelineTask gets new .metrics property to keep an associated OCHTTPPipelineTaskMetrics object
	- OCHTTPPipeline support for OCHTTPPipelineTaskMetrics
		- takes and keeps OCHTTPPipelineTaskMetrics internally
			- only records whose net transfer time exceeded a threshold (0.01 seconds atm)
			- only holds onto records for 10 minutes
		- can provide estimates on
			- how long a given HTTP request will take to complete before sending it
			- how confident it is about that estimate
- OCConnection
	- new method to estimate transfer times for requests
		- queries longLived and command pipelines under the hood and uses the estimate with the higher confidence
	- new method to provide a pipeline depending on estimated transfer time
		- will return the command pipeline (typically backed by a local NSURLSession) for requests that are likely to finish within 2 minutes
		- will return the longlived pipeline (typically backed by a background NSURLSession) for
			- requests that are estimated to take longer than 2 minutes
			- requests whose estimation has a confidence below 15%
			- requests where no estimate is available
	- uploads and downloads now dispatch their requests to different pipelines depending on estimated transfer time
  • Loading branch information
felix-schwarz committed Apr 17, 2019
1 parent ed8dab0 commit cead9cf
Show file tree
Hide file tree
Showing 9 changed files with 494 additions and 4 deletions.
8 changes: 8 additions & 0 deletions ownCloudSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@
DC594B1F21EFD7F900B882C4 /* BookmarkManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC594B1E21EFD7F900B882C4 /* BookmarkManagerTests.m */; };
DC5A20312074E8890083DB7D /* CoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC5A20302074E8890083DB7D /* CoreTests.m */; };
DC5A794F21E5FAF20045BCAA /* OCConnection+Signals.m in Sources */ = {isa = PBXBuildFile; fileRef = DC5A794D21E5FAF20045BCAA /* OCConnection+Signals.m */; };
DC5AD95422665AC800277DB0 /* OCHTTPPipelineTaskMetrics.h in Headers */ = {isa = PBXBuildFile; fileRef = DC5AD95222665AC800277DB0 /* OCHTTPPipelineTaskMetrics.h */; settings = {ATTRIBUTES = (Public, ); }; };
DC5AD95522665AC800277DB0 /* OCHTTPPipelineTaskMetrics.m in Sources */ = {isa = PBXBuildFile; fileRef = DC5AD95322665AC800277DB0 /* OCHTTPPipelineTaskMetrics.m */; };
DC61E931221423D2002889D6 /* HTTPPipelineTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DC61E930221423D2002889D6 /* HTTPPipelineTests.m */; };
DC62FD62211B11FD0034B628 /* OCItem+OCThumbnail.h in Headers */ = {isa = PBXBuildFile; fileRef = DC62FD60211B11FD0034B628 /* OCItem+OCThumbnail.h */; };
DC62FD63211B11FD0034B628 /* OCItem+OCThumbnail.m in Sources */ = {isa = PBXBuildFile; fileRef = DC62FD61211B11FD0034B628 /* OCItem+OCThumbnail.m */; };
Expand Down Expand Up @@ -769,6 +771,8 @@
DC5A20302074E8890083DB7D /* CoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CoreTests.m; sourceTree = "<group>"; };
DC5A20322074F9020083DB7D /* Ocean.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ocean.entitlements; sourceTree = SOURCE_ROOT; };
DC5A794D21E5FAF20045BCAA /* OCConnection+Signals.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCConnection+Signals.m"; sourceTree = "<group>"; };
DC5AD95222665AC800277DB0 /* OCHTTPPipelineTaskMetrics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCHTTPPipelineTaskMetrics.h; sourceTree = "<group>"; };
DC5AD95322665AC800277DB0 /* OCHTTPPipelineTaskMetrics.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCHTTPPipelineTaskMetrics.m; sourceTree = "<group>"; };
DC61E930221423D2002889D6 /* HTTPPipelineTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HTTPPipelineTests.m; sourceTree = "<group>"; };
DC62FD60211B11FD0034B628 /* OCItem+OCThumbnail.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCItem+OCThumbnail.h"; sourceTree = "<group>"; };
DC62FD61211B11FD0034B628 /* OCItem+OCThumbnail.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "OCItem+OCThumbnail.m"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1535,6 +1539,8 @@
DC4B116F220830F20062BCDD /* OCHTTPPipelineBackend.h */,
DCE48DD7220E1C7B00839E97 /* OCHTTPPipelineTaskCache.m */,
DCE48DD6220E1C7A00839E97 /* OCHTTPPipelineTaskCache.h */,
DC5AD95322665AC800277DB0 /* OCHTTPPipelineTaskMetrics.m */,
DC5AD95222665AC800277DB0 /* OCHTTPPipelineTaskMetrics.h */,
);
path = Pipeline;
sourceTree = "<group>";
Expand Down Expand Up @@ -2321,6 +2327,7 @@
DCC8FA0F2029C6A400EB6701 /* OCQueryChangeSet.h in Headers */,
DC701484220B090B009D4FD9 /* OCHTTPTypes.h in Headers */,
DC708CCE2141306100FE43CA /* OCSyncActionCopyMove.h in Headers */,
DC5AD95422665AC800277DB0 /* OCHTTPPipelineTaskMetrics.h in Headers */,
DCC8F9D9202854FB00EB6701 /* OCCore.h in Headers */,
DC4AFAB4206AE61400189B9A /* OCSQLiteQuery.h in Headers */,
DC4AFAA6206A6E7100189B9A /* OCSQLiteResultSet.h in Headers */,
Expand Down Expand Up @@ -2900,6 +2907,7 @@
DCADC0452072CCC900DB8E83 /* OCCoreItemListTask.m in Sources */,
DC576EC7226484E30087316D /* OCBackgroundManager.m in Sources */,
DCB0A46521B922A400FAC4E9 /* OCCoreConnectionStatusSignalProvider.m in Sources */,
DC5AD95522665AC800277DB0 /* OCHTTPPipelineTaskMetrics.m in Sources */,
DC708CE5214135E200FE43CA /* OCSyncActionDownload.m in Sources */,
DCEEB2E62044B0A400189B9A /* OCAuthenticationMethod+OCTools.m in Sources */,
DC139CC920DBB8440090175A /* OCChecksum.m in Sources */,
Expand Down
58 changes: 56 additions & 2 deletions ownCloudSDK/Connection/OCConnection.m
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,60 @@ - (void)_handleRetrieveItemListAtPathResult:(OCHTTPRequest *)request error:(NSEr
}
}

#pragma mark - Estimates
- (NSNumber *)estimatedTimeForTransferRequest:(OCHTTPRequest *)request withExpectedResponseLength:(NSUInteger)expectedResponseLength confidence:(double * _Nullable)outConfidence
{
double longlivedConfidence = 0;
NSNumber *longLivedEstimatedTime = [self.longLivedPipeline estimatedTimeForRequest:request withExpectedResponseLength:expectedResponseLength confidence:&longlivedConfidence];

double commandConfidence = 0;
NSNumber *commandEstimatedTime = [self.commandPipeline estimatedTimeForRequest:request withExpectedResponseLength:expectedResponseLength confidence:&commandConfidence];

double estimationConfidence = 0;
NSNumber *estimatedTime = nil;

if ((commandEstimatedTime != nil) && (commandConfidence > longlivedConfidence))
{
estimatedTime = commandEstimatedTime;
estimationConfidence = commandConfidence;
}
else if ((longLivedEstimatedTime != nil) && (longlivedConfidence > commandConfidence))
{
estimatedTime = longLivedEstimatedTime;
estimationConfidence = longlivedConfidence;
}

if (outConfidence != NULL)
{
*outConfidence = estimationConfidence;
}

return (estimatedTime);
}

- (OCHTTPPipeline *)transferPipelineForRequest:(OCHTTPRequest *)request withExpectedResponseLength:(NSUInteger)expectedResponseLength
{
double confidenceLevel = 0;
NSNumber *estimatedTime = nil;

if (!OCProcessManager.isProcessExtension)
{
if ((estimatedTime = [self estimatedTimeForTransferRequest:request withExpectedResponseLength:expectedResponseLength confidence:&confidenceLevel]) != nil)
{
BOOL useCommandPipeline = (estimatedTime.doubleValue <= 120.0) && (confidenceLevel >= 0.15);

OCLogDebug(@"Expected finish time: %@ (in %@ seconds, confidence %.02f) - pipeline: %@", [NSDate dateWithTimeIntervalSinceNow:estimatedTime.doubleValue], estimatedTime, confidenceLevel, (useCommandPipeline ? @"command" : @"longLived"));

if (useCommandPipeline)
{
return (self.commandPipeline);
}
}
}

return (self.longLivedPipeline);
}

#pragma mark - Actions

#pragma mark - File transfer: upload
Expand Down Expand Up @@ -1285,7 +1339,7 @@ - (OCProgress *)uploadFileFromURL:(NSURL *)sourceURL withName:(NSString *)fileNa
request.requestObserver = options[OCConnectionOptionRequestObserverKey];
}

[self.longLivedPipeline enqueueRequest:request forPartitionID:self.partitionID];
[[self transferPipelineForRequest:request withExpectedResponseLength:1000] enqueueRequest:request forPartitionID:self.partitionID];

requestProgress = request.progress;
requestProgress.progress.eventType = OCEventTypeUpload;
Expand Down Expand Up @@ -1434,7 +1488,7 @@ - (OCProgress *)downloadItem:(OCItem *)item to:(NSURL *)targetURL options:(NSDic
[self attachToPipelines];

// Enqueue request
[self.longLivedPipeline enqueueRequest:request forPartitionID:self.partitionID];
[[self transferPipelineForRequest:request withExpectedResponseLength:item.size] enqueueRequest:request forPartitionID:self.partitionID];

requestProgress = request.progress;
requestProgress.progress.eventType = OCEventTypeDownload;
Expand Down
3 changes: 3 additions & 0 deletions ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Progress
- (nullable NSProgress *)progressForRequestID:(OCHTTPRequestID)requestID;

#pragma mark - Metrics
- (nullable NSNumber *)estimatedTimeForRequest:(OCHTTPRequest *)request withExpectedResponseLength:(NSUInteger)expectedResponseLength confidence:(double * _Nullable)outConfidence;//!< If a sufficient amount of metrics could be collected, returns the estimated number of seconds it'll take the request to be sent and a response of expectedResponseLength be received.

#pragma mark - Internal job queue
- (void)queueBlock:(dispatch_block_t)block withBusy:(BOOL)withBusy; //!< Add a block for execution on the internal job queue.

Expand Down
184 changes: 184 additions & 0 deletions ownCloudSDK/HTTP/Pipeline/OCHTTPPipeline.m
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ @interface OCHTTPPipeline ()
NSMutableSet<OCHTTPPipelinePartitionID> *_partitionsInDestruction;
NSMutableDictionary<OCHTTPPipelinePartitionID, NSMutableArray<dispatch_block_t> *> *_partitionEmptyHandlers;

NSMutableArray<OCHTTPPipelineTaskMetrics *> *_metricsHistory;
NSTimeInterval _metricsHistoryMaxAge;
NSTimeInterval _metricsMinimumTotalTransferDurationRelevancyThreshold;

dispatch_group_t _busyGroup;
}

Expand All @@ -68,6 +72,10 @@ - (instancetype)initWithIdentifier:(OCHTTPPipelineID)identifier backend:(nullabl
_sessionCompletionHandlersByIdentifiers = [NSMutableDictionary new];
_partitionsInDestruction = [NSMutableSet new];

_metricsHistory = [NSMutableArray new];
_metricsHistoryMaxAge = 10 * 60; //!< Metrics records are used for computation for a maximum of 10 minutes
_metricsMinimumTotalTransferDurationRelevancyThreshold = 0.01; // Only metrics with a minimum total transfer duration of X secs should be considered relevant

_busyGroup = dispatch_group_create();

// Set backend
Expand Down Expand Up @@ -1617,6 +1625,8 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)urlSessionTa
{
OCLogDebug(@"Known task [taskIdentifier=<%lu>, url=%@] didCompleteWithError=%@", urlSessionTask.taskIdentifier, OCLogPrivate(urlSessionTask.currentRequest.URL), error);

[self addMetrics:task.metrics withTask:urlSessionTask];

if (error != nil)
{
OCHTTPResponse *response = [task responseFromURLSessionTask:urlSessionTask];
Expand Down Expand Up @@ -1644,13 +1654,21 @@ - (void)URLSession:(NSURLSession *)session taskIsWaitingForConnectivity:(NSURLSe

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)urlSessionTask didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
OCHTTPPipelineTask *task = nil;
NSError *backendError = nil;

if (OCLogger.logLevel <= OCLogLevelDebug)
{
NSString *XRequestID = [urlSessionTask.currentRequest.allHTTPHeaderFields objectForKey:@"X-Request-ID"];
NSArray <OCLogTagName> *extraTags = [NSArray arrayWithObjects: @"HTTP", @"Metrics", urlSessionTask.currentRequest.HTTPMethod, OCLogTagTypedID(@"RequestID", XRequestID), OCLogTagTypedID(@"URLSessionTaskID", @(urlSessionTask.taskIdentifier)), nil];

OCPLogDebug(OCLogOptionLogRequestsAndResponses, extraTags, @"Task [taskIdentifier=<%lu>, url=%@] didFinishCollectingMetrics: %@", urlSessionTask.taskIdentifier, OCLogPrivate(urlSessionTask.currentRequest.URL), [metrics compactSummaryWithTask:urlSessionTask]);
}

if ((task = [self.backend retrieveTaskForPipeline:self URLSession:session task:urlSessionTask error:&backendError]) != nil)
{
task.metrics = [[OCHTTPPipelineTaskMetrics alloc] initWithURLSessionTaskMetrics:metrics];
}
}

- (void)URLSession:(NSURLSession *)session
Expand Down Expand Up @@ -1894,6 +1912,172 @@ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTas
// OCLogDebug(@"DOWNLOADTASK FINISHED: %@ %@ %@", downloadTask, location, request);
}

#pragma mark - Metrics
- (void)addMetrics:(OCHTTPPipelineTaskMetrics *)metrics withTask:(NSURLSessionTask *)task
{
if ((metrics != nil) && (task != nil))
{
if ((metrics.totalTransferDuration.doubleValue > _metricsMinimumTotalTransferDurationRelevancyThreshold) &&
((-[metrics.date timeIntervalSinceNow]) < _metricsHistoryMaxAge))
{
[metrics addTransferSizesFromURLSessionTask:task];

@synchronized(_metricsHistory)
{
// Drop outdated records
[_metricsHistory filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(OCHTTPPipelineTaskMetrics *filterMetrics, NSDictionary<NSString *,id> * _Nullable bindings) {
return ((-[filterMetrics.date timeIntervalSinceNow]) < self->_metricsHistoryMaxAge);
}]];

// Add new record
[_metricsHistory addObject:metrics];
}
}
}
}

- (BOOL)weightedAverageSendBytesPerSecond:(NSNumber **)outSendBytesPerSecond receiveBytesPerSecond:(NSNumber **)outReceiveBytesPerSecond transportationDelaySeconds:(NSNumber **)outTransportationDelaySeconds largestSendBytes:(NSUInteger *)outLargestSendBytes largestReceiveBytes:(NSUInteger *)outLargestReceiveBytes forHostname:(NSString *)hostname
{
@synchronized(_metricsHistory)
{
NSUInteger totalSendBytesPerSecond = 0;
NSUInteger totalSendBytesPerSecondSamples = 0;

NSUInteger totalReceiveBytesPerSecond = 0;
NSUInteger totalReceiveBytesPerSecondSamples = 0;

NSTimeInterval totalTransportationDelaySeconds = 0;
NSUInteger totalTransportationDelaySamples = 0;

NSUInteger totalSentBytes = 0;
NSUInteger totalReceivedBytes = 0;

NSUInteger largestSentBytes = 0;
NSUInteger largestReceiveBytes = 0;

for (OCHTTPPipelineTaskMetrics *metrics in _metricsHistory)
{
if (((-[metrics.date timeIntervalSinceNow]) < _metricsHistoryMaxAge) && [metrics.hostname isEqual:hostname])
{
NSNumber *number;

if ((number = metrics.sentBytesPerSecond) != nil)
{
NSUInteger totalBytes = metrics.totalRequestSizeBytes.unsignedIntegerValue;

totalSendBytesPerSecond += number.unsignedIntegerValue * totalBytes;
totalSendBytesPerSecondSamples += 1;

totalSentBytes += totalBytes;

if (largestSentBytes < totalBytes)
{
largestSentBytes = totalBytes;
}
}

if ((number = metrics.receivedBytesPerSecond) != nil)
{
NSUInteger totalBytes = metrics.totalResponseSizeBytes.unsignedIntegerValue;

totalReceiveBytesPerSecond += number.unsignedIntegerValue * totalBytes;
totalReceiveBytesPerSecondSamples += 1;

totalReceivedBytes += metrics.totalResponseSizeBytes.unsignedIntegerValue;

if (largestReceiveBytes < totalBytes)
{
largestReceiveBytes = totalBytes;
}
}

if ((number = metrics.serverProcessingTimeInterval) != nil)
{
totalTransportationDelaySeconds += number.doubleValue;
totalTransportationDelaySamples += 1;
}
}
}

if ((totalSendBytesPerSecondSamples > 0) && (totalSentBytes > 0))
{
*outSendBytesPerSecond = @(totalSendBytesPerSecond / (totalSentBytes * totalSendBytesPerSecondSamples));
}
else
{
*outSendBytesPerSecond = nil;
}

if ((totalReceiveBytesPerSecondSamples > 0) && (totalReceivedBytes > 0))
{
*outReceiveBytesPerSecond = @(totalReceiveBytesPerSecond / (totalReceivedBytes * totalReceiveBytesPerSecondSamples));
}
else
{
*outReceiveBytesPerSecond = nil;
}

if (totalTransportationDelaySamples > 0)
{
*outTransportationDelaySeconds = @(totalTransportationDelaySeconds / ((NSTimeInterval)totalTransportationDelaySamples));
}
else
{
*outTransportationDelaySeconds = nil;
}

*outLargestReceiveBytes = largestReceiveBytes;
*outLargestSendBytes = largestSentBytes;

return ((totalSendBytesPerSecondSamples > 0) && (totalReceiveBytesPerSecondSamples > 0) && (totalTransportationDelaySeconds > 0));
}
}

- (nullable NSNumber *)estimatedTimeForRequest:(OCHTTPRequest *)request withExpectedResponseLength:(NSUInteger)expectedResponseLength confidence:(double *)outConfidence
{
NSNumber *averageSendBytesPerSecond=nil, *averageReceiveBytesPerSecond=nil, *averageTransportationDelay = nil;
NSUInteger largestSendBytes = 0, largestReceivesBytes = 0;

if ([self weightedAverageSendBytesPerSecond:&averageSendBytesPerSecond receiveBytesPerSecond:&averageReceiveBytesPerSecond transportationDelaySeconds:&averageTransportationDelay largestSendBytes:&largestSendBytes largestReceiveBytes:&largestReceivesBytes forHostname:request.url.host])
{
NSUInteger requestSize = 0, responseSize = expectedResponseLength;
NSTimeInterval estimatedTimeToComplete = 0;
CGFloat confidenceLevel = 0;

[request prepareForScheduling];

requestSize += [OCHTTPPipelineTaskMetrics lengthOfHeaderDictionary:request.headerFields method:request.method url:request.effectiveURL];

if (request.bodyURL != nil)
{
NSNumber *fileSize = nil;

if ([request.bodyURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL])
{
requestSize += fileSize.unsignedIntegerValue;
}
}
else
{
requestSize += request.bodyData.length;
}

estimatedTimeToComplete = ((NSTimeInterval)requestSize / (NSTimeInterval)averageSendBytesPerSecond.unsignedIntegerValue) + ((NSTimeInterval)responseSize / (NSTimeInterval)averageReceiveBytesPerSecond.unsignedIntegerValue) + averageTransportationDelay.doubleValue;

confidenceLevel = (CGFloat)1.0 / ((((CGFloat)(requestSize*requestSize) / (CGFloat)largestSendBytes) + ((CGFloat)(responseSize*responseSize) / (CGFloat)largestReceivesBytes)) / ((CGFloat)(requestSize + responseSize)));

OCLogDebug(@"weightedAverage sendBPS=%@, receiveBPS=%@, transportationDelay=%@ - bytesToSend=%lu, bytesToReceive=%lu - estimatedTimeToComplete=%.02f, confidenceLevel=%.03f", averageSendBytesPerSecond, averageReceiveBytesPerSecond, averageTransportationDelay, requestSize, responseSize, estimatedTimeToComplete, confidenceLevel);

if (outConfidence != NULL)
{
*outConfidence = confidenceLevel;
}

return (@(estimatedTimeToComplete));
}

return (nil);
}

#pragma mark - Progress
- (nullable NSProgress *)progressForRequestID:(OCHTTPRequestID)requestID
Expand Down
3 changes: 3 additions & 0 deletions ownCloudSDK/HTTP/Pipeline/OCHTTPPipelineTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#import <Foundation/Foundation.h>
#import "OCHTTPPipeline.h"
#import "OCHTTPPipelineTaskMetrics.h"

NS_ASSUME_NONNULL_BEGIN

Expand Down Expand Up @@ -56,6 +57,8 @@ NS_ASSUME_NONNULL_BEGIN
@property(nullable,strong,nonatomic) OCHTTPResponse *response; //!< The response. Lazily deserializes .responseData as needed.
@property(nullable,strong,nonatomic,readonly) NSData *responseData; //!< The serialized response. Lazily serializes .response as needed.

@property(nullable,strong) OCHTTPPipelineTaskMetrics *metrics; //!< (optional) metrics for the task (typically not serialized)

@property(assign) BOOL finished; //!< The task has been finished

#pragma mark - Init
Expand Down
Loading

0 comments on commit cead9cf

Please sign in to comment.