blob: cce453adc1e96a75e755888a6eb1636528d8c4eb [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "components/cronet/ios/cronet_metrics.h"
#include <objc/runtime.h>
#include "base/lazy_instance.h"
#include "base/strings/sys_string_conversions.h"
@implementation CronetTransactionMetrics
@synthesize request = _request;
@synthesize response = _response;
@synthesize fetchStartDate = _fetchStartDate;
@synthesize domainLookupStartDate = _domainLookupStartDate;
@synthesize domainLookupEndDate = _domainLookupEndDate;
@synthesize connectStartDate = _connectStartDate;
@synthesize secureConnectionStartDate = _secureConnectionStartDate;
@synthesize secureConnectionEndDate = _secureConnectionEndDate;
@synthesize connectEndDate = _connectEndDate;
@synthesize requestStartDate = _requestStartDate;
@synthesize requestEndDate = _requestEndDate;
@synthesize responseStartDate = _responseStartDate;
@synthesize responseEndDate = _responseEndDate;
@synthesize networkProtocolName = _networkProtocolName;
@synthesize proxyConnection = _proxyConnection;
@synthesize reusedConnection = _reusedConnection;
@synthesize resourceFetchType = _resourceFetchType;
// The NSURLSessionTaskTransactionMetrics and NSURLSessionTaskMetrics classes
// are not supposed to be extended. Its default init method initialized an
// internal class, and therefore needs to be overridden to explicitly
// initialize (and return) an instance of this class.
// The |self = old_self| swap is necessary because [super init] must be
// assigned to self (or returned immediately), but in this case is returning
// a value of the wrong type.
- (instancetype)init {
id old_self = self;
self = [super init];
self = old_self;
return old_self;
}
- (NSString*)description {
return [NSString
stringWithFormat:
@""
"fetchStartDate: %@\n"
"domainLookupStartDate: %@\n"
"domainLookupEndDate: %@\n"
"connectStartDate: %@\n"
"secureConnectionStartDate: %@\n"
"secureConnectionEndDate: %@\n"
"connectEndDate: %@\n"
"requestStartDate: %@\n"
"requestEndDate: %@\n"
"responseStartDate: %@\n"
"responseEndDate: %@\n"
"networkProtocolName: %@\n"
"proxyConnection: %i\n"
"reusedConnection: %i\n"
"resourceFetchType: %lu\n",
[self fetchStartDate], [self domainLookupStartDate],
[self domainLookupEndDate], [self connectStartDate],
[self secureConnectionStartDate], [self secureConnectionEndDate],
[self connectEndDate], [self requestStartDate], [self requestEndDate],
[self responseStartDate], [self responseEndDate],
[self networkProtocolName], [self isProxyConnection],
[self isReusedConnection], (long)[self resourceFetchType]];
}
@end
@implementation CronetMetrics
@synthesize transactionMetrics = _transactionMetrics;
- (instancetype)init {
id old_self = self;
self = [super init];
self = old_self;
return old_self;
}
@end
namespace {
using Metrics = net::MetricsDelegate::Metrics;
// Synchronizes access to |gTaskMetricsMap|.
base::LazyInstance<base::Lock>::Leaky gTaskMetricsMapLock =
LAZY_INSTANCE_INITIALIZER;
// A global map that contains metrics information for pending URLSessionTasks.
// The map has to be "leaky"; otherwise, it will be destroyed on the main thread
// when the client app terminates. When the client app terminates, the network
// thread may still be finishing some work that requires access to the map.
base::LazyInstance<std::map<NSURLSessionTask*, std::unique_ptr<Metrics>>>::Leaky
gTaskMetricsMap = LAZY_INSTANCE_INITIALIZER;
// Helper method that converts the ticks data found in LoadTimingInfo to an
// NSDate value to be used in client-side data.
NSDate* TicksToDate(const net::LoadTimingInfo& reference,
const base::TimeTicks& ticks) {
if (ticks.is_null())
return nil;
base::Time ticks_since_1970 =
(reference.request_start_time + (ticks - reference.request_start));
return [NSDate dateWithTimeIntervalSince1970:ticks_since_1970.ToDoubleT()];
}
// Converts Metrics metrics data into CronetTransactionMetrics (which
// importantly implements the NSURLSessionTaskTransactionMetrics API)
CronetTransactionMetrics* NativeToIOSMetrics(Metrics& metrics)
NS_AVAILABLE_IOS(10.0) {
NSURLSessionTask* task = metrics.task;
const net::LoadTimingInfo& load_timing_info = metrics.load_timing_info;
const net::HttpResponseInfo& response_info = metrics.response_info;
CronetTransactionMetrics* transaction_metrics =
[[CronetTransactionMetrics alloc] init];
[transaction_metrics setRequest:[task currentRequest]];
[transaction_metrics setResponse:[task response]];
transaction_metrics.fetchStartDate =
[NSDate dateWithTimeIntervalSince1970:load_timing_info.request_start_time
.ToDoubleT()];
transaction_metrics.domainLookupStartDate =
TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_start);
transaction_metrics.domainLookupEndDate =
TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_end);
transaction_metrics.connectStartDate = TicksToDate(
load_timing_info, load_timing_info.connect_timing.connect_start);
transaction_metrics.secureConnectionStartDate =
TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_start);
transaction_metrics.secureConnectionEndDate =
TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_end);
transaction_metrics.connectEndDate = TicksToDate(
load_timing_info, load_timing_info.connect_timing.connect_end);
transaction_metrics.requestStartDate =
TicksToDate(load_timing_info, load_timing_info.send_start);
transaction_metrics.requestEndDate =
TicksToDate(load_timing_info, load_timing_info.send_end);
transaction_metrics.responseStartDate =
TicksToDate(load_timing_info, load_timing_info.receive_headers_end);
transaction_metrics.responseEndDate = [NSDate
dateWithTimeIntervalSince1970:metrics.response_end_time.ToDoubleT()];
transaction_metrics.networkProtocolName =
base::SysUTF8ToNSString(net::HttpResponseInfo::ConnectionInfoToString(
response_info.connection_info));
transaction_metrics.proxyConnection = !response_info.proxy_server.is_direct();
// If the connect timing information is null, then there was no connection
// establish - i.e., one was reused.
// The corrolary to this is that, if reusedConnection is YES, then
// domainLookupStartDate, domainLookupEndDate, connectStartDate,
// connectEndDate, secureConnectionStartDate, and secureConnectionEndDate are
// all meaningless.
transaction_metrics.reusedConnection =
load_timing_info.connect_timing.connect_start.is_null();
// Guess the resource fetch type based on some heuristics about what data is
// present.
if (response_info.was_cached) {
transaction_metrics.resourceFetchType =
NSURLSessionTaskMetricsResourceFetchTypeLocalCache;
} else if (!load_timing_info.push_start.is_null()) {
transaction_metrics.resourceFetchType =
NSURLSessionTaskMetricsResourceFetchTypeServerPush;
} else {
transaction_metrics.resourceFetchType =
NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad;
}
return transaction_metrics;
}
} // namespace
// A blank implementation of NSURLSessionDelegate that contains no methods.
// It is used as a substitution for a session delegate when the client
// either creates a session without a delegate or passes 'nil' as its value.
@interface BlankNSURLSessionDelegate : NSObject<NSURLSessionDelegate>
@end
@implementation BlankNSURLSessionDelegate : NSObject
@end
// In order for Cronet to use the iOS metrics collection API, it needs to
// replace the normal NSURLSession mechanism for calling into the delegate
// (so it can provide metrics from net/, instead of the empty metrics that iOS
// would provide otherwise.
// To this end, Cronet's startInternal method replaces the NSURLSession's
// sessionWithConfiguration method to inject a delegateProxy in between the
// client delegate and iOS code.
// This class represrents that delegateProxy. The important function is the
// didFinishCollectingMetrics callback, which when a request is being handled
// by Cronet, replaces the metrics collected by iOS with those connected by
// Cronet.
@interface URLSessionTaskDelegateProxy : NSProxy<NSURLSessionTaskDelegate>
- (instancetype)initWithDelegate:(id<NSURLSessionDelegate>)delegate;
@end
@implementation URLSessionTaskDelegateProxy {
id<NSURLSessionDelegate> _delegate;
BOOL _respondsToDidFinishCollectingMetrics;
}
// As this is a proxy delegate, it needs to be initialized with a real client
// delegate, to whom all of the method invocations will eventually get passed.
- (instancetype)initWithDelegate:(id<NSURLSessionDelegate>)delegate {
// If the client passed a real delegate, use it. Otherwise, create a blank
// delegate that will handle method invocations that are forwarded by this
// proxy implementation. It is incorrect to forward calls to a 'nil' object.
if (delegate) {
_delegate = delegate;
} else {
_delegate = [[BlankNSURLSessionDelegate alloc] init];
}
_respondsToDidFinishCollectingMetrics =
[_delegate respondsToSelector:@selector
(URLSession:task:didFinishCollectingMetrics:)];
return self;
}
// Any methods other than didFinishCollectingMetrics should be forwarded
// directly to the client delegate.
- (void)forwardInvocation:(NSInvocation*)invocation {
[invocation setTarget:_delegate];
[invocation invoke];
}
// And for that reason, URLSessionTaskDelegateProxy should act like it responds
// to any of the selectors that the client delegate does.
- (nullable NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
return [(id)_delegate methodSignatureForSelector:sel];
}
// didFinishCollectionMetrics ultimately calls into the corresponding method on
// the client delegate (if it exists), but first replaces the iOS-supplied
// metrics with metrics collected by Cronet (if they exist).
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics
NS_AVAILABLE_IOS(10.0) {
std::unique_ptr<Metrics> netMetrics =
cronet::CronetMetricsDelegate::MetricsForTask(task);
if (_respondsToDidFinishCollectingMetrics) {
if (netMetrics) {
CronetTransactionMetrics* cronetTransactionMetrics =
NativeToIOSMetrics(*netMetrics);
CronetMetrics* cronetMetrics = [[CronetMetrics alloc] init];
[cronetMetrics setTransactionMetrics:@[ cronetTransactionMetrics ]];
[(id<NSURLSessionTaskDelegate>)_delegate URLSession:session
task:task
didFinishCollectingMetrics:cronetMetrics];
} else {
// If there are no metrics is Cronet's task->metrics map, then Cronet is
// not handling this request, so just transparently pass iOS's collected
// metrics.
[(id<NSURLSessionTaskDelegate>)_delegate URLSession:session
task:task
didFinishCollectingMetrics:metrics];
}
}
}
- (BOOL)respondsToSelector:(SEL)aSelector {
// Regardless whether the underlying session delegate handles
// URLSession:task:didFinishCollectingMetrics: or not, always
// return 'YES' for that selector. Otherwise, the method may
// not be called, causing unbounded growth of |gTaskMetricsMap|.
if (aSelector == @selector(URLSession:task:didFinishCollectingMetrics:)) {
return YES;
}
return [_delegate respondsToSelector:aSelector];
}
@end
@implementation NSURLSession (Cronet)
+ (NSURLSession*)
hookSessionWithConfiguration:(NSURLSessionConfiguration*)configuration
delegate:(nullable id<NSURLSessionDelegate>)delegate
delegateQueue:(nullable NSOperationQueue*)queue {
URLSessionTaskDelegateProxy* delegate_proxy =
[[URLSessionTaskDelegateProxy alloc] initWithDelegate:delegate];
// Because the the method implementations are swapped, this is not a
// recursive call, and instead just forwards the call to the original
// sessionWithConfiguration method.
return [self hookSessionWithConfiguration:configuration
delegate:delegate_proxy
delegateQueue:queue];
}
@end
namespace cronet {
std::unique_ptr<Metrics> CronetMetricsDelegate::MetricsForTask(
NSURLSessionTask* task) {
base::AutoLock auto_lock(gTaskMetricsMapLock.Get());
auto metrics_search = gTaskMetricsMap.Get().find(task);
if (metrics_search == gTaskMetricsMap.Get().end()) {
return nullptr;
}
std::unique_ptr<Metrics> metrics = std::move(metrics_search->second);
// Remove the entry to free memory.
gTaskMetricsMap.Get().erase(metrics_search);
return metrics;
}
void CronetMetricsDelegate::OnStartNetRequest(NSURLSessionTask* task) {
if (@available(iOS 10, *)) {
base::AutoLock auto_lock(gTaskMetricsMapLock.Get());
if ([task state] == NSURLSessionTaskStateRunning) {
gTaskMetricsMap.Get()[task] = nullptr;
}
}
}
void CronetMetricsDelegate::OnStopNetRequest(std::unique_ptr<Metrics> metrics) {
if (@available(iOS 10, *)) {
base::AutoLock auto_lock(gTaskMetricsMapLock.Get());
auto metrics_search = gTaskMetricsMap.Get().find(metrics->task);
if (metrics_search != gTaskMetricsMap.Get().end())
metrics_search->second = std::move(metrics);
}
}
size_t CronetMetricsDelegate::GetMetricsMapSize() {
base::AutoLock auto_lock(gTaskMetricsMapLock.Get());
return gTaskMetricsMap.Get().size();
}
#pragma mark - Swizzle
void SwizzleSessionWithConfiguration() {
Class nsurlsession_class = object_getClass([NSURLSession class]);
SEL original_selector =
@selector(sessionWithConfiguration:delegate:delegateQueue:);
SEL swizzled_selector =
@selector(hookSessionWithConfiguration:delegate:delegateQueue:);
Method original_method =
class_getInstanceMethod(nsurlsession_class, original_selector);
Method swizzled_method =
class_getInstanceMethod(nsurlsession_class, swizzled_selector);
method_exchangeImplementations(original_method, swizzled_method);
}
} // namespace cronet