FPRAppActivityTracker.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #import "FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.h"
  15. #import <Foundation/Foundation.h>
  16. #import <Network/Network.h>
  17. #import <UIKit/UIKit.h>
  18. #import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
  19. #import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
  20. #import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h"
  21. #import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
  22. #import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
  23. #import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
  24. #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
  25. static NSDate *appStartTime = nil;
  26. static NSDate *doubleDispatchTime = nil;
  27. static NSDate *applicationDidFinishLaunchTime = nil;
  28. static NSTimeInterval gAppStartMaxValidDuration = 60 * 60; // 60 minutes.
  29. static FPRCPUGaugeData *gAppStartCPUGaugeData = nil;
  30. static FPRMemoryGaugeData *gAppStartMemoryGaugeData = nil;
  31. static BOOL isActivePrewarm = NO;
  32. NSString *const kFPRAppStartTraceName = @"_as";
  33. NSString *const kFPRAppStartStageNameTimeToUI = @"_astui";
  34. NSString *const kFPRAppStartStageNameTimeToFirstDraw = @"_astfd";
  35. NSString *const kFPRAppStartStageNameTimeToUserInteraction = @"_asti";
  36. NSString *const kFPRAppTraceNameForegroundSession = @"_fs";
  37. NSString *const kFPRAppTraceNameBackgroundSession = @"_bs";
  38. NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
  39. NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
  40. NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
  41. NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
  42. @interface FPRAppActivityTracker ()
  43. /** The foreground session trace. Will be set only when the app is in the foreground. */
  44. @property(nonatomic, readwrite) FIRTrace *foregroundSessionTrace;
  45. /** The background session trace. Will be set only when the app is in the background. */
  46. @property(nonatomic, readwrite) FIRTrace *backgroundSessionTrace;
  47. /** Current running state of the application. */
  48. @property(nonatomic, readwrite) FPRApplicationState applicationState;
  49. /** Current network connection type of the application. */
  50. @property(nonatomic, readwrite) firebase_perf_v1_NetworkConnectionInfo_NetworkType networkType;
  51. /** Network monitor object to track network movements. */
  52. @property(nonatomic, readwrite) nw_path_monitor_t monitor;
  53. /** Queue used to track the network monitoring changes. */
  54. @property(nonatomic, readwrite) dispatch_queue_t monitorQueue;
  55. /** Trace to measure the app start performance. */
  56. @property(nonatomic) FIRTrace *appStartTrace;
  57. /** Tracks if the gauge metrics are dispatched. */
  58. @property(nonatomic) BOOL appStartGaugeMetricDispatched;
  59. /** Firebase Performance Configuration object */
  60. @property(nonatomic) FPRConfigurations *configurations;
  61. /** Starts tracking app active sessions. */
  62. - (void)startAppActivityTracking;
  63. @end
  64. @implementation FPRAppActivityTracker
  65. + (void)load {
  66. // This is an approximation of the app start time.
  67. appStartTime = [NSDate date];
  68. // When an app is prewarmed, Apple sets env variable ActivePrewarm to 1, then the env variable is
  69. // deleted after didFinishLaunching
  70. isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"];
  71. gAppStartCPUGaugeData = fprCollectCPUMetric();
  72. gAppStartMemoryGaugeData = fprCollectMemoryMetric();
  73. [[NSNotificationCenter defaultCenter] addObserver:self
  74. selector:@selector(windowDidBecomeVisible:)
  75. name:UIWindowDidBecomeVisibleNotification
  76. object:nil];
  77. [[NSNotificationCenter defaultCenter] addObserver:self
  78. selector:@selector(applicationDidFinishLaunching:)
  79. name:UIApplicationDidFinishLaunchingNotification
  80. object:nil];
  81. }
  82. + (void)windowDidBecomeVisible:(NSNotification *)notification {
  83. FPRAppActivityTracker *activityTracker = [self sharedInstance];
  84. [activityTracker startAppActivityTracking];
  85. [[NSNotificationCenter defaultCenter] removeObserver:self
  86. name:UIWindowDidBecomeVisibleNotification
  87. object:nil];
  88. }
  89. + (void)applicationDidFinishLaunching:(NSNotification *)notification {
  90. applicationDidFinishLaunchTime = [NSDate date];
  91. [[NSNotificationCenter defaultCenter] removeObserver:self
  92. name:UIApplicationDidFinishLaunchingNotification
  93. object:nil];
  94. }
  95. + (instancetype)sharedInstance {
  96. static FPRAppActivityTracker *instance;
  97. static dispatch_once_t onceToken;
  98. dispatch_once(&onceToken, ^{
  99. instance = [[self alloc] initAppActivityTracker];
  100. });
  101. return instance;
  102. }
  103. /**
  104. * Custom initializer to create an app activity tracker.
  105. */
  106. - (instancetype)initAppActivityTracker {
  107. self = [super init];
  108. if (self != nil) {
  109. _applicationState = FPRApplicationStateUnknown;
  110. _appStartGaugeMetricDispatched = NO;
  111. _configurations = [FPRConfigurations sharedInstance];
  112. [self startTrackingNetwork];
  113. }
  114. return self;
  115. }
  116. - (void)startAppActivityTracking {
  117. [[NSNotificationCenter defaultCenter] addObserver:self
  118. selector:@selector(appDidBecomeActiveNotification:)
  119. name:UIApplicationDidBecomeActiveNotification
  120. object:[UIApplication sharedApplication]];
  121. [[NSNotificationCenter defaultCenter] addObserver:self
  122. selector:@selector(appWillResignActiveNotification:)
  123. name:UIApplicationWillResignActiveNotification
  124. object:[UIApplication sharedApplication]];
  125. }
  126. - (FIRTrace *)activeTrace {
  127. if (self.foregroundSessionTrace) {
  128. return self.foregroundSessionTrace;
  129. }
  130. return self.backgroundSessionTrace;
  131. }
  132. - (void)startTrackingNetwork {
  133. self.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_NONE;
  134. if (@available(iOS 12, tvOS 12, *)) {
  135. dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class(
  136. DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, DISPATCH_QUEUE_PRIORITY_DEFAULT);
  137. self.monitorQueue = dispatch_queue_create("com.google.firebase.perf.network.monitor", attrs);
  138. self.monitor = nw_path_monitor_create();
  139. nw_path_monitor_set_queue(self.monitor, self.monitorQueue);
  140. __weak FPRAppActivityTracker *weakSelf = self;
  141. nw_path_monitor_set_update_handler(self.monitor, ^(nw_path_t _Nonnull path) {
  142. BOOL isWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi);
  143. BOOL isCellular = nw_path_uses_interface_type(path, nw_interface_type_cellular);
  144. BOOL isEthernet = nw_path_uses_interface_type(path, nw_interface_type_wired);
  145. if (isWiFi) {
  146. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_WIFI;
  147. } else if (isCellular) {
  148. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_MOBILE;
  149. } else if (isEthernet) {
  150. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_ETHERNET;
  151. }
  152. });
  153. nw_path_monitor_start(self.monitor);
  154. }
  155. }
  156. /**
  157. * Checks if the prewarming feature is available on the current device.
  158. *
  159. * @return true if the OS could prewarm apps on the current device
  160. */
  161. - (BOOL)isPrewarmAvailable {
  162. BOOL canPrewarm = NO;
  163. // Guarding for double dispatch which does not work below iOS 13, and 0.1% of app start also show
  164. // signs of prewarming on iOS 14 go/paste/5533761933410304
  165. if (@available(iOS 13, *)) {
  166. canPrewarm = YES;
  167. }
  168. return canPrewarm;
  169. }
  170. /**
  171. RC flag for dropping all app start events
  172. */
  173. - (BOOL)isAppStartEnabled {
  174. return [self.configurations prewarmDetectionMode] != PrewarmDetectionModeKeepNone;
  175. }
  176. /**
  177. RC flag for enabling prewarm-detection using ActivePrewarm environment variable
  178. */
  179. - (BOOL)isActivePrewarmEnabled {
  180. PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
  181. return (mode == PrewarmDetectionModeActivePrewarm);
  182. }
  183. /**
  184. Checks if the current app start is a prewarmed app start
  185. */
  186. - (BOOL)isApplicationPreWarmed {
  187. if (![self isPrewarmAvailable]) {
  188. return NO;
  189. }
  190. BOOL isPrewarmed = NO;
  191. if (isActivePrewarm == YES) {
  192. isPrewarmed = isPrewarmed || [self isActivePrewarmEnabled];
  193. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:1];
  194. } else {
  195. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
  196. }
  197. return isPrewarmed;
  198. }
  199. /**
  200. * This gets called whenever the app becomes active. A new trace will be created to track the active
  201. * foreground session. Any background session trace that was running in the past will be stopped.
  202. *
  203. * @param notification Notification received during app launch.
  204. */
  205. - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
  206. self.applicationState = FPRApplicationStateForeground;
  207. static dispatch_once_t onceToken;
  208. dispatch_once(&onceToken, ^{
  209. self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
  210. [self.appStartTrace startWithStartTime:appStartTime];
  211. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
  212. // Start measuring time to first draw on the App start trace.
  213. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
  214. });
  215. // If ever the app start trace had it life in background stage, do not send the trace.
  216. if (self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
  217. self.appStartTrace = nil;
  218. }
  219. // Stop the active background session trace.
  220. [self.backgroundSessionTrace stop];
  221. self.backgroundSessionTrace = nil;
  222. // Start foreground session trace.
  223. FIRTrace *appTrace =
  224. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameForegroundSession];
  225. [appTrace start];
  226. self.foregroundSessionTrace = appTrace;
  227. // Start measuring time to make the app interactive on the App start trace.
  228. static BOOL TTIStageStarted = NO;
  229. if (!TTIStageStarted) {
  230. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
  231. TTIStageStarted = YES;
  232. // Assumption here is that - the app becomes interactive in the next runloop cycle.
  233. // It is possible that the app does more things later, but for now we are not measuring that.
  234. dispatch_async(dispatch_get_main_queue(), ^{
  235. NSTimeInterval startTimeSinceEpoch = [self.appStartTrace startTimeSinceEpoch];
  236. NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
  237. // The below check is to account for 2 scenarios.
  238. // 1. The app gets started in the background and might come to foreground a lot later.
  239. // 2. The app is launched, but immediately backgrounded for some reason and the actual launch
  240. // happens a lot later.
  241. // Dropping the app start trace in such situations where the launch time is taking more than
  242. // 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
  243. if ((currentTimeSinceEpoch - startTimeSinceEpoch < gAppStartMaxValidDuration) &&
  244. [self isAppStartEnabled] && ![self isApplicationPreWarmed]) {
  245. [self.appStartTrace stop];
  246. } else {
  247. [self.appStartTrace cancel];
  248. }
  249. });
  250. }
  251. }
  252. /**
  253. * This gets called whenever the app resigns its active status. The currently active foreground
  254. * session trace will be stopped and a background session trace will be started.
  255. *
  256. * @param notification Notification received during app resigning active status.
  257. */
  258. - (void)appWillResignActiveNotification:(NSNotification *)notification {
  259. // Dispatch the collected gauge metrics.
  260. if (!self.appStartGaugeMetricDispatched) {
  261. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartCPUGaugeData];
  262. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartMemoryGaugeData];
  263. self.appStartGaugeMetricDispatched = YES;
  264. }
  265. self.applicationState = FPRApplicationStateBackground;
  266. // Stop foreground session trace.
  267. [self.foregroundSessionTrace stop];
  268. self.foregroundSessionTrace = nil;
  269. // Start background session trace.
  270. self.backgroundSessionTrace =
  271. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameBackgroundSession];
  272. [self.backgroundSessionTrace start];
  273. }
  274. - (void)dealloc {
  275. if (@available(iOS 12, tvOS 12, *)) {
  276. nw_path_monitor_cancel(self.monitor);
  277. }
  278. [[NSNotificationCenter defaultCenter] removeObserver:self
  279. name:UIApplicationDidBecomeActiveNotification
  280. object:[UIApplication sharedApplication]];
  281. [[NSNotificationCenter defaultCenter] removeObserver:self
  282. name:UIApplicationWillResignActiveNotification
  283. object:[UIApplication sharedApplication]];
  284. }
  285. @end