Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 59 additions & 17 deletions FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
#import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"

static NSDate *appStartTime = nil;
static NSDate *doubleDispatchTime = nil;
Expand Down Expand Up @@ -71,6 +72,9 @@ @interface FPRAppActivityTracker ()
/** Tracks if the gauge metrics are dispatched. */
@property(nonatomic) BOOL appStartGaugeMetricDispatched;

/** Tracks if app start trace completion logic has been executed. */
@property(nonatomic) BOOL appStartTraceCompleted;

/** Firebase Performance Configuration object */
@property(nonatomic) FPRConfigurations *configurations;

Expand Down Expand Up @@ -113,6 +117,18 @@ + (void)windowDidBecomeVisible:(NSNotification *)notification {

+ (void)applicationDidFinishLaunching:(NSNotification *)notification {
applicationDidFinishLaunchTime = [NSDate date];

// Detect a background launch and invalidate app start time
// this prevents we measure duration from background launch
UIApplicationState state = [UIApplication sharedApplication].applicationState;
if (state == UIApplicationStateBackground) {
// App launched in background so we invalidate the captured app start time
// to prevent incorrect measurement when user later opens the app
appStartTime = nil;
FPRLogDebug(kFPRTraceNotCreated,
@"Background launch detected. App start measurement will be skipped.");
}

[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidFinishLaunchingNotification
object:nil];
Expand All @@ -135,6 +151,7 @@ - (instancetype)initAppActivityTracker {
if (self != nil) {
_applicationState = FPRApplicationStateUnknown;
_appStartGaugeMetricDispatched = NO;
_appStartTraceCompleted = NO;
_configurations = [FPRConfigurations sharedInstance];
[self startTrackingNetwork];
}
Expand Down Expand Up @@ -242,6 +259,15 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Early bailout if background launch was detected, appStartTime will be nil if the app was
// launched in background
if (appStartTime == nil) {
FPRLogDebug(kFPRTraceNotCreated,
@"App start trace skipped due to background launch. "
@"This prevents reporting incorrect multi-minute/hour durations.");
return;
}

self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
[self.appStartTrace startWithStartTime:appStartTime];
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
Expand All @@ -250,9 +276,13 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
});

// If ever the app start trace had it life in background stage, do not send the trace.
if (self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
// If ever the app start trace had its life in background stage, do not send the trace.
if (self.appStartTrace &&
self.appStartTrace.backgroundTraceState != FPRTraceStateForegroundOnly) {
[self.appStartTrace cancel];
self.appStartTrace = nil;
FPRLogDebug(kFPRTraceNotCreated,
@"App start trace cancelled due to background state contamination.");
}

// Stop the active background session trace.
Expand All @@ -266,28 +296,40 @@ - (void)appDidBecomeActiveNotification:(NSNotification *)notification {
self.foregroundSessionTrace = appTrace;

// Start measuring time to make the app interactive on the App start trace.
static BOOL TTIStageStarted = NO;
if (!TTIStageStarted) {
if (!self.appStartTraceCompleted && self.appStartTrace) {
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
TTIStageStarted = YES;
self.appStartTraceCompleted = YES;

// Assumption here is that - the app becomes interactive in the next runloop cycle.
// It is possible that the app does more things later, but for now we are not measuring that.
__weak typeof(self) weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
NSTimeInterval startTimeSinceEpoch = [self.appStartTrace startTimeSinceEpoch];
NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf || !strongSelf.appStartTrace) {
return;
}

// The below check is to account for 2 scenarios.
// 1. The app gets started in the background and might come to foreground a lot later.
// 2. The app is launched, but immediately backgrounded for some reason and the actual launch
// happens a lot later.
// Dropping the app start trace in such situations where the launch time is taking more than
// 60 minutes. This is an approximation, but a more agreeable timelimit for app start.
if ((currentTimeSinceEpoch - startTimeSinceEpoch < gAppStartMaxValidDuration) &&
[self isAppStartEnabled] && ![self isApplicationPreWarmed]) {
[self.appStartTrace stop];
NSTimeInterval startTimeSinceEpoch = [strongSelf.appStartTrace startTimeSinceEpoch];
NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
NSTimeInterval elapsed = currentTimeSinceEpoch - startTimeSinceEpoch;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call it measuredAppStartTime.


// The below check accounts for multiple scenarios:
// 1. App started in background and comes to foreground later
// 2. App launched but immediately backgrounded
// 3. Network delays during startup inflating metrics
// 4. iOS prewarm scenarios
BOOL shouldCompleteTrace = (elapsed < gAppStartMaxValidDuration) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the logic. Let's call the variable to be something more relevant - shouldDispatchAppStartTrace.

[strongSelf isAppStartEnabled] &&
![strongSelf isApplicationPreWarmed];

if (shouldCompleteTrace) {
[strongSelf.appStartTrace stop];
} else {
[self.appStartTrace cancel];
[strongSelf.appStartTrace cancel];
if (elapsed >= gAppStartMaxValidDuration) {
FPRLogDebug(kFPRTraceInvalidName,
@"App start trace cancelled due to excessive duration: %.2fs", elapsed);
}
}
});
}
Expand Down
Loading