From c879a387e0ca4abcdff9e37eb0e826f7142342b1 Mon Sep 17 00:00:00 2001 From: Lukhnos Liu Date: Wed, 25 Mar 2020 10:21:39 -0700 Subject: [PATCH] Support metrics collection. (#187) (#188) This adds support to handle the `-URLSession:task:didFinishCollectingMetrics:` delegate method in the `NSURLSessionTaskDelegate protocol`. A new callback block type, `GTMSessionFetcherMetricsCollectionBlock`, is also added. This feature is enabled when compiled for iOS 10+, macOS 10.12+, Mac Catalyst 13.0+, tvOS 10.0+, or watchOS 3.0+. --- Source/GTMSessionFetcher.h | 10 ++ Source/GTMSessionFetcher.m | 19 +++ Source/GTMSessionFetcherService.h | 3 + Source/GTMSessionFetcherService.m | 12 ++ .../UnitTests/GTMSessionFetcherFetchingTest.m | 117 ++++++++++++++++++ .../UnitTests/GTMSessionFetcherServiceTest.m | 25 ++++ 6 files changed, 186 insertions(+) diff --git a/Source/GTMSessionFetcher.h b/Source/GTMSessionFetcher.h index 1937a327..0504aa75 100644 --- a/Source/GTMSessionFetcher.h +++ b/Source/GTMSessionFetcher.h @@ -560,6 +560,9 @@ typedef void (^GTMSessionFetcherRetryBlock)(BOOL suggestedWillRetry, NSError * GTM_NULLABLE_TYPE error, GTMSessionFetcherRetryResponse response); +API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) +typedef void (^GTMSessionFetcherMetricsCollectionBlock)(NSURLSessionTaskMetrics *metrics); + typedef void (^GTMSessionFetcherTestResponse)(NSHTTPURLResponse * GTM_NULLABLE_TYPE response, NSData * GTM_NULLABLE_TYPE data, NSError * GTM_NULLABLE_TYPE error); @@ -996,6 +999,13 @@ NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NS // See comments at the top of this file. @property(atomic, copy, GTM_NULLABLE) GTMSessionFetcherRetryBlock retryBlock; +// The optional block for collecting the metrics of the present session. +// +// This is called on the callback queue. +@property(atomic, copy, GTM_NULLABLE) + GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE( + ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)); + // Retry intervals must be strictly less than maxRetryInterval, else // they will be limited to maxRetryInterval and no further retries will // be attempted. Setting maxRetryInterval to 0.0 will reset it to the diff --git a/Source/GTMSessionFetcher.m b/Source/GTMSessionFetcher.m index 409b20ec..3dc64597 100644 --- a/Source/GTMSessionFetcher.m +++ b/Source/GTMSessionFetcher.m @@ -1795,6 +1795,9 @@ - (void)releaseCallbacks { self.retryBlock = nil; self.testBlock = nil; self.resumeDataBlock = nil; + if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) { + self.metricsCollectionBlock = nil; + } } - (void)forgetSessionIdentifierForFetcher { @@ -2853,6 +2856,21 @@ - (void)URLSession:(NSURLSession *)session }]; } +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics + API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) { + @synchronized(self) { + GTMSessionMonitorSynchronized(self); + GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock = _metricsCollectionBlock; + if (metricsCollectionBlock) { + [self invokeOnCallbackQueueUnlessStopped:^{ + metricsCollectionBlock(metrics); + }]; + } + } +} + #if TARGET_OS_IPHONE - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { GTM_LOG_SESSION_DELEGATE(@"%@ %p URLSessionDidFinishEventsForBackgroundURLSession:%@", @@ -3462,6 +3480,7 @@ + (GTM_NULLABLE GTMSessionFetcherSystemCompletionHandler)systemCompletionHandler sendProgressBlock = _sendProgressBlock, willCacheURLResponseBlock = _willCacheURLResponseBlock, retryBlock = _retryBlock, + metricsCollectionBlock = _metricsCollectionBlock, retryFactor = _retryFactor, allowedInsecureSchemes = _allowedInsecureSchemes, allowLocalhostRequest = _allowLocalhostRequest, diff --git a/Source/GTMSessionFetcherService.h b/Source/GTMSessionFetcherService.h index fb743cad..312abaa7 100644 --- a/Source/GTMSessionFetcherService.h +++ b/Source/GTMSessionFetcherService.h @@ -63,6 +63,9 @@ extern NSString *const kGTMSessionFetcherServiceSessionKey; @property(atomic, assign) NSTimeInterval maxRetryInterval; @property(atomic, assign) NSTimeInterval minRetryInterval; @property(atomic, copy, GTM_NULLABLE) GTM_NSDictionaryOf(NSString *, id) *properties; +@property(atomic, copy, GTM_NULLABLE) + GTMSessionFetcherMetricsCollectionBlock metricsCollectionBlock API_AVAILABLE( + ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)); #if GTM_BACKGROUND_TASK_FETCHING @property(atomic, assign) BOOL skipBackgroundTask; diff --git a/Source/GTMSessionFetcherService.m b/Source/GTMSessionFetcherService.m index bd44787b..f9942c01 100644 --- a/Source/GTMSessionFetcherService.m +++ b/Source/GTMSessionFetcherService.m @@ -121,6 +121,7 @@ @implementation GTMSessionFetcherService { retryBlock = _retryBlock, maxRetryInterval = _maxRetryInterval, minRetryInterval = _minRetryInterval, + metricsCollectionBlock = _metricsCollectionBlock, properties = _properties, unusedSessionTimeout = _unusedSessionTimeout, testBlock = _testBlock; @@ -186,6 +187,9 @@ - (id)fetcherWithRequest:(NSURLRequest *)request fetcher.retryBlock = self.retryBlock; fetcher.maxRetryInterval = self.maxRetryInterval; fetcher.minRetryInterval = self.minRetryInterval; + if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) { + fetcher.metricsCollectionBlock = self.metricsCollectionBlock; + } fetcher.properties = self.properties; fetcher.service = self; if (self.cookieStorageMethod >= 0) { @@ -1281,6 +1285,14 @@ - (void)URLSession:(NSURLSession *)session didCompleteWithError:error]; } +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics + API_AVAILABLE(ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) { + id fetcher = [self fetcherForTask:task]; + [fetcher URLSession:session task:task didFinishCollectingMetrics:metrics]; +} + // NSURLSessionDataDelegate protocol methods. - (void)URLSession:(NSURLSession *)session diff --git a/Source/UnitTests/GTMSessionFetcherFetchingTest.m b/Source/UnitTests/GTMSessionFetcherFetchingTest.m index 3e618d03..4f1fcdb4 100644 --- a/Source/UnitTests/GTMSessionFetcherFetchingTest.m +++ b/Source/UnitTests/GTMSessionFetcherFetchingTest.m @@ -194,6 +194,9 @@ - (void)assertCallbacksReleasedForFetcher:(GTMSessionFetcher *)fetcher { XCTAssertNil(fetcher.downloadProgressBlock); XCTAssertNil(fetcher.willCacheURLResponseBlock); XCTAssertNil(fetcher.retryBlock); + if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, watchOS 3.0, *)) { + XCTAssertNil(fetcher.metricsCollectionBlock); + } XCTAssertNil(fetcher.testBlock); if ([fetcher isKindOfClass:[GTMSessionUploadFetcher class]]) { @@ -1849,6 +1852,120 @@ - (void)testInsecureRequests_WithoutFetcherService { [self testInsecureRequests]; } +- (void)testCollectingMetrics_WithSuccessfulFetch API_AVAILABLE(ios(10.0), macosx(10.12), + tvos(10.0), watchos(3.0)) { + if (!_isServerRunning) return; + + NSString *localURLString = [self localURLStringToTestFileName:kGTMGettysburgFileName]; + GTMSessionFetcher *fetcher = [self fetcherWithURLString:localURLString]; + __block NSURLSessionTaskMetrics *collectedMetrics = nil; + + fetcher.metricsCollectionBlock = ^(NSURLSessionTaskMetrics *_Nonnull metrics) { + collectedMetrics = metrics; + }; + + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + [self assertSuccessfulGettysburgFetchWithFetcher:fetcher data:data error:error]; + }]; + XCTAssertTrue([fetcher waitForCompletionWithTimeout:_timeoutInterval], @"timed out"); + [self assertCallbacksReleasedForFetcher:fetcher]; + + XCTAssertNotNil(collectedMetrics); + XCTAssertEqual(collectedMetrics.transactionMetrics.count, 1); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].fetchStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].connectStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].connectEndDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].requestStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].requestEndDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].responseStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].responseEndDate); +} + +- (void)testCollectingMetrics_WithSuccessfulFetch_WithoutFetcherService API_AVAILABLE( + ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) { + _fetcherService = nil; + [self testCollectingMetrics_WithSuccessfulFetch]; +} + +- (void)testCollectingMetrics_WithWrongFetch_FaildToConnect API_AVAILABLE(ios(10.0), macosx(10.12), + tvos(10.0), + watchos(3.0)) { + if (!_isServerRunning) return; + + // Fetch a live, invalid URL + NSString *badURLString = @"http://localhost:86/"; + + GTMSessionFetcher *fetcher = [self fetcherWithURLString:badURLString]; + + __block NSURLSessionTaskMetrics *collectedMetrics = nil; + fetcher.metricsCollectionBlock = ^(NSURLSessionTaskMetrics *_Nonnull metrics) { + collectedMetrics = metrics; + }; + + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + XCTAssertNotNil(error); + }]; + XCTAssertTrue([fetcher waitForCompletionWithTimeout:_timeoutInterval], @"timed out"); + [self assertCallbacksReleasedForFetcher:fetcher]; + + XCTAssertNotNil(collectedMetrics); + XCTAssertEqual(collectedMetrics.transactionMetrics.count, 1); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].fetchStartDate); + + // Connetion not established, and therefore the following metrics do not exist. + XCTAssertNil(collectedMetrics.transactionMetrics[0].connectStartDate); + XCTAssertNil(collectedMetrics.transactionMetrics[0].connectEndDate); + XCTAssertNil(collectedMetrics.transactionMetrics[0].requestStartDate); + XCTAssertNil(collectedMetrics.transactionMetrics[0].requestEndDate); + XCTAssertNil(collectedMetrics.transactionMetrics[0].responseStartDate); + XCTAssertNil(collectedMetrics.transactionMetrics[0].responseEndDate); +} + +- (void)testCollectingMetrics_WithWrongFetch_FaildToConnect_WithoutFetcherService API_AVAILABLE( + ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) { + _fetcherService = nil; + [self testCollectingMetrics_WithWrongFetch_FaildToConnect]; +} + +- (void)testCollectingMetrics_WithWrongFetch_BadStatusCode API_AVAILABLE(ios(10.0), macosx(10.12), + tvos(10.0), watchos(3.0)) { + if (!_isServerRunning) return; + + NSString *statusURLString = [self localURLStringToTestFileName:kGTMGettysburgFileName + parameters:@{@"status" : @"400"}]; + + GTMSessionFetcher *fetcher = [self fetcherWithURLString:statusURLString]; + + __block NSURLSessionTaskMetrics *collectedMetrics = nil; + fetcher.metricsCollectionBlock = ^(NSURLSessionTaskMetrics *_Nonnull metrics) { + collectedMetrics = metrics; + }; + + [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { + XCTAssertNotNil(error); + }]; + XCTAssertTrue([fetcher waitForCompletionWithTimeout:_timeoutInterval], @"timed out"); + [self assertCallbacksReleasedForFetcher:fetcher]; + + XCTAssertNotNil(collectedMetrics); + XCTAssertEqual(collectedMetrics.transactionMetrics.count, 1); + + // A 400 HTTP response is still a complete response, and therefore these metrics exist. + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].fetchStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].connectStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].connectEndDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].requestStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].requestEndDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].responseStartDate); + XCTAssertNotNil(collectedMetrics.transactionMetrics[0].responseEndDate); +} + +- (void)testCollectingMetrics_WithWrongFetch_BadStatusCode_WithoutFetcherService API_AVAILABLE( + ios(10.0), macosx(10.12), tvos(10.0), watchos(3.0)) { + _fetcherService = nil; + [self testCollectingMetrics_WithWrongFetch_BadStatusCode]; +} + #pragma mark - TestBlock Tests - (void)testFetcherTestBlock { diff --git a/Source/UnitTests/GTMSessionFetcherServiceTest.m b/Source/UnitTests/GTMSessionFetcherServiceTest.m index b1ced203..32d60322 100644 --- a/Source/UnitTests/GTMSessionFetcherServiceTest.m +++ b/Source/UnitTests/GTMSessionFetcherServiceTest.m @@ -820,4 +820,29 @@ - (void)testDelegateDispatcherForFetcher { [session invalidateAndCancel]; } +- (void)testFetcherUsingMetricsCollectionBlockFromFetcherService API_AVAILABLE(ios(10.0), + macosx(10.12), + tvos(10.0), + watchos(3.0)) { + if (!_isServerRunning) return; + + __block NSURLSessionTaskMetrics *collectedMetrics = nil; + + GTMSessionFetcherService *service = [[GTMSessionFetcherService alloc] init]; + service.metricsCollectionBlock = ^(NSURLSessionTaskMetrics *_Nonnull metrics) { + collectedMetrics = metrics; + }; + + NSURL *fetchURL = [_testServer localURLForFile:kValidFileName]; + GTMSessionFetcher *fetcher = [service fetcherWithURL:fetchURL]; + [fetcher beginFetchWithCompletionHandler:^(NSData *fetchData, NSError *fetchError) { + XCTAssertNotNil(fetchData); + XCTAssertNil(fetchError); + }]; + + [service waitForCompletionOfAllFetchersWithTimeout:10]; + + XCTAssertNotNil(collectedMetrics); +} + @end