FPRAppActivityTracker.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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/Common/FPRDiagnostics.h"
  20. #import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
  21. #import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h"
  22. #import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
  23. #import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
  24. #import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
  25. #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
  26. static NSDate *appStartTime = nil;
  27. static NSDate *doubleDispatchTime = nil;
  28. static NSDate *applicationDidFinishLaunchTime = nil;
  29. static NSTimeInterval gAppStartMaxValidDuration = 60 * 60; // 60 minutes.
  30. static FPRCPUGaugeData *gAppStartCPUGaugeData = nil;
  31. static FPRMemoryGaugeData *gAppStartMemoryGaugeData = nil;
  32. static BOOL isActivePrewarm = NO;
  33. NSString *const kFPRAppStartTraceName = @"_as";
  34. NSString *const kFPRAppStartStageNameTimeToUI = @"_astui";
  35. NSString *const kFPRAppStartStageNameTimeToFirstDraw = @"_astfd";
  36. NSString *const kFPRAppStartStageNameTimeToUserInteraction = @"_asti";
  37. NSString *const kFPRAppTraceNameForegroundSession = @"_fs";
  38. NSString *const kFPRAppTraceNameBackgroundSession = @"_bs";
  39. NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
  40. NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
  41. NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
  42. NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
  43. @interface FPRAppActivityTracker ()
  44. /** The foreground session trace. Will be set only when the app is in the foreground. */
  45. @property(nonatomic, readwrite) FIRTrace *foregroundSessionTrace;
  46. /** The background session trace. Will be set only when the app is in the background. */
  47. @property(nonatomic, readwrite) FIRTrace *backgroundSessionTrace;
  48. /** Current running state of the application. */
  49. @property(nonatomic, readwrite) FPRApplicationState applicationState;
  50. /** Current network connection type of the application. */
  51. @property(nonatomic, readwrite) firebase_perf_v1_NetworkConnectionInfo_NetworkType networkType;
  52. /** Network monitor object to track network movements. */
  53. @property(nonatomic, readwrite) nw_path_monitor_t monitor;
  54. /** Queue used to track the network monitoring changes. */
  55. @property(nonatomic, readwrite) dispatch_queue_t monitorQueue;
  56. /** Trace to measure the app start performance. */
  57. @property(nonatomic) FIRTrace *appStartTrace;
  58. /** Tracks if the gauge metrics are dispatched. */
  59. @property(nonatomic) BOOL appStartGaugeMetricDispatched;
  60. /** Tracks if app start trace completion logic has been executed. */
  61. @property(nonatomic) BOOL appStartTraceCompleted;
  62. /** Firebase Performance Configuration object */
  63. @property(nonatomic) FPRConfigurations *configurations;
  64. /** Starts tracking app active sessions. */
  65. - (void)startAppActivityTracking;
  66. @end
  67. @implementation FPRAppActivityTracker
  68. + (void)load {
  69. // This is an approximation of the app start time.
  70. appStartTime = [NSDate date];
  71. // When an app is prewarmed, Apple sets env variable ActivePrewarm to 1, then the env variable is
  72. // deleted after didFinishLaunching
  73. isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"];
  74. gAppStartCPUGaugeData = fprCollectCPUMetric();
  75. gAppStartMemoryGaugeData = fprCollectMemoryMetric();
  76. [[NSNotificationCenter defaultCenter] addObserver:self
  77. selector:@selector(windowDidBecomeVisible:)
  78. name:UIWindowDidBecomeVisibleNotification
  79. object:nil];
  80. [[NSNotificationCenter defaultCenter] addObserver:self
  81. selector:@selector(applicationDidFinishLaunching:)
  82. name:UIApplicationDidFinishLaunchingNotification
  83. object:nil];
  84. }
  85. + (void)windowDidBecomeVisible:(NSNotification *)notification {
  86. FPRAppActivityTracker *activityTracker = [self sharedInstance];
  87. [activityTracker startAppActivityTracking];
  88. [[NSNotificationCenter defaultCenter] removeObserver:self
  89. name:UIWindowDidBecomeVisibleNotification
  90. object:nil];
  91. }
  92. + (void)applicationDidFinishLaunching:(NSNotification *)notification {
  93. applicationDidFinishLaunchTime = [NSDate date];
  94. // Detect a background launch and invalidate app start time
  95. // this prevents we measure duration from background launch
  96. UIApplicationState state = [UIApplication sharedApplication].applicationState;
  97. if (state == UIApplicationStateBackground) {
  98. // App launched in background so we invalidate the captured app start time
  99. // to prevent incorrect measurement when user later opens the app
  100. appStartTime = nil;
  101. FPRLogDebug(kFPRTraceNotCreated,
  102. @"Background launch detected. App start measurement will be skipped.");
  103. }
  104. [[NSNotificationCenter defaultCenter] removeObserver:self
  105. name:UIApplicationDidFinishLaunchingNotification
  106. object:nil];
  107. }
  108. + (instancetype)sharedInstance {
  109. static FPRAppActivityTracker *instance;
  110. static dispatch_once_t onceToken;
  111. dispatch_once(&onceToken, ^{
  112. instance = [[self alloc] initAppActivityTracker];
  113. });
  114. return instance;
  115. }
  116. /**
  117. * Custom initializer to create an app activity tracker.
  118. */
  119. - (instancetype)initAppActivityTracker {
  120. self = [super init];
  121. if (self != nil) {
  122. _applicationState = FPRApplicationStateUnknown;
  123. _appStartGaugeMetricDispatched = NO;
  124. _appStartTraceCompleted = NO;
  125. _configurations = [FPRConfigurations sharedInstance];
  126. [self startTrackingNetwork];
  127. }
  128. return self;
  129. }
  130. - (void)startAppActivityTracking {
  131. [[NSNotificationCenter defaultCenter] addObserver:self
  132. selector:@selector(appDidBecomeActiveNotification:)
  133. name:UIApplicationDidBecomeActiveNotification
  134. object:[UIApplication sharedApplication]];
  135. [[NSNotificationCenter defaultCenter] addObserver:self
  136. selector:@selector(appWillResignActiveNotification:)
  137. name:UIApplicationWillResignActiveNotification
  138. object:[UIApplication sharedApplication]];
  139. }
  140. - (FIRTrace *)activeTrace {
  141. if (self.foregroundSessionTrace) {
  142. return self.foregroundSessionTrace;
  143. }
  144. return self.backgroundSessionTrace;
  145. }
  146. - (void)startTrackingNetwork {
  147. self.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_NONE;
  148. dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class(
  149. DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, DISPATCH_QUEUE_PRIORITY_DEFAULT);
  150. self.monitorQueue = dispatch_queue_create("com.google.firebase.perf.network.monitor", attrs);
  151. self.monitor = nw_path_monitor_create();
  152. nw_path_monitor_set_queue(self.monitor, self.monitorQueue);
  153. __weak FPRAppActivityTracker *weakSelf = self;
  154. nw_path_monitor_set_update_handler(self.monitor, ^(nw_path_t _Nonnull path) {
  155. BOOL isWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi);
  156. BOOL isCellular = nw_path_uses_interface_type(path, nw_interface_type_cellular);
  157. BOOL isEthernet = nw_path_uses_interface_type(path, nw_interface_type_wired);
  158. if (isWiFi) {
  159. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_WIFI;
  160. } else if (isCellular) {
  161. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_MOBILE;
  162. } else if (isEthernet) {
  163. weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_ETHERNET;
  164. }
  165. });
  166. nw_path_monitor_start(self.monitor);
  167. }
  168. /**
  169. * Checks if the prewarming feature is available on the current device.
  170. *
  171. * @return true if the OS could prewarm apps on the current device
  172. */
  173. - (BOOL)isPrewarmAvailable {
  174. return YES;
  175. }
  176. /**
  177. RC flag for dropping all app start events
  178. */
  179. - (BOOL)isAppStartEnabled {
  180. return [self.configurations prewarmDetectionMode] != PrewarmDetectionModeKeepNone;
  181. }
  182. /**
  183. RC flag for enabling prewarm-detection using ActivePrewarm environment variable
  184. */
  185. - (BOOL)isActivePrewarmEnabled {
  186. PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
  187. return (mode == PrewarmDetectionModeActivePrewarm);
  188. }
  189. /**
  190. Checks if the current app start is a prewarmed app start
  191. */
  192. - (BOOL)isApplicationPreWarmed {
  193. if (![self isPrewarmAvailable]) {
  194. return NO;
  195. }
  196. BOOL isPrewarmed = NO;
  197. if (isActivePrewarm == YES) {
  198. isPrewarmed = isPrewarmed || [self isActivePrewarmEnabled];
  199. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:1];
  200. } else {
  201. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
  202. }
  203. return isPrewarmed;
  204. }
  205. /**
  206. * This gets called whenever the app becomes active. A new trace will be created to track the active
  207. * foreground session. Any background session trace that was running in the past will be stopped.
  208. *
  209. * @param notification Notification received during app launch.
  210. */
  211. - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
  212. self.applicationState = FPRApplicationStateForeground;
  213. static dispatch_once_t onceToken;
  214. dispatch_once(&onceToken, ^{
  215. // Early bailout if background launch was detected, appStartTime will be nil if the app was
  216. // launched in background
  217. if (appStartTime == nil) {
  218. FPRLogDebug(kFPRTraceNotCreated,
  219. @"App start trace skipped due to background launch. "
  220. @"This prevents reporting incorrect multi-minute/hour durations.");
  221. return;
  222. }
  223. self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
  224. [self.appStartTrace startWithStartTime:appStartTime];
  225. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
  226. // Start measuring time to first draw on the App start trace.
  227. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
  228. });
  229. // If ever the app start trace had its life in background stage, do not send the trace.
  230. if (self.appStartTrace &&
  231. self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
  232. [self.appStartTrace cancel];
  233. self.appStartTrace = nil;
  234. FPRLogDebug(kFPRTraceNotCreated,
  235. @"App start trace cancelled due to background state contamination.");
  236. }
  237. // Stop the active background session trace.
  238. [self.backgroundSessionTrace stop];
  239. self.backgroundSessionTrace = nil;
  240. // Start foreground session trace.
  241. FIRTrace *appTrace =
  242. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameForegroundSession];
  243. [appTrace start];
  244. self.foregroundSessionTrace = appTrace;
  245. // Start measuring time to make the app interactive on the App start trace.
  246. if (!self.appStartTraceCompleted && self.appStartTrace) {
  247. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
  248. self.appStartTraceCompleted = YES;
  249. // Assumption here is that - the app becomes interactive in the next runloop cycle.
  250. // It is possible that the app does more things later, but for now we are not measuring that.
  251. __weak typeof(self) weakSelf = self;
  252. dispatch_async(dispatch_get_main_queue(), ^{
  253. __strong typeof(weakSelf) strongSelf = weakSelf;
  254. if (!strongSelf || !strongSelf.appStartTrace) {
  255. return;
  256. }
  257. NSTimeInterval startTimeSinceEpoch = [strongSelf.appStartTrace startTimeSinceEpoch];
  258. NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
  259. NSTimeInterval measuredAppStartTime = currentTimeSinceEpoch - startTimeSinceEpoch;
  260. // The below check accounts for multiple scenarios:
  261. // 1. App started in background and comes to foreground later
  262. // 2. App launched but immediately backgroundedfor some reason and the actual launch
  263. // happens a lot later.
  264. // 3. Network delays during startup inflating metrics
  265. // 4. iOS prewarm scenarios
  266. // 5. Dropping the app start trace in such situations where the launch time is taking more
  267. // than 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
  268. BOOL shouldDispatchAppStartTrace = (measuredAppStartTime < gAppStartMaxValidDuration) &&
  269. [strongSelf isAppStartEnabled] &&
  270. ![strongSelf isApplicationPreWarmed];
  271. if (shouldDispatchAppStartTrace) {
  272. [strongSelf.appStartTrace stop];
  273. } else {
  274. [strongSelf.appStartTrace cancel];
  275. if (measuredAppStartTime >= gAppStartMaxValidDuration) {
  276. FPRLogDebug(kFPRTraceInvalidName,
  277. @"App start trace cancelled due to excessive duration: %.2fs",
  278. measuredAppStartTime);
  279. }
  280. }
  281. });
  282. }
  283. }
  284. /**
  285. * This gets called whenever the app resigns its active status. The currently active foreground
  286. * session trace will be stopped and a background session trace will be started.
  287. *
  288. * @param notification Notification received during app resigning active status.
  289. */
  290. - (void)appWillResignActiveNotification:(NSNotification *)notification {
  291. // Dispatch the collected gauge metrics.
  292. if (!self.appStartGaugeMetricDispatched) {
  293. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartCPUGaugeData];
  294. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartMemoryGaugeData];
  295. self.appStartGaugeMetricDispatched = YES;
  296. }
  297. self.applicationState = FPRApplicationStateBackground;
  298. // Stop foreground session trace.
  299. [self.foregroundSessionTrace stop];
  300. self.foregroundSessionTrace = nil;
  301. // Start background session trace.
  302. self.backgroundSessionTrace =
  303. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameBackgroundSession];
  304. [self.backgroundSessionTrace start];
  305. }
  306. - (void)dealloc {
  307. nw_path_monitor_cancel(self.monitor);
  308. [[NSNotificationCenter defaultCenter] removeObserver:self
  309. name:UIApplicationDidBecomeActiveNotification
  310. object:[UIApplication sharedApplication]];
  311. [[NSNotificationCenter defaultCenter] removeObserver:self
  312. name:UIApplicationWillResignActiveNotification
  313. object:[UIApplication sharedApplication]];
  314. }
  315. @end