// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #import "FirebasePerformance/Sources/FPRProtoUtils.h" #ifdef TARGET_HAS_MOBILE_CONNECTIVITY #import #import #endif #import #import "FirebasePerformance/Sources/Common/FPRConstants.h" #import "FirebasePerformance/Sources/FIRPerformance+Internal.h" #import "FirebasePerformance/Sources/FPRDataUtils.h" #import "FirebasePerformance/Sources/Public/FIRPerformance.h" #import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h" #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h" #import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeData.h" #import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeData.h" #define BYTES_TO_KB(x) (x / 1024) static GPBStringInt64Dictionary *FPRGetProtoCounterForDictionary( NSDictionary *dictionary); static FPRMSGNetworkRequestMetric_HttpMethod FPRHTTPMethodForString(NSString *methodString); static FPRMSGNetworkConnectionInfo_NetworkType FPRNetworkConnectionInfoNetworkType(void); #ifdef TARGET_HAS_MOBILE_CONNECTIVITY static FPRMSGNetworkConnectionInfo_MobileSubtype FPRCellularNetworkType(void); #endif NSArray *FPRMakeFirstSessionVerbose(NSArray *sessions); #pragma mark - Public methods FPRMSGPerfMetric *FPRGetPerfMetricMessage(NSString *appID) { FPRMSGPerfMetric *perfMetricMessage = [FPRMSGPerfMetric message]; perfMetricMessage.applicationInfo = FPRGetApplicationInfoMessage(); perfMetricMessage.applicationInfo.googleAppId = appID; return perfMetricMessage; } FPRMSGApplicationInfo *FPRGetApplicationInfoMessage() { FPRMSGApplicationInfo *appInfoMessage = [FPRMSGApplicationInfo message]; FPRMSGIosApplicationInfo *iosAppInfo = [FPRMSGIosApplicationInfo message]; NSBundle *mainBundle = [NSBundle mainBundle]; iosAppInfo.bundleShortVersion = [mainBundle infoDictionary][@"CFBundleShortVersionString"]; iosAppInfo.sdkVersion = [NSString stringWithUTF8String:kFPRSDKVersion]; iosAppInfo.networkConnectionInfo.networkType = FPRNetworkConnectionInfoNetworkType(); #ifdef TARGET_HAS_MOBILE_CONNECTIVITY CTTelephonyNetworkInfo *networkInfo = FPRNetworkInfo(); CTCarrier *provider = networkInfo.subscriberCellularProvider; NSString *mccMnc = FPRValidatedMccMnc(provider.mobileCountryCode, provider.mobileNetworkCode); if (mccMnc) { iosAppInfo.mccMnc = mccMnc; } if (iosAppInfo.networkConnectionInfo.networkType == FPRMSGNetworkConnectionInfo_NetworkType_Mobile) { iosAppInfo.networkConnectionInfo.mobileSubtype = FPRCellularNetworkType(); } #endif appInfoMessage.iosAppInfo = iosAppInfo; appInfoMessage.customAttributes = [[FIRPerformance sharedInstance].attributes mutableCopy]; return appInfoMessage; } FPRMSGTraceMetric *FPRGetTraceMetric(FIRTrace *trace) { if (trace == nil) { return nil; } FPRMSGTraceMetric *traceMetric = [FPRMSGTraceMetric message]; traceMetric.name = trace.name; // Set if the trace is an internally created trace. traceMetric.isAuto = trace.isInternal; // Convert the trace duration from seconds to microseconds. traceMetric.durationUs = trace.totalTraceTimeInterval * USEC_PER_SEC; // Convert the start time from seconds to microseconds. traceMetric.clientStartTimeUs = trace.startTimeSinceEpoch * USEC_PER_SEC; traceMetric.counters = FPRGetProtoCounterForDictionary(trace.counters); NSMutableArray *subtraces = [[NSMutableArray alloc] init]; [trace.stages enumerateObjectsUsingBlock:^(FIRTrace *_Nonnull stage, NSUInteger idx, BOOL *_Nonnull stop) { [subtraces addObject:FPRGetTraceMetric(stage)]; }]; traceMetric.subtracesArray = subtraces; traceMetric.customAttributes = [trace.attributes mutableCopy]; // Fillin session details traceMetric.perfSessionsArray = [[NSMutableArray alloc] init]; NSArray *orderedSessions = FPRMakeFirstSessionVerbose(trace.sessions); [orderedSessions enumerateObjectsUsingBlock:^(FPRSessionDetails *_Nonnull session, NSUInteger index, BOOL *_Nonnull stop) { FPRMSGPerfSession *perfSession = [FPRMSGPerfSession message]; perfSession.sessionId = session.sessionId; perfSession.sessionVerbosityArray = [GPBEnumArray array]; if ((session.options & FPRSessionOptionsEvents) || (session.options & FPRSessionOptionsGauges)) { [perfSession.sessionVerbosityArray addValue:FPRMSGSessionVerbosity_GaugesAndSystemEvents]; } [traceMetric.perfSessionsArray addObject:perfSession]; }]; return traceMetric; } FPRMSGNetworkRequestMetric *FPRGetNetworkRequestMetric(FPRNetworkTrace *trace) { if (trace == nil) { return nil; } // If there is no valid status code, do not send the event to backend. if (!trace.hasValidResponseCode) { return nil; } FPRMSGNetworkRequestMetric *networkMetric = [FPRMSGNetworkRequestMetric message]; networkMetric.URL = trace.trimmedURLString; networkMetric.HTTPMethod = FPRHTTPMethodForString(trace.URLRequest.HTTPMethod); // Convert the start time from seconds to microseconds. networkMetric.clientStartTimeUs = trace.startTimeSinceEpoch * USEC_PER_SEC; networkMetric.requestPayloadBytes = trace.requestSize; networkMetric.responsePayloadBytes = trace.responseSize; networkMetric.HTTPResponseCode = trace.responseCode; networkMetric.responseContentType = trace.responseContentType; if (trace.responseError) { networkMetric.networkClientErrorReason = FPRMSGNetworkRequestMetric_NetworkClientErrorReason_GenericClientError; } NSTimeInterval requestTimeUs = USEC_PER_SEC * [trace timeIntervalBetweenCheckpointState:FPRNetworkTraceCheckpointStateInitiated andState:FPRNetworkTraceCheckpointStateRequestCompleted]; if (requestTimeUs > 0) { networkMetric.timeToRequestCompletedUs = requestTimeUs; } NSTimeInterval responseIntiationTimeUs = USEC_PER_SEC * [trace timeIntervalBetweenCheckpointState:FPRNetworkTraceCheckpointStateInitiated andState:FPRNetworkTraceCheckpointStateResponseReceived]; if (responseIntiationTimeUs > 0) { networkMetric.timeToResponseInitiatedUs = responseIntiationTimeUs; } NSTimeInterval responseCompletedUs = USEC_PER_SEC * [trace timeIntervalBetweenCheckpointState:FPRNetworkTraceCheckpointStateInitiated andState:FPRNetworkTraceCheckpointStateResponseCompleted]; if (responseCompletedUs > 0) { networkMetric.timeToResponseCompletedUs = responseCompletedUs; } networkMetric.customAttributes = [trace.attributes mutableCopy]; // Fillin session details NSArray *orderedSessions = FPRMakeFirstSessionVerbose(trace.sessions); networkMetric.perfSessionsArray = [[NSMutableArray alloc] init]; [orderedSessions enumerateObjectsUsingBlock:^(FPRSessionDetails *_Nonnull session, NSUInteger index, BOOL *_Nonnull stop) { FPRMSGPerfSession *perfSession = [FPRMSGPerfSession message]; perfSession.sessionId = session.sessionId; perfSession.sessionVerbosityArray = [GPBEnumArray array]; if ((session.options & FPRSessionOptionsEvents) || (session.options & FPRSessionOptionsGauges)) { [perfSession.sessionVerbosityArray addValue:FPRMSGSessionVerbosity_GaugesAndSystemEvents]; } [networkMetric.perfSessionsArray addObject:perfSession]; }]; return networkMetric; } FPRMSGGaugeMetric *FPRGetGaugeMetric(NSArray *gaugeData, NSString *sessionId) { if (gaugeData == nil || gaugeData.count == 0) { return nil; } if (sessionId == nil || sessionId.length == 0) { return nil; } FPRMSGGaugeMetric *gaugeMetric = [FPRMSGGaugeMetric message]; gaugeMetric.sessionId = sessionId; NSMutableArray *cpuReadings = [[NSMutableArray alloc] init]; NSMutableArray *memoryReadings = [[NSMutableArray alloc] init]; [gaugeData enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { if ([obj isKindOfClass:[FPRCPUGaugeData class]]) { FPRCPUGaugeData *gaugeData = (FPRCPUGaugeData *)obj; FPRMSGCpuMetricReading *cpuReading = [FPRMSGCpuMetricReading message]; cpuReading.clientTimeUs = gaugeData.collectionTime.timeIntervalSince1970 * USEC_PER_SEC; cpuReading.systemTimeUs = gaugeData.systemTime; cpuReading.userTimeUs = gaugeData.userTime; [cpuReadings addObject:cpuReading]; } if ([obj isKindOfClass:[FPRMemoryGaugeData class]]) { FPRMemoryGaugeData *gaugeData = (FPRMemoryGaugeData *)obj; FPRMSGIosMemoryReading *memoryReading = [FPRMSGIosMemoryReading message]; memoryReading.clientTimeUs = gaugeData.collectionTime.timeIntervalSince1970 * USEC_PER_SEC; memoryReading.usedAppHeapMemoryKb = (int32_t)BYTES_TO_KB(gaugeData.heapUsed); memoryReading.freeAppHeapMemoryKb = (int32_t)BYTES_TO_KB(gaugeData.heapAvailable); [memoryReadings addObject:memoryReading]; } }]; gaugeMetric.cpuMetricReadingsArray = cpuReadings; gaugeMetric.iosMemoryReadingsArray = memoryReadings; return gaugeMetric; } FPRMSGApplicationProcessState FPRApplicationProcessState(FPRTraceState state) { FPRMSGApplicationProcessState processState = FPRMSGApplicationProcessState_ApplicationProcessStateUnknown; switch (state) { case FPRTraceStateForegroundOnly: processState = FPRMSGApplicationProcessState_Foreground; break; case FPRTraceStateBackgroundOnly: processState = FPRMSGApplicationProcessState_Background; break; case FPRTraceStateBackgroundAndForeground: processState = FPRMSGApplicationProcessState_ForegroundBackground; break; default: break; } return processState; } #ifdef TARGET_HAS_MOBILE_CONNECTIVITY CTTelephonyNetworkInfo *FPRNetworkInfo() { static CTTelephonyNetworkInfo *networkInfo; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ networkInfo = [[CTTelephonyNetworkInfo alloc] init]; }); return networkInfo; } #endif #pragma mark - Proto creation utilities /** Converts a dictionary of to a GPBStringInt64Dictionary proto object. * @return Reference to a GPBStringInt64Dictionary object. */ static GPBStringInt64Dictionary *FPRGetProtoCounterForDictionary( NSDictionary *dictionary) { GPBStringInt64Dictionary *counterDictionary = [[GPBStringInt64Dictionary alloc] init]; [dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, NSNumber *_Nonnull value, BOOL *_Nonnull stop) { [counterDictionary setInt64:[value longLongValue] forKey:key]; }]; return counterDictionary; } /** Converts the network method string to a value defined in the enum * FPRMSGNetworkRequestMetric_HttpMethod. * @return Enum value of the method string. If there is no mapping value defined for the method * string FPRMSGNetworkRequestMetric_HttpMethod_HTTPMethodUnknown is returned. */ static FPRMSGNetworkRequestMetric_HttpMethod FPRHTTPMethodForString(NSString *methodString) { static NSDictionary *HTTPToFPRNetworkTraceMethod; static dispatch_once_t onceToken = 0; dispatch_once(&onceToken, ^{ HTTPToFPRNetworkTraceMethod = @{ @"GET" : @(FPRMSGNetworkRequestMetric_HttpMethod_Get), @"POST" : @(FPRMSGNetworkRequestMetric_HttpMethod_Post), @"PUT" : @(FPRMSGNetworkRequestMetric_HttpMethod_Put), @"DELETE" : @(FPRMSGNetworkRequestMetric_HttpMethod_Delete), @"HEAD" : @(FPRMSGNetworkRequestMetric_HttpMethod_Head), @"PATCH" : @(FPRMSGNetworkRequestMetric_HttpMethod_Patch), @"OPTIONS" : @(FPRMSGNetworkRequestMetric_HttpMethod_Options), @"TRACE" : @(FPRMSGNetworkRequestMetric_HttpMethod_Trace), @"CONNECT" : @(FPRMSGNetworkRequestMetric_HttpMethod_Connect), }; }); NSNumber *HTTPMethod = HTTPToFPRNetworkTraceMethod[methodString]; if (HTTPMethod == nil) { return FPRMSGNetworkRequestMetric_HttpMethod_HTTPMethodUnknown; } return HTTPMethod.intValue; } /** Get the current network connection type in NetworkConnectionInfo_NetworkType format. * @return Current network connection type. */ static FPRMSGNetworkConnectionInfo_NetworkType FPRNetworkConnectionInfoNetworkType() { FPRMSGNetworkConnectionInfo_NetworkType networkType = FPRMSGNetworkConnectionInfo_NetworkType_None; static SCNetworkReachabilityRef reachabilityRef = 0; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ reachabilityRef = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault, "google.com"); }); SCNetworkReachabilityFlags reachabilityFlags = 0; SCNetworkReachabilityGetFlags(reachabilityRef, &reachabilityFlags); // Parse the network flags to set the network type. if (reachabilityFlags & kSCNetworkReachabilityFlagsReachable) { if (reachabilityFlags & kSCNetworkReachabilityFlagsIsWWAN) { networkType = FPRMSGNetworkConnectionInfo_NetworkType_Mobile; } else { networkType = FPRMSGNetworkConnectionInfo_NetworkType_Wifi; } } return networkType; } #ifdef TARGET_HAS_MOBILE_CONNECTIVITY /** Get the current cellular network connection type in NetworkConnectionInfo_MobileSubtype format. * @return Current cellular network connection type. */ static FPRMSGNetworkConnectionInfo_MobileSubtype FPRCellularNetworkType() { static NSDictionary *cellularNetworkToMobileSubtype; static dispatch_once_t onceToken = 0; dispatch_once(&onceToken, ^{ cellularNetworkToMobileSubtype = @{ CTRadioAccessTechnologyGPRS : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Gprs), CTRadioAccessTechnologyEdge : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Edge), CTRadioAccessTechnologyWCDMA : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Cdma), CTRadioAccessTechnologyHSDPA : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Hsdpa), CTRadioAccessTechnologyHSUPA : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Hsupa), CTRadioAccessTechnologyCDMA1x : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Cdma), CTRadioAccessTechnologyCDMAEVDORev0 : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Evdo0), CTRadioAccessTechnologyCDMAEVDORevA : @(FPRMSGNetworkConnectionInfo_MobileSubtype_EvdoA), CTRadioAccessTechnologyCDMAEVDORevB : @(FPRMSGNetworkConnectionInfo_MobileSubtype_EvdoB), CTRadioAccessTechnologyeHRPD : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Ehrpd), CTRadioAccessTechnologyLTE : @(FPRMSGNetworkConnectionInfo_MobileSubtype_Lte) }; }); NSString *networkString = FPRNetworkInfo().currentRadioAccessTechnology; NSNumber *cellularNetworkType = cellularNetworkToMobileSubtype[networkString]; return cellularNetworkType.intValue; } #endif /** Reorders the list of sessions to make sure the first session is verbose if at least one session * in the list is verbose. * @return Ordered list of sessions. */ NSArray *FPRMakeFirstSessionVerbose(NSArray *sessions) { NSMutableArray *orderedSessions = [[NSMutableArray alloc] initWithArray:sessions]; __block NSInteger firstVerboseSessionIndex = -1; [sessions enumerateObjectsUsingBlock:^(FPRSessionDetails *session, NSUInteger idx, BOOL *stop) { if ([session isVerbose]) { firstVerboseSessionIndex = idx; *stop = YES; } }]; if (firstVerboseSessionIndex > 0) { FPRSessionDetails *verboseSession = orderedSessions[firstVerboseSessionIndex]; [orderedSessions removeObjectAtIndex:firstVerboseSessionIndex]; [orderedSessions insertObject:verboseSession atIndex:0]; } return [orderedSessions copy]; }