1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "components/cronet/ios/cronet_metrics.h" 6 7#include <objc/runtime.h> 8 9#include "base/lazy_instance.h" 10#include "base/strings/sys_string_conversions.h" 11 12@implementation CronetTransactionMetrics 13 14@synthesize request = _request; 15@synthesize response = _response; 16 17@synthesize fetchStartDate = _fetchStartDate; 18@synthesize domainLookupStartDate = _domainLookupStartDate; 19@synthesize domainLookupEndDate = _domainLookupEndDate; 20@synthesize connectStartDate = _connectStartDate; 21@synthesize secureConnectionStartDate = _secureConnectionStartDate; 22@synthesize secureConnectionEndDate = _secureConnectionEndDate; 23@synthesize connectEndDate = _connectEndDate; 24@synthesize requestStartDate = _requestStartDate; 25@synthesize requestEndDate = _requestEndDate; 26@synthesize responseStartDate = _responseStartDate; 27@synthesize responseEndDate = _responseEndDate; 28 29@synthesize networkProtocolName = _networkProtocolName; 30@synthesize proxyConnection = _proxyConnection; 31@synthesize reusedConnection = _reusedConnection; 32@synthesize resourceFetchType = _resourceFetchType; 33 34// The NSURLSessionTaskTransactionMetrics and NSURLSessionTaskMetrics classes 35// are not supposed to be extended. Its default init method initialized an 36// internal class, and therefore needs to be overridden to explicitly 37// initialize (and return) an instance of this class. 38// The |self = old_self| swap is necessary because [super init] must be 39// assigned to self (or returned immediately), but in this case is returning 40// a value of the wrong type. 41 42- (instancetype)init { 43 id old_self = self; 44 self = [super init]; 45 self = old_self; 46 return old_self; 47} 48 49- (NSString*)description { 50 return [NSString 51 stringWithFormat: 52 @"" 53 "fetchStartDate: %@\n" 54 "domainLookupStartDate: %@\n" 55 "domainLookupEndDate: %@\n" 56 "connectStartDate: %@\n" 57 "secureConnectionStartDate: %@\n" 58 "secureConnectionEndDate: %@\n" 59 "connectEndDate: %@\n" 60 "requestStartDate: %@\n" 61 "requestEndDate: %@\n" 62 "responseStartDate: %@\n" 63 "responseEndDate: %@\n" 64 "networkProtocolName: %@\n" 65 "proxyConnection: %i\n" 66 "reusedConnection: %i\n" 67 "resourceFetchType: %lu\n", 68 [self fetchStartDate], [self domainLookupStartDate], 69 [self domainLookupEndDate], [self connectStartDate], 70 [self secureConnectionStartDate], [self secureConnectionEndDate], 71 [self connectEndDate], [self requestStartDate], [self requestEndDate], 72 [self responseStartDate], [self responseEndDate], 73 [self networkProtocolName], [self isProxyConnection], 74 [self isReusedConnection], (long)[self resourceFetchType]]; 75} 76 77@end 78 79@implementation CronetMetrics 80 81@synthesize transactionMetrics = _transactionMetrics; 82 83- (instancetype)init { 84 id old_self = self; 85 self = [super init]; 86 self = old_self; 87 return old_self; 88} 89 90@end 91 92namespace { 93 94using Metrics = net::MetricsDelegate::Metrics; 95 96// Synchronizes access to |gTaskMetricsMap|. 97base::LazyInstance<base::Lock>::Leaky gTaskMetricsMapLock = 98 LAZY_INSTANCE_INITIALIZER; 99 100// A global map that contains metrics information for pending URLSessionTasks. 101// The map has to be "leaky"; otherwise, it will be destroyed on the main thread 102// when the client app terminates. When the client app terminates, the network 103// thread may still be finishing some work that requires access to the map. 104base::LazyInstance<std::map<NSURLSessionTask*, std::unique_ptr<Metrics>>>::Leaky 105 gTaskMetricsMap = LAZY_INSTANCE_INITIALIZER; 106 107// Helper method that converts the ticks data found in LoadTimingInfo to an 108// NSDate value to be used in client-side data. 109NSDate* TicksToDate(const net::LoadTimingInfo& reference, 110 const base::TimeTicks& ticks) { 111 if (ticks.is_null()) 112 return nil; 113 base::Time ticks_since_1970 = 114 (reference.request_start_time + (ticks - reference.request_start)); 115 return [NSDate dateWithTimeIntervalSince1970:ticks_since_1970.ToDoubleT()]; 116} 117 118// Converts Metrics metrics data into CronetTransactionMetrics (which 119// importantly implements the NSURLSessionTaskTransactionMetrics API) 120CronetTransactionMetrics* NativeToIOSMetrics(Metrics& metrics) 121 NS_AVAILABLE_IOS(10.0) { 122 NSURLSessionTask* task = metrics.task; 123 const net::LoadTimingInfo& load_timing_info = metrics.load_timing_info; 124 const net::HttpResponseInfo& response_info = metrics.response_info; 125 126 CronetTransactionMetrics* transaction_metrics = 127 [[CronetTransactionMetrics alloc] init]; 128 129 [transaction_metrics setRequest:[task currentRequest]]; 130 [transaction_metrics setResponse:[task response]]; 131 132 transaction_metrics.fetchStartDate = 133 [NSDate dateWithTimeIntervalSince1970:load_timing_info.request_start_time 134 .ToDoubleT()]; 135 136 transaction_metrics.domainLookupStartDate = 137 TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_start); 138 transaction_metrics.domainLookupEndDate = 139 TicksToDate(load_timing_info, load_timing_info.connect_timing.dns_end); 140 141 transaction_metrics.connectStartDate = TicksToDate( 142 load_timing_info, load_timing_info.connect_timing.connect_start); 143 transaction_metrics.secureConnectionStartDate = 144 TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_start); 145 transaction_metrics.secureConnectionEndDate = 146 TicksToDate(load_timing_info, load_timing_info.connect_timing.ssl_end); 147 transaction_metrics.connectEndDate = TicksToDate( 148 load_timing_info, load_timing_info.connect_timing.connect_end); 149 150 transaction_metrics.requestStartDate = 151 TicksToDate(load_timing_info, load_timing_info.send_start); 152 transaction_metrics.requestEndDate = 153 TicksToDate(load_timing_info, load_timing_info.send_end); 154 transaction_metrics.responseStartDate = 155 TicksToDate(load_timing_info, load_timing_info.receive_headers_end); 156 transaction_metrics.responseEndDate = [NSDate 157 dateWithTimeIntervalSince1970:metrics.response_end_time.ToDoubleT()]; 158 159 transaction_metrics.networkProtocolName = 160 base::SysUTF8ToNSString(net::HttpResponseInfo::ConnectionInfoToString( 161 response_info.connection_info)); 162 transaction_metrics.proxyConnection = !response_info.proxy_server.is_direct(); 163 164 // If the connect timing information is null, then there was no connection 165 // establish - i.e., one was reused. 166 // The corrolary to this is that, if reusedConnection is YES, then 167 // domainLookupStartDate, domainLookupEndDate, connectStartDate, 168 // connectEndDate, secureConnectionStartDate, and secureConnectionEndDate are 169 // all meaningless. 170 transaction_metrics.reusedConnection = 171 load_timing_info.connect_timing.connect_start.is_null(); 172 173 // Guess the resource fetch type based on some heuristics about what data is 174 // present. 175 if (response_info.was_cached) { 176 transaction_metrics.resourceFetchType = 177 NSURLSessionTaskMetricsResourceFetchTypeLocalCache; 178 } else if (!load_timing_info.push_start.is_null()) { 179 transaction_metrics.resourceFetchType = 180 NSURLSessionTaskMetricsResourceFetchTypeServerPush; 181 } else { 182 transaction_metrics.resourceFetchType = 183 NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad; 184 } 185 186 return transaction_metrics; 187} 188 189} // namespace 190 191// A blank implementation of NSURLSessionDelegate that contains no methods. 192// It is used as a substitution for a session delegate when the client 193// either creates a session without a delegate or passes 'nil' as its value. 194@interface BlankNSURLSessionDelegate : NSObject<NSURLSessionDelegate> 195@end 196 197@implementation BlankNSURLSessionDelegate : NSObject 198@end 199 200// In order for Cronet to use the iOS metrics collection API, it needs to 201// replace the normal NSURLSession mechanism for calling into the delegate 202// (so it can provide metrics from net/, instead of the empty metrics that iOS 203// would provide otherwise. 204// To this end, Cronet's startInternal method replaces the NSURLSession's 205// sessionWithConfiguration method to inject a delegateProxy in between the 206// client delegate and iOS code. 207// This class represrents that delegateProxy. The important function is the 208// didFinishCollectingMetrics callback, which when a request is being handled 209// by Cronet, replaces the metrics collected by iOS with those connected by 210// Cronet. 211@interface URLSessionTaskDelegateProxy : NSProxy<NSURLSessionTaskDelegate> 212- (instancetype)initWithDelegate:(id<NSURLSessionDelegate>)delegate; 213@end 214 215@implementation URLSessionTaskDelegateProxy { 216 id<NSURLSessionDelegate> _delegate; 217 BOOL _respondsToDidFinishCollectingMetrics; 218} 219 220// As this is a proxy delegate, it needs to be initialized with a real client 221// delegate, to whom all of the method invocations will eventually get passed. 222- (instancetype)initWithDelegate:(id<NSURLSessionDelegate>)delegate { 223 // If the client passed a real delegate, use it. Otherwise, create a blank 224 // delegate that will handle method invocations that are forwarded by this 225 // proxy implementation. It is incorrect to forward calls to a 'nil' object. 226 if (delegate) { 227 _delegate = delegate; 228 } else { 229 _delegate = [[BlankNSURLSessionDelegate alloc] init]; 230 } 231 232 _respondsToDidFinishCollectingMetrics = 233 [_delegate respondsToSelector:@selector 234 (URLSession:task:didFinishCollectingMetrics:)]; 235 return self; 236} 237 238// Any methods other than didFinishCollectingMetrics should be forwarded 239// directly to the client delegate. 240- (void)forwardInvocation:(NSInvocation*)invocation { 241 [invocation setTarget:_delegate]; 242 [invocation invoke]; 243} 244 245// And for that reason, URLSessionTaskDelegateProxy should act like it responds 246// to any of the selectors that the client delegate does. 247- (nullable NSMethodSignature*)methodSignatureForSelector:(SEL)sel { 248 return [(id)_delegate methodSignatureForSelector:sel]; 249} 250 251// didFinishCollectionMetrics ultimately calls into the corresponding method on 252// the client delegate (if it exists), but first replaces the iOS-supplied 253// metrics with metrics collected by Cronet (if they exist). 254- (void)URLSession:(NSURLSession*)session 255 task:(NSURLSessionTask*)task 256 didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics 257 NS_AVAILABLE_IOS(10.0) { 258 std::unique_ptr<Metrics> netMetrics = 259 cronet::CronetMetricsDelegate::MetricsForTask(task); 260 261 if (_respondsToDidFinishCollectingMetrics) { 262 if (netMetrics) { 263 CronetTransactionMetrics* cronetTransactionMetrics = 264 NativeToIOSMetrics(*netMetrics); 265 266 CronetMetrics* cronetMetrics = [[CronetMetrics alloc] init]; 267 [cronetMetrics setTransactionMetrics:@[ cronetTransactionMetrics ]]; 268 269 [(id<NSURLSessionTaskDelegate>)_delegate URLSession:session 270 task:task 271 didFinishCollectingMetrics:cronetMetrics]; 272 } else { 273 // If there are no metrics is Cronet's task->metrics map, then Cronet is 274 // not handling this request, so just transparently pass iOS's collected 275 // metrics. 276 [(id<NSURLSessionTaskDelegate>)_delegate URLSession:session 277 task:task 278 didFinishCollectingMetrics:metrics]; 279 } 280 } 281} 282 283- (BOOL)respondsToSelector:(SEL)aSelector { 284 // Regardless whether the underlying session delegate handles 285 // URLSession:task:didFinishCollectingMetrics: or not, always 286 // return 'YES' for that selector. Otherwise, the method may 287 // not be called, causing unbounded growth of |gTaskMetricsMap|. 288 if (aSelector == @selector(URLSession:task:didFinishCollectingMetrics:)) { 289 return YES; 290 } 291 return [_delegate respondsToSelector:aSelector]; 292} 293 294@end 295 296@implementation NSURLSession (Cronet) 297 298+ (NSURLSession*) 299hookSessionWithConfiguration:(NSURLSessionConfiguration*)configuration 300 delegate:(nullable id<NSURLSessionDelegate>)delegate 301 delegateQueue:(nullable NSOperationQueue*)queue { 302 URLSessionTaskDelegateProxy* delegate_proxy = 303 [[URLSessionTaskDelegateProxy alloc] initWithDelegate:delegate]; 304 // Because the the method implementations are swapped, this is not a 305 // recursive call, and instead just forwards the call to the original 306 // sessionWithConfiguration method. 307 return [self hookSessionWithConfiguration:configuration 308 delegate:delegate_proxy 309 delegateQueue:queue]; 310} 311 312@end 313 314namespace cronet { 315 316std::unique_ptr<Metrics> CronetMetricsDelegate::MetricsForTask( 317 NSURLSessionTask* task) { 318 base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); 319 auto metrics_search = gTaskMetricsMap.Get().find(task); 320 if (metrics_search == gTaskMetricsMap.Get().end()) { 321 return nullptr; 322 } 323 324 std::unique_ptr<Metrics> metrics = std::move(metrics_search->second); 325 // Remove the entry to free memory. 326 gTaskMetricsMap.Get().erase(metrics_search); 327 328 return metrics; 329} 330 331void CronetMetricsDelegate::OnStartNetRequest(NSURLSessionTask* task) { 332 base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); 333 if ([task state] == NSURLSessionTaskStateRunning) { 334 gTaskMetricsMap.Get()[task] = nullptr; 335 } 336} 337 338void CronetMetricsDelegate::OnStopNetRequest(std::unique_ptr<Metrics> metrics) { 339 base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); 340 auto metrics_search = gTaskMetricsMap.Get().find(metrics->task); 341 if (metrics_search != gTaskMetricsMap.Get().end()) 342 metrics_search->second = std::move(metrics); 343} 344 345size_t CronetMetricsDelegate::GetMetricsMapSize() { 346 base::AutoLock auto_lock(gTaskMetricsMapLock.Get()); 347 return gTaskMetricsMap.Get().size(); 348} 349 350#pragma mark - Swizzle 351 352void SwizzleSessionWithConfiguration() { 353 Class nsurlsession_class = object_getClass([NSURLSession class]); 354 355 SEL original_selector = 356 @selector(sessionWithConfiguration:delegate:delegateQueue:); 357 SEL swizzled_selector = 358 @selector(hookSessionWithConfiguration:delegate:delegateQueue:); 359 360 Method original_method = 361 class_getInstanceMethod(nsurlsession_class, original_selector); 362 Method swizzled_method = 363 class_getInstanceMethod(nsurlsession_class, swizzled_selector); 364 365 method_exchangeImplementations(original_method, swizzled_method); 366} 367 368} // namespace cronet 369