FPRAppActivityTracker.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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 <UIKit/UIKit.h>
  17. #import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
  18. #import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h"
  19. #import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
  20. #import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
  21. #import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
  22. #import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
  23. #import <GoogleUtilities/GULAppEnvironmentUtil.h>
  24. static NSDate *appStartTime = nil;
  25. static NSDate *doubleDispatchTime = nil;
  26. static NSDate *applicationDidFinishLaunchTime = nil;
  27. static NSTimeInterval gAppStartMaxValidDuration = 60 * 60; // 60 minutes.
  28. static FPRCPUGaugeData *gAppStartCPUGaugeData = nil;
  29. static FPRMemoryGaugeData *gAppStartMemoryGaugeData = nil;
  30. NSString *const kFPRAppStartTraceName = @"_as";
  31. NSString *const kFPRAppStartStageNameTimeToUI = @"_astui";
  32. NSString *const kFPRAppStartStageNameTimeToFirstDraw = @"_astfd";
  33. NSString *const kFPRAppStartStageNameTimeToUserInteraction = @"_asti";
  34. NSString *const kFPRAppTraceNameForegroundSession = @"_fs";
  35. NSString *const kFPRAppTraceNameBackgroundSession = @"_bs";
  36. NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
  37. NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
  38. NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
  39. NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
  40. NSString *const kFPRAppCounterNameDoubleDispatch = @"_fsddc";
  41. @interface FPRAppActivityTracker ()
  42. /** The foreground session trace. Will be set only when the app is in the foreground. */
  43. @property(nonatomic, readwrite) FIRTrace *foregroundSessionTrace;
  44. /** The background session trace. Will be set only when the app is in the background. */
  45. @property(nonatomic, readwrite) FIRTrace *backgroundSessionTrace;
  46. /** Current running state of the application. */
  47. @property(nonatomic, readwrite) FPRApplicationState applicationState;
  48. /** Trace to measure the app start performance. */
  49. @property(nonatomic) FIRTrace *appStartTrace;
  50. /** Tracks if the gauge metrics are dispatched. */
  51. @property(nonatomic) BOOL appStartGaugeMetricDispatched;
  52. /** Firebase Performance Configuration object */
  53. @property(nonatomic) FPRConfigurations *configurations;
  54. /** Starts tracking app active sessions. */
  55. - (void)startAppActivityTracking;
  56. @end
  57. @implementation FPRAppActivityTracker
  58. + (void)load {
  59. // This is an approximation of the app start time.
  60. appStartTime = [NSDate date];
  61. // Double dispatch is used to detect prewarming, but if it causes hang or crash in the future
  62. // developers can disable it by setting a plist flag "fireperf_disable_dd" to true
  63. if ([[[NSBundle mainBundle] objectForInfoDictionaryKey:@"fireperf_disable_dd"] boolValue] == NO) {
  64. dispatch_async(dispatch_get_main_queue(), ^{
  65. dispatch_async(dispatch_get_main_queue(), ^{
  66. doubleDispatchTime = [NSDate date];
  67. });
  68. });
  69. }
  70. gAppStartCPUGaugeData = fprCollectCPUMetric();
  71. gAppStartMemoryGaugeData = fprCollectMemoryMetric();
  72. [[NSNotificationCenter defaultCenter] addObserver:self
  73. selector:@selector(windowDidBecomeVisible:)
  74. name:UIWindowDidBecomeVisibleNotification
  75. object:nil];
  76. [[NSNotificationCenter defaultCenter] addObserver:self
  77. selector:@selector(applicationDidFinishLaunching:)
  78. name:UIApplicationDidFinishLaunchingNotification
  79. object:nil];
  80. }
  81. + (void)windowDidBecomeVisible:(NSNotification *)notification {
  82. FPRAppActivityTracker *activityTracker = [self sharedInstance];
  83. [activityTracker startAppActivityTracking];
  84. [[NSNotificationCenter defaultCenter] removeObserver:self
  85. name:UIWindowDidBecomeVisibleNotification
  86. object:nil];
  87. }
  88. + (void)applicationDidFinishLaunching:(NSNotification *)notification {
  89. applicationDidFinishLaunchTime = [NSDate date];
  90. [[NSNotificationCenter defaultCenter] removeObserver:self
  91. name:UIApplicationDidFinishLaunchingNotification
  92. object:nil];
  93. }
  94. + (instancetype)sharedInstance {
  95. static FPRAppActivityTracker *instance;
  96. static dispatch_once_t onceToken;
  97. dispatch_once(&onceToken, ^{
  98. instance = [[self alloc] initAppActivityTracker];
  99. });
  100. return instance;
  101. }
  102. /**
  103. * Custom initializer to create an app activity tracker.
  104. */
  105. - (instancetype)initAppActivityTracker {
  106. self = [super init];
  107. _applicationState = FPRApplicationStateUnknown;
  108. _appStartGaugeMetricDispatched = NO;
  109. _configurations = [FPRConfigurations sharedInstance];
  110. return self;
  111. }
  112. - (void)startAppActivityTracking {
  113. [[NSNotificationCenter defaultCenter] addObserver:self
  114. selector:@selector(appDidBecomeActiveNotification:)
  115. name:UIApplicationDidBecomeActiveNotification
  116. object:[UIApplication sharedApplication]];
  117. [[NSNotificationCenter defaultCenter] addObserver:self
  118. selector:@selector(appWillResignActiveNotification:)
  119. name:UIApplicationWillResignActiveNotification
  120. object:[UIApplication sharedApplication]];
  121. }
  122. - (FIRTrace *)activeTrace {
  123. if (self.foregroundSessionTrace) {
  124. return self.foregroundSessionTrace;
  125. }
  126. return self.backgroundSessionTrace;
  127. }
  128. /**
  129. * Checks if prewarming is available for the platform on current device.
  130. * It is available when running iOS 15 and above.
  131. *
  132. * @return true if the platform could prewarm apps on the current device
  133. */
  134. + (BOOL)isPrewarmAvailable {
  135. if (![[[GULAppEnvironmentUtil applePlatform] lowercaseString] isEqualToString:@"ios"]) {
  136. return NO;
  137. }
  138. NSString *systemVersion = [GULAppEnvironmentUtil systemVersion];
  139. if ([systemVersion length] == 0) {
  140. return NO;
  141. }
  142. return [systemVersion compare:@"15" options:NSNumericSearch] != NSOrderedAscending;
  143. }
  144. /**
  145. RC flag for dropping all app start events
  146. */
  147. - (BOOL)isAppStartEnabled {
  148. return [self.configurations prewarmDetectionMode] != PrewarmDetectionModeKeepNone;
  149. }
  150. /**
  151. RC flag for enabling prewarm-detection using ActivePrewarm environment variable
  152. */
  153. - (BOOL)isActivePrewarmEnabled {
  154. PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
  155. return (mode == PrewarmDetectionModeActivePrewarm ||
  156. mode == PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
  157. }
  158. /**
  159. RC flag for enabling prewarm-detection using double dispatch method
  160. */
  161. - (BOOL)isDoubleDispatchEnabled {
  162. PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
  163. return (mode == PrewarmDetectionModeDoubleDispatch ||
  164. mode == PrewarmDetectionModeActivePrewarmOrDoubleDispatch);
  165. }
  166. /**
  167. Checks if the current app start is a prewarmed app start
  168. */
  169. - (BOOL)isApplicationPreWarmed {
  170. if (![FPRAppActivityTracker isPrewarmAvailable]) {
  171. return NO;
  172. }
  173. BOOL isPrewarmed = NO;
  174. NSDictionary<NSString *, NSString *> *environment = [NSProcessInfo processInfo].environment;
  175. BOOL activePrewarmFlagValue = [environment[@"ActivePrewarm"] boolValue];
  176. if (activePrewarmFlagValue == YES) {
  177. isPrewarmed = isPrewarmed || [self isActivePrewarmEnabled];
  178. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:1];
  179. } else {
  180. [self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
  181. }
  182. if ([doubleDispatchTime compare:applicationDidFinishLaunchTime] == NSOrderedAscending) {
  183. isPrewarmed = isPrewarmed || [self isDoubleDispatchEnabled];
  184. [self.activeTrace incrementMetric:kFPRAppCounterNameDoubleDispatch byInt:1];
  185. } else if (doubleDispatchTime) {
  186. [self.activeTrace incrementMetric:kFPRAppCounterNameDoubleDispatch byInt:0];
  187. }
  188. return isPrewarmed;
  189. }
  190. /**
  191. * This gets called whenever the app becomes active. A new trace will be created to track the active
  192. * foreground session. Any background session trace that was running in the past will be stopped.
  193. *
  194. * @param notification Notification received during app launch.
  195. */
  196. - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
  197. self.applicationState = FPRApplicationStateForeground;
  198. static dispatch_once_t onceToken;
  199. dispatch_once(&onceToken, ^{
  200. self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
  201. [self.appStartTrace startWithStartTime:appStartTime];
  202. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
  203. // Start measuring time to first draw on the App start trace.
  204. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
  205. });
  206. // If ever the app start trace had it life in background stage, do not send the trace.
  207. if (self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
  208. self.appStartTrace = nil;
  209. }
  210. // Stop the active background session trace.
  211. [self.backgroundSessionTrace stop];
  212. self.backgroundSessionTrace = nil;
  213. // Start foreground session trace.
  214. FIRTrace *appTrace =
  215. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameForegroundSession];
  216. [appTrace start];
  217. self.foregroundSessionTrace = appTrace;
  218. // Start measuring time to make the app interactive on the App start trace.
  219. static BOOL TTIStageStarted = NO;
  220. if (!TTIStageStarted) {
  221. [self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
  222. TTIStageStarted = YES;
  223. // Assumption here is that - the app becomes interactive in the next runloop cycle.
  224. // It is possible that the app does more things later, but for now we are not measuring that.
  225. dispatch_async(dispatch_get_main_queue(), ^{
  226. NSTimeInterval startTimeSinceEpoch = [self.appStartTrace startTimeSinceEpoch];
  227. NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
  228. // The below check is to account for 2 scenarios.
  229. // 1. The app gets started in the background and might come to foreground a lot later.
  230. // 2. The app is launched, but immediately backgrounded for some reason and the actual launch
  231. // happens a lot later.
  232. // Dropping the app start trace in such situations where the launch time is taking more than
  233. // 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
  234. if ((currentTimeSinceEpoch - startTimeSinceEpoch < gAppStartMaxValidDuration) &&
  235. [self isAppStartEnabled] && ![self isApplicationPreWarmed]) {
  236. [self.appStartTrace stop];
  237. } else {
  238. [self.appStartTrace cancel];
  239. }
  240. });
  241. }
  242. // Let the session manager to start tracking app activity changes.
  243. [[FPRSessionManager sharedInstance] startTrackingAppStateChanges];
  244. }
  245. /**
  246. * This gets called whenever the app resigns its active status. The currently active foreground
  247. * session trace will be stopped and a background session trace will be started.
  248. *
  249. * @param notification Notification received during app resigning active status.
  250. */
  251. - (void)appWillResignActiveNotification:(NSNotification *)notification {
  252. // Dispatch the collected gauge metrics.
  253. if (!self.appStartGaugeMetricDispatched) {
  254. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartCPUGaugeData];
  255. [[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartMemoryGaugeData];
  256. self.appStartGaugeMetricDispatched = YES;
  257. }
  258. self.applicationState = FPRApplicationStateBackground;
  259. // Stop foreground session trace.
  260. [self.foregroundSessionTrace stop];
  261. self.foregroundSessionTrace = nil;
  262. // Start background session trace.
  263. self.backgroundSessionTrace =
  264. [[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameBackgroundSession];
  265. [self.backgroundSessionTrace start];
  266. }
  267. - (void)dealloc {
  268. [[NSNotificationCenter defaultCenter] removeObserver:self
  269. name:UIApplicationDidBecomeActiveNotification
  270. object:[UIApplication sharedApplication]];
  271. [[NSNotificationCenter defaultCenter] removeObserver:self
  272. name:UIApplicationWillResignActiveNotification
  273. object:[UIApplication sharedApplication]];
  274. }
  275. @end