Files

400 lines
16 KiB
Objective-C

// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "FirebasePerformance/Sources/AppActivity/FPRAppActivityTracker.h"
#import <Foundation/Foundation.h>
#import <Network/Network.h>
#import <UIKit/UIKit.h>
#import "FirebasePerformance/Sources/AppActivity/FPRSessionManager.h"
#import "FirebasePerformance/Sources/Common/FPRDiagnostics.h"
#import "FirebasePerformance/Sources/Configurations/FPRConfigurations.h"
#import "FirebasePerformance/Sources/Gauges/CPU/FPRCPUGaugeCollector+Private.h"
#import "FirebasePerformance/Sources/Gauges/FPRGaugeManager.h"
#import "FirebasePerformance/Sources/Gauges/Memory/FPRMemoryGaugeCollector+Private.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Internal.h"
#import "FirebasePerformance/Sources/Timer/FIRTrace+Private.h"
static NSDate *appStartTime = nil;
static NSDate *doubleDispatchTime = nil;
static NSDate *applicationDidFinishLaunchTime = nil;
static NSTimeInterval gAppStartMaxValidDuration = 60 * 60; // 60 minutes.
static FPRCPUGaugeData *gAppStartCPUGaugeData = nil;
static FPRMemoryGaugeData *gAppStartMemoryGaugeData = nil;
static BOOL isActivePrewarm = NO;
static BOOL sLaunchedInBackgroundState = NO;
NSString *const kFPRAppStartTraceName = @"_as";
NSString *const kFPRAppStartStageNameTimeToUI = @"_astui";
NSString *const kFPRAppStartStageNameTimeToFirstDraw = @"_astfd";
NSString *const kFPRAppStartStageNameTimeToUserInteraction = @"_asti";
NSString *const kFPRAppTraceNameForegroundSession = @"_fs";
NSString *const kFPRAppTraceNameBackgroundSession = @"_bs";
NSString *const kFPRAppCounterNameTraceEventsRateLimited = @"_fstec";
NSString *const kFPRAppCounterNameNetworkTraceEventsRateLimited = @"_fsntc";
NSString *const kFPRAppCounterNameTraceNotStopped = @"_tsns";
NSString *const kFPRAppCounterNameActivePrewarm = @"_fsapc";
@interface FPRAppActivityTracker ()
/** The foreground session trace. Will be set only when the app is in the foreground. */
@property(nonatomic, readwrite) FIRTrace *foregroundSessionTrace;
/** The background session trace. Will be set only when the app is in the background. */
@property(nonatomic, readwrite) FIRTrace *backgroundSessionTrace;
/** Current running state of the application. */
@property(nonatomic, readwrite) FPRApplicationState applicationState;
/** Current network connection type of the application. */
@property(nonatomic, readwrite) firebase_perf_v1_NetworkConnectionInfo_NetworkType networkType;
/** Network monitor object to track network movements. */
@property(nonatomic, readwrite) nw_path_monitor_t monitor;
/** Queue used to track the network monitoring changes. */
@property(nonatomic, readwrite) dispatch_queue_t monitorQueue;
/** Trace to measure the app start performance. */
@property(nonatomic) FIRTrace *appStartTrace;
/** 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;
/** Starts tracking app active sessions. */
- (void)startAppActivityTracking;
@end
@implementation FPRAppActivityTracker
+ (void)load {
// This is an approximation of the app start time.
appStartTime = [NSDate date];
// When an app is prewarmed, Apple sets env variable ActivePrewarm to 1, then the env variable is
// deleted after didFinishLaunching
isActivePrewarm = [NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"];
gAppStartCPUGaugeData = fprCollectCPUMetric();
gAppStartMemoryGaugeData = fprCollectMemoryMetric();
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidBecomeVisible:)
name:UIWindowDidBecomeVisibleNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidFinishLaunching:)
name:UIApplicationDidFinishLaunchingNotification
object:nil];
}
+ (void)windowDidBecomeVisible:(NSNotification *)notification {
FPRAppActivityTracker *activityTracker = [self sharedInstance];
[activityTracker startAppActivityTracking];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidBecomeVisibleNotification
object:nil];
}
+ (void)applicationDidFinishLaunching:(NSNotification *)notification {
applicationDidFinishLaunchTime = [NSDate date];
UIApplicationState state = [UIApplication sharedApplication].applicationState;
if (state == UIApplicationStateBackground) {
// Check if the app uses the UIScene lifecycle. Scene-based apps (SwiftUI or UIKit with scenes)
// connect scenes after didFinishLaunching, so applicationState may report .background even on
// normal foreground launches. Non-scene apps report .background reliably.
NSDictionary *sceneManifest =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"];
if (sceneManifest) {
// Scene-based apps may report .background here even on foreground launches
// because scenes haven't connected yet. Defer decision to didBecomeActive.
sLaunchedInBackgroundState = YES;
} else {
// Non-scene app - .background is reliable, invalidate app start.
appStartTime = nil;
}
}
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidFinishLaunchingNotification
object:nil];
}
+ (instancetype)sharedInstance {
static FPRAppActivityTracker *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] initAppActivityTracker];
});
return instance;
}
/**
* Custom initializer to create an app activity tracker.
*/
- (instancetype)initAppActivityTracker {
self = [super init];
if (self != nil) {
_applicationState = FPRApplicationStateUnknown;
_appStartGaugeMetricDispatched = NO;
_appStartTraceCompleted = NO;
_configurations = [FPRConfigurations sharedInstance];
[self startTrackingNetwork];
}
return self;
}
- (void)startAppActivityTracking {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appDidBecomeActiveNotification:)
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(appWillResignActiveNotification:)
name:UIApplicationWillResignActiveNotification
object:[UIApplication sharedApplication]];
}
- (FIRTrace *)activeTrace {
if (self.foregroundSessionTrace) {
return self.foregroundSessionTrace;
}
return self.backgroundSessionTrace;
}
- (void)startTrackingNetwork {
self.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_NONE;
dispatch_queue_attr_t attrs = dispatch_queue_attr_make_with_qos_class(
DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, DISPATCH_QUEUE_PRIORITY_DEFAULT);
self.monitorQueue = dispatch_queue_create("com.google.firebase.perf.network.monitor", attrs);
self.monitor = nw_path_monitor_create();
nw_path_monitor_set_queue(self.monitor, self.monitorQueue);
__weak FPRAppActivityTracker *weakSelf = self;
nw_path_monitor_set_update_handler(self.monitor, ^(nw_path_t _Nonnull path) {
BOOL isWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi);
BOOL isCellular = nw_path_uses_interface_type(path, nw_interface_type_cellular);
BOOL isEthernet = nw_path_uses_interface_type(path, nw_interface_type_wired);
if (isWiFi) {
weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_WIFI;
} else if (isCellular) {
weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_MOBILE;
} else if (isEthernet) {
weakSelf.networkType = firebase_perf_v1_NetworkConnectionInfo_NetworkType_ETHERNET;
}
});
nw_path_monitor_start(self.monitor);
}
/**
* Checks if the prewarming feature is available on the current device.
*
* @return true if the OS could prewarm apps on the current device
*/
- (BOOL)isPrewarmAvailable {
return YES;
}
/**
RC flag for dropping all app start events
*/
- (BOOL)isAppStartEnabled {
return [self.configurations prewarmDetectionMode] != PrewarmDetectionModeKeepNone;
}
/**
RC flag for enabling prewarm-detection using ActivePrewarm environment variable
*/
- (BOOL)isActivePrewarmEnabled {
PrewarmDetectionMode mode = [self.configurations prewarmDetectionMode];
return (mode == PrewarmDetectionModeActivePrewarm);
}
/**
Checks if the current app start is a prewarmed app start
*/
- (BOOL)isApplicationPreWarmed {
if (![self isPrewarmAvailable]) {
return NO;
}
BOOL isPrewarmed = NO;
if (isActivePrewarm == YES) {
isPrewarmed = isPrewarmed || [self isActivePrewarmEnabled];
[self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:1];
} else {
[self.activeTrace incrementMetric:kFPRAppCounterNameActivePrewarm byInt:0];
}
return isPrewarmed;
}
/**
* This gets called whenever the app becomes active. A new trace will be created to track the active
* foreground session. Any background session trace that was running in the past will be stopped.
*
* @param notification Notification received during app launch.
*/
- (void)appDidBecomeActiveNotification:(NSNotification *)notification {
self.applicationState = FPRApplicationStateForeground;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (appStartTime == nil) {
FPRLogDebug(kFPRTraceNotCreated, @"App start trace skipped due to background launch.");
return;
}
// Only applies to scene-based apps where .background at launch may be a false positive.
// Distinguish genuine background launches by checking how long ago +load captured appStartTime.
// A real foreground launch reaches didBecomeActive within seconds; a background launch that is
// later opened by the user will have a much larger gap.
NSDictionary *sceneManifest =
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationSceneManifest"];
if (sLaunchedInBackgroundState && sceneManifest) {
NSTimeInterval timeSinceStart = [[NSDate date] timeIntervalSinceDate:appStartTime];
// App launched in background so we invalidate the captured app start time
// to prevent incorrect measurement when user later opens the app
if (timeSinceStart > gAppStartMaxValidDuration) {
FPRLogDebug(kFPRTraceNotCreated,
@"Background launch detected. App start measurement will be skipped.");
return;
}
}
self.appStartTrace = [[FIRTrace alloc] initInternalTraceWithName:kFPRAppStartTraceName];
[self.appStartTrace startWithStartTime:appStartTime];
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUI startTime:appStartTime];
// Start measuring time to first draw on the App start trace.
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToFirstDraw];
});
// 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. Trace had background state.");
}
// Stop the active background session trace.
[self.backgroundSessionTrace stop];
self.backgroundSessionTrace = nil;
// Start foreground session trace.
FIRTrace *appTrace =
[[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameForegroundSession];
[appTrace start];
self.foregroundSessionTrace = appTrace;
// Start measuring time to make the app interactive on the App start trace.
if (!self.appStartTraceCompleted && self.appStartTrace) {
[self.appStartTrace startStageNamed:kFPRAppStartStageNameTimeToUserInteraction];
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(), ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf || !strongSelf.appStartTrace) {
return;
}
NSTimeInterval startTimeSinceEpoch = [strongSelf.appStartTrace startTimeSinceEpoch];
NSTimeInterval currentTimeSinceEpoch = [[NSDate date] timeIntervalSince1970];
NSTimeInterval measuredAppStartTime = currentTimeSinceEpoch - startTimeSinceEpoch;
// The below check accounts for multiple scenarios:
// 1. App started in background and comes to foreground later
// 2. App launched but immediately backgroundedfor some reason and the actual launch
// happens a lot later.
// 3. Network delays during startup inflating metrics
// 4. iOS prewarm scenarios
// 5. 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.
BOOL shouldDispatchAppStartTrace = (measuredAppStartTime < gAppStartMaxValidDuration) &&
[strongSelf isAppStartEnabled] &&
![strongSelf isApplicationPreWarmed];
if (shouldDispatchAppStartTrace) {
[strongSelf.appStartTrace stop];
} else {
[strongSelf.appStartTrace cancel];
if (measuredAppStartTime >= gAppStartMaxValidDuration) {
FPRLogDebug(kFPRTraceInvalidName,
@"App start trace cancelled due to excessive duration: %.2fs",
measuredAppStartTime);
}
}
});
}
}
/**
* This gets called whenever the app resigns its active status. The currently active foreground
* session trace will be stopped and a background session trace will be started.
*
* @param notification Notification received during app resigning active status.
*/
- (void)appWillResignActiveNotification:(NSNotification *)notification {
// Dispatch the collected gauge metrics.
if (!self.appStartGaugeMetricDispatched) {
[[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartCPUGaugeData];
[[FPRGaugeManager sharedInstance] dispatchMetric:gAppStartMemoryGaugeData];
self.appStartGaugeMetricDispatched = YES;
}
self.applicationState = FPRApplicationStateBackground;
// Stop foreground session trace.
[self.foregroundSessionTrace stop];
self.foregroundSessionTrace = nil;
// Start background session trace.
self.backgroundSessionTrace =
[[FIRTrace alloc] initInternalTraceWithName:kFPRAppTraceNameBackgroundSession];
[self.backgroundSessionTrace start];
}
- (void)dealloc {
nw_path_monitor_cancel(self.monitor);
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidBecomeActiveNotification
object:[UIApplication sharedApplication]];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationWillResignActiveNotification
object:[UIApplication sharedApplication]];
}
@end