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