From bb424d44fdd8a4d41c43c86387de3b32e1941cf8 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 16:52:55 +0800 Subject: [PATCH 1/3] Added the URLSessionTaskMetrics support for downloader && operation, which can be used for network metrics --- SDWebImage/Core/SDWebImageDownloader.m | 9 +++++++++ SDWebImage/Core/SDWebImageDownloaderOperation.h | 7 +++++++ SDWebImage/Core/SDWebImageDownloaderOperation.m | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index 94bfa049..c836cc59 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -498,6 +498,15 @@ didReceiveResponse:(NSURLResponse *)response } } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + + // Identify the operation that runs this task and pass it the delegate method + NSOperation *dataOperation = [self operationWithTask:task]; + if ([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) { + [dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics]; + } +} + @end @implementation SDWebImageDownloadToken diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.h b/SDWebImage/Core/SDWebImageDownloaderOperation.h index e987ba42..3b93aa71 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.h +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.h @@ -36,6 +36,7 @@ @optional @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; +@property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (strong, nonatomic, nullable) NSURLCredential *credential; @property (assign, nonatomic) double minimumProgressInterval; @@ -62,6 +63,12 @@ */ @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; +/** + * The collected metrics from `-URLSession:task:didFinishCollectingMetrics:`. + * This can be used to collect the network metrics like download duration, DNS lookup duration, SSL handshake dureation, etc. See Apple's documentation: https://developer.apple.com/documentation/foundation/urlsessiontaskmetrics + */ +@property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + /** * The credential used for authentication challenges in `-URLSession:task:didReceiveChallenge:completionHandler:`. * diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 6527eddd..355e207e 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -52,6 +52,8 @@ typedef NSMutableDictionary SDCallbacksDictionary; @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; +@property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + @property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue; // the queue to do image decoding #if SD_UIKIT @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; @@ -512,6 +514,10 @@ didReceiveResponse:(NSURLResponse *)response } } +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { + self.metrics = metrics; +} + #pragma mark Helper methods + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions { SDWebImageOptions options = 0; From ed894ecff59aa5acb3a11dfb21490f974a76066f Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 17:32:46 +0800 Subject: [PATCH 2/3] Added the metrics in the download token, make it easy to grab the information right in completion block (is this useful ?) --- SDWebImage/Core/SDWebImageDownloader.h | 5 +++++ SDWebImage/Core/SDWebImageDownloader.m | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDWebImageDownloader.h b/SDWebImage/Core/SDWebImageDownloader.h index 571b72a2..a365395c 100644 --- a/SDWebImage/Core/SDWebImageDownloader.h +++ b/SDWebImage/Core/SDWebImageDownloader.h @@ -128,6 +128,11 @@ typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock; */ @property (nonatomic, strong, nullable, readonly) NSURLResponse *response; +/** + The download's metrics. This will be nil if download operation does not support metrics. + */ +@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); + @end diff --git a/SDWebImage/Core/SDWebImageDownloader.m b/SDWebImage/Core/SDWebImageDownloader.m index c836cc59..d7deee95 100644 --- a/SDWebImage/Core/SDWebImageDownloader.m +++ b/SDWebImage/Core/SDWebImageDownloader.m @@ -24,6 +24,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; @property (nonatomic, strong, nullable, readwrite) NSURL *url; @property (nonatomic, strong, nullable, readwrite) NSURLRequest *request; @property (nonatomic, strong, nullable, readwrite) NSURLResponse *response; +@property (nonatomic, strong, nullable, readwrite) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (nonatomic, weak, nullable, readwrite) id downloadOperationCancelToken; @property (nonatomic, weak, nullable) NSOperation *downloadOperation; @property (nonatomic, assign, getter=isCancelled) BOOL cancelled; @@ -519,18 +520,30 @@ didReceiveResponse:(NSURLResponse *)response self = [super init]; if (self) { _downloadOperation = downloadOperation; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:downloadOperation]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:downloadOperation]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidStop:) name:SDWebImageDownloadStopNotification object:downloadOperation]; } return self; } -- (void)downloadReceiveResponse:(NSNotification *)notification { +- (void)downloadDidReceiveResponse:(NSNotification *)notification { NSOperation *downloadOperation = notification.object; if (downloadOperation && downloadOperation == self.downloadOperation) { self.response = downloadOperation.response; } } +- (void)downloadDidStop:(NSNotification *)notification { + NSOperation *downloadOperation = notification.object; + if (downloadOperation && downloadOperation == self.downloadOperation) { + if ([downloadOperation respondsToSelector:@selector(metrics)]) { + if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) { + self.metrics = downloadOperation.metrics; + } + } + } +} + - (void)cancel { @synchronized (self) { if (self.isCancelled) { From d56636e15b2d106f275ae9ea0887a0da04b7024d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 30 Jan 2020 18:33:16 +0800 Subject: [PATCH 3/3] Update the Example and Test case about URLSessionMetrics, expose the API in UIVIew+WebCache to make it easy to write code (or user have to write NSStringFromClass) --- .../SDWebImage Demo/MasterViewController.m | 20 +++++++++--- SDWebImage/Core/UIView+WebCache.h | 7 +++++ Tests/Tests/SDWebImageDownloaderTests.m | 31 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index f131f55f..f3aecbea 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -113,10 +113,22 @@ } cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; - [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] - placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 - context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}]; + __weak SDAnimatedImageView *imageView = cell.customImageView; + [imageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] + placeholderImage:placeholderImage + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 + context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))} + progress:nil + completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { + SDWebImageCombinedOperation *operation = [imageView sd_imageLoadOperationForKey:imageView.sd_latestOperationKey]; + SDWebImageDownloadToken *token = operation.loaderOperation; + if (@available(iOS 10.0, *)) { + NSURLSessionTaskMetrics *metrics = token.metrics; + if (metrics) { + printf("Metrics: %s download in (%f) seconds\n", [imageURL.absoluteString cStringUsingEncoding:NSUTF8StringEncoding], metrics.taskInterval.duration); + } + } + }]; return cell; } diff --git a/SDWebImage/Core/UIView+WebCache.h b/SDWebImage/Core/UIView+WebCache.h index d0a7966f..c7e12a47 100644 --- a/SDWebImage/Core/UIView+WebCache.h +++ b/SDWebImage/Core/UIView+WebCache.h @@ -31,6 +31,13 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL; +/** + * Get the current image operation key. Operation key is used to identify the different queries for one view instance (like UIButton). + * See more about this in `SDWebImageContextSetImageOperationKey`. + * @note You can use method `UIView+WebCacheOperation` to invesigate different queries' operation. + */ +@property (nonatomic, strong, readonly, nullable) NSString *sd_latestOperationKey; + /** * The current image loading progress associated to the view. The unit count is the received size and excepted size of download. * The `totalUnitCount` and `completedUnitCount` will be reset to 0 after a new image loading start (change from current queue). And they will be set to `SDWebImageProgressUnitCountUnknown` if the progressBlock not been called but the image loading success to mark the progress finished (change from main queue). diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m index e5f0a2b5..467623ac 100644 --- a/Tests/Tests/SDWebImageDownloaderTests.m +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -642,6 +642,37 @@ }]; } +- (void)test26DownloadURLSessionMetrics { + XCTestExpectation *expectation1 = [self expectationWithDescription:@"Download URLSessionMetrics works"]; + + SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] init]; + + __block SDWebImageDownloadToken *token; + token = [downloader downloadImageWithURL:[NSURL URLWithString:kTestJPEGURL] completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) { + expect(error).beNil(); + if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, *)) { + NSURLSessionTaskMetrics *metrics = token.metrics; + expect(metrics).notTo.beNil(); + expect(metrics.redirectCount).equal(0); + expect(metrics.transactionMetrics.count).equal(1); + NSURLSessionTaskTransactionMetrics *metric = metrics.transactionMetrics.firstObject; + // Metrcis Test + expect(metric.fetchStartDate).notTo.beNil(); + expect(metric.connectStartDate).notTo.beNil(); + expect(metric.connectEndDate).notTo.beNil(); + expect(metric.networkProtocolName).equal(@"http/1.1"); + expect(metric.resourceFetchType).equal(NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad); + expect(metric.isProxyConnection).beFalsy(); + expect(metric.isReusedConnection).beFalsy(); + } + [expectation1 fulfill]; + }]; + + [self waitForExpectationsWithCommonTimeoutUsingHandler:^(NSError * _Nullable error) { + [downloader invalidateSessionAndCancel:YES]; + }]; +} + #pragma mark - SDWebImageLoader - (void)test30CustomImageLoaderWorks { XCTestExpectation *expectation = [self expectationWithDescription:@"Custom image not works"];