blob: 61a0b1f2aeebbd67f5cbc6ddf8fd85b2d95e1041 [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/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;
// 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
// 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 {
_delegate = delegate;
_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];
}
}
}
@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 {
// static
NSObject* CronetMetricsDelegate::task_metrics_map_lock_ =
[[NSObject alloc] init];
std::map<NSURLSessionTask*, std::unique_ptr<Metrics>>
CronetMetricsDelegate::task_metrics_map_;
std::unique_ptr<Metrics> CronetMetricsDelegate::MetricsForTask(
NSURLSessionTask* task) {
@synchronized(task_metrics_map_lock_) {
auto metrics_search = task_metrics_map_.find(task);
if (metrics_search == task_metrics_map_.end()) {
return nullptr;
}
std::unique_ptr<Metrics> metrics = std::move(metrics_search->second);
// Remove the entry to free memory.
task_metrics_map_.erase(metrics_search);
return metrics;
}
}
void CronetMetricsDelegate::OnStartNetRequest(NSURLSessionTask* task) {
if (@available(iOS 10, *)) {
@synchronized(task_metrics_map_lock_) {
if ([task state] == NSURLSessionTaskStateRunning) {
task_metrics_map_[task] = nullptr;
}
}
}
}
void CronetMetricsDelegate::OnStopNetRequest(std::unique_ptr<Metrics> metrics) {
if (@available(iOS 10, *)) {
@synchronized(task_metrics_map_lock_) {
auto metrics_search = task_metrics_map_.find(metrics->task);
if (metrics_search != task_metrics_map_.end())
metrics_search->second = std::move(metrics);
}
}
}
#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