Skip to content

Commit 4daaefd

Browse files
JesusRojassandrewheard
authored andcommitted
Fix app start trace outliers from network delays (#10733) (#15409)
1 parent 7c6bbcb commit 4daaefd

File tree

2 files changed

+62
-15
lines changed

2 files changed

+62
-15
lines changed

FirebasePerformance/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22
- [fixed] Prevent race condition crash in FPRTraceBackgroundActivityTracker. (#14273)
3+
- [fixed] Fix app start trace outliers from network delays. (#10733)
34

45
# 12.3.0
56
- [fixed] Add missing nanopb dependency to fix SwiftPM builds when building

FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.m

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
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: %.2fs",
335+
measuredAppStartTime);
336+
}
291337
}
292338
});
293339
}

0 commit comments

Comments
 (0)