1919#import  < UIKit/UIKit.h> 
2020
2121#import  " FirebasePerformance/Sources/AppActivity/FPRSessionManager.h" 
22+ #import  " FirebasePerformance/Sources/Common/FPRDiagnostics.h" 
2223#import  " FirebasePerformance/Sources/Configurations/FPRConfigurations.h" 
2324#import  " FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h" 
2425#import  " FirebasePerformance/Sources/Gauges/FPRGaugeManager.h" 
@@ -71,6 +72,9 @@ @interface FPRAppActivityTracker ()
7172/* * Tracks if the gauge metrics are dispatched. */ 
7273@property (nonatomic ) BOOL  appStartGaugeMetricDispatched;
7374
75+ /* * Tracks if app start trace completion logic has been executed. */ 
76+ @property (nonatomic ) BOOL  appStartTraceCompleted;
77+ 
7478/* * Firebase Performance Configuration object */ 
7579@property (nonatomic ) FPRConfigurations *configurations;
7680
@@ -113,6 +117,18 @@ + (void)windowDidBecomeVisible:(NSNotification *)notification {
113117
114118+ (void )applicationDidFinishLaunching : (NSNotification  *)notification  {
115119  applicationDidFinishLaunchTime = [NSDate  date ];
120+ 
121+   //  Detect a background launch and invalidate app start time
122+   //  this prevents we measure duration from background launch
123+   UIApplicationState state = [UIApplication sharedApplication ].applicationState ;
124+   if  (state == UIApplicationStateBackground) {
125+     //  App launched in background so we invalidate the captured app start time
126+     //  to prevent incorrect measurement when user later opens the app
127+     appStartTime = nil ;
128+     FPRLogDebug (kFPRTraceNotCreated ,
129+                 @" Background launch detected. App start measurement will be skipped."  );
130+   }
131+ 
116132  [[NSNotificationCenter  defaultCenter ] removeObserver: self 
117133                                                  name: UIApplicationDidFinishLaunchingNotification
118134                                                object: nil ];
@@ -135,6 +151,7 @@ - (instancetype)initAppActivityTracker {
135151  if  (self != nil ) {
136152    _applicationState = FPRApplicationStateUnknown;
137153    _appStartGaugeMetricDispatched = NO ;
154+     _appStartTraceCompleted = NO ;
138155    _configurations = [FPRConfigurations sharedInstance ];
139156    [self  startTrackingNetwork ];
140157  }
@@ -242,6 +259,15 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
242259
243260  static  dispatch_once_t  onceToken;
244261  dispatch_once (&onceToken, ^{
262+     //  Early bailout if background launch was detected, appStartTime will be nil if the app was
263+     //  launched in background
264+     if  (appStartTime == nil ) {
265+       FPRLogDebug (kFPRTraceNotCreated ,
266+                   @" App start trace skipped due to background launch. " 
267+                   @" This prevents reporting incorrect multi-minute/hour durations."  );
268+       return ;
269+     }
270+ 
245271    self.appStartTrace  = [[FIRTrace alloc ] initInternalTraceWithName: kFPRAppStartTraceName ];
246272    [self .appStartTrace startWithStartTime: appStartTime];
247273    [self .appStartTrace startStageNamed: kFPRAppStartStageNameTimeToUI  startTime: appStartTime];
@@ -250,9 +276,13 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
250276    [self .appStartTrace startStageNamed: kFPRAppStartStageNameTimeToFirstDraw ];
251277  });
252278
253-   //  If ever the app start trace had it life in background stage, do not send the trace.
254-   if  (self.appStartTrace .backgroundTraceState  != FPRTraceStateForegroundOnly) {
279+   //  If ever the app start trace had its life in background stage, do not send the trace.
280+   if  (self.appStartTrace  &&
281+       self.appStartTrace .backgroundTraceState  != FPRTraceStateForegroundOnly) {
282+     [self .appStartTrace cancel ];
255283    self.appStartTrace  = nil ;
284+     FPRLogDebug (kFPRTraceNotCreated ,
285+                 @" App start trace cancelled due to background state contamination."  );
256286  }
257287
258288  //  Stop the active background session trace.
@@ -266,28 +296,44 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
266296  self.foregroundSessionTrace  = appTrace;
267297
268298  //  Start measuring time to make the app interactive on the App start trace.
269-   static  BOOL  TTIStageStarted = NO ;
270-   if  (!TTIStageStarted) {
299+   if  (!self.appStartTraceCompleted  && self.appStartTrace ) {
271300    [self .appStartTrace startStageNamed: kFPRAppStartStageNameTimeToUserInteraction ];
272-     TTIStageStarted  = YES ;
301+     self. appStartTraceCompleted  = YES ;
273302
274303    //  Assumption here is that - the app becomes interactive in the next runloop cycle.
275304    //  It is possible that the app does more things later, but for now we are not measuring that.
305+     __weak typeof (self) weakSelf = self;
276306    dispatch_async (dispatch_get_main_queue (), ^{
277-       NSTimeInterval  startTimeSinceEpoch = [self .appStartTrace startTimeSinceEpoch ];
307+       __strong typeof (weakSelf) strongSelf = weakSelf;
308+       if  (!strongSelf || !strongSelf.appStartTrace ) {
309+         return ;
310+       }
311+ 
312+       NSTimeInterval  startTimeSinceEpoch = [strongSelf.appStartTrace startTimeSinceEpoch ];
278313      NSTimeInterval  currentTimeSinceEpoch = [[NSDate  date ] timeIntervalSince1970 ];
314+       NSTimeInterval  measuredAppStartTime = currentTimeSinceEpoch - startTimeSinceEpoch;
279315
280-       //  The below check is to account  for 2  scenarios. 
281-       //  1. The app gets  started in the  background and might come  to foreground a lot  later. 
282-       //  2. The app is  launched,  but immediately backgrounded for  some reason and the actual launch
316+       //  The below check accounts  for multiple  scenarios: 
317+       //  1. App  started in background and comes  to foreground later
318+       //  2. App  launched but immediately backgroundedfor  some reason and the actual launch
283319      //  happens a lot later.
284-       //  Dropping the app start trace in such situations where the launch time is taking more than
285-       //  60 minutes. This is an approximation, but a more agreeable timelimit for app start.
286-       if  ((currentTimeSinceEpoch - startTimeSinceEpoch < gAppStartMaxValidDuration ) &&
287-           [self  isAppStartEnabled ] && ![self  isApplicationPreWarmed ]) {
288-         [self .appStartTrace stop ];
320+       //  3. Network delays during startup inflating metrics
321+       //  4. iOS prewarm scenarios
322+       //  5. Dropping the app start trace in such situations where the launch time is taking more
323+       //  than 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
324+       BOOL  shouldDispatchAppStartTrace = (measuredAppStartTime < gAppStartMaxValidDuration ) &&
325+                                          [strongSelf isAppStartEnabled ] &&
326+                                          ![strongSelf isApplicationPreWarmed ];
327+ 
328+       if  (shouldDispatchAppStartTrace) {
329+         [strongSelf.appStartTrace stop ];
289330      } else  {
290-         [self .appStartTrace cancel ];
331+         [strongSelf.appStartTrace cancel ];
332+         if  (measuredAppStartTime >= gAppStartMaxValidDuration ) {
333+           FPRLogDebug (kFPRTraceInvalidName ,
334+                       @" App start trace cancelled due to excessive duration: %.2f s"  ,
335+                       measuredAppStartTime);
336+         }
291337      }
292338    });
293339  }
0 commit comments