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